From 15641f65dd44597b87fe66709d141f19703e5490 Mon Sep 17 00:00:00 2001 From: Yair Cohen Date: Mon, 11 Dec 2023 15:05:23 +0200 Subject: [PATCH] DEV2-4411: code lens (#699) * add lens for JS, TS and Java * add "ask" * add analytics * provider per language * set 'freeCompilerArgs += "-Xjvm-default=enable"' * support kotlin * support php * support rust * revert * typescript * small changes * fix test * Yair/code lens tests (#705) * tests * tests * add java dependency only when running tests * try * fix * remove unneeded --------- Co-authored-by: Amir Tuval * pass isChatEnabled function to the inlayHintProvider * fix tests --------- Co-authored-by: Amir Tuval --- .github/workflows/ci.yml | 2 +- Common/build.gradle | 1 + .../chat/lens/TabnineLensBaseProvider.kt | 44 +++++++ .../chat/lens/TabnineLensCollector.kt | 120 ++++++++++++++++++ .../tabnineCommon/general/StaticConfig.java | 4 + .../resources/icons/tabnine-lens-icon.png | Bin 0 -> 562 bytes Tabnine/build.gradle | 4 + .../java/com/tabnine/chat/lens/TabnineLens.kt | 16 +++ .../src/main/resources/META-INF/plugin.xml | 6 + .../tabnine/testUtils/TabnineMatchers.java | 3 +- .../test/kotlin/TabnineLensIntegrationTest.kt | 61 +++++++++ TabnineSelfHosted/build.gradle | 1 + .../chat/lens/TabnineLens.kt | 16 +++ .../src/main/resources/META-INF/plugin.xml | 6 + TabnineSelfHostedForMarketplace/build.gradle | 1 + 15 files changed, 282 insertions(+), 3 deletions(-) create mode 100644 Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensBaseProvider.kt create mode 100644 Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensCollector.kt create mode 100644 Common/src/main/resources/icons/tabnine-lens-icon.png create mode 100644 Tabnine/src/main/java/com/tabnine/chat/lens/TabnineLens.kt create mode 100644 Tabnine/src/test/kotlin/TabnineLensIntegrationTest.kt create mode 100644 TabnineSelfHosted/src/main/java/com/tabnineSelfHosted/chat/lens/TabnineLens.kt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6776ccd..089688b7f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - name: Build & Test - run: ./gradlew build --scan + run: ./gradlew build --scan -PtestMode=true - if: always() name: Publish Test Report uses: scacap/action-surefire-report@v1 diff --git a/Common/build.gradle b/Common/build.gradle index e85a13f82..4e977ab67 100644 --- a/Common/build.gradle +++ b/Common/build.gradle @@ -22,6 +22,7 @@ targetCompatibility = 9 tasks.withType(KotlinCompile).configureEach { kotlinOptions { jvmTarget = "9" + freeCompilerArgs += "-Xjvm-default=enable" } } diff --git a/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensBaseProvider.kt b/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensBaseProvider.kt new file mode 100644 index 000000000..b6b234dcc --- /dev/null +++ b/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensBaseProvider.kt @@ -0,0 +1,44 @@ +package com.tabnineCommon.chat.lens + +import com.intellij.codeInsight.hints.ChangeListener +import com.intellij.codeInsight.hints.ImmediateConfigurable +import com.intellij.codeInsight.hints.InlayHintsProvider +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.NoSettings +import com.intellij.codeInsight.hints.SettingsKey +import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiFile +import javax.swing.JComponent +import javax.swing.JPanel + +open class TabnineLensBaseProvider(private val supportedElementTypes: List, private val isChatEnabled: () -> Boolean) : InlayHintsProvider { + override fun getCollectorFor( + file: PsiFile, + editor: Editor, + settings: NoSettings, + sink: InlayHintsSink + ) = TabnineLensCollector(editor, supportedElementTypes, isChatEnabled) + + override val key: SettingsKey = SettingsKey("tabnine.chat.inlay.provider") + + override val name: String = "Tabnine: chat actions" + + override val previewText: String? = null + + override fun createSettings(): NoSettings = NoSettings() + + override fun createConfigurable(settings: NoSettings): ImmediateConfigurable { + return object : ImmediateConfigurable { + override fun createComponent(listener: ChangeListener): JComponent { + return JPanel() + } + } + } +} + +open class TabnineLensJavaBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("CLASS", "METHOD"), isChatEnabled) +open class TabnineLensPythonBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("Py:CLASS_DECLARATION", "Py:FUNCTION_DECLARATION"), isChatEnabled) +open class TabnineLensTypescriptBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("JS:FUNCTION_DECLARATION", "JS:ES6_CLASS", "JS:CLASS", "JS:TYPESCRIPT_FUNCTION", "JS:TYPESCRIPT_CLASS"), isChatEnabled) +open class TabnineLensKotlinBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("CLASS", "FUN"), isChatEnabled) +open class TabnineLensPhpBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("Class", "Class method", "Function"), isChatEnabled) +open class TabnineLensRustBaseProvider(isChatEnabled: () -> Boolean) : TabnineLensBaseProvider(listOf("FUNCTION"), isChatEnabled) diff --git a/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensCollector.kt b/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensCollector.kt new file mode 100644 index 000000000..a937501f2 --- /dev/null +++ b/Common/src/main/java/com/tabnineCommon/chat/lens/TabnineLensCollector.kt @@ -0,0 +1,120 @@ +package com.tabnineCommon.chat.lens + +import com.intellij.codeInsight.hints.FactoryInlayHintsCollector +import com.intellij.codeInsight.hints.InlayHintsSink +import com.intellij.codeInsight.hints.InlayPresentationFactory +import com.intellij.codeInsight.hints.presentation.InlayPresentation +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.ui.Messages +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiElement +import com.intellij.psi.util.elementType +import com.intellij.refactoring.suggested.startOffset +import com.tabnineCommon.binary.requests.analytics.EventRequest +import com.tabnineCommon.chat.actions.common.ChatActionCommunicator +import com.tabnineCommon.general.DependencyContainer +import com.tabnineCommon.general.StaticConfig +import java.awt.Point +import java.awt.event.MouseEvent + +class TabnineLensCollector( + editor: Editor, + private val enabledElementTypes: List, + private val isChatEnabled: () -> Boolean +) : FactoryInlayHintsCollector(editor) { + companion object { + private const val ID = "com.tabnine.chat.lens" + } + + private val binaryRequestFacade = DependencyContainer.instanceOfBinaryRequestFacade() + + override fun collect(element: PsiElement, editor: Editor, sink: InlayHintsSink): Boolean { + if (!isChatEnabled()) { + return false + } + if (element.elementType.toString() in enabledElementTypes) { + sink.addBlockElement( + offset = element.startOffset, + relatesToPrecedingText = true, + showAbove = true, + priority = 0, + presentation = factory.seq( + factory.textSpacePlaceholder(countLeadingWhitespace(editor, element), false), + factory.icon(StaticConfig.getTabnineLensIcon()), + buildQuickActionItem("Explain", "/explain-code", editor, element, false), + buildQuickActionItem("Test", "/generate-test-for-code", editor, element, true), + buildQuickActionItem("Document", "/document-code", editor, element, true), + buildQuickActionItem("Fix", "/fix-code", editor, element, true), + buildAskActionItem("Ask", editor, element), + ) + ) + } + return true + } + + private fun buildQuickActionItem(label: String, intent: String, editor: Editor, element: PsiElement, includeSeparator: Boolean): InlayPresentation { + return factory.seq( + factory.smallText(" "), + factory.smallText(if (includeSeparator) "| " else ""), + factory.referenceOnHover( + factory.smallText(label), + object : InlayPresentationFactory.ClickListener { + override fun onClick(event: MouseEvent, translated: Point) { + sendClickEvent(intent) + + selectElementRange(editor, element) + ChatActionCommunicator.sendMessageToChat(editor.project!!, ID, intent) + } + }, + ) + ) + } + + private fun buildAskActionItem(label: String, editor: Editor, element: PsiElement): InlayPresentation { + return factory.seq( + factory.smallText(" "), + factory.smallText("| "), + factory.referenceOnHover( + factory.smallText(label), + object : InlayPresentationFactory.ClickListener { + override fun onClick(event: MouseEvent, translated: Point) { + sendClickEvent("ask") + + val result = + Messages.showInputDialog("How can I assist with this code?", "Ask Tabnine", StaticConfig.getTabnineIcon()) + .takeUnless { it.isNullOrBlank() } + ?: return + + selectElementRange(editor, element) + ChatActionCommunicator.sendMessageToChat(editor.project!!, ID, result) + } + }, + ) + ) + } + + private fun selectElementRange(editor: Editor, element: PsiElement) { + val selectionModel = editor.selectionModel + val range = element.textRange + selectionModel.setSelection(range.startOffset, range.endOffset) + } + + private fun sendClickEvent(intent: String) { + binaryRequestFacade.executeRequest( + EventRequest( + "chat-code-lens-click", + mapOf("intent" to intent) + ) + ) + } + + private fun countLeadingWhitespace(editor: Editor, element: PsiElement): Int { + val lineNumber = editor.document.getLineNumber(element.startOffset) + return editor.document.getText( + TextRange( + editor.document.getLineStartOffset(lineNumber), + editor.document.getLineEndOffset(lineNumber) + ) + ).takeWhile { it.isWhitespace() }.length + } +} diff --git a/Common/src/main/java/com/tabnineCommon/general/StaticConfig.java b/Common/src/main/java/com/tabnineCommon/general/StaticConfig.java index 67ce33704..d5c9090e5 100644 --- a/Common/src/main/java/com/tabnineCommon/general/StaticConfig.java +++ b/Common/src/main/java/com/tabnineCommon/general/StaticConfig.java @@ -67,6 +67,10 @@ public static Icon getTabnineIcon() { return IconLoader.findIcon("/icons/tabnine-icon-13px.png"); } + public static Icon getTabnineLensIcon() { + return IconLoader.findIcon("/icons/tabnine-lens-icon.png"); + } + public static Icon getGlyphIcon() { return IconLoader.findIcon("icons/icon-13px.png"); } diff --git a/Common/src/main/resources/icons/tabnine-lens-icon.png b/Common/src/main/resources/icons/tabnine-lens-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b37ed652d9544c4178410163f028180e4ec78888 GIT binary patch literal 562 zcmV-20?qx2P)Px$>`6pHR47v|kw0h?Q5?tL@BK}3mzWGvDRfuqSP(7vFEwds6*>s&BJJo@I_T;k zEeb}2j1G0GR*--e5lN?lmJC*lAXqT8V{0yLa_`>VyLYc7_B(v1&+_2|=wudI*sQd( ziDgAi(75631Dr~2UR_2ZAh3N5`uBT$KihNod4ph9bL1(G8}e&)n{EoN$FA86u(%N6xFl1g>1Xt3*uK(Pj3{dP4*sQ)os=p1 zHDt+Hz{ww#CR>Q{wnx>xqV@+gi(XBopc=WEywa*X^**<>GUpFVj!TN#f=r=SAm~uY zPy!p1Ud?9GRgamx!L~fJ;x6?)>|={x_M*`qF%weYiIC%uuzu^XCaI|QGA5r8TfX=D zKfHR|lI^p4G$u(=6DTSUYw&62u!h~-^R}b&QLHQXUpb3UANs=~#if`+K{NR4b2uY8 zN}ztX`$RFk=V#$(l-OxMLpC&nv524@4QMOOaj(tedClNE)bGfVj%zD{fSVzOp-|Yj zF-;npJG=iJb5W7xhUWNNg5;M$l+_2;-B0UtN7&3o47yUFMj+t2Cb*55sUIX__uT>n z19)I>Ej)XK+)mb9Q`B6F1zzhGXmfcMh3F*rG1(dG_tz%}m;e9(07*qoM6N<$g6|aw AP5=M^ literal 0 HcmV?d00001 diff --git a/Tabnine/build.gradle b/Tabnine/build.gradle index c119886df..6934d9071 100644 --- a/Tabnine/build.gradle +++ b/Tabnine/build.gradle @@ -23,6 +23,7 @@ targetCompatibility = 9 tasks.withType(KotlinCompile).configureEach { kotlinOptions { jvmTarget = "9" + freeCompilerArgs += "-Xjvm-default=enable" } } @@ -60,11 +61,14 @@ test { } } +def neededPlugins = project.hasProperty('testMode') ? ['java'] : [] + intellij { version = '2020.2' type = 'IC' updateSinceUntilBuild = false pluginName = 'TabNine' + plugins = neededPlugins } def PRODUCTION_CHANNEL = null diff --git a/Tabnine/src/main/java/com/tabnine/chat/lens/TabnineLens.kt b/Tabnine/src/main/java/com/tabnine/chat/lens/TabnineLens.kt new file mode 100644 index 000000000..efa00096d --- /dev/null +++ b/Tabnine/src/main/java/com/tabnine/chat/lens/TabnineLens.kt @@ -0,0 +1,16 @@ +package com.tabnine.chat.lens + +import com.tabnine.chat.ChatEnabledState +import com.tabnineCommon.chat.lens.TabnineLensJavaBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensKotlinBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensPhpBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensPythonBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensRustBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensTypescriptBaseProvider + +open class TabnineLensJavaProvider : TabnineLensJavaBaseProvider({ ChatEnabledState.instance.get().enabled }) +open class TabnineLensPythonProvider : TabnineLensPythonBaseProvider({ ChatEnabledState.instance.get().enabled }) +open class TabnineLensTypescriptProvider : TabnineLensTypescriptBaseProvider({ ChatEnabledState.instance.get().enabled }) +open class TabnineLensKotlinProvider : TabnineLensKotlinBaseProvider({ ChatEnabledState.instance.get().enabled }) +open class TabnineLensPhpProvider : TabnineLensPhpBaseProvider({ ChatEnabledState.instance.get().enabled }) +open class TabnineLensRustProvider : TabnineLensRustBaseProvider({ ChatEnabledState.instance.get().enabled }) diff --git a/Tabnine/src/main/resources/META-INF/plugin.xml b/Tabnine/src/main/resources/META-INF/plugin.xml index d0e4c0ec7..529e34590 100644 --- a/Tabnine/src/main/resources/META-INF/plugin.xml +++ b/Tabnine/src/main/resources/META-INF/plugin.xml @@ -143,6 +143,12 @@ com.tabnine.chat.actions.TabnineQuickFixAction Tabnine intentions + + + + + + Matcher> emptyOptional() { } @NotNull - public static Matcher> versionMatch(@Nonnull String version) { + public static Matcher> versionMatch(@NotNull String version) { return new BaseMatcher>() { @Override public void describeTo(Description description) { diff --git a/Tabnine/src/test/kotlin/TabnineLensIntegrationTest.kt b/Tabnine/src/test/kotlin/TabnineLensIntegrationTest.kt new file mode 100644 index 000000000..8a672945b --- /dev/null +++ b/Tabnine/src/test/kotlin/TabnineLensIntegrationTest.kt @@ -0,0 +1,61 @@ + +import com.intellij.codeInsight.hints.CollectorWithSettings +import com.intellij.codeInsight.hints.InlayHintsSinkImpl +import com.intellij.codeInsight.hints.NoSettings +import com.intellij.openapi.editor.Inlay +import com.intellij.testFramework.fixtures.LightPlatformCodeInsightFixture4TestCase +import com.tabnineCommon.chat.lens.TabnineLensJavaBaseProvider +import org.junit.Test + +class TabnineLensIntegrationTest : LightPlatformCodeInsightFixture4TestCase() { + + @Test + fun `should show inlay hints for java function when chat is enabled`() { + setJavaFile() + val provider = TabnineLensJavaBaseProvider { true } + runCollector(provider) + val inlays = getRenderedInlays() + + assertEquals(inlays[0].offset, 0) + assertEquals(inlays[1].offset, 20) + assertEquals(inlays.size, 2) + } + + @Test + fun `should not show inlay hints for java function when chat is disabled`() { + setJavaFile() + val provider = TabnineLensJavaBaseProvider { false } + runCollector(provider) + val inlays = getRenderedInlays() + + assertEquals(inlays.size, 0) + } + + private fun setJavaFile() { + myFixture.configureByText( + "Test.java", + "public class Test {\n public void test() {\n System.out.println(\"Hello World\");\n }\n}" + ) + } + + private fun runCollector(provider: TabnineLensJavaBaseProvider) { + val file = myFixture.file + val editor = myFixture.editor + val sink = InlayHintsSinkImpl(editor) + + val collector = provider.getCollectorFor(file, editor, NoSettings(), sink) + val collectorWithSettings = CollectorWithSettings(collector, provider.key, file.language, sink) + collectorWithSettings.collectTraversingAndApply( + editor, + file, + true + ) + } + + private fun getRenderedInlays(): MutableList> { + return myFixture.editor.inlayModel.getBlockElementsInRange( + myFixture.file.textRange.startOffset, + myFixture.file.textRange.endOffset + ) + } +} diff --git a/TabnineSelfHosted/build.gradle b/TabnineSelfHosted/build.gradle index 7ec8090e4..de5d2aeff 100644 --- a/TabnineSelfHosted/build.gradle +++ b/TabnineSelfHosted/build.gradle @@ -23,6 +23,7 @@ targetCompatibility = 9 tasks.withType(KotlinCompile).configureEach { kotlinOptions { jvmTarget = "9" + freeCompilerArgs += "-Xjvm-default=enable" } } diff --git a/TabnineSelfHosted/src/main/java/com/tabnineSelfHosted/chat/lens/TabnineLens.kt b/TabnineSelfHosted/src/main/java/com/tabnineSelfHosted/chat/lens/TabnineLens.kt new file mode 100644 index 000000000..9b6a89a18 --- /dev/null +++ b/TabnineSelfHosted/src/main/java/com/tabnineSelfHosted/chat/lens/TabnineLens.kt @@ -0,0 +1,16 @@ +package com.tabnineSelfHosted.chat.lens + +import com.tabnineCommon.chat.lens.TabnineLensJavaBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensKotlinBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensPhpBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensPythonBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensRustBaseProvider +import com.tabnineCommon.chat.lens.TabnineLensTypescriptBaseProvider +import com.tabnineSelfHosted.chat.SelfHostedChatEnabledState + +open class TabnineLensJavaProvider : TabnineLensJavaBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) +open class TabnineLensPythonProvider : TabnineLensPythonBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) +open class TabnineLensTypescriptProvider : TabnineLensTypescriptBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) +open class TabnineLensKotlinProvider : TabnineLensKotlinBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) +open class TabnineLensPhpProvider : TabnineLensPhpBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) +open class TabnineLensRustProvider : TabnineLensRustBaseProvider({ SelfHostedChatEnabledState.instance.get().enabled }) diff --git a/TabnineSelfHosted/src/main/resources/META-INF/plugin.xml b/TabnineSelfHosted/src/main/resources/META-INF/plugin.xml index 8298dc610..5ebe94fed 100644 --- a/TabnineSelfHosted/src/main/resources/META-INF/plugin.xml +++ b/TabnineSelfHosted/src/main/resources/META-INF/plugin.xml @@ -120,6 +120,12 @@ Other Tabnine AI users, including Tabnine Enterprise SaaS, should use th com.tabnineSelfHosted.chat.actions.TabnineQuickFixAction Tabnine intentions + + + + + + diff --git a/TabnineSelfHostedForMarketplace/build.gradle b/TabnineSelfHostedForMarketplace/build.gradle index e97568aeb..a7684651c 100644 --- a/TabnineSelfHostedForMarketplace/build.gradle +++ b/TabnineSelfHostedForMarketplace/build.gradle @@ -23,6 +23,7 @@ targetCompatibility = 9 tasks.withType(KotlinCompile).configureEach { kotlinOptions { jvmTarget = "9" + freeCompilerArgs += "-Xjvm-default=enable" } }