IJPL-189563 LSP: support multi-entry per line for code vision and extract codeVisionTestFixture from java-tests

GitOrigin-RevId: 49dd70e32e5734b91c9b564507feeda82fc1ee1b
This commit is contained in:
Vladimir Goncharov
2026-02-13 12:11:41 +00:00
committed by intellij-monorepo-bot
parent 8d442c78c9
commit 33db93e4b3
8 changed files with 163 additions and 4 deletions
@@ -1,4 +1,4 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInsight.codeVision
import com.intellij.codeInsight.daemon.impl.JavaInheritorsCodeVisionProvider
@@ -11,6 +11,7 @@ import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.writeAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.module.Module
import com.intellij.platform.testFramework.junit5.codeInsight.fixture.codeVisionFixture
import com.intellij.platform.testFramework.junit5.projectStructure.fixture.withSharedSourceEnabled
import com.intellij.psi.PsiDirectory
import com.intellij.psi.impl.file.impl.sharedSourceRootFixture
@@ -120,6 +120,7 @@ c:com.intellij.codeInsight.actions.VcsFacade
- a:getName():java.lang.String
- getPlaceholderCollector(com.intellij.openapi.editor.Editor,com.intellij.psi.PsiFile):com.intellij.codeInsight.codeVision.CodeVisionPlaceholderCollector
- a:getRelativeOrderings():java.util.List
- getSingleEntryPerLine():Z
- handleClick(com.intellij.openapi.editor.Editor,com.intellij.openapi.util.TextRange,com.intellij.codeInsight.codeVision.CodeVisionEntry):V
- handleExtraAction(com.intellij.openapi.editor.Editor,com.intellij.openapi.util.TextRange,com.intellij.codeInsight.codeVision.CodeVisionEntry,java.lang.String):V
- handleExtraAction(com.intellij.openapi.editor.Editor,com.intellij.openapi.util.TextRange,java.lang.String):V
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.codeVision
import com.intellij.codeInsight.codeVision.settings.PlatformCodeVisionIds
@@ -112,6 +112,16 @@ interface CodeVisionProvider<T> {
val id: String
/**
* Whether only a single entry per line should be displayed for this provider.
*
* When `true`, if multiple entries from this provider appear on the same line,
* only the last one will be shown. When `false`, all entries are displayed.
*/
val singleEntryPerLine: Boolean
get() = true
/**
* Used to group provider in the settings panel and to share same behavior (like position and ext.) and description.
* To group different provider implement [com.intellij.codeInsight.codeVision.settings.CodeVisionGroupSettingProvider].
@@ -1,4 +1,4 @@
// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.codeVision
import com.intellij.codeInsight.codeVision.ui.CodeVisionView
@@ -131,7 +131,15 @@ open class EditorCodeVisionContext(
continue
}
val lastFilteredLineLenses = lineLenses.groupBy { it.codeVisionEntryOrThrow.providerId }.map { it.value.last() }
val lastFilteredLineLenses = lineLenses.groupBy { it.codeVisionEntryOrThrow.providerId }.flatMap { (providerId, lenses) ->
val provider = codeVisionHost.getProviderById(providerId)
if (provider?.singleEntryPerLine == true) {
listOf(lenses.last())
}
else {
lenses
}
}
val groupedLenses = lastFilteredLineLenses.groupBy { codeVisionHost.getAnchorForEntry(it.codeVisionEntryOrThrow) }
val anchoringRange = groupedLenses.first().value.first()
@@ -19,6 +19,9 @@ jvm_library(
"//platform/testFramework/junit5",
"//platform/core-api:core",
"//platform/core-ui",
"//platform/lang-impl",
"//platform/lang-api:lang",
"//platform/projectModel-api:projectModel",
]
)
### auto-generated section `build intellij.platform.testFramework.junit5.codeInsight` end
@@ -13,5 +13,8 @@
<orderEntry type="module" module-name="intellij.platform.testFramework.junit5" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.lang.impl" />
<orderEntry type="module" module-name="intellij.platform.lang" />
<orderEntry type="module" module-name="intellij.platform.projectModel" />
</component>
</module>
@@ -1,5 +1,10 @@
<idea-plugin>
<dependencies>
<!-- region Generated dependencies - run `Generate Product Layouts` to regenerate -->
<module name="intellij.platform.lang"/>
<module name="intellij.platform.lang.impl"/>
<module name="intellij.platform.projectModel"/>
<module name="intellij.platform.testFramework.junit5"/>
<!-- endregion -->
</dependencies>
</idea-plugin>
@@ -0,0 +1,128 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.testFramework.junit5.codeInsight.fixture
import com.intellij.codeInsight.codeVision.CodeVisionHost
import com.intellij.codeInsight.codeVision.CodeVisionInitializer
import com.intellij.codeInsight.codeVision.settings.CodeVisionSettings
import com.intellij.codeInsight.codeVision.ui.model.CodeVisionListData
import com.intellij.codeInsight.codeVision.ui.renderers.CodeVisionInlayRenderer
import com.intellij.codeInsight.daemon.impl.HighlightInfo
import com.intellij.codeInsight.hints.InlayDumpUtil
import com.intellij.codeInsight.multiverse.CodeInsightContext
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.readAction
import com.intellij.openapi.application.writeIntentReadAction
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.waitForSmartMode
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import com.intellij.testFramework.TestModeFlags
import com.intellij.testFramework.fixtures.BasePlatformTestCase.assertEquals
import com.intellij.testFramework.fixtures.impl.CodeInsightTestFixtureImpl
import com.intellij.testFramework.junit5.fixture.TestFixture
import com.intellij.testFramework.junit5.fixture.testFixture
import com.intellij.util.ArrayUtilRt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.TestOnly
@TestOnly
class CodeVisionTestFixture(
private val editor: Editor,
private val virtualFile: VirtualFile,
private val project: Project,
private val disposable: Disposable,
private val onlyCodeVisionHintsAllowed: Boolean = false,
) {
fun doHighlighting(file: PsiFile): List<HighlightInfo> =
CodeInsightTestFixtureImpl.instantiateAndRun(file, editor, ArrayUtilRt.EMPTY_INT_ARRAY, false, false)
@TestOnly
suspend fun testProviders(
context: CodeInsightContext,
expectedText: String,
vararg enabledProviderGroupIds: String,
beforeAssert: suspend () -> Unit = {},
) {
val sourceText = InlayDumpUtil.removeInlays(expectedText)
project.waitForSmartMode()
val codeVisionHost = readAction { CodeVisionInitializer.getInstance(project).getCodeVisionHost() }
withContext(Dispatchers.EDT) {
writeIntentReadAction {
val settings = CodeVisionSettings.getInstance()
val file = requireNotNull(PsiManager.getInstance(project).findFile(virtualFile, context))
codeVisionHost.providers.map { it.groupId }.toSet().forEach {
settings.setProviderEnabled(it, enabledProviderGroupIds.contains(it))
}
TestModeFlags.set(CodeVisionHost.isCodeVisionTestKey, true, disposable)
codeVisionHost.providers.forEach {
if (it.id == "vcs.code.vision" && enabledProviderGroupIds.contains(it.groupId)) {
it.preparePreview(editor, file)
}
}
val document =
requireNotNull(FileDocumentManager.getInstance().getDocument(virtualFile))
WriteCommandAction.runWriteCommandAction(project) {
document.setText(sourceText)
PsiDocumentManager.getInstance(project).commitDocument(editor.document)
}
val newFile = requireNotNull(PsiDocumentManager.getInstance(project).getPsiFile(document, context))
doHighlighting(newFile)
codeVisionHost.calculateCodeVisionSync(editor, disposable)
}
}
beforeAssert()
assertText(expectedText)
}
private suspend fun assertText(expectedText: String) {
val sourceText = InlayDumpUtil.removeInlays(expectedText)
val actualText = withContext(Dispatchers.EDT) {
writeIntentReadAction {
dumpCodeVisionHints(sourceText)
}
}
assertEquals(expectedText, actualText)
}
private fun dumpCodeVisionHints(sourceText: String): String {
return InlayDumpUtil.dumpInlays(
sourceText, editor,
filter = {
val rendererSupported = it.renderer is CodeVisionInlayRenderer
if (onlyCodeVisionHintsAllowed && !rendererSupported) error("renderer not supported")
rendererSupported
},
renderer = { _, inlay ->
inlay.getUserData(CodeVisionListData.KEY)!!.visibleLens.joinToString(prefix = "[",
postfix = "]",
separator = " ") { it.longPresentation }
})
}
}
@TestOnly
fun codeVisionFixture(editorFixture: TestFixture<Editor>, fileFixture: TestFixture<PsiFile>): TestFixture<CodeVisionTestFixture> =
testFixture("code-vision-fixture") {
val disposable = Disposer.newDisposable()
val editor = editorFixture.init()
val file = fileFixture.init()
val codeVisionFixture = CodeVisionTestFixture(editor, file.virtualFile, file.project, disposable)
initialized(codeVisionFixture) {
// cleanup
Disposer.dispose(disposable)
}
}