Skip to content

Commit

Permalink
DEV2-3931 add quick fix action
Browse files Browse the repository at this point in the history
  • Loading branch information
yonip23 committed Oct 8, 2023
1 parent 53abc37 commit 64c2992
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,10 @@ import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.wm.ToolWindowManager
import com.tabnineCommon.chat.ChatBrowser
import com.tabnineCommon.chat.ChatEnabled
import com.tabnineCommon.chat.Consts.CHAT_ICON
import com.tabnineCommon.chat.Consts.CHAT_TOOL_WINDOW_ID
import com.tabnineCommon.general.DependencyContainer
import org.jetbrains.concurrency.runAsync
import com.tabnineCommon.chat.actions.common.ChatActionCommunicator

data class AskChatPayload(private val input: String)

Expand All @@ -35,49 +30,12 @@ class AskChatAction private constructor() : AnAction("Ask Tabnine", "Ask tabnine

override fun actionPerformed(e: AnActionEvent) {
val project = e.project ?: return
val browser = getBrowser(project) ?: return
val ourToolWindow = ToolWindowManager.getInstance(project)
.getToolWindow(CHAT_TOOL_WINDOW_ID) ?: return

val result =
Messages.showInputDialog("What do you have in mind?", "Ask Tabnine", CHAT_ICON)
.takeUnless { it.isNullOrBlank() }
?: return

if (browser.isLoaded()) {
ourToolWindow.activate {
submitMessageToChat(project, result)
}
} else {
browser.registerBrowserLoadedListener(ID) {
runAsync {
Thread.sleep(1000)
submitMessageToChat(project, result)
}
}
ourToolWindow.activate(null)
}
}

private fun submitMessageToChat(project: Project, result: String) {
sendMessage(project, TabnineActionRequest("submit-message", AskChatPayload(result)))
}

private fun sendMessage(project: Project, message: TabnineActionRequest) {
val browser = getBrowser(project) ?: return
val messageJson = DependencyContainer.instanceOfGson().toJson(message)

Logger.getInstance(javaClass).info("Sending message: $messageJson")
browser.jbCefBrowser.cefBrowser.executeJavaScript("window.postMessage($messageJson, '*')", "", 0)
}

private fun getBrowser(project: Project): ChatBrowser? {
val browser = ChatBrowser.getInstance(project)
if (browser == null) {
Logger.getInstance(javaClass).warn("Browser not found on project ${project.name}")
return null
}
return browser
ChatActionCommunicator.sendMessageToChat(project, ID, result)
}

override fun update(e: AnActionEvent) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.tabnineCommon.chat.actions

import com.intellij.application.subscribe
import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl
import com.intellij.codeInsight.intention.IntentionAction
import com.intellij.codeInsight.intention.PsiElementBaseIntentionAction
import com.intellij.lang.annotation.HighlightSeverity
import com.intellij.openapi.Disposable
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Iconable
import com.intellij.openapi.util.TextRange
import com.intellij.psi.PsiElement
import com.intellij.util.Processor
import com.intellij.util.messages.Topic
import com.tabnineCommon.chat.ChatEnabled
import com.tabnineCommon.chat.actions.common.ChatActionCommunicator
import com.tabnineCommon.general.StaticConfig
import javax.swing.Icon

class TabnineQuickFixAction : PsiElementBaseIntentionAction(), IntentionAction, Disposable, Iconable {
// `ChatActionCommunicator.sendMessageToChat` requires an EDT thread, but the `invoke` method of `IntentionAction`
// is not called from the EDT thread.
// Also, when calling `ApplicationManager.getApplication().invokeLater` directly in the `invoke` implementation,
// you get this error: `Side effect not allowed: INVOKE_LATER`.
//
// To work around this issue, we use an internal topic that'll call `ApplicationManager.getApplication().invokeLater`
// in it's own context (thread), circumventing the issue.
private val workaroundTopic = Topic.create("TabnineQuickFixAction", WorkaroundHandler::class.java)

companion object {
private const val FAMILY_NAME: String = "Fix using Tabnine"
private const val ID: String = "com.tabnine.chat.actions.TabnineQuickFixAction"
}

init {
workaroundTopic.subscribe(
this,
object : WorkaroundHandler {
override fun handle(project: Project, value: String) {
ApplicationManager.getApplication().invokeLater {
ChatActionCommunicator.sendMessageToChat(project, ID, value)
}
}
}
)
}

override fun getText(): String {
return FAMILY_NAME
}

override fun getFamilyName(): String {
return FAMILY_NAME
}

override fun isAvailable(project: Project, editor: Editor?, element: PsiElement): Boolean {
if (!ChatEnabled.getInstance().enabled) return false

if (editor == null) return false
val textRangeToSearch = getSelectedRange(editor) ?: element.textRange
var foundFixes = false
DaemonCodeAnalyzerImpl.processHighlights(
editor.document,
project,
HighlightSeverity.WARNING,
textRangeToSearch.startOffset,
textRangeToSearch.endOffset,
Processor {
foundFixes = true
return@Processor false
}
)

return foundFixes
}

override fun invoke(project: Project, editor: Editor?, element: PsiElement) {
ApplicationManager
.getApplication()
.messageBus
.syncPublisher(workaroundTopic)
.handle(project, "/fix-code")
}

private fun getSelectedRange(editor: Editor): TextRange? {
val selectionModel = editor.selectionModel
return if (selectionModel.hasSelection()) {
TextRange(selectionModel.selectionStart, selectionModel.selectionEnd)
} else {
null
}
}

override fun dispose() {
}

override fun getIcon(flags: Int): Icon {
return StaticConfig.getTabnineIcon()
}
}

interface WorkaroundHandler {
fun handle(project: Project, value: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.tabnineCommon.chat.actions.common

import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.ToolWindowManager
import com.tabnineCommon.chat.ChatBrowser
import com.tabnineCommon.chat.Consts
import com.tabnineCommon.chat.actions.AskChatPayload
import com.tabnineCommon.chat.actions.TabnineActionRequest
import com.tabnineCommon.general.DependencyContainer
import org.jetbrains.concurrency.runAsync

object ChatActionCommunicator {
fun sendMessageToChat(project: Project, actionId: String, value: String) {
val browser = getBrowser(project) ?: return
val ourToolWindow = ToolWindowManager.getInstance(project)
.getToolWindow(Consts.CHAT_TOOL_WINDOW_ID) ?: return

if (browser.isLoaded()) {
ourToolWindow.activate {
submitMessageToChat(project, value)
}
} else {
browser.registerBrowserLoadedListener(actionId) {
runAsync {
Thread.sleep(1000)
submitMessageToChat(project, value)
}
}
ourToolWindow.activate(null)
}
}

private fun submitMessageToChat(project: Project, result: String) {
sendMessage(project, TabnineActionRequest("submit-message", AskChatPayload(result)))
}

private fun sendMessage(project: Project, message: TabnineActionRequest) {
val browser = getBrowser(project) ?: return
val messageJson = DependencyContainer.instanceOfGson().toJson(message)

Logger.getInstance(javaClass).info("Sending message: $messageJson")
browser.jbCefBrowser.cefBrowser.executeJavaScript("window.postMessage($messageJson, '*')", "", 0)
}

private fun getBrowser(project: Project): ChatBrowser? {
val browser = ChatBrowser.getInstance(project)
if (browser == null) {
Logger.getInstance(javaClass).warn("Browser not found on project ${project.name}")
return null
}
return browser
}
}
4 changes: 4 additions & 0 deletions Common/src/main/resources/META-INF/common-plugin.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@
id="com.tabnineCommon.userSettings.AppSettingsConfigurable" displayName="Tabnine"/>
<toolWindow factoryClass="com.tabnineCommon.chat.TabnineChatWebViewFactory" id="Tabnine Chat"
anchor="right" canCloseContents="false" icon="/icons/tabnine-tool-window-icon.svg" />
<intentionAction>
<className>com.tabnineCommon.chat.actions.TabnineQuickFixAction</className>
<category>Tabnine intentions</category>
</intentionAction>
</extensions>

<actions>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<html lang="en">
<body>
Use Tabnine to fix the selected code
</body>
</html>

0 comments on commit 64c2992

Please sign in to comment.