Skip to content

Commit

Permalink
DEV2-4411: code lens (#699)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* pass isChatEnabled function to the inlayHintProvider

* fix tests

---------

Co-authored-by: Amir Tuval <[email protected]>
  • Loading branch information
yairco1990 and amircodota authored Dec 11, 2023
1 parent c599d57 commit 15641f6
Show file tree
Hide file tree
Showing 15 changed files with 282 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Common/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ targetCompatibility = 9
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "9"
freeCompilerArgs += "-Xjvm-default=enable"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>, private val isChatEnabled: () -> Boolean) : InlayHintsProvider<NoSettings> {
override fun getCollectorFor(
file: PsiFile,
editor: Editor,
settings: NoSettings,
sink: InlayHintsSink
) = TabnineLensCollector(editor, supportedElementTypes, isChatEnabled)

override val key: SettingsKey<NoSettings> = 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)
Original file line number Diff line number Diff line change
@@ -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<String>,
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions Tabnine/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ targetCompatibility = 9
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "9"
freeCompilerArgs += "-Xjvm-default=enable"
}
}

Expand Down Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions Tabnine/src/main/java/com/tabnine/chat/lens/TabnineLens.kt
Original file line number Diff line number Diff line change
@@ -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 })
6 changes: 6 additions & 0 deletions Tabnine/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@
<className>com.tabnine.chat.actions.TabnineQuickFixAction</className>
<category>Tabnine intentions</category>
</intentionAction>
<codeInsight.inlayProvider language="JAVA" implementationClass="com.tabnine.chat.lens.TabnineLensJavaProvider" />
<codeInsight.inlayProvider language="TypeScript" implementationClass="com.tabnine.chat.lens.TabnineLensTypescriptProvider" />
<codeInsight.inlayProvider language="Python" implementationClass="com.tabnine.chat.lens.TabnineLensPythonProvider" />
<codeInsight.inlayProvider language="kotlin" implementationClass="com.tabnine.chat.lens.TabnineLensKotlinProvider" />
<codeInsight.inlayProvider language="PHP" implementationClass="com.tabnine.chat.lens.TabnineLensPhpProvider" />
<codeInsight.inlayProvider language="Rust" implementationClass="com.tabnine.chat.lens.TabnineLensRustProvider" />
</extensions>
<actions>
<action
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import javax.annotation.Nonnull;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
Expand Down Expand Up @@ -104,7 +103,7 @@ public static <T> Matcher<Optional<T>> emptyOptional() {
}

@NotNull
public static Matcher<Optional<BinaryVersion>> versionMatch(@Nonnull String version) {
public static Matcher<Optional<BinaryVersion>> versionMatch(@NotNull String version) {
return new BaseMatcher<Optional<BinaryVersion>>() {
@Override
public void describeTo(Description description) {
Expand Down
61 changes: 61 additions & 0 deletions Tabnine/src/test/kotlin/TabnineLensIntegrationTest.kt
Original file line number Diff line number Diff line change
@@ -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<Inlay<*>> {
return myFixture.editor.inlayModel.getBlockElementsInRange(
myFixture.file.textRange.startOffset,
myFixture.file.textRange.endOffset
)
}
}
1 change: 1 addition & 0 deletions TabnineSelfHosted/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ targetCompatibility = 9
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "9"
freeCompilerArgs += "-Xjvm-default=enable"
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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 })
6 changes: 6 additions & 0 deletions TabnineSelfHosted/src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ Other Tabnine AI users, <b> including Tabnine Enterprise SaaS</b>, should use th
<className>com.tabnineSelfHosted.chat.actions.TabnineQuickFixAction</className>
<category>Tabnine intentions</category>
</intentionAction>
<codeInsight.inlayProvider language="JAVA" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensJavaProvider" />
<codeInsight.inlayProvider language="TypeScript" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensTypescriptProvider" />
<codeInsight.inlayProvider language="Python" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensPythonProvider" />
<codeInsight.inlayProvider language="kotlin" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensKotlinProvider" />
<codeInsight.inlayProvider language="PHP" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensPhpProvider" />
<codeInsight.inlayProvider language="Rust" implementationClass="com.tabnineSelfHosted.chat.lens.TabnineLensRustProvider" />
</extensions>
<actions>
<action class="com.tabnineSelfHosted.LoginLogoutAction" id="com.tabnineSelfHosted.LoginLogoutAction" text="Login/Logout of Tabnine">
Expand Down
1 change: 1 addition & 0 deletions TabnineSelfHostedForMarketplace/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ targetCompatibility = 9
tasks.withType(KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = "9"
freeCompilerArgs += "-Xjvm-default=enable"
}
}

Expand Down

0 comments on commit 15641f6

Please sign in to comment.