mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
[webSymbols] Make WebSymbols part of JavaScript test framework to allow 3rd parties to run tests in Gradle setup
GitOrigin-RevId: 7bbdaa39e8b05c2537f21c68f36b8f72887837d8
This commit is contained in:
committed by
intellij-monorepo-bot
parent
980706b939
commit
b853b4f3af
2
.idea/modules.xml
generated
2
.idea/modules.xml
generated
@@ -900,6 +900,8 @@
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" filepath="$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" filepath="$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.testFramework.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.testFramework.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.tests.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.tests.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" filepath="$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" />
|
||||
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" />
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/gen" isTestSource="false" generated="true" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
@@ -28,7 +27,5 @@
|
||||
<orderEntry type="library" name="jetbrains.markdown" level="project" />
|
||||
<orderEntry type="library" name="gson" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" scope="TEST" />
|
||||
<orderEntry type="module" module-name="intellij.java.rt" scope="TEST" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$/testFramework">
|
||||
<sourceFolder url="file://$MODULE_DIR$/testFramework" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module" module-name="intellij.platform.webSymbols" />
|
||||
<orderEntry type="module" module-name="intellij.platform.analysis" />
|
||||
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
|
||||
<orderEntry type="library" name="JUnit4" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.usageView" />
|
||||
<orderEntry type="module" module-name="intellij.platform.refactoring" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.analysis.impl" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
|
||||
<orderEntry type="library" name="fastutil-min" level="project" />
|
||||
<orderEntry type="library" name="commons-compress" level="project" />
|
||||
<orderEntry type="library" name="jetbrains.markdown" level="project" />
|
||||
<orderEntry type="library" name="gson" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" />
|
||||
<orderEntry type="module" module-name="intellij.java.rt" />
|
||||
</component>
|
||||
</module>
|
||||
36
platform/webSymbols/intellij.platform.webSymbols.tests.iml
Normal file
36
platform/webSymbols/intellij.platform.webSymbols.tests.iml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$/testData">
|
||||
<sourceFolder url="file://$MODULE_DIR$/testData" type="java-test-resource" />
|
||||
</content>
|
||||
<content url="file://$MODULE_DIR$/testSrc">
|
||||
<sourceFolder url="file://$MODULE_DIR$/testSrc" isTestSource="true" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
<orderEntry type="module" module-name="intellij.platform.analysis" />
|
||||
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core.ui" />
|
||||
<orderEntry type="module" module-name="intellij.platform.editor.ex" />
|
||||
<orderEntry type="library" scope="TEST" name="JUnit4" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.core" />
|
||||
<orderEntry type="module" module-name="intellij.platform.usageView" />
|
||||
<orderEntry type="module" module-name="intellij.platform.refactoring" />
|
||||
<orderEntry type="library" name="jackson-databind" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.analysis.impl" />
|
||||
<orderEntry type="library" name="jackson" level="project" />
|
||||
<orderEntry type="library" name="jackson-module-kotlin" level="project" />
|
||||
<orderEntry type="library" name="fastutil-min" level="project" />
|
||||
<orderEntry type="library" name="commons-compress" level="project" />
|
||||
<orderEntry type="library" name="jetbrains.markdown" level="project" />
|
||||
<orderEntry type="library" name="gson" level="project" />
|
||||
<orderEntry type="module" module-name="intellij.platform.markdown.utils" />
|
||||
<orderEntry type="module" module-name="intellij.platform.testFramework" />
|
||||
<orderEntry type="module" module-name="intellij.platform.webSymbols" />
|
||||
<orderEntry type="module" module-name="intellij.platform.webSymbols.testFramework" />
|
||||
<orderEntry type="module" module-name="intellij.java.rt" />
|
||||
</component>
|
||||
</module>
|
||||
@@ -17,6 +17,7 @@ import com.intellij.webSymbols.query.WebSymbolsListSymbolsQueryParams
|
||||
import com.intellij.webSymbols.query.WebSymbolsNameMatchQueryParams
|
||||
import com.intellij.webSymbols.query.WebSymbolsQueryParams
|
||||
import com.intellij.webSymbols.webTypes.json.WebTypes
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import javax.swing.Icon
|
||||
|
||||
|
||||
@@ -36,9 +37,11 @@ internal fun Icon.scaleToHeight(height: Int): Icon {
|
||||
return IconUtil.scale(this, null, scale)
|
||||
}
|
||||
|
||||
internal fun <T> List<T>.selectBest(segmentsProvider: (T) -> List<WebSymbolNameSegment>,
|
||||
priorityProvider: (T) -> WebSymbol.Priority?,
|
||||
isExtension: (T) -> Boolean) =
|
||||
internal fun <T> List<T>.selectBest(
|
||||
segmentsProvider: (T) -> List<WebSymbolNameSegment>,
|
||||
priorityProvider: (T) -> WebSymbol.Priority?,
|
||||
isExtension: (T) -> Boolean,
|
||||
) =
|
||||
if (size > 1) {
|
||||
var bestWeight: IntArray = intArrayOf(0, 0, 0)
|
||||
|
||||
@@ -109,5 +112,6 @@ internal fun WebSymbolNameSegment.copy(
|
||||
): WebSymbolNameSegmentImpl =
|
||||
(this as WebSymbolNameSegmentImpl).copy(apiStatus, priority, proximity, problem, symbols)
|
||||
|
||||
internal fun WebSymbolNameSegment.canUnwrapSymbols(): Boolean =
|
||||
@ApiStatus.Internal
|
||||
fun WebSymbolNameSegment.canUnwrapSymbols(): Boolean =
|
||||
(this as WebSymbolNameSegmentImpl).canUnwrapSymbols()
|
||||
@@ -12,9 +12,11 @@ import com.intellij.webSymbols.query.WebSymbolNameConversionRules
|
||||
import com.intellij.webSymbols.query.WebSymbolNameConverter
|
||||
import com.intellij.webSymbols.query.WebSymbolNamesProvider
|
||||
import com.intellij.webSymbols.query.WebSymbolNamesProvider.Target.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.util.*
|
||||
|
||||
internal class WebSymbolNamesProviderImpl(
|
||||
@ApiStatus.Internal
|
||||
class WebSymbolNamesProviderImpl(
|
||||
private val framework: FrameworkId?,
|
||||
private val configuration: List<WebSymbolNameConversionRules>,
|
||||
private val modificationTracker: ModificationTracker,
|
||||
@@ -76,9 +78,11 @@ internal class WebSymbolNamesProviderImpl(
|
||||
else
|
||||
listOf(qualifiedName.name)
|
||||
|
||||
override fun adjustRename(qualifiedName: WebSymbolQualifiedName,
|
||||
newName: String,
|
||||
occurence: String): String {
|
||||
override fun adjustRename(
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
newName: String,
|
||||
occurence: String,
|
||||
): String {
|
||||
if (qualifiedName.name == occurence) return newName
|
||||
|
||||
val oldVariants = getNames(qualifiedName, WebSymbolNamesProvider.Target.NAMES_QUERY)
|
||||
|
||||
@@ -9,22 +9,29 @@ import com.intellij.util.applyIf
|
||||
import com.intellij.util.asSafely
|
||||
import com.intellij.util.concurrency.annotations.RequiresReadLock
|
||||
import com.intellij.util.containers.Stack
|
||||
import com.intellij.webSymbols.*
|
||||
import com.intellij.webSymbols.WebSymbol
|
||||
import com.intellij.webSymbols.WebSymbolQualifiedKind
|
||||
import com.intellij.webSymbols.WebSymbolQualifiedName
|
||||
import com.intellij.webSymbols.WebSymbolsScope
|
||||
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
|
||||
import com.intellij.webSymbols.context.WebSymbolsContext
|
||||
import com.intellij.webSymbols.impl.filterByQueryParams
|
||||
import com.intellij.webSymbols.impl.selectBest
|
||||
import com.intellij.webSymbols.query.*
|
||||
import com.intellij.webSymbols.utils.*
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.util.*
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
override val namesProvider: WebSymbolNamesProvider,
|
||||
override val resultsCustomizer: WebSymbolsQueryResultsCustomizer,
|
||||
override val context: WebSymbolsContext,
|
||||
override val allowResolve: Boolean) : WebSymbolsQueryExecutor {
|
||||
@ApiStatus.Internal
|
||||
class WebSymbolsQueryExecutorImpl(
|
||||
rootScope: List<WebSymbolsScope>,
|
||||
override val namesProvider: WebSymbolNamesProvider,
|
||||
override val resultsCustomizer: WebSymbolsQueryResultsCustomizer,
|
||||
override val context: WebSymbolsContext,
|
||||
override val allowResolve: Boolean,
|
||||
) : WebSymbolsQueryExecutor {
|
||||
|
||||
private val rootScope: List<WebSymbolsScope> = initializeCompoundScopes(rootScope)
|
||||
|
||||
@@ -60,28 +67,34 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
}
|
||||
}
|
||||
|
||||
override fun runNameMatchQuery(path: List<WebSymbolQualifiedName>,
|
||||
virtualSymbols: Boolean,
|
||||
abstractSymbols: Boolean,
|
||||
strictScope: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
|
||||
override fun runNameMatchQuery(
|
||||
path: List<WebSymbolQualifiedName>,
|
||||
virtualSymbols: Boolean,
|
||||
abstractSymbols: Boolean,
|
||||
strictScope: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbol> =
|
||||
runNameMatchQuery(path, WebSymbolsNameMatchQueryParams.create(this, virtualSymbols, abstractSymbols, strictScope), additionalScope)
|
||||
|
||||
override fun runListSymbolsQuery(path: List<WebSymbolQualifiedName>,
|
||||
qualifiedKind: WebSymbolQualifiedKind,
|
||||
expandPatterns: Boolean,
|
||||
virtualSymbols: Boolean,
|
||||
abstractSymbols: Boolean,
|
||||
strictScope: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
|
||||
override fun runListSymbolsQuery(
|
||||
path: List<WebSymbolQualifiedName>,
|
||||
qualifiedKind: WebSymbolQualifiedKind,
|
||||
expandPatterns: Boolean,
|
||||
virtualSymbols: Boolean,
|
||||
abstractSymbols: Boolean,
|
||||
strictScope: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbol> =
|
||||
runListSymbolsQuery(path + qualifiedKind.withName(""),
|
||||
WebSymbolsListSymbolsQueryParams.create(this, expandPatterns = expandPatterns, virtualSymbols = virtualSymbols,
|
||||
abstractSymbols = abstractSymbols, strictScope = strictScope), additionalScope)
|
||||
abstractSymbols = abstractSymbols, strictScope = strictScope), additionalScope)
|
||||
|
||||
override fun runCodeCompletionQuery(path: List<WebSymbolQualifiedName>,
|
||||
position: Int,
|
||||
virtualSymbols: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
|
||||
override fun runCodeCompletionQuery(
|
||||
path: List<WebSymbolQualifiedName>,
|
||||
position: Int,
|
||||
virtualSymbols: Boolean,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbolCodeCompletionItem> =
|
||||
runCodeCompletionQuery(path, WebSymbolsCodeCompletionQueryParams.create(this, position, virtualSymbols), additionalScope)
|
||||
|
||||
override fun withNameConversionRules(rules: List<WebSymbolNameConversionRules>): WebSymbolsQueryExecutor =
|
||||
@@ -103,11 +116,13 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
return rootScope.flatMap {
|
||||
if (it is WebSymbolsCompoundScope) {
|
||||
it.getScopes(compoundScopeQueryExecutor)
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
listOf(it)
|
||||
}
|
||||
}
|
||||
} else return rootScope
|
||||
}
|
||||
else return rootScope
|
||||
}
|
||||
|
||||
private fun buildQueryScope(additionalScope: List<WebSymbolsScope>): MutableSet<WebSymbolsScope> {
|
||||
@@ -122,12 +137,16 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
return finalScope
|
||||
}
|
||||
|
||||
private fun runNameMatchQuery(path: List<WebSymbolQualifiedName>,
|
||||
queryParams: WebSymbolsNameMatchQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
|
||||
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsNameMatchQueryParams ->
|
||||
private fun runNameMatchQuery(
|
||||
path: List<WebSymbolQualifiedName>,
|
||||
queryParams: WebSymbolsNameMatchQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbol> =
|
||||
runQuery(path, queryParams, additionalScope) {
|
||||
finalContext: Collection<WebSymbolsScope>,
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsNameMatchQueryParams,
|
||||
->
|
||||
val result = finalContext
|
||||
.takeLastUntilExclusiveScopeFor(qualifiedName.qualifiedKind)
|
||||
.asSequence()
|
||||
@@ -143,11 +162,15 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
result
|
||||
}
|
||||
|
||||
private fun runListSymbolsQuery(path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsListSymbolsQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbol> =
|
||||
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsListSymbolsQueryParams ->
|
||||
private fun runListSymbolsQuery(
|
||||
path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsListSymbolsQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbol> =
|
||||
runQuery(path, queryParams, additionalScope) {
|
||||
finalContext: Collection<WebSymbolsScope>,
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsListSymbolsQueryParams,
|
||||
->
|
||||
val result = finalContext
|
||||
.takeLastUntilExclusiveScopeFor(qualifiedName.qualifiedKind)
|
||||
.asSequence()
|
||||
@@ -190,11 +213,15 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
result
|
||||
}
|
||||
|
||||
private fun runCodeCompletionQuery(path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsCodeCompletionQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
|
||||
runQuery(path, queryParams, additionalScope) { finalContext: Collection<WebSymbolsScope>,
|
||||
pathSection: WebSymbolQualifiedName,
|
||||
params: WebSymbolsCodeCompletionQueryParams ->
|
||||
private fun runCodeCompletionQuery(
|
||||
path: List<WebSymbolQualifiedName>, queryParams: WebSymbolsCodeCompletionQueryParams,
|
||||
additionalScope: List<WebSymbolsScope>,
|
||||
): List<WebSymbolCodeCompletionItem> =
|
||||
runQuery(path, queryParams, additionalScope) {
|
||||
finalContext: Collection<WebSymbolsScope>,
|
||||
pathSection: WebSymbolQualifiedName,
|
||||
params: WebSymbolsCodeCompletionQueryParams,
|
||||
->
|
||||
var proximityBase = 0
|
||||
var nextProximityBase = 0
|
||||
var previousName: String? = null
|
||||
@@ -243,7 +270,8 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
context: Collection<WebSymbolsScope>,
|
||||
pathSection: WebSymbolQualifiedName,
|
||||
params: P,
|
||||
) -> List<T>): List<T> {
|
||||
) -> List<T>,
|
||||
): List<T> {
|
||||
ProgressManager.checkCanceled()
|
||||
if (path.isEmpty()) return emptyList()
|
||||
|
||||
@@ -306,8 +334,10 @@ internal class WebSymbolsQueryExecutorImpl(rootScope: List<WebSymbolsScope>,
|
||||
?: item
|
||||
}
|
||||
|
||||
private fun WebSymbol.expandPattern(context: Stack<WebSymbolsScope>,
|
||||
params: WebSymbolsListSymbolsQueryParams): List<WebSymbol> =
|
||||
private fun WebSymbol.expandPattern(
|
||||
context: Stack<WebSymbolsScope>,
|
||||
params: WebSymbolsListSymbolsQueryParams,
|
||||
): List<WebSymbol> =
|
||||
pattern?.let { pattern ->
|
||||
context.push(this)
|
||||
try {
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.intellij.webSymbols.patterns.impl.applyIcons
|
||||
import com.intellij.webSymbols.query.*
|
||||
import com.intellij.webSymbols.query.impl.WebSymbolMatchImpl
|
||||
import com.intellij.webSymbols.references.WebSymbolReferenceProblem.ProblemKind
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import java.util.*
|
||||
import javax.swing.Icon
|
||||
import kotlin.contracts.ExperimentalContracts
|
||||
@@ -137,9 +138,11 @@ fun WebSymbolNameSegment.withSymbols(symbols: List<WebSymbol>): WebSymbolNameSeg
|
||||
fun WebSymbolMatch.withSegments(segments: List<WebSymbolNameSegment>): WebSymbolMatch =
|
||||
(this as WebSymbolMatchImpl).withSegments(segments)
|
||||
|
||||
fun WebSymbol.match(nameToMatch: String,
|
||||
params: WebSymbolsNameMatchQueryParams,
|
||||
context: Stack<WebSymbolsScope>): List<WebSymbol> {
|
||||
fun WebSymbol.match(
|
||||
nameToMatch: String,
|
||||
params: WebSymbolsNameMatchQueryParams,
|
||||
context: Stack<WebSymbolsScope>,
|
||||
): List<WebSymbol> {
|
||||
pattern?.let { pattern ->
|
||||
context.push(this)
|
||||
try {
|
||||
@@ -168,9 +171,11 @@ fun WebSymbol.match(nameToMatch: String,
|
||||
}
|
||||
}
|
||||
|
||||
fun WebSymbol.toCodeCompletionItems(name: String,
|
||||
params: WebSymbolsCodeCompletionQueryParams,
|
||||
context: Stack<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
|
||||
fun WebSymbol.toCodeCompletionItems(
|
||||
name: String,
|
||||
params: WebSymbolsCodeCompletionQueryParams,
|
||||
context: Stack<WebSymbolsScope>,
|
||||
): List<WebSymbolCodeCompletionItem> =
|
||||
pattern?.let { pattern ->
|
||||
context.push(this)
|
||||
try {
|
||||
@@ -286,13 +291,15 @@ fun WebSymbolApiStatus?.coalesceWith(other: WebSymbolApiStatus?): WebSymbolApiSt
|
||||
}
|
||||
is WebSymbolApiStatus.Experimental -> when (other) {
|
||||
is WebSymbolApiStatus.Obsolete,
|
||||
is WebSymbolApiStatus.Deprecated -> other
|
||||
is WebSymbolApiStatus.Deprecated,
|
||||
-> other
|
||||
else -> this
|
||||
}
|
||||
is WebSymbolApiStatus.Stable -> when (other) {
|
||||
is WebSymbolApiStatus.Obsolete,
|
||||
is WebSymbolApiStatus.Deprecated,
|
||||
is WebSymbolApiStatus.Experimental -> other
|
||||
is WebSymbolApiStatus.Experimental,
|
||||
-> other
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
@@ -377,9 +384,11 @@ fun NavigationTarget.createPsiRangeNavigationItem(element: PsiElement, offsetWit
|
||||
}
|
||||
}
|
||||
|
||||
fun WebSymbolsScope.getDefaultCodeCompletions(qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsCodeCompletionQueryParams,
|
||||
scope: Stack<WebSymbolsScope>): List<WebSymbolCodeCompletionItem> =
|
||||
fun WebSymbolsScope.getDefaultCodeCompletions(
|
||||
qualifiedName: WebSymbolQualifiedName,
|
||||
params: WebSymbolsCodeCompletionQueryParams,
|
||||
scope: Stack<WebSymbolsScope>,
|
||||
): List<WebSymbolCodeCompletionItem> =
|
||||
getSymbols(qualifiedName.qualifiedKind,
|
||||
WebSymbolsListSymbolsQueryParams.create(
|
||||
params.queryExecutor,
|
||||
@@ -391,7 +400,8 @@ fun WebSymbolsScope.getDefaultCodeCompletions(qualifiedName: WebSymbolQualifiedN
|
||||
internal val List<WebSymbolsScope>.lastWebSymbol: WebSymbol?
|
||||
get() = this.lastOrNull { it is WebSymbol } as? WebSymbol
|
||||
|
||||
internal fun createModificationTracker(trackersPointers: List<Pointer<out ModificationTracker>>): ModificationTracker =
|
||||
@ApiStatus.Internal
|
||||
fun createModificationTracker(trackersPointers: List<Pointer<out ModificationTracker>>): ModificationTracker =
|
||||
ModificationTracker {
|
||||
var modCount = 0L
|
||||
for (tracker in trackersPointers) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
|
||||
import com.intellij.webSymbols.query.WebSymbolsQueryExecutor
|
||||
import com.intellij.webSymbols.webTypes.filters.WebSymbolsFilter
|
||||
|
||||
class WebSymbolsFilterEP internal constructor() : CustomLoadingExtensionPointBean<WebSymbolsFilter>() {
|
||||
class WebSymbolsFilterEP() : CustomLoadingExtensionPointBean<WebSymbolsFilter>() {
|
||||
|
||||
companion object {
|
||||
private val EP_NAME = ExtensionPointName<WebSymbolsFilterEP>("com.intellij.webSymbols.webTypes.filter")
|
||||
@@ -20,16 +20,20 @@ class WebSymbolsFilterEP internal constructor() : CustomLoadingExtensionPointBea
|
||||
?: NOOP_FILTER
|
||||
|
||||
private val NOOP_FILTER = object : WebSymbolsFilter {
|
||||
override fun filterCodeCompletions(codeCompletions: List<WebSymbolCodeCompletionItem>,
|
||||
queryExecutor: WebSymbolsQueryExecutor,
|
||||
scope: List<WebSymbolsScope>,
|
||||
properties: Map<String, Any>): List<WebSymbolCodeCompletionItem> =
|
||||
override fun filterCodeCompletions(
|
||||
codeCompletions: List<WebSymbolCodeCompletionItem>,
|
||||
queryExecutor: WebSymbolsQueryExecutor,
|
||||
scope: List<WebSymbolsScope>,
|
||||
properties: Map<String, Any>,
|
||||
): List<WebSymbolCodeCompletionItem> =
|
||||
codeCompletions
|
||||
|
||||
override fun filterNameMatches(matches: List<WebSymbol>,
|
||||
queryExecutor: WebSymbolsQueryExecutor,
|
||||
scope: List<WebSymbolsScope>,
|
||||
properties: Map<String, Any>): List<WebSymbol> =
|
||||
override fun filterNameMatches(
|
||||
matches: List<WebSymbol>,
|
||||
queryExecutor: WebSymbolsQueryExecutor,
|
||||
scope: List<WebSymbolsScope>,
|
||||
properties: Map<String, Any>,
|
||||
): List<WebSymbol> =
|
||||
matches
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.webSymbols
|
||||
|
||||
import com.intellij.psi.PsiElement
|
||||
@@ -46,8 +46,10 @@ open class DebugOutputPrinter {
|
||||
protected open fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any) =
|
||||
builder.append("<recursive value of class ${value.javaClass.simpleName}>")
|
||||
|
||||
protected open fun StringBuilder.printMap(level: Int,
|
||||
map: Map<*, *>): StringBuilder =
|
||||
protected open fun StringBuilder.printMap(
|
||||
level: Int,
|
||||
map: Map<*, *>,
|
||||
): StringBuilder =
|
||||
printObject(level) {
|
||||
for (entry in map) {
|
||||
printProperty(it, entry.key.toString(), entry.value)
|
||||
@@ -69,8 +71,10 @@ open class DebugOutputPrinter {
|
||||
return this
|
||||
}
|
||||
|
||||
protected open fun StringBuilder.printObject(level: Int,
|
||||
printer: (level: Int) -> Unit): StringBuilder {
|
||||
protected open fun StringBuilder.printObject(
|
||||
level: Int,
|
||||
printer: (level: Int) -> Unit,
|
||||
): StringBuilder {
|
||||
append("{\n")
|
||||
printer(level + 1)
|
||||
indent(level).append("}")
|
||||
@@ -0,0 +1,663 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:JvmName("WebTestUtil")
|
||||
|
||||
package com.intellij.webSymbols
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightSettings
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2
|
||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2.Companion.testGTDUOutcome
|
||||
import com.intellij.find.usages.api.SearchTarget
|
||||
import com.intellij.find.usages.api.UsageOptions
|
||||
import com.intellij.find.usages.impl.AllSearchOptions
|
||||
import com.intellij.find.usages.impl.buildUsageViewQuery
|
||||
import com.intellij.injected.editor.DocumentWindow
|
||||
import com.intellij.injected.editor.EditorWindow
|
||||
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
|
||||
import com.intellij.lang.injection.InjectedLanguageManager
|
||||
import com.intellij.model.psi.PsiSymbolReference
|
||||
import com.intellij.model.psi.impl.referencesAt
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
|
||||
import com.intellij.platform.testFramework.core.FileComparisonFailedError
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiPolyVariantReference
|
||||
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.psi.search.SearchScope
|
||||
import com.intellij.psi.util.elementsAtOffsetUp
|
||||
import com.intellij.refactoring.rename.api.RenameTarget
|
||||
import com.intellij.refactoring.rename.symbol.SymbolRenameTargetFactory
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import com.intellij.testFramework.TestDataFile
|
||||
import com.intellij.testFramework.UsefulTestCase
|
||||
import com.intellij.testFramework.UsefulTestCase.assertEmpty
|
||||
import com.intellij.testFramework.assertInstanceOf
|
||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||
import com.intellij.testFramework.fixtures.TestLookupElementPresentation
|
||||
import com.intellij.usages.Usage
|
||||
import com.intellij.util.ObjectUtils.coalesce
|
||||
import com.intellij.util.concurrency.AppExecutorUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.webSymbols.declarations.WebSymbolDeclaration
|
||||
import com.intellij.webSymbols.declarations.WebSymbolDeclarationProvider
|
||||
import com.intellij.webSymbols.impl.canUnwrapSymbols
|
||||
import com.intellij.webSymbols.query.WebSymbolMatch
|
||||
import com.intellij.webSymbols.query.WebSymbolsQueryExecutorFactory
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Assert
|
||||
import java.io.File
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
fun UsefulTestCase.enableAstLoadingFilter() {
|
||||
Registry.get("ast.loading.filter").setValue(true, testRootDisposable)
|
||||
}
|
||||
|
||||
fun UsefulTestCase.enableIdempotenceChecksOnEveryCache() {
|
||||
Registry.get("platform.random.idempotence.check.rate").setValue(1, testRootDisposable)
|
||||
}
|
||||
|
||||
fun <T> noAutoComplete(code: () -> T): T {
|
||||
val old = CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION
|
||||
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = false
|
||||
try {
|
||||
return code()
|
||||
}
|
||||
finally {
|
||||
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = old
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkNoDocumentationAtCaret() {
|
||||
assertNull(renderDocAtCaret())
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkDocumentationAtCaret() {
|
||||
checkDocumentation(renderDocAtCaret())
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkLookupItems(
|
||||
renderPriority: Boolean = false,
|
||||
renderTypeText: Boolean = false,
|
||||
renderTailText: Boolean = false,
|
||||
renderProximity: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
renderDisplayEffects: Boolean = renderPriority,
|
||||
checkDocumentation: Boolean = false,
|
||||
containsCheck: Boolean = false,
|
||||
locations: List<String> = emptyList(),
|
||||
fileName: String = InjectedLanguageManager.getInstance(project).getTopLevelFile(file).virtualFile.nameWithoutExtension,
|
||||
expectedDataLocation: String = "",
|
||||
lookupItemFilter: (item: LookupElementInfo) -> Boolean = { true },
|
||||
) {
|
||||
val hasDir = expectedDataLocation.isNotEmpty()
|
||||
|
||||
fun checkLookupDocumentation(fileSuffix: String = "") {
|
||||
if (!checkDocumentation) return
|
||||
val lookupsToCheck = renderLookupItems(false, false, lookupFilter = lookupItemFilter).filter { it.isNotBlank() }
|
||||
val lookupElements = lookupElements!!.asSequence().associateBy { it.lookupString }
|
||||
for (lookupString in lookupsToCheck) {
|
||||
val lookupElement = lookupElements[lookupString]
|
||||
assertNotNull("Missing lookup string: $lookupString", lookupElement)
|
||||
val doc = IdeDocumentationTargetProvider.getInstance(project)
|
||||
.documentationTargets(editor, file, lookupElement!!)
|
||||
?.firstOrNull()
|
||||
?.let { computeDocumentationBlocking(it.createPointer()) }
|
||||
?.html
|
||||
?.trim()
|
||||
|
||||
val sanitizedLookupString = lookupString.replace(Regex("[*\"?<>/\\[\\]:;|,#]"), "_")
|
||||
checkDocumentation(doc ?: "<no documentation>", "$fileSuffix#$sanitizedLookupString", expectedDataLocation)
|
||||
}
|
||||
}
|
||||
|
||||
noAutoComplete {
|
||||
if (locations.isEmpty()) {
|
||||
completeBasic()
|
||||
checkListByFile(
|
||||
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
|
||||
lookupItemFilter),
|
||||
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".txt",
|
||||
containsCheck
|
||||
)
|
||||
checkLookupDocumentation()
|
||||
}
|
||||
else {
|
||||
locations.forEachIndexed { index, location ->
|
||||
moveToOffsetBySignature(location)
|
||||
completeBasic()
|
||||
try {
|
||||
checkListByFile(
|
||||
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
|
||||
lookupItemFilter),
|
||||
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".${index + 1}.txt",
|
||||
containsCheck
|
||||
)
|
||||
}
|
||||
catch (e: FileComparisonFailedError) {
|
||||
throw FileComparisonFailedError(e.message + "\nFor location: $location",
|
||||
e.expectedStringPresentation, e.actualStringPresentation,
|
||||
e.filePath, e.actualFilePath)
|
||||
}
|
||||
checkLookupDocumentation(".${index + 1}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LookupElementInfo(
|
||||
val lookupElement: LookupElement,
|
||||
val lookupString: String,
|
||||
val displayText: String?,
|
||||
val tailText: String?,
|
||||
val typeText: String?,
|
||||
val priority: Double,
|
||||
val proximity: Int?,
|
||||
val isStrikeout: Boolean,
|
||||
val isItemTextBold: Boolean,
|
||||
val isItemTextItalic: Boolean,
|
||||
val isItemTextUnderline: Boolean,
|
||||
val isTypeGreyed: Boolean,
|
||||
) {
|
||||
fun render(
|
||||
renderPriority: Boolean,
|
||||
renderTypeText: Boolean,
|
||||
renderTailText: Boolean,
|
||||
renderProximity: Boolean,
|
||||
renderDisplayText: Boolean,
|
||||
renderDisplayEffects: Boolean,
|
||||
): String {
|
||||
val result = StringBuilder()
|
||||
result.append(lookupString)
|
||||
if (renderPriority || renderTypeText || renderTailText || renderProximity || renderDisplayText || renderDisplayEffects) {
|
||||
result.append(" (")
|
||||
|
||||
fun renderIf(switch: Boolean, name: String, value: String?) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append("=").append(value?.let { "'$it'" } ?: "null").append(";")
|
||||
}
|
||||
}
|
||||
|
||||
fun renderIf(switch: Boolean, name: String, value: Number?) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append("=").append(value ?: "null").append(";")
|
||||
}
|
||||
}
|
||||
|
||||
fun renderIf(switch: Boolean, name: String) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append(";")
|
||||
}
|
||||
}
|
||||
|
||||
renderIf(renderDisplayText, "displayText", displayText)
|
||||
renderIf(renderTailText, "tailText", tailText)
|
||||
renderIf(renderTypeText, "typeText", typeText)
|
||||
renderIf(renderPriority, "priority", priority)
|
||||
renderIf(renderProximity, "proximity", proximity)
|
||||
|
||||
if (renderDisplayEffects) {
|
||||
renderIf(isStrikeout, "strikeout")
|
||||
renderIf(isItemTextBold, "bold")
|
||||
renderIf(isItemTextItalic, "italic")
|
||||
renderIf(isItemTextUnderline, "underline")
|
||||
renderIf(renderTypeText && isTypeGreyed, "typeGreyed")
|
||||
}
|
||||
|
||||
if (result.last() == ';') {
|
||||
result.setLength(result.length - 1)
|
||||
}
|
||||
result.append(")")
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CodeInsightTestFixture.checkDocumentation(
|
||||
actualDocumentation: String?,
|
||||
fileSuffix: String = ".expected",
|
||||
directory: String = "",
|
||||
) {
|
||||
assertNotNull("No documentation rendered", actualDocumentation)
|
||||
val expectedFile = InjectedLanguageManager.getInstance(project).getTopLevelFile(file)
|
||||
.virtualFile.nameWithoutExtension + fileSuffix + ".html"
|
||||
val path = "$testDataPath/$directory/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
Logger.getInstance("DocumentationTest").warn("File $file not found - created!")
|
||||
}
|
||||
val expectedDocumentation = FileUtil.loadFile(file, "UTF-8", true).trim()
|
||||
if (expectedDocumentation != actualDocumentation) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedDocumentation, actualDocumentation!!, path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CodeInsightTestFixture.renderDocAtCaret(): String? =
|
||||
IdeDocumentationTargetProvider.getInstance(project)
|
||||
.documentationTargets(editor, file, caretOffset)
|
||||
.mapNotNull { computeDocumentationBlocking(it.createPointer())?.html }
|
||||
.also { assertTrue("More then one documentation rendered:\n\n${it.joinToString("\n\n")}", it.size <= 1) }
|
||||
.getOrNull(0)
|
||||
?.trim()
|
||||
?.replace(Regex("<a href=\"psi_element:[^\"]*/unitTest[0-9]+/"), "<a href=\"psi_element:///src/")
|
||||
|
||||
|
||||
infix fun ((item: LookupElementInfo) -> Boolean).and(other: (item: LookupElementInfo) -> Boolean): (item: LookupElementInfo) -> Boolean =
|
||||
{
|
||||
this(it) && other(it)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun CodeInsightTestFixture.renderLookupItems(
|
||||
renderPriority: Boolean,
|
||||
renderTypeText: Boolean,
|
||||
renderTailText: Boolean = false,
|
||||
renderProximity: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
renderDisplayEffects: Boolean = renderPriority,
|
||||
lookupFilter: (item: LookupElementInfo) -> Boolean = { true },
|
||||
): List<String> =
|
||||
lookupElements?.asSequence()
|
||||
?.map {
|
||||
val presentation = TestLookupElementPresentation.renderReal(it)
|
||||
LookupElementInfo(it, it.lookupString, presentation.itemText, presentation.tailText,
|
||||
presentation.typeText, (it as? PrioritizedLookupElement<*>)?.priority ?: 0.0,
|
||||
(it as? PrioritizedLookupElement<*>)?.explicitProximity,
|
||||
presentation.isStrikeout, presentation.isItemTextBold,
|
||||
presentation.isItemTextItalic, presentation.isItemTextUnderlined,
|
||||
presentation.isTypeGrayed)
|
||||
}
|
||||
?.filter(lookupFilter)
|
||||
?.sortedWith(
|
||||
Comparator.comparing { it: LookupElementInfo -> -it.priority }
|
||||
.thenComparingInt { -(it.proximity ?: 0) }
|
||||
.thenComparing { it: LookupElementInfo -> it.lookupString })
|
||||
?.map {
|
||||
it.render(
|
||||
renderPriority = renderPriority,
|
||||
renderTypeText = renderTypeText,
|
||||
renderTailText = renderTailText,
|
||||
renderProximity = renderProximity,
|
||||
renderDisplayText = renderDisplayText,
|
||||
renderDisplayEffects = renderDisplayEffects,
|
||||
)
|
||||
}
|
||||
?.toList()
|
||||
?: emptyList()
|
||||
|
||||
fun CodeInsightTestFixture.moveToOffsetBySignature(signature: String) {
|
||||
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
||||
val offset = file.findOffsetBySignature(signature)
|
||||
editor.caretModel.moveToOffset(offset)
|
||||
}
|
||||
|
||||
fun PsiFile.findOffsetBySignature(signature: String): Int {
|
||||
var str = signature
|
||||
val caretSignature = "<caret>"
|
||||
val caretOffset = str.indexOf(caretSignature)
|
||||
assert(caretOffset >= 0) { "Caret offset is required" }
|
||||
str = str.substring(0, caretOffset) + str.substring(caretOffset + caretSignature.length)
|
||||
val pos = text.indexOf(str)
|
||||
assertTrue("Failed to locate '$str' in: \n $text", pos >= 0)
|
||||
return pos + caretOffset
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.webSymbolAtCaret(): WebSymbol? =
|
||||
injectionThenHost(file, caretOffset) { file, offset ->
|
||||
file.webSymbolDeclarationsAt(offset).takeIf { it.isNotEmpty() }
|
||||
?: if (offset > 0) file.webSymbolDeclarationsAt(offset - 1).takeIf { it.isNotEmpty() } else null
|
||||
}
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.also { if (it.size > 1) throw AssertionError("Multiple WebSymbolDeclarations found at caret position: $it") }
|
||||
?.firstOrNull()
|
||||
?.symbol
|
||||
?: injectionThenHost(file, caretOffset) { file, offset ->
|
||||
file.referencesAt(offset).filter { it.absoluteRange.contains(offset) }.takeIf { it.isNotEmpty() }
|
||||
?: if (offset > 0) file.referencesAt(offset - 1).filter { it.absoluteRange.contains(offset - 1) }.takeIf { it.isNotEmpty() } else null
|
||||
}
|
||||
?.also { if (it.size > 1) throw AssertionError("Multiple PsiSymbolReferences found at caret position: $it") }
|
||||
?.resolveToWebSymbols()
|
||||
?.also {
|
||||
if (it.size > 1) {
|
||||
throw AssertionError("More than one symbol at caret position: $it")
|
||||
}
|
||||
}
|
||||
?.getOrNull(0)
|
||||
|
||||
|
||||
fun CodeInsightTestFixture.webSymbolSourceAtCaret(): PsiElement? =
|
||||
webSymbolAtCaret()?.let { it as? PsiSourcedWebSymbol }?.source
|
||||
|
||||
fun CodeInsightTestFixture.resolveWebSymbolReference(signature: String): WebSymbol {
|
||||
val symbols = multiResolveWebSymbolReference(signature)
|
||||
if (symbols.isEmpty()) {
|
||||
throw AssertionError("Reference resolves to null at '$signature'")
|
||||
}
|
||||
if (symbols.size != 1) {
|
||||
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
|
||||
+ symbols)
|
||||
}
|
||||
return symbols[0]
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.multiResolveWebSymbolReference(signature: String): List<WebSymbol> {
|
||||
val signatureOffset = file.findOffsetBySignature(signature)
|
||||
return injectionThenHost(file, signatureOffset) { file, offset ->
|
||||
file.referencesAt(offset)
|
||||
.let { refs ->
|
||||
if (refs.size > 1) {
|
||||
val filtered = refs.filter { it.absoluteRange.contains(signatureOffset) }
|
||||
if (filtered.size == 1)
|
||||
filtered
|
||||
else throw AssertionError("Multiple PsiSymbolReferences found at $signature: $refs")
|
||||
}
|
||||
else refs
|
||||
}
|
||||
.resolveToWebSymbols()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun Collection<PsiSymbolReference>.resolveToWebSymbols(): List<WebSymbol> =
|
||||
asSequence()
|
||||
.flatMap { it.resolveReference() }
|
||||
.filterIsInstance<WebSymbol>()
|
||||
.flatMap {
|
||||
if (it is WebSymbolMatch
|
||||
&& it.nameSegments.size == 1
|
||||
&& it.nameSegments[0].canUnwrapSymbols()) {
|
||||
it.nameSegments[0].symbols
|
||||
}
|
||||
else listOf(it)
|
||||
}
|
||||
.toList()
|
||||
|
||||
private fun PsiFile.webSymbolDeclarationsAt(offset: Int): Collection<WebSymbolDeclaration> {
|
||||
for ((element, offsetInElement) in elementsAtOffsetUp(offset)) {
|
||||
val declarations = WebSymbolDeclarationProvider.getAllDeclarations(element, offsetInElement)
|
||||
if (declarations.isNotEmpty()) {
|
||||
return declarations
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun <T> injectionThenHost(file: PsiFile, offset: Int, computation: (PsiFile, Int) -> T?): T? {
|
||||
val injectedFile = InjectedLanguageManager.getInstance(file.project).findInjectedElementAt(file, offset)?.containingFile
|
||||
if (injectedFile != null) {
|
||||
PsiDocumentManager.getInstance(file.project).getDocument(injectedFile)
|
||||
?.let { it as? DocumentWindow }
|
||||
?.hostToInjected(offset)
|
||||
?.takeIf { it >= 0 }
|
||||
?.let { computation(injectedFile, it) }
|
||||
?.let { return it }
|
||||
}
|
||||
return computation(file, offset)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.resolveToWebSymbolSource(signature: String): PsiElement {
|
||||
val webSymbol = resolveWebSymbolReference(signature)
|
||||
val result = assertInstanceOf<PsiSourcedWebSymbol>(webSymbol).source
|
||||
assertNotNull("WebSymbol $webSymbol source is null", result)
|
||||
return result!!
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.resolveReference(signature: String): PsiElement {
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
var ref = file.findReferenceAt(offsetBySignature)
|
||||
if (ref === null) {
|
||||
//possibly an injection
|
||||
ref = InjectedLanguageManager.getInstance(project)
|
||||
.findInjectedElementAt(file, offsetBySignature)
|
||||
?.findReferenceAt(0)
|
||||
}
|
||||
|
||||
assertNotNull("No reference at '$signature'", ref)
|
||||
var resolve = ref!!.resolve()
|
||||
if (resolve == null && ref is PsiPolyVariantReference) {
|
||||
val results = ref.multiResolve(false).filter { it.isValidResult }
|
||||
if (results.size > 1) {
|
||||
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
|
||||
+ results)
|
||||
}
|
||||
else if (results.size == 1) {
|
||||
resolve = results[0].element
|
||||
}
|
||||
|
||||
}
|
||||
assertNotNull("Reference resolves to null at '$signature'", resolve)
|
||||
return resolve!!
|
||||
}
|
||||
|
||||
|
||||
fun CodeInsightTestFixture.multiResolveReference(signature: String): List<PsiElement> {
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
val ref = file.findReferenceAt(offsetBySignature)
|
||||
assertNotNull("No reference at '$signature'", ref)
|
||||
assertTrue("PsiPolyVariantReference expected", ref is PsiPolyVariantReference)
|
||||
val resolveResult = (ref as PsiPolyVariantReference).multiResolve(false)
|
||||
assertFalse("Empty reference resolution at '$signature'", resolveResult.isEmpty())
|
||||
return resolveResult.mapNotNull { it.element }
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun CodeInsightTestFixture.assertUnresolvedReference(signature: String, okWithNoRef: Boolean = false, allowSelfReference: Boolean = false) {
|
||||
assertEmpty("Reference at $signature should not resolve to WebSymbols.", multiResolveWebSymbolReference(signature))
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
val ref = file.findReferenceAt(offsetBySignature)
|
||||
if (okWithNoRef && ref == null) {
|
||||
return
|
||||
}
|
||||
assertNotNull("Expected not null reference for signature '$signature' at offset $offsetBySignature in file\n${file.text}", ref)
|
||||
val resolved = ref!!.resolve()
|
||||
if (ref.element == resolved && allowSelfReference) {
|
||||
if (ref is PsiPolyVariantReference) {
|
||||
assertEmpty(ref.multiResolve(false).filter { it.element != ref.element })
|
||||
}
|
||||
return
|
||||
}
|
||||
assertNull(
|
||||
"Expected that reference for signature '$signature' at offset $offsetBySignature resolves to null but resolved to $resolved (${resolved?.text}) in file ${resolved?.containingFile?.name}",
|
||||
resolved)
|
||||
if (ref is PsiPolyVariantReference) {
|
||||
assertEmpty(ref.multiResolve(false))
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.findUsages(target: SearchTarget): MutableCollection<out Usage> {
|
||||
val project = project
|
||||
|
||||
val searchScope = coalesce<SearchScope>(target.maximalSearchScope, GlobalSearchScope.allScope(project))
|
||||
val allOptions = AllSearchOptions(UsageOptions.createOptions(searchScope), true)
|
||||
return buildUsageViewQuery(getProject(), target, allOptions).findAll()
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@RequiresEdt
|
||||
fun CodeInsightTestFixture.checkGTDUOutcome(expectedOutcome: GotoDeclarationOrUsageHandler2.GTDUOutcome?, signature: String? = null) {
|
||||
if (signature != null) {
|
||||
moveToOffsetBySignature(signature)
|
||||
}
|
||||
val actualSignature = signature ?: editor.currentPositionSignature
|
||||
val editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file)
|
||||
val offset = editor.caretModel.offset
|
||||
|
||||
val gtduOutcome = ReadAction
|
||||
.nonBlocking(Callable {
|
||||
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: file
|
||||
testGTDUOutcome(editor, file, offset)
|
||||
})
|
||||
.submit(AppExecutorUtil.getAppExecutorService())
|
||||
.get()
|
||||
Assert.assertEquals(actualSignature,
|
||||
expectedOutcome,
|
||||
gtduOutcome)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkGotoDeclaration(fromSignature: String?, declarationSignature: String, expectedFileName: String? = null) {
|
||||
checkGTDUOutcome(GotoDeclarationOrUsageHandler2.GTDUOutcome.GTD, fromSignature)
|
||||
val actualSignature = fromSignature ?: editor.currentPositionSignature
|
||||
performEditorAction("GotoDeclaration")
|
||||
val targetEditor = FileEditorManager.getInstance(project).selectedTextEditor?.topLevelEditor
|
||||
if (targetEditor == null) throw NullPointerException(actualSignature)
|
||||
val targetFile = PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)!!
|
||||
if (expectedFileName != null) {
|
||||
assertEquals(actualSignature, expectedFileName, PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)?.name)
|
||||
}
|
||||
else {
|
||||
assertEquals(actualSignature, targetEditor, editor.topLevelEditor)
|
||||
}
|
||||
if (!declarationSignature.contains("<caret>") || targetFile.findOffsetBySignature(
|
||||
declarationSignature) != targetEditor.caretModel.offset) {
|
||||
assertEquals("For go to from: $actualSignature",
|
||||
declarationSignature + if (!declarationSignature.contains("<caret>")) ""
|
||||
else (" [" + file.findOffsetBySignature(declarationSignature) + "]"),
|
||||
targetEditor.currentPositionSignature + "[${targetEditor.caretModel.offset}]")
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkListByFile(actualList: List<String>, @TestDataFile expectedFile: String, containsCheck: Boolean) {
|
||||
val path = "$testDataPath/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists() && file.createNewFile()) {
|
||||
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
|
||||
}
|
||||
val actualContents = actualList.joinToString("\n").trim() + "\n"
|
||||
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
|
||||
if (containsCheck) {
|
||||
val expectedList = FileUtil.loadLines(file, "UTF-8").filter { it.isNotBlank() }
|
||||
val actualSet = actualList.toSet()
|
||||
if (!expectedList.all { actualSet.contains(it) }) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
|
||||
}
|
||||
}
|
||||
else if (expectedContents != actualContents) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkTextByFile(actualContents: String, @TestDataFile expectedFile: String) {
|
||||
val path = "$testDataPath/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists() && file.createNewFile()) {
|
||||
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
|
||||
}
|
||||
val actualContentsTrimmed = actualContents.trim() + "\n"
|
||||
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
|
||||
if (expectedContents != actualContentsTrimmed) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContentsTrimmed, path)
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.canRenameWebSymbolAtCaret() =
|
||||
webSymbolAtCaret().let {
|
||||
it is RenameTarget || it?.renameTarget != null || (it is PsiSourcedWebSymbol && it.source != null)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.renameWebSymbol(newName: String) {
|
||||
val symbol = webSymbolAtCaret() ?: throw AssertionError("No WebSymbol at caret")
|
||||
var target: RenameTarget? = null
|
||||
|
||||
for (factory: SymbolRenameTargetFactory in SymbolRenameTargetFactory.EP_NAME.extensions) {
|
||||
target = factory.renameTarget(project, symbol)
|
||||
if (target != null) break
|
||||
}
|
||||
if (target == null) {
|
||||
target = when (symbol) {
|
||||
is RenameTarget -> symbol
|
||||
is PsiSourcedWebSymbol -> {
|
||||
val psiTarget = symbol.source
|
||||
?: throw AssertionError("Symbol $symbol provides null source")
|
||||
renameElement(psiTarget, newName)
|
||||
return
|
||||
}
|
||||
else -> symbol.renameTarget
|
||||
?: throw AssertionError("Symbol $symbol does not provide rename target nor is a PsiSourcedWebSymbol")
|
||||
}
|
||||
}
|
||||
if (target.createPointer().dereference() == null) {
|
||||
throw AssertionError("Target $target pointer dereferences to null")
|
||||
}
|
||||
renameTarget(target, newName)
|
||||
}
|
||||
|
||||
|
||||
fun doCompletionItemsTest(
|
||||
fixture: CodeInsightTestFixture,
|
||||
fileName: String,
|
||||
goldFileWithExtension: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
) {
|
||||
val fileNameNoExt = FileUtil.getNameWithoutExtension(fileName)
|
||||
fixture.configureByFile(fileName)
|
||||
WriteAction.runAndWait<Throwable> { WebSymbolsQueryExecutorFactory.getInstance(fixture.project) }
|
||||
|
||||
val document = fixture.getDocument(fixture.file)
|
||||
|
||||
val offsets = mutableListOf<Pair<Int, Boolean>>()
|
||||
|
||||
WriteAction.runAndWait<Throwable> {
|
||||
CommandProcessor.getInstance().executeCommand(fixture.project, {
|
||||
val chars = document.charsSequence
|
||||
var pos: Int
|
||||
while (chars.indexOf('|').also { pos = it } >= 0) {
|
||||
val strict = chars.length > pos + 1 && chars[pos + 1] == '!'
|
||||
offsets.add(Pair(pos, strict))
|
||||
if (strict)
|
||||
document.deleteString(pos, pos + 2)
|
||||
else
|
||||
document.deleteString(pos, pos + 1)
|
||||
}
|
||||
}, null, null)
|
||||
PsiDocumentManager.getInstance(fixture.project).commitDocument(document)
|
||||
}
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
noAutoComplete {
|
||||
offsets.forEachIndexed { index, (offset, strict) ->
|
||||
fixture.editor.caretModel.moveToOffset(offset)
|
||||
fixture.completeBasic()
|
||||
|
||||
fixture.checkListByFile(
|
||||
fixture.renderLookupItems(true, true, true, renderDisplayText = renderDisplayText),
|
||||
"gold/${if (goldFileWithExtension) fileName else fileNameNoExt}.${index}.txt", !strict)
|
||||
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Editor.currentPositionSignature: String
|
||||
get() {
|
||||
val caretPos = caretModel.offset
|
||||
val text = document.text
|
||||
return (text.substring(max(0, caretPos - 15), caretPos) + "<caret>" +
|
||||
text.substring(caretPos, min(caretPos + 15, text.length)))
|
||||
.replace("\n", "\\n")
|
||||
}
|
||||
|
||||
|
||||
private val Editor.topLevelEditor
|
||||
get() = if (this is EditorWindow) delegate else this
|
||||
@@ -0,0 +1,134 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.webSymbols.query
|
||||
|
||||
import com.intellij.util.applyIf
|
||||
import com.intellij.webSymbols.DebugOutputPrinter
|
||||
import com.intellij.webSymbols.PsiSourcedWebSymbol
|
||||
import com.intellij.webSymbols.WebSymbol
|
||||
import com.intellij.webSymbols.WebSymbolApiStatus
|
||||
import com.intellij.webSymbols.WebSymbolNameSegment
|
||||
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
|
||||
import com.intellij.webSymbols.html.WebSymbolHtmlAttributeValue
|
||||
import com.intellij.webSymbols.utils.completeMatch
|
||||
import com.intellij.webSymbols.utils.nameSegments
|
||||
import java.util.Locale
|
||||
import java.util.Stack
|
||||
|
||||
open class WebSymbolsDebugOutputPrinter : DebugOutputPrinter() {
|
||||
|
||||
private val parents = Stack<WebSymbol>()
|
||||
|
||||
override fun printValueImpl(builder: StringBuilder, level: Int, value: Any?): StringBuilder =
|
||||
when (value) {
|
||||
is WebSymbolCodeCompletionItem -> builder.printCodeCompletionItem(level, value)
|
||||
is WebSymbol -> builder.printSymbol(level, value)
|
||||
is WebSymbolHtmlAttributeValue -> builder.printAttributeValue(level, value)
|
||||
is WebSymbolNameSegment -> builder.printSegment(level, value)
|
||||
is WebSymbolApiStatus -> builder.printApiStatus(value)
|
||||
is Set<*> -> builder.printSet(value)
|
||||
else -> super.printValueImpl(builder, level, value)
|
||||
}
|
||||
|
||||
override fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any): StringBuilder =
|
||||
if (value is WebSymbol)
|
||||
if (parents.peek() == value) builder.append("<self>") else builder.append("<recursive>")
|
||||
else
|
||||
super.printRecursiveValue(builder, level, value)
|
||||
|
||||
private fun StringBuilder.printCodeCompletionItem(topLevel: Int, item: WebSymbolCodeCompletionItem): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "name", item.name)
|
||||
printProperty(level, "priority", item.priority ?: WebSymbol.Priority.NORMAL)
|
||||
printProperty(level, "proximity", item.proximity?.takeIf { it > 0 })
|
||||
printProperty(level, "displayName", item.displayName.takeIf { it != item.name })
|
||||
printProperty(level, "offset", item.offset.takeIf { it != 0 })
|
||||
printProperty(level, "completeAfterInsert", item.completeAfterInsert.takeIf { it })
|
||||
printProperty(level, "completeAfterChars", item.completeAfterChars.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "aliases", item.aliases.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "source", item.symbol)
|
||||
}
|
||||
|
||||
private fun StringBuilder.printSet(set: Set<*>): StringBuilder {
|
||||
return append(set.toString())
|
||||
}
|
||||
|
||||
private fun StringBuilder.printSymbol(topLevel: Int, source: WebSymbol): StringBuilder {
|
||||
if (parents.contains(source)) {
|
||||
if (parents.peek() == source) append("<self>") else append("<recursive>")
|
||||
return this
|
||||
}
|
||||
printObject(topLevel) { level ->
|
||||
if (source.pattern != null) {
|
||||
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/<pattern>")
|
||||
printProperty(level, "name", source.name)
|
||||
}
|
||||
else {
|
||||
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/" + source.name)
|
||||
}
|
||||
printProperty(level, "origin", "${source.origin.library}@${source.origin.version} (${source.origin.framework ?: "<none>"})")
|
||||
printProperty(level, "source", (source as? PsiSourcedWebSymbol)?.source)
|
||||
printProperty(level, "type", source.type)
|
||||
printProperty(level, "attrValue", source.attributeValue)
|
||||
printProperty(level, "complete", source.completeMatch)
|
||||
printProperty(level, "description", source.description?.ellipsis(45))
|
||||
printProperty(level, "docUrl", source.docUrl)
|
||||
printProperty(level, "descriptionSections", source.descriptionSections.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "abstract", source.abstract.takeIf { it })
|
||||
printProperty(level, "virtual", source.virtual.takeIf { it })
|
||||
printProperty(level, "apiStatus", source.apiStatus.takeIf { it !is WebSymbolApiStatus.Stable || it.since != null })
|
||||
printProperty(level, "priority", source.priority ?: WebSymbol.Priority.NORMAL)
|
||||
printProperty(level, "proximity", source.proximity?.takeIf { it > 0 })
|
||||
printProperty(level, "has-pattern", if (source.pattern != null) true else null)
|
||||
printProperty(level, "properties", source.properties.takeIf { it.isNotEmpty() })
|
||||
parents.push(source)
|
||||
printProperty(level, "segments", source.nameSegments)
|
||||
parents.pop()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
private fun StringBuilder.printSegment(topLevel: Int,
|
||||
segment: WebSymbolNameSegment
|
||||
): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "name-part", parents.peek().let { if (it.pattern == null) segment.getName(parents.peek()) else "" })
|
||||
printProperty(level, "display-name", segment.displayName)
|
||||
printProperty(level, "apiStatus", segment.apiStatus)
|
||||
printProperty(level, "priority", segment.priority?.takeIf { it != WebSymbol.Priority.NORMAL })
|
||||
printProperty(level, "matchScore", segment.matchScore.takeIf { it != segment.end - segment.start })
|
||||
printProperty(level, "problem", segment.problem)
|
||||
val symbols = segment.symbols.filter { !it.extension }
|
||||
if (symbols.size > 1) {
|
||||
printProperty(level, "symbols", symbols)
|
||||
}
|
||||
else if (symbols.size == 1) {
|
||||
printProperty(level, "symbol", symbols[0])
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.printAttributeValue(topLevel: Int, value: WebSymbolHtmlAttributeValue): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "kind", value.kind)
|
||||
.printProperty(level, "type", value.type)
|
||||
.printProperty(level, "langType", value.langType)
|
||||
.printProperty(level, "required", value.required)
|
||||
.printProperty(level, "default", value.default)
|
||||
}
|
||||
|
||||
private fun StringBuilder.printApiStatus(apiStatus: WebSymbolApiStatus): StringBuilder =
|
||||
when (apiStatus) {
|
||||
is WebSymbolApiStatus.Deprecated -> append("deprecated")
|
||||
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Obsolete -> append("obsolete")
|
||||
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Experimental -> append("experimental")
|
||||
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Stable -> append("stable")
|
||||
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,666 +1,7 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
@file:JvmName("WebTestUtil")
|
||||
|
||||
package com.intellij.webSymbols
|
||||
|
||||
import com.intellij.codeInsight.CodeInsightSettings
|
||||
import com.intellij.codeInsight.completion.PrioritizedLookupElement
|
||||
import com.intellij.codeInsight.lookup.LookupElement
|
||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2
|
||||
import com.intellij.codeInsight.navigation.actions.GotoDeclarationOrUsageHandler2.Companion.testGTDUOutcome
|
||||
import com.intellij.find.usages.api.SearchTarget
|
||||
import com.intellij.find.usages.api.UsageOptions
|
||||
import com.intellij.find.usages.impl.AllSearchOptions
|
||||
import com.intellij.find.usages.impl.buildUsageViewQuery
|
||||
import com.intellij.injected.editor.DocumentWindow
|
||||
import com.intellij.injected.editor.EditorWindow
|
||||
import com.intellij.lang.documentation.ide.IdeDocumentationTargetProvider
|
||||
import com.intellij.lang.injection.InjectedLanguageManager
|
||||
import com.intellij.model.psi.PsiSymbolReference
|
||||
import com.intellij.model.psi.impl.referencesAt
|
||||
import com.intellij.openapi.application.ReadAction
|
||||
import com.intellij.openapi.application.WriteAction
|
||||
import com.intellij.openapi.command.CommandProcessor
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.util.io.FileUtil
|
||||
import com.intellij.openapi.util.registry.Registry
|
||||
import com.intellij.platform.backend.documentation.impl.computeDocumentationBlocking
|
||||
import com.intellij.platform.testFramework.core.FileComparisonFailedError
|
||||
import com.intellij.psi.PsiDocumentManager
|
||||
import com.intellij.psi.PsiElement
|
||||
import com.intellij.psi.PsiFile
|
||||
import com.intellij.psi.PsiPolyVariantReference
|
||||
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil
|
||||
import com.intellij.psi.search.GlobalSearchScope
|
||||
import com.intellij.psi.search.SearchScope
|
||||
import com.intellij.psi.util.elementsAtOffsetUp
|
||||
import com.intellij.refactoring.rename.api.RenameTarget
|
||||
import com.intellij.refactoring.rename.symbol.SymbolRenameTargetFactory
|
||||
import com.intellij.testFramework.PlatformTestUtil
|
||||
import com.intellij.testFramework.TestDataFile
|
||||
import com.intellij.testFramework.UsefulTestCase
|
||||
import com.intellij.testFramework.UsefulTestCase.assertEmpty
|
||||
import com.intellij.testFramework.assertInstanceOf
|
||||
import com.intellij.testFramework.fixtures.CodeInsightTestFixture
|
||||
import com.intellij.testFramework.fixtures.TestLookupElementPresentation
|
||||
import com.intellij.usages.Usage
|
||||
import com.intellij.util.ObjectUtils.coalesce
|
||||
import com.intellij.util.concurrency.AppExecutorUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.webSymbols.declarations.WebSymbolDeclaration
|
||||
import com.intellij.webSymbols.declarations.WebSymbolDeclarationProvider
|
||||
import com.intellij.webSymbols.impl.canUnwrapSymbols
|
||||
import com.intellij.webSymbols.query.WebSymbolMatch
|
||||
import com.intellij.webSymbols.query.WebSymbolsQueryExecutorFactory
|
||||
import junit.framework.TestCase.*
|
||||
import org.junit.Assert
|
||||
import org.opentest4j.AssertionFailedError
|
||||
import java.io.File
|
||||
import java.util.concurrent.Callable
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
internal val webSymbolsTestsDataPath: String
|
||||
get() = "${PlatformTestUtil.getCommunityPath()}/platform/webSymbols/testData/"
|
||||
|
||||
fun UsefulTestCase.enableAstLoadingFilter() {
|
||||
Registry.get("ast.loading.filter").setValue(true, testRootDisposable)
|
||||
}
|
||||
|
||||
fun UsefulTestCase.enableIdempotenceChecksOnEveryCache() {
|
||||
Registry.get("platform.random.idempotence.check.rate").setValue(1, testRootDisposable)
|
||||
}
|
||||
|
||||
fun <T> noAutoComplete(code: () -> T): T {
|
||||
val old = CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION
|
||||
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = false
|
||||
try {
|
||||
return code()
|
||||
}
|
||||
finally {
|
||||
CodeInsightSettings.getInstance().AUTOCOMPLETE_ON_CODE_COMPLETION = old
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkNoDocumentationAtCaret() {
|
||||
assertNull(renderDocAtCaret())
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkDocumentationAtCaret() {
|
||||
checkDocumentation(renderDocAtCaret())
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkLookupItems(
|
||||
renderPriority: Boolean = false,
|
||||
renderTypeText: Boolean = false,
|
||||
renderTailText: Boolean = false,
|
||||
renderProximity: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
renderDisplayEffects: Boolean = renderPriority,
|
||||
checkDocumentation: Boolean = false,
|
||||
containsCheck: Boolean = false,
|
||||
locations: List<String> = emptyList(),
|
||||
fileName: String = InjectedLanguageManager.getInstance(project).getTopLevelFile(file).virtualFile.nameWithoutExtension,
|
||||
expectedDataLocation: String = "",
|
||||
lookupItemFilter: (item: LookupElementInfo) -> Boolean = { true },
|
||||
) {
|
||||
val hasDir = expectedDataLocation.isNotEmpty()
|
||||
|
||||
fun checkLookupDocumentation(fileSuffix: String = "") {
|
||||
if (!checkDocumentation) return
|
||||
val lookupsToCheck = renderLookupItems(false, false, lookupFilter = lookupItemFilter).filter { it.isNotBlank() }
|
||||
val lookupElements = lookupElements!!.asSequence().associateBy { it.lookupString }
|
||||
for (lookupString in lookupsToCheck) {
|
||||
val lookupElement = lookupElements[lookupString]
|
||||
assertNotNull("Missing lookup string: $lookupString", lookupElement)
|
||||
val doc = IdeDocumentationTargetProvider.getInstance(project)
|
||||
.documentationTargets(editor, file, lookupElement!!)
|
||||
?.firstOrNull()
|
||||
?.let { computeDocumentationBlocking(it.createPointer()) }
|
||||
?.html
|
||||
?.trim()
|
||||
|
||||
val sanitizedLookupString = lookupString.replace(Regex("[*\"?<>/\\[\\]:;|,#]"), "_")
|
||||
checkDocumentation(doc ?: "<no documentation>", "$fileSuffix#$sanitizedLookupString", expectedDataLocation)
|
||||
}
|
||||
}
|
||||
|
||||
noAutoComplete {
|
||||
if (locations.isEmpty()) {
|
||||
completeBasic()
|
||||
checkListByFile(
|
||||
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
|
||||
lookupItemFilter),
|
||||
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".txt",
|
||||
containsCheck
|
||||
)
|
||||
checkLookupDocumentation()
|
||||
}
|
||||
else {
|
||||
locations.forEachIndexed { index, location ->
|
||||
moveToOffsetBySignature(location)
|
||||
completeBasic()
|
||||
try {
|
||||
checkListByFile(
|
||||
renderLookupItems(renderPriority, renderTypeText, renderTailText, renderProximity, renderDisplayText, renderDisplayEffects,
|
||||
lookupItemFilter),
|
||||
expectedDataLocation + (if (hasDir) "/items" else "$fileName.items") + ".${index + 1}.txt",
|
||||
containsCheck
|
||||
)
|
||||
} catch (e: FileComparisonFailedError) {
|
||||
throw FileComparisonFailedError(e.message + "\nFor location: $location",
|
||||
e.expectedStringPresentation, e.actualStringPresentation,
|
||||
e.filePath, e.actualFilePath)
|
||||
}
|
||||
checkLookupDocumentation(".${index + 1}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class LookupElementInfo(
|
||||
val lookupElement: LookupElement,
|
||||
val lookupString: String,
|
||||
val displayText: String?,
|
||||
val tailText: String?,
|
||||
val typeText: String?,
|
||||
val priority: Double,
|
||||
val proximity: Int?,
|
||||
val isStrikeout: Boolean,
|
||||
val isItemTextBold: Boolean,
|
||||
val isItemTextItalic: Boolean,
|
||||
val isItemTextUnderline: Boolean,
|
||||
val isTypeGreyed: Boolean,
|
||||
) {
|
||||
fun render(
|
||||
renderPriority: Boolean,
|
||||
renderTypeText: Boolean,
|
||||
renderTailText: Boolean,
|
||||
renderProximity: Boolean,
|
||||
renderDisplayText: Boolean,
|
||||
renderDisplayEffects: Boolean,
|
||||
): String {
|
||||
val result = StringBuilder()
|
||||
result.append(lookupString)
|
||||
if (renderPriority || renderTypeText || renderTailText || renderProximity || renderDisplayText || renderDisplayEffects) {
|
||||
result.append(" (")
|
||||
|
||||
fun renderIf(switch: Boolean, name: String, value: String?) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append("=").append(value?.let { "'$it'" } ?: "null").append(";")
|
||||
}
|
||||
}
|
||||
|
||||
fun renderIf(switch: Boolean, name: String, value: Number?) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append("=").append(value ?: "null").append(";")
|
||||
}
|
||||
}
|
||||
|
||||
fun renderIf(switch: Boolean, name: String) {
|
||||
if (switch) {
|
||||
if (result.last() == ';') {
|
||||
result.append(" ")
|
||||
}
|
||||
result.append(name).append(";")
|
||||
}
|
||||
}
|
||||
|
||||
renderIf(renderDisplayText, "displayText", displayText)
|
||||
renderIf(renderTailText, "tailText", tailText)
|
||||
renderIf(renderTypeText, "typeText", typeText)
|
||||
renderIf(renderPriority, "priority", priority)
|
||||
renderIf(renderProximity, "proximity", proximity)
|
||||
|
||||
if (renderDisplayEffects) {
|
||||
renderIf(isStrikeout, "strikeout")
|
||||
renderIf(isItemTextBold, "bold")
|
||||
renderIf(isItemTextItalic, "italic")
|
||||
renderIf(isItemTextUnderline, "underline")
|
||||
renderIf(renderTypeText && isTypeGreyed, "typeGreyed")
|
||||
}
|
||||
|
||||
if (result.last() == ';') {
|
||||
result.setLength(result.length - 1)
|
||||
}
|
||||
result.append(")")
|
||||
}
|
||||
|
||||
return result.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun CodeInsightTestFixture.checkDocumentation(
|
||||
actualDocumentation: String?,
|
||||
fileSuffix: String = ".expected",
|
||||
directory: String = "",
|
||||
) {
|
||||
assertNotNull("No documentation rendered", actualDocumentation)
|
||||
val expectedFile = InjectedLanguageManager.getInstance(project).getTopLevelFile(file)
|
||||
.virtualFile.nameWithoutExtension + fileSuffix + ".html"
|
||||
val path = "$testDataPath/$directory/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
Logger.getInstance("DocumentationTest").warn("File $file not found - created!")
|
||||
}
|
||||
val expectedDocumentation = FileUtil.loadFile(file, "UTF-8", true).trim()
|
||||
if (expectedDocumentation != actualDocumentation) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedDocumentation, actualDocumentation!!, path)
|
||||
}
|
||||
}
|
||||
|
||||
private fun CodeInsightTestFixture.renderDocAtCaret(): String? =
|
||||
IdeDocumentationTargetProvider.getInstance(project)
|
||||
.documentationTargets(editor, file, caretOffset)
|
||||
.mapNotNull { computeDocumentationBlocking(it.createPointer())?.html }
|
||||
.also { assertTrue("More then one documentation rendered:\n\n${it.joinToString("\n\n")}", it.size <= 1) }
|
||||
.getOrNull(0)
|
||||
?.trim()
|
||||
?.replace(Regex("<a href=\"psi_element:[^\"]*/unitTest[0-9]+/"), "<a href=\"psi_element:///src/")
|
||||
|
||||
|
||||
infix fun ((item: LookupElementInfo) -> Boolean).and(other: (item: LookupElementInfo) -> Boolean): (item: LookupElementInfo) -> Boolean =
|
||||
{
|
||||
this(it) && other(it)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun CodeInsightTestFixture.renderLookupItems(
|
||||
renderPriority: Boolean,
|
||||
renderTypeText: Boolean,
|
||||
renderTailText: Boolean = false,
|
||||
renderProximity: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
renderDisplayEffects: Boolean = renderPriority,
|
||||
lookupFilter: (item: LookupElementInfo) -> Boolean = { true },
|
||||
): List<String> =
|
||||
lookupElements?.asSequence()
|
||||
?.map {
|
||||
val presentation = TestLookupElementPresentation.renderReal(it)
|
||||
LookupElementInfo(it, it.lookupString, presentation.itemText, presentation.tailText,
|
||||
presentation.typeText, (it as? PrioritizedLookupElement<*>)?.priority ?: 0.0,
|
||||
(it as? PrioritizedLookupElement<*>)?.explicitProximity,
|
||||
presentation.isStrikeout, presentation.isItemTextBold,
|
||||
presentation.isItemTextItalic, presentation.isItemTextUnderlined,
|
||||
presentation.isTypeGrayed)
|
||||
}
|
||||
?.filter(lookupFilter)
|
||||
?.sortedWith(
|
||||
Comparator.comparing { it: LookupElementInfo -> -it.priority }
|
||||
.thenComparingInt { -(it.proximity ?: 0) }
|
||||
.thenComparing { it: LookupElementInfo -> it.lookupString })
|
||||
?.map {
|
||||
it.render(
|
||||
renderPriority = renderPriority,
|
||||
renderTypeText = renderTypeText,
|
||||
renderTailText = renderTailText,
|
||||
renderProximity = renderProximity,
|
||||
renderDisplayText = renderDisplayText,
|
||||
renderDisplayEffects = renderDisplayEffects,
|
||||
)
|
||||
}
|
||||
?.toList()
|
||||
?: emptyList()
|
||||
|
||||
fun CodeInsightTestFixture.moveToOffsetBySignature(signature: String) {
|
||||
PsiDocumentManager.getInstance(project).commitAllDocuments()
|
||||
val offset = file.findOffsetBySignature(signature)
|
||||
editor.caretModel.moveToOffset(offset)
|
||||
}
|
||||
|
||||
fun PsiFile.findOffsetBySignature(signature: String): Int {
|
||||
var str = signature
|
||||
val caretSignature = "<caret>"
|
||||
val caretOffset = str.indexOf(caretSignature)
|
||||
assert(caretOffset >= 0) { "Caret offset is required" }
|
||||
str = str.substring(0, caretOffset) + str.substring(caretOffset + caretSignature.length)
|
||||
val pos = text.indexOf(str)
|
||||
assertTrue("Failed to locate '$str' in: \n $text", pos >= 0)
|
||||
return pos + caretOffset
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.webSymbolAtCaret(): WebSymbol? =
|
||||
injectionThenHost(file, caretOffset) { file, offset ->
|
||||
file.webSymbolDeclarationsAt(offset).takeIf { it.isNotEmpty() }
|
||||
?: if (offset > 0) file.webSymbolDeclarationsAt(offset - 1).takeIf { it.isNotEmpty() } else null
|
||||
}
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
?.also { if (it.size > 1) throw AssertionError("Multiple WebSymbolDeclarations found at caret position: $it") }
|
||||
?.firstOrNull()
|
||||
?.symbol
|
||||
?: injectionThenHost(file, caretOffset) { file, offset ->
|
||||
file.referencesAt(offset).filter { it.absoluteRange.contains(offset) }.takeIf { it.isNotEmpty() }
|
||||
?: if (offset > 0) file.referencesAt(offset - 1).filter { it.absoluteRange.contains(offset - 1) }.takeIf { it.isNotEmpty() } else null
|
||||
}
|
||||
?.also { if (it.size > 1) throw AssertionError("Multiple PsiSymbolReferences found at caret position: $it") }
|
||||
?.resolveToWebSymbols()
|
||||
?.also {
|
||||
if (it.size > 1) {
|
||||
throw AssertionError("More than one symbol at caret position: $it")
|
||||
}
|
||||
}
|
||||
?.getOrNull(0)
|
||||
|
||||
|
||||
fun CodeInsightTestFixture.webSymbolSourceAtCaret(): PsiElement? =
|
||||
webSymbolAtCaret()?.let { it as? PsiSourcedWebSymbol }?.source
|
||||
|
||||
fun CodeInsightTestFixture.resolveWebSymbolReference(signature: String): WebSymbol {
|
||||
val symbols = multiResolveWebSymbolReference(signature)
|
||||
if (symbols.isEmpty()) {
|
||||
throw AssertionError("Reference resolves to null at '$signature'")
|
||||
}
|
||||
if (symbols.size != 1) {
|
||||
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
|
||||
+ symbols)
|
||||
}
|
||||
return symbols[0]
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.multiResolveWebSymbolReference(signature: String): List<WebSymbol> {
|
||||
val signatureOffset = file.findOffsetBySignature(signature)
|
||||
return injectionThenHost(file, signatureOffset) { file, offset ->
|
||||
file.referencesAt(offset)
|
||||
.let { refs ->
|
||||
if (refs.size > 1) {
|
||||
val filtered = refs.filter { it.absoluteRange.contains(signatureOffset) }
|
||||
if (filtered.size == 1)
|
||||
filtered
|
||||
else throw AssertionError("Multiple PsiSymbolReferences found at $signature: $refs")
|
||||
}
|
||||
else refs
|
||||
}
|
||||
.resolveToWebSymbols()
|
||||
.takeIf { it.isNotEmpty() }
|
||||
} ?: emptyList()
|
||||
}
|
||||
|
||||
private fun Collection<PsiSymbolReference>.resolveToWebSymbols(): List<WebSymbol> =
|
||||
asSequence()
|
||||
.flatMap { it.resolveReference() }
|
||||
.filterIsInstance<WebSymbol>()
|
||||
.flatMap {
|
||||
if (it is WebSymbolMatch
|
||||
&& it.nameSegments.size == 1
|
||||
&& it.nameSegments[0].canUnwrapSymbols()) {
|
||||
it.nameSegments[0].symbols
|
||||
}
|
||||
else listOf(it)
|
||||
}
|
||||
.toList()
|
||||
|
||||
private fun PsiFile.webSymbolDeclarationsAt(offset: Int): Collection<WebSymbolDeclaration> {
|
||||
for ((element, offsetInElement) in elementsAtOffsetUp(offset)) {
|
||||
val declarations = WebSymbolDeclarationProvider.getAllDeclarations(element, offsetInElement)
|
||||
if (declarations.isNotEmpty()) {
|
||||
return declarations
|
||||
}
|
||||
}
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun <T> injectionThenHost(file: PsiFile, offset: Int, computation: (PsiFile, Int) -> T?): T? {
|
||||
val injectedFile = InjectedLanguageManager.getInstance(file.project).findInjectedElementAt(file, offset)?.containingFile
|
||||
if (injectedFile != null) {
|
||||
PsiDocumentManager.getInstance(file.project).getDocument(injectedFile)
|
||||
?.let { it as? DocumentWindow }
|
||||
?.hostToInjected(offset)
|
||||
?.takeIf { it >= 0 }
|
||||
?.let { computation(injectedFile, it) }
|
||||
?.let { return it }
|
||||
}
|
||||
return computation(file, offset)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.resolveToWebSymbolSource(signature: String): PsiElement {
|
||||
val webSymbol = resolveWebSymbolReference(signature)
|
||||
val result = assertInstanceOf<PsiSourcedWebSymbol>(webSymbol).source
|
||||
assertNotNull("WebSymbol $webSymbol source is null", result)
|
||||
return result!!
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.resolveReference(signature: String): PsiElement {
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
var ref = file.findReferenceAt(offsetBySignature)
|
||||
if (ref === null) {
|
||||
//possibly an injection
|
||||
ref = InjectedLanguageManager.getInstance(project)
|
||||
.findInjectedElementAt(file, offsetBySignature)
|
||||
?.findReferenceAt(0)
|
||||
}
|
||||
|
||||
assertNotNull("No reference at '$signature'", ref)
|
||||
var resolve = ref!!.resolve()
|
||||
if (resolve == null && ref is PsiPolyVariantReference) {
|
||||
val results = ref.multiResolve(false).filter { it.isValidResult }
|
||||
if (results.size > 1) {
|
||||
throw AssertionError("Reference resolves to more than one element at '" + signature + "': "
|
||||
+ results)
|
||||
}
|
||||
else if (results.size == 1) {
|
||||
resolve = results[0].element
|
||||
}
|
||||
|
||||
}
|
||||
assertNotNull("Reference resolves to null at '$signature'", resolve)
|
||||
return resolve!!
|
||||
}
|
||||
|
||||
|
||||
fun CodeInsightTestFixture.multiResolveReference(signature: String): List<PsiElement> {
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
val ref = file.findReferenceAt(offsetBySignature)
|
||||
assertNotNull("No reference at '$signature'", ref)
|
||||
assertTrue("PsiPolyVariantReference expected", ref is PsiPolyVariantReference)
|
||||
val resolveResult = (ref as PsiPolyVariantReference).multiResolve(false)
|
||||
assertFalse("Empty reference resolution at '$signature'", resolveResult.isEmpty())
|
||||
return resolveResult.mapNotNull { it.element }
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun CodeInsightTestFixture.assertUnresolvedReference(signature: String, okWithNoRef: Boolean = false, allowSelfReference: Boolean = false) {
|
||||
assertEmpty("Reference at $signature should not resolve to WebSymbols.", multiResolveWebSymbolReference(signature))
|
||||
val offsetBySignature = file.findOffsetBySignature(signature)
|
||||
val ref = file.findReferenceAt(offsetBySignature)
|
||||
if (okWithNoRef && ref == null) {
|
||||
return
|
||||
}
|
||||
assertNotNull("Expected not null reference for signature '$signature' at offset $offsetBySignature in file\n${file.text}", ref)
|
||||
val resolved = ref!!.resolve()
|
||||
if (ref.element == resolved && allowSelfReference) {
|
||||
if (ref is PsiPolyVariantReference) {
|
||||
assertEmpty(ref.multiResolve(false).filter { it.element != ref.element })
|
||||
}
|
||||
return
|
||||
}
|
||||
assertNull(
|
||||
"Expected that reference for signature '$signature' at offset $offsetBySignature resolves to null but resolved to $resolved (${resolved?.text}) in file ${resolved?.containingFile?.name}",
|
||||
resolved)
|
||||
if (ref is PsiPolyVariantReference) {
|
||||
assertEmpty(ref.multiResolve(false))
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.findUsages(target: SearchTarget): MutableCollection<out Usage> {
|
||||
val project = project
|
||||
|
||||
val searchScope = coalesce<SearchScope>(target.maximalSearchScope, GlobalSearchScope.allScope(project))
|
||||
val allOptions = AllSearchOptions(UsageOptions.createOptions(searchScope), true)
|
||||
return buildUsageViewQuery(getProject(), target, allOptions).findAll()
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@RequiresEdt
|
||||
fun CodeInsightTestFixture.checkGTDUOutcome(expectedOutcome: GotoDeclarationOrUsageHandler2.GTDUOutcome?, signature: String? = null) {
|
||||
if (signature != null) {
|
||||
moveToOffsetBySignature(signature)
|
||||
}
|
||||
val actualSignature = signature ?: editor.currentPositionSignature
|
||||
val editor = InjectedLanguageUtil.getEditorForInjectedLanguageNoCommit(editor, file)
|
||||
val offset = editor.caretModel.offset
|
||||
|
||||
val gtduOutcome = ReadAction
|
||||
.nonBlocking(Callable {
|
||||
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document) ?: file
|
||||
testGTDUOutcome(editor, file, offset)
|
||||
})
|
||||
.submit(AppExecutorUtil.getAppExecutorService())
|
||||
.get()
|
||||
Assert.assertEquals(actualSignature,
|
||||
expectedOutcome,
|
||||
gtduOutcome)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkGotoDeclaration(fromSignature: String?, declarationSignature: String, expectedFileName: String? = null) {
|
||||
checkGTDUOutcome(GotoDeclarationOrUsageHandler2.GTDUOutcome.GTD, fromSignature)
|
||||
val actualSignature = fromSignature ?: editor.currentPositionSignature
|
||||
performEditorAction("GotoDeclaration")
|
||||
val targetEditor = FileEditorManager.getInstance(project).selectedTextEditor?.topLevelEditor
|
||||
if (targetEditor == null) throw NullPointerException(actualSignature)
|
||||
val targetFile = PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)!!
|
||||
if (expectedFileName != null) {
|
||||
assertEquals(actualSignature, expectedFileName, PsiDocumentManager.getInstance(project).getPsiFile(targetEditor.document)?.name)
|
||||
}
|
||||
else {
|
||||
assertEquals(actualSignature, targetEditor, editor.topLevelEditor)
|
||||
}
|
||||
if (!declarationSignature.contains("<caret>") || targetFile.findOffsetBySignature(
|
||||
declarationSignature) != targetEditor.caretModel.offset) {
|
||||
assertEquals("For go to from: $actualSignature",
|
||||
declarationSignature + if (!declarationSignature.contains("<caret>")) ""
|
||||
else (" [" + file.findOffsetBySignature(declarationSignature) + "]"),
|
||||
targetEditor.currentPositionSignature + "[${targetEditor.caretModel.offset}]")
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkListByFile(actualList: List<String>, @TestDataFile expectedFile: String, containsCheck: Boolean) {
|
||||
val path = "$testDataPath/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists() && file.createNewFile()) {
|
||||
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
|
||||
}
|
||||
val actualContents = actualList.joinToString("\n").trim() + "\n"
|
||||
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
|
||||
if (containsCheck) {
|
||||
val expectedList = FileUtil.loadLines(file, "UTF-8").filter { it.isNotBlank() }
|
||||
val actualSet = actualList.toSet()
|
||||
if (!expectedList.all { actualSet.contains(it) }) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
|
||||
}
|
||||
}
|
||||
else if (expectedContents != actualContents) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContents, path)
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.checkTextByFile(actualContents: String, @TestDataFile expectedFile: String) {
|
||||
val path = "$testDataPath/$expectedFile"
|
||||
val file = File(path)
|
||||
if (!file.exists() && file.createNewFile()) {
|
||||
Logger.getInstance("#WebTestUtilKt").warn("File $file has been created.")
|
||||
}
|
||||
val actualContentsTrimmed = actualContents.trim() + "\n"
|
||||
val expectedContents = FileUtil.loadFile(file, "UTF-8", true).trim() + "\n"
|
||||
if (expectedContents != actualContentsTrimmed) {
|
||||
throw FileComparisonFailedError(expectedFile, expectedContents, actualContentsTrimmed, path)
|
||||
}
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.canRenameWebSymbolAtCaret() =
|
||||
webSymbolAtCaret().let {
|
||||
it is RenameTarget || it?.renameTarget != null || (it is PsiSourcedWebSymbol && it.source != null)
|
||||
}
|
||||
|
||||
fun CodeInsightTestFixture.renameWebSymbol(newName: String) {
|
||||
val symbol = webSymbolAtCaret() ?: throw AssertionError("No WebSymbol at caret")
|
||||
var target: RenameTarget? = null
|
||||
|
||||
for (factory: SymbolRenameTargetFactory in SymbolRenameTargetFactory.EP_NAME.extensions) {
|
||||
target = factory.renameTarget(project, symbol)
|
||||
if (target != null) break
|
||||
}
|
||||
if (target == null) {
|
||||
target = when (symbol) {
|
||||
is RenameTarget -> symbol
|
||||
is PsiSourcedWebSymbol -> {
|
||||
val psiTarget = symbol.source
|
||||
?: throw AssertionError("Symbol $symbol provides null source")
|
||||
renameElement(psiTarget, newName)
|
||||
return
|
||||
}
|
||||
else -> symbol.renameTarget
|
||||
?: throw AssertionError("Symbol $symbol does not provide rename target nor is a PsiSourcedWebSymbol")
|
||||
}
|
||||
}
|
||||
if (target.createPointer().dereference() == null) {
|
||||
throw AssertionError("Target $target pointer dereferences to null")
|
||||
}
|
||||
renameTarget(target, newName)
|
||||
}
|
||||
|
||||
|
||||
fun doCompletionItemsTest(
|
||||
fixture: CodeInsightTestFixture,
|
||||
fileName: String,
|
||||
goldFileWithExtension: Boolean = false,
|
||||
renderDisplayText: Boolean = false,
|
||||
) {
|
||||
val fileNameNoExt = FileUtil.getNameWithoutExtension(fileName)
|
||||
fixture.configureByFile(fileName)
|
||||
WriteAction.runAndWait<Throwable> { WebSymbolsQueryExecutorFactory.getInstance(fixture.project) }
|
||||
|
||||
val document = fixture.getDocument(fixture.file)
|
||||
|
||||
val offsets = mutableListOf<Pair<Int, Boolean>>()
|
||||
|
||||
WriteAction.runAndWait<Throwable> {
|
||||
CommandProcessor.getInstance().executeCommand(fixture.project, {
|
||||
val chars = document.charsSequence
|
||||
var pos: Int
|
||||
while (chars.indexOf('|').also { pos = it } >= 0) {
|
||||
val strict = chars.length > pos + 1 && chars[pos + 1] == '!'
|
||||
offsets.add(Pair(pos, strict))
|
||||
if (strict)
|
||||
document.deleteString(pos, pos + 2)
|
||||
else
|
||||
document.deleteString(pos, pos + 1)
|
||||
}
|
||||
}, null, null)
|
||||
PsiDocumentManager.getInstance(fixture.project).commitDocument(document)
|
||||
}
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
noAutoComplete {
|
||||
offsets.forEachIndexed { index, (offset, strict) ->
|
||||
fixture.editor.caretModel.moveToOffset(offset)
|
||||
fixture.completeBasic()
|
||||
|
||||
fixture.checkListByFile(
|
||||
fixture.renderLookupItems(true, true, true, renderDisplayText = renderDisplayText),
|
||||
"gold/${if (goldFileWithExtension) fileName else fileNameNoExt}.${index}.txt", !strict)
|
||||
|
||||
PlatformTestUtil.dispatchAllInvocationEventsInIdeEventQueue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val Editor.currentPositionSignature: String
|
||||
get() {
|
||||
val caretPos = caretModel.offset
|
||||
val text = document.text
|
||||
return (text.substring(max(0, caretPos - 15), caretPos) + "<caret>" +
|
||||
text.substring(caretPos, min(caretPos + 15, text.length)))
|
||||
.replace("\n", "\\n")
|
||||
}
|
||||
|
||||
|
||||
private val Editor.topLevelEditor
|
||||
get() = if (this is EditorWindow) delegate else this
|
||||
@@ -1,128 +1,3 @@
|
||||
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.webSymbols.query
|
||||
|
||||
import com.intellij.util.applyIf
|
||||
import com.intellij.webSymbols.*
|
||||
import com.intellij.webSymbols.completion.WebSymbolCodeCompletionItem
|
||||
import com.intellij.webSymbols.html.WebSymbolHtmlAttributeValue
|
||||
import com.intellij.webSymbols.utils.completeMatch
|
||||
import com.intellij.webSymbols.utils.nameSegments
|
||||
import java.util.*
|
||||
|
||||
open class WebSymbolsDebugOutputPrinter : DebugOutputPrinter() {
|
||||
|
||||
private val parents = Stack<WebSymbol>()
|
||||
|
||||
override fun printValueImpl(builder: StringBuilder, level: Int, value: Any?): StringBuilder =
|
||||
when (value) {
|
||||
is WebSymbolCodeCompletionItem -> builder.printCodeCompletionItem(level, value)
|
||||
is WebSymbol -> builder.printSymbol(level, value)
|
||||
is WebSymbolHtmlAttributeValue -> builder.printAttributeValue(level, value)
|
||||
is WebSymbolNameSegment -> builder.printSegment(level, value)
|
||||
is WebSymbolApiStatus -> builder.printApiStatus(value)
|
||||
is Set<*> -> builder.printSet(value)
|
||||
else -> super.printValueImpl(builder, level, value)
|
||||
}
|
||||
|
||||
override fun printRecursiveValue(builder: StringBuilder, level: Int, value: Any): StringBuilder =
|
||||
if (value is WebSymbol)
|
||||
if (parents.peek() == value) builder.append("<self>") else builder.append("<recursive>")
|
||||
else
|
||||
super.printRecursiveValue(builder, level, value)
|
||||
|
||||
private fun StringBuilder.printCodeCompletionItem(topLevel: Int, item: WebSymbolCodeCompletionItem): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "name", item.name)
|
||||
printProperty(level, "priority", item.priority ?: WebSymbol.Priority.NORMAL)
|
||||
printProperty(level, "proximity", item.proximity?.takeIf { it > 0 })
|
||||
printProperty(level, "displayName", item.displayName.takeIf { it != item.name })
|
||||
printProperty(level, "offset", item.offset.takeIf { it != 0 })
|
||||
printProperty(level, "completeAfterInsert", item.completeAfterInsert.takeIf { it })
|
||||
printProperty(level, "completeAfterChars", item.completeAfterChars.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "aliases", item.aliases.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "source", item.symbol)
|
||||
}
|
||||
|
||||
private fun StringBuilder.printSet(set: Set<*>): StringBuilder {
|
||||
return append(set.toString())
|
||||
}
|
||||
|
||||
private fun StringBuilder.printSymbol(topLevel: Int, source: WebSymbol): StringBuilder {
|
||||
if (parents.contains(source)) {
|
||||
if (parents.peek() == source) append("<self>") else append("<recursive>")
|
||||
return this
|
||||
}
|
||||
printObject(topLevel) { level ->
|
||||
if (source.pattern != null) {
|
||||
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/<pattern>")
|
||||
printProperty(level, "name", source.name)
|
||||
}
|
||||
else {
|
||||
printProperty(level, "matchedName", source.namespace.lowercase(Locale.US) + "/" + source.kind + "/" + source.name)
|
||||
}
|
||||
printProperty(level, "origin", "${source.origin.library}@${source.origin.version} (${source.origin.framework ?: "<none>"})")
|
||||
printProperty(level, "source", (source as? PsiSourcedWebSymbol)?.source)
|
||||
printProperty(level, "type", source.type)
|
||||
printProperty(level, "attrValue", source.attributeValue)
|
||||
printProperty(level, "complete", source.completeMatch)
|
||||
printProperty(level, "description", source.description?.ellipsis(45))
|
||||
printProperty(level, "docUrl", source.docUrl)
|
||||
printProperty(level, "descriptionSections", source.descriptionSections.takeIf { it.isNotEmpty() })
|
||||
printProperty(level, "abstract", source.abstract.takeIf { it })
|
||||
printProperty(level, "virtual", source.virtual.takeIf { it })
|
||||
printProperty(level, "apiStatus", source.apiStatus.takeIf { it !is WebSymbolApiStatus.Stable || it.since != null })
|
||||
printProperty(level, "priority", source.priority ?: WebSymbol.Priority.NORMAL)
|
||||
printProperty(level, "proximity", source.proximity?.takeIf { it > 0 })
|
||||
printProperty(level, "has-pattern", if (source.pattern != null) true else null)
|
||||
printProperty(level, "properties", source.properties.takeIf { it.isNotEmpty() })
|
||||
parents.push(source)
|
||||
printProperty(level, "segments", source.nameSegments)
|
||||
parents.pop()
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
|
||||
private fun StringBuilder.printSegment(topLevel: Int,
|
||||
segment: WebSymbolNameSegment): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "name-part", parents.peek().let { if (it.pattern == null) segment.getName(parents.peek()) else "" })
|
||||
printProperty(level, "display-name", segment.displayName)
|
||||
printProperty(level, "apiStatus", segment.apiStatus)
|
||||
printProperty(level, "priority", segment.priority?.takeIf { it != WebSymbol.Priority.NORMAL })
|
||||
printProperty(level, "matchScore", segment.matchScore.takeIf { it != segment.end - segment.start })
|
||||
printProperty(level, "problem", segment.problem)
|
||||
val symbols = segment.symbols.filter { !it.extension }
|
||||
if (symbols.size > 1) {
|
||||
printProperty(level, "symbols", symbols)
|
||||
}
|
||||
else if (symbols.size == 1) {
|
||||
printProperty(level, "symbol", symbols[0])
|
||||
}
|
||||
}
|
||||
|
||||
private fun StringBuilder.printAttributeValue(topLevel: Int, value: WebSymbolHtmlAttributeValue): StringBuilder =
|
||||
printObject(topLevel) { level ->
|
||||
printProperty(level, "kind", value.kind)
|
||||
.printProperty(level, "type", value.type)
|
||||
.printProperty(level, "langType", value.langType)
|
||||
.printProperty(level, "required", value.required)
|
||||
.printProperty(level, "default", value.default)
|
||||
}
|
||||
|
||||
private fun StringBuilder.printApiStatus(apiStatus: WebSymbolApiStatus): StringBuilder =
|
||||
when (apiStatus) {
|
||||
is WebSymbolApiStatus.Deprecated -> append("deprecated")
|
||||
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Obsolete -> append("obsolete")
|
||||
.applyIf(apiStatus.since != null) { append(" in ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Experimental -> append("experimental")
|
||||
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
|
||||
.applyIf(apiStatus.message != null) { append(" (").append(apiStatus.message).append(")") }
|
||||
is WebSymbolApiStatus.Stable -> append("stable")
|
||||
.applyIf(apiStatus.since != null) { append(" since ").append(apiStatus.since) }
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user