From 33db93e4b3b3a5de1c295ae90933b9ac499d7922 Mon Sep 17 00:00:00 2001 From: Vladimir Goncharov Date: Fri, 13 Feb 2026 11:00:01 +0100 Subject: [PATCH] IJPL-189563 LSP: support multi-entry per line for code vision and extract codeVisionTestFixture from java-tests GitOrigin-RevId: 49dd70e32e5734b91c9b564507feeda82fc1ee1b --- .../JavaMultiverseCodeVisionProviderTest.kt | 3 +- platform/lang-impl/api-dump-experimental.txt | 1 + .../codeVision/CodeVisionProvider.kt | 12 +- .../codeVision/EditorCodeVisionContext.kt | 12 +- .../junit5/codeInsight/BUILD.bazel | 3 + ...tform.testFramework.junit5.codeInsight.iml | 3 + ...tform.testFramework.junit5.codeInsight.xml | 5 + .../src/fixture/CodeVisionTestFixture.kt | 128 ++++++++++++++++++ 8 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 platform/testFramework/junit5/codeInsight/src/fixture/CodeVisionTestFixture.kt diff --git a/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt b/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt index d2faf0715b86..1b11218f7000 100644 --- a/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt +++ b/java/java-tests/testSrc/com/intellij/java/codeInsight/codeVision/JavaMultiverseCodeVisionProviderTest.kt @@ -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 diff --git a/platform/lang-impl/api-dump-experimental.txt b/platform/lang-impl/api-dump-experimental.txt index 6f722d76e4b8..77d2429ab880 100644 --- a/platform/lang-impl/api-dump-experimental.txt +++ b/platform/lang-impl/api-dump-experimental.txt @@ -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 diff --git a/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionProvider.kt b/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionProvider.kt index 34579ae7cea7..9db51349190e 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionProvider.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/codeVision/CodeVisionProvider.kt @@ -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 { 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]. diff --git a/platform/lang-impl/src/com/intellij/codeInsight/codeVision/EditorCodeVisionContext.kt b/platform/lang-impl/src/com/intellij/codeInsight/codeVision/EditorCodeVisionContext.kt index f30d445c7361..4be4d3e0e005 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/codeVision/EditorCodeVisionContext.kt +++ b/platform/lang-impl/src/com/intellij/codeInsight/codeVision/EditorCodeVisionContext.kt @@ -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() diff --git a/platform/testFramework/junit5/codeInsight/BUILD.bazel b/platform/testFramework/junit5/codeInsight/BUILD.bazel index ec62301c2590..0c98f5f5b1a9 100644 --- a/platform/testFramework/junit5/codeInsight/BUILD.bazel +++ b/platform/testFramework/junit5/codeInsight/BUILD.bazel @@ -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 \ No newline at end of file diff --git a/platform/testFramework/junit5/codeInsight/intellij.platform.testFramework.junit5.codeInsight.iml b/platform/testFramework/junit5/codeInsight/intellij.platform.testFramework.junit5.codeInsight.iml index cf1c8724ad69..782255db65b2 100644 --- a/platform/testFramework/junit5/codeInsight/intellij.platform.testFramework.junit5.codeInsight.iml +++ b/platform/testFramework/junit5/codeInsight/intellij.platform.testFramework.junit5.codeInsight.iml @@ -13,5 +13,8 @@ + + + \ No newline at end of file diff --git a/platform/testFramework/junit5/codeInsight/resources/intellij.platform.testFramework.junit5.codeInsight.xml b/platform/testFramework/junit5/codeInsight/resources/intellij.platform.testFramework.junit5.codeInsight.xml index 61ddbd5e999c..07702efcc04c 100644 --- a/platform/testFramework/junit5/codeInsight/resources/intellij.platform.testFramework.junit5.codeInsight.xml +++ b/platform/testFramework/junit5/codeInsight/resources/intellij.platform.testFramework.junit5.codeInsight.xml @@ -1,5 +1,10 @@ + + + + + \ No newline at end of file diff --git a/platform/testFramework/junit5/codeInsight/src/fixture/CodeVisionTestFixture.kt b/platform/testFramework/junit5/codeInsight/src/fixture/CodeVisionTestFixture.kt new file mode 100644 index 000000000000..701ffa712a45 --- /dev/null +++ b/platform/testFramework/junit5/codeInsight/src/fixture/CodeVisionTestFixture.kt @@ -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 = + 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, fileFixture: TestFixture): TestFixture = + 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) + } + + } \ No newline at end of file