diff --git a/Common/src/main/java/com/tabnineCommon/chat/actions/AskChatAction.kt b/Common/src/main/java/com/tabnineCommon/chat/actions/AskChatAction.kt index 32648263a..75e957382 100644 --- a/Common/src/main/java/com/tabnineCommon/chat/actions/AskChatAction.kt +++ b/Common/src/main/java/com/tabnineCommon/chat/actions/AskChatAction.kt @@ -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) @@ -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) { diff --git a/Common/src/main/java/com/tabnineCommon/chat/actions/TabnineQuickFixAction.kt b/Common/src/main/java/com/tabnineCommon/chat/actions/TabnineQuickFixAction.kt new file mode 100644 index 000000000..4e66bf4b9 --- /dev/null +++ b/Common/src/main/java/com/tabnineCommon/chat/actions/TabnineQuickFixAction.kt @@ -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) +} diff --git a/Common/src/main/java/com/tabnineCommon/chat/actions/common/ChatActionCommunicator.kt b/Common/src/main/java/com/tabnineCommon/chat/actions/common/ChatActionCommunicator.kt new file mode 100644 index 000000000..647a8babd --- /dev/null +++ b/Common/src/main/java/com/tabnineCommon/chat/actions/common/ChatActionCommunicator.kt @@ -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 + } +} diff --git a/Common/src/main/resources/META-INF/common-plugin.xml b/Common/src/main/resources/META-INF/common-plugin.xml index 75e524003..6b03f7917 100644 --- a/Common/src/main/resources/META-INF/common-plugin.xml +++ b/Common/src/main/resources/META-INF/common-plugin.xml @@ -121,6 +121,10 @@ id="com.tabnineCommon.userSettings.AppSettingsConfigurable" displayName="Tabnine"/> + + com.tabnineCommon.chat.actions.TabnineQuickFixAction + Tabnine intentions + diff --git a/Common/src/main/resources/intentionDescriptions/TabnineQuickFixAction/description.html b/Common/src/main/resources/intentionDescriptions/TabnineQuickFixAction/description.html new file mode 100644 index 000000000..714c81702 --- /dev/null +++ b/Common/src/main/resources/intentionDescriptions/TabnineQuickFixAction/description.html @@ -0,0 +1,5 @@ + + +Use Tabnine to fix the selected code + + \ No newline at end of file