From 7c5a13d7061916b7cfeb4023b956bfc1910554e8 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Fri, 25 Oct 2024 14:15:01 +0800 Subject: [PATCH] feat(intellij): add chat panel actions. (#3316) --- .../actions/OpenChatToolWindow.kt | 14 -- .../ToggleInlineCompletionTriggerMode.kt | 6 +- .../actions/chat/AddFileToChat.kt | 10 + .../actions/chat/AddSelectionToChat.kt | 14 ++ .../intellijtabby/actions/chat/ChatAction.kt | 43 ++++ .../actions/chat/ChatActionHandler.kt | 11 + .../intellijtabby/actions/chat/Explain.kt | 14 ++ .../tabbyml/intellijtabby/actions/chat/Fix.kt | 14 ++ .../actions/chat/GenerateDocs.kt | 14 ++ .../actions/chat/GenerateTests.kt | 14 ++ .../actions/chat/OpenChatToolWindow.kt | 14 ++ .../actions/chat/ToggleChatToolWindow.kt | 38 +++ .../actions/inlineCompletion/TabAccept.kt | 8 +- .../Trigger.kt} | 5 +- .../tabbyml/intellijtabby/chat/ChatBrowser.kt | 223 ++++++++++++------ .../intellijtabby/chat/ChatBrowserFactory.kt | 28 +++ .../intellijtabby/settings/KeymapSettings.kt | 6 +- .../widgets/ChatToolWindowFactory.kt | 12 +- .../src/main/resources/META-INF/plugin.xml | 73 +++++- 19 files changed, 448 insertions(+), 113 deletions(-) delete mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenChatToolWindow.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt rename clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/{TriggerInlineCompletion.kt => inlineCompletion/Trigger.kt} (91%) create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenChatToolWindow.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenChatToolWindow.kt deleted file mode 100644 index b546fd21501b..000000000000 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/OpenChatToolWindow.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.tabbyml.intellijtabby.actions - -import com.intellij.openapi.actionSystem.AnAction -import com.intellij.openapi.actionSystem.AnActionEvent -import com.intellij.openapi.wm.ToolWindow -import com.intellij.openapi.wm.ToolWindowManager - -class OpenChatToolWindow : AnAction() { - override fun actionPerformed(e: AnActionEvent) { - val toolWindowManager: ToolWindowManager = e.project?.let { ToolWindowManager.getInstance(it) } ?: return - val toolWindow: ToolWindow = toolWindowManager.getToolWindow("Tabby") ?: return - toolWindow.show() - } -} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt index c44e8398cbf4..d0102e0a7098 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/ToggleInlineCompletionTriggerMode.kt @@ -20,10 +20,10 @@ class ToggleInlineCompletionTriggerMode : AnAction() { override fun update(e: AnActionEvent) { if (settings.completionTriggerMode == SettingsState.TriggerMode.AUTOMATIC) { - e.presentation.text = "Switch to Manual Mode" - e.presentation.description = "Manual trigger inline completion suggestions on demand." + e.presentation.text = "Disable Auto Inline Completion" + e.presentation.description = "You can trigger inline completion manually." } else { - e.presentation.text = "Switch to Automatic Mode" + e.presentation.text = "Enable Auto Inline Completion" e.presentation.description = "Show inline completion suggestions automatically." } } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt new file mode 100644 index 000000000000..0ed1d9f01753 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddFileToChat.kt @@ -0,0 +1,10 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class AddFileToChat : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.addActiveEditorAsContext(false) + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt new file mode 100644 index 000000000000..f66bc9418963 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/AddSelectionToChat.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class AddSelectionToChat : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.addActiveEditorAsContext(true) + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt new file mode 100644 index 000000000000..b7888bb135ff --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatAction.kt @@ -0,0 +1,43 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.editor.actionSystem.EditorActionHandler +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindowManager +import com.tabbyml.intellijtabby.chat.ChatBrowser +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory +import com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory + +abstract class ChatAction(private val chatActionHandler: ChatActionHandler) : + EditorAction(object : EditorActionHandler() { + private fun openChatToolWindow(project: Project, runnable: Runnable?) { + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return + toolWindow.show(runnable) + } + + private fun findActiveChatBrowser(editor: Editor): ChatBrowser? { + val project = editor.project ?: return null + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return null + val chatBrowserFactory = project.service() + return chatBrowserFactory.getChatBrowser(toolWindow) + } + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + val project = editor.project ?: return + openChatToolWindow(project) { + val chatBrowser = findActiveChatBrowser(editor) ?: return@openChatToolWindow + chatActionHandler.doExecute(editor, chatBrowser) + } + } + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext?): Boolean { + val chatBrowser = findActiveChatBrowser(editor) + return chatActionHandler.isEnabled(editor, chatBrowser) + } + }) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt new file mode 100644 index 000000000000..a1734cc1c233 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ChatActionHandler.kt @@ -0,0 +1,11 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +interface ChatActionHandler { + fun doExecute(editor: Editor, chatBrowser: ChatBrowser) + fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return true + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt new file mode 100644 index 000000000000..041e90b78903 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Explain.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class Explain : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.explainSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt new file mode 100644 index 000000000000..1ba7bbbc705d --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/Fix.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class Fix : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.fixSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt new file mode 100644 index 000000000000..678096048e39 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateDocs.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class GenerateDocs : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.generateDocsForSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt new file mode 100644 index 000000000000..fdba03b0ce43 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/GenerateTests.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.editor.Editor +import com.tabbyml.intellijtabby.chat.ChatBrowser + +class GenerateTests : ChatAction(object : ChatActionHandler { + override fun doExecute(editor: Editor, chatBrowser: ChatBrowser) { + chatBrowser.generateTestsForSelectedText() + } + + override fun isEnabled(editor: Editor, chatBrowser: ChatBrowser?): Boolean { + return editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() } + } +}) \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt new file mode 100644 index 000000000000..3a021de35d52 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/OpenChatToolWindow.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.wm.ToolWindowManager +import com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory + +class OpenChatToolWindow : AnAction() { + override fun actionPerformed(e: AnActionEvent) { + val toolWindowManager = e.project?.let { ToolWindowManager.getInstance(it) } ?: return + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return + toolWindow.show() + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt new file mode 100644 index 000000000000..7cf151c3752f --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/chat/ToggleChatToolWindow.kt @@ -0,0 +1,38 @@ +package com.tabbyml.intellijtabby.actions.chat + +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.wm.IdeFocusManager +import com.intellij.openapi.wm.ToolWindowManager +import com.tabbyml.intellijtabby.actionPromoter.HasPriority +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory +import com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory + +class ToggleChatToolWindow : AnAction(), HasPriority { + override fun actionPerformed(e: AnActionEvent) { + val project = e.project ?: return + val toolWindowManager = ToolWindowManager.getInstance(project) + val toolWindow = toolWindowManager.getToolWindow(ChatToolWindowFactory.TOOL_WINDOW_ID) ?: return + + val editor = FileEditorManager.getInstance(project).selectedTextEditor + val chatBrowserFactory = project.service() + val chatBrowser = chatBrowserFactory.getChatBrowser(toolWindow) + if (toolWindow.isActive) { + if (editor != null) { + IdeFocusManager.getInstance(project).requestFocus(editor.contentComponent, true) + } + } else { + toolWindow.show { + toolWindow.activate { + if (editor != null && chatBrowser != null && editor.selectionModel.let { it.hasSelection() && !it.selectedText.isNullOrBlank() }) { + chatBrowser.addActiveEditorAsContext(true) + } + } + } + } + } + + override val priority: Int = 1 +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt index 7b0f9d115dc7..5dfa8bf74005 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/TabAccept.kt @@ -1,6 +1,5 @@ package com.tabbyml.intellijtabby.actions.inlineCompletion -import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.tabbyml.intellijtabby.completion.InlineCompletionService @@ -17,9 +16,4 @@ class TabAccept : InlineCompletionAction(object : InlineCompletionActionHandler ): Boolean { return !inlineCompletionService.isInlineCompletionStartWithIndentation() } -}) { - override fun update(e: AnActionEvent) { - super.update(e) - e.presentation.isVisible = false - } -} +}) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerInlineCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt similarity index 91% rename from clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerInlineCompletion.kt rename to clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt index a8121ad84bef..12a8948202c2 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerInlineCompletion.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/inlineCompletion/Trigger.kt @@ -1,4 +1,4 @@ -package com.tabbyml.intellijtabby.actions +package com.tabbyml.intellijtabby.actions.inlineCompletion import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction @@ -8,8 +8,7 @@ import com.intellij.openapi.components.serviceOrNull import com.tabbyml.intellijtabby.actionPromoter.HasPriority import com.tabbyml.intellijtabby.completion.InlineCompletionService - -class TriggerInlineCompletion : AnAction(), HasPriority { +class Trigger : AnAction(), HasPriority { override fun actionPerformed(e: AnActionEvent) { val inlineCompletionService = e.getRequiredData(CommonDataKeys.PROJECT).serviceOrNull() ?: return diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt index 8764714ee731..b61b7ecb2199 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowser.kt @@ -13,6 +13,7 @@ import com.intellij.openapi.editor.colors.EditorColorsListener import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.editor.colors.EditorFontType import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.ui.jcef.JBCefBrowser @@ -33,24 +34,23 @@ import java.awt.datatransfer.StringSelection import java.io.File -class ChatBrowser(private val project: Project) { +class ChatBrowser(private val project: Project) : JBCefBrowser() { private val logger = Logger.getInstance(ChatBrowser::class.java) private val gson = Gson() private val combinedState = project.service() private val gitProvider = project.service() private val messageBusConnection = project.messageBus.connect() - private val browser = JBCefBrowser() - private val reloadHandler = JBCefJSQuery.create(browser as JBCefBrowserBase) - private val chatPanelRequestHandler = JBCefJSQuery.create(browser as JBCefBrowserBase) + private val reloadHandler = JBCefJSQuery.create(this as JBCefBrowserBase) + private val chatPanelRequestHandler = JBCefJSQuery.create(this as JBCefBrowserBase) private var currentConfig: ServerInfo.ServerInfoConfig? = null - - val browserComponent = browser.component + private var isChatPanelLoaded = false + private val pendingScripts: MutableList = mutableListOf() private data class ChatPanelRequest( val method: String, - val params: List, + val params: List, ) private data class FileContext( @@ -68,9 +68,9 @@ class ChatBrowser(private val project: Project) { } init { - browserComponent.isVisible = false + component.isVisible = false - browser.jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { + jbCefClient.addLoadHandler(object : CefLoadHandlerAdapter() { override fun onLoadingStateChange( browser: CefBrowser?, isLoading: Boolean, @@ -81,7 +81,7 @@ class ChatBrowser(private val project: Project) { handleLoaded() } } - }, browser.cefBrowser) + }, cefBrowser) reloadHandler.addHandler { reloadContent(true) @@ -94,7 +94,7 @@ class ChatBrowser(private val project: Project) { return@addHandler JBCefJSQuery.Response("") } - this.browser.loadHTML(HTML_CONTENT) + loadHTML(HTML_CONTENT) messageBusConnection.subscribe(CombinedState.Listener.TOPIC, object : CombinedState.Listener { override fun stateChanged(state: CombinedState.State) { @@ -109,11 +109,91 @@ class ChatBrowser(private val project: Project) { }) } + fun explainSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + val context = getActiveFileContext() + chatPanelSendMessage(PROMPT_EXPLAIN, context) + } + } + + fun fixSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + // FIXME(@icycodes): collect the diagnostic message provided by IDE + val context = getActiveFileContext() + chatPanelSendMessage(PROMPT_FIX, context) + } + } + + fun generateDocsForSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + val context = getActiveFileContext() + chatPanelSendMessage(PROMPT_GENERATE_DOCS, context) + } + } + + fun generateTestsForSelectedText() { + BackgroundTaskUtil.executeOnPooledThread(this) { + val context = getActiveFileContext() + chatPanelSendMessage(PROMPT_GENERATE_TESTS, context) + } + } + + fun addActiveEditorAsContext(useSelectedText: Boolean) { + BackgroundTaskUtil.executeOnPooledThread(this) { + val context = getActiveFileContext(useSelectedText) ?: return@executeOnPooledThread + chatPanelAddRelevantContext(context) + } + } + + private fun getActiveFileContext(useSelectedText: Boolean = true): FileContext? { + return FileEditorManager.getInstance(project).selectedTextEditor?.let { editor -> + ReadAction.compute?, Throwable> { + val document = editor.document + if (useSelectedText) { + val selectionModel = editor.selectionModel + val text = selectionModel.selectedText.takeUnless { it.isNullOrBlank() } ?: return@compute null + Triple( + text, + document.getLineNumber(selectionModel.selectionStart) + 1, + document.getLineNumber(selectionModel.selectionEnd) + 1, + ) + } else { + val text = document.text.takeUnless { it.isBlank() } ?: return@compute null + Triple( + text, + 1, + document.lineCount, + ) + } + }?.let { context -> + val uri = editor.virtualFile?.url + val gitRepo = uri?.let { gitProvider.getRepository(it) } + val relativeBase = gitRepo?.root ?: project.guessProjectDir()?.url + val relativePath = uri?.let { + if (!relativeBase.isNullOrBlank() && it.startsWith(relativeBase)) { + it.substringAfter(relativeBase).trimStart(File.separatorChar) + } else it + } + logger.debug("Active context: context: $context, uri: $uri, gitRepo: $gitRepo, relativePath: $relativePath, relativeBase: $relativeBase") + + FileContext( + range = FileContext.LineRange( + start = context.second, + end = context.third, + ), + filepath = relativePath ?: "", + content = context.first, + gitUrl = gitRepo?.remotes?.firstOrNull()?.url ?: "", + ) + } + } + } + private fun handleLoaded() { jsInjectHandlers() jsApplyStyle() reloadContent() - browserComponent.isVisible = true + component.isVisible = true } private val isDarkTheme get() = EditorColorsManager.getInstance().isDarkEditor @@ -175,8 +255,11 @@ class ChatBrowser(private val project: Project) { showContent(error) } else { val config = combinedState.state.agentServerInfo?.config - if (config != null && (force || currentConfig != config)) { - showContent("Loading Tabby chat panel...") + if (config == null) { + showContent("Initializing...") + } else if (force || currentConfig != config) { + showContent("Loading chat panel...") + isChatPanelLoaded = false currentConfig = config jsLoadChatPanel() } @@ -214,41 +297,8 @@ class ChatBrowser(private val project: Project) { val relevantContext: List? = request.params.getOrNull(1)?.let { gson.fromJson(gson.toJson(it), object : TypeToken?>() {}.type) } - val activeContext = FileEditorManager.getInstance(project).selectedTextEditor?.let { editor -> - ReadAction.compute?, Throwable> { - val selectionModel = editor.selectionModel - val document = editor.document - val text = selectionModel.selectedText - val startLine = document.getLineNumber(selectionModel.selectionStart) + 1 - val endLine = document.getLineNumber(selectionModel.selectionEnd) + 1 - if (!text.isNullOrBlank()) { - Triple(text, startLine, endLine) - } else { - null - } - }?.let { selection -> - val uri = editor.virtualFile?.url - val gitRepo = uri?.let { gitProvider.getRepository(it) } - val relativeBase = gitRepo?.root ?: project.guessProjectDir()?.url - val relativePath = uri?.let { - if (!relativeBase.isNullOrBlank() && it.startsWith(relativeBase)) { - it.substringAfter(relativeBase).trimStart(File.separatorChar) - } else it - } - logger.debug("Active selection: selection: $selection, uri: $uri, gitRepo: $gitRepo, relativePath: $relativePath, relativeBase: $relativeBase") - - FileContext( - range = FileContext.LineRange( - start = selection.second, - end = selection.third, - ), - filepath = relativePath ?: "", - content = selection.first, - gitUrl = gitRepo?.remotes?.firstOrNull()?.url ?: "", - ) - } - } - sendMessage(message, null, relevantContext, activeContext) + val activeContext = getActiveFileContext() + chatPanelSendMessage(message, null, relevantContext, activeContext) } } @@ -277,9 +327,12 @@ class ChatBrowser(private val project: Project) { return } } + isChatPanelLoaded = true chatPanelInit() chatPanelUpdateTheme() showContent() + pendingScripts.forEach { executeJs(it) } + pendingScripts.clear() } "onCopy" -> { @@ -313,11 +366,11 @@ class ChatBrowser(private val project: Project) { jsSendRequestToChatPanel(request) } - private fun sendMessage( + private fun chatPanelSendMessage( message: String, - selectContext: FileContext?, - relevantContext: List?, - activeContext: FileContext? + selectContext: FileContext? = null, + relevantContext: List? = null, + activeContext: FileContext? = null, ) { val request = ChatPanelRequest( "sendMessage", @@ -330,7 +383,16 @@ class ChatBrowser(private val project: Project) { ) ) ) - logger.debug("sendMessage: $request") + logger.debug("chatPanelSendMessage: $request") + jsSendRequestToChatPanel(request) + } + + private fun chatPanelAddRelevantContext(context: FileContext) { + val request = ChatPanelRequest( + "addRelevantContext", + listOf(context) + ) + logger.debug("chatPanelAddRelevantContext: $request") jsSendRequestToChatPanel(request) } @@ -346,6 +408,15 @@ class ChatBrowser(private val project: Project) { jsSendRequestToChatPanel(request) } + private fun chatPanelUpdateActiveSelection(context: FileContext?) { + val request = ChatPanelRequest( + "updateActiveSelection", + listOf(context) + ) + logger.debug("chatPanelUpdateActiveSelection: $request") + jsSendRequestToChatPanel(request) + } + // js functions private fun jsInjectHandlers() { @@ -357,7 +428,7 @@ class ChatBrowser(private val project: Project) { reloadHandler.inject(""), chatPanelRequestHandler.inject("message"), ) - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + executeJs(script) } private fun jsApplyStyle() { @@ -365,30 +436,38 @@ class ChatBrowser(private val project: Project) { "applyStyle('%s')", gson.toJson(mapOf("theme" to if (isDarkTheme) "dark" else "light", "css" to buildCss())) ) - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + executeJs(script) } private fun jsShowMessage(message: String?) { val script = if (message != null) "showMessage('${message}')" else "showMessage()" - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + executeJs(script) } private fun jsShowChatPanel(visible: Boolean) { val script = String.format("showChatPanel(%s)", if (visible) "true" else "false") - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + executeJs(script) } private fun jsLoadChatPanel() { val config = currentConfig ?: return val chatUrl = String.format("%s/chat?client=intellij", config.endpoint) val script = String.format("loadChatPanel('%s')", chatUrl) - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + executeJs(script) } private fun jsSendRequestToChatPanel(request: ChatPanelRequest) { val json = gson.toJson(request) val script = String.format("sendRequestToChatPanel('%s')", escapeCharacters(json)) - browser.cefBrowser.executeJavaScript(script, browser.cefBrowser.url, 0) + if (isChatPanelLoaded) { + executeJs(script) + } else { + pendingScripts.add(script) + } + } + + private fun executeJs(script: String) { + cefBrowser.executeJavaScript(script, cefBrowser.url, 0) } companion object { @@ -485,6 +564,12 @@ class ChatBrowser(private val project: Project) { private const val TABBY_CHAT_PANEL_API_VERSION_RANGE = "~0.2.0" private const val TABBY_SERVER_VERSION_RANGE = ">=0.18.0" + + private const val PROMPT_EXPLAIN: String = "Explain the selected code:" + private const val PROMPT_FIX: String = "Identify and fix potential bugs in the selected code:" + private const val PROMPT_GENERATE_DOCS: String = "Generate documentation for the selected code:" + private const val PROMPT_GENERATE_TESTS: String = "Generate a unit test for the selected code:" + private const val HTML_CONTENT = """ @@ -599,18 +684,20 @@ class ChatBrowser(private val project: Project) { ); } + window.addEventListener("focus", (event) => { + const chat = getChatPanel(); + if (chat.style.cssText == "display: block;") { + setTimeout(() => { + chat.contentWindow.focus(); + }, 1); + } + }); + window.addEventListener("message", (event) => { const chat = getChatPanel(); // server to client requests - if (event.source === chat.contentWindow) { - // handle copy action - if (typeof event.data === "object" && "action" in event.data && event.data.action === "copy") { - if (navigator.clipboard?.writeText) { - navigator.clipboard.writeText(event.data.data); - } - } - + if (event.source === chat.contentWindow) { // adapter for @quilted/threads requests if (Array.isArray(event.data) && event.data.length >= 2) { const [kind, data] = event.data; diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt new file mode 100644 index 000000000000..793df9f59001 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatBrowserFactory.kt @@ -0,0 +1,28 @@ +package com.tabbyml.intellijtabby.chat + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.ToolWindow + +@Service(Service.Level.PROJECT) +class ChatBrowserFactory(private val project: Project) : Disposable { + private val registry: MutableMap = mutableMapOf() + + fun createChatBrowser(toolWindow: ToolWindow): ChatBrowser { + val chatBrowser = ChatBrowser(project) + registry[toolWindow] = chatBrowser + return chatBrowser + } + + fun getChatBrowser(toolWindow: ToolWindow): ChatBrowser? { + return registry[toolWindow] + } + + override fun dispose() { + registry.forEach { + it.value.dispose() + } + registry.clear() + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt index c7250bdc97c0..59699c2e6908 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/settings/KeymapSettings.kt @@ -74,22 +74,24 @@ class KeymapSettings(private val project: Project) { companion object { private val DEFAULT_KEYMAP_SCHEMA = mapOf( - "Tabby.TriggerInlineCompletion" to listOf( + "Tabby.InlineCompletion.Trigger" to listOf( KeyboardShortcut.fromString("ctrl BACK_SLASH"), KeyboardShortcut.fromString("alt BACK_SLASH") ), "Tabby.InlineCompletion.TabAccept" to listOf(KeyboardShortcut.fromString("TAB")), "Tabby.InlineCompletion.AcceptNextLine" to listOf(KeyboardShortcut.fromString("ctrl TAB")), "Tabby.InlineCompletion.AcceptNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), "Tabby.InlineCompletion.Dismiss" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.Chat.ToggleChatToolWindow" to listOf(KeyboardShortcut.fromString("ctrl L")), ) private val TABBY_STYLE_KEYMAP_SCHEMA = mapOf( - "Tabby.TriggerInlineCompletion" to listOf( + "Tabby.InlineCompletion.Trigger" to listOf( KeyboardShortcut.fromString("ctrl BACK_SLASH"), KeyboardShortcut.fromString("alt BACK_SLASH") ), "Tabby.InlineCompletion.TabAccept" to listOf(KeyboardShortcut.fromString("ctrl TAB")), "Tabby.InlineCompletion.AcceptNextLine" to listOf(KeyboardShortcut.fromString("TAB")), "Tabby.InlineCompletion.AcceptNextWord" to listOf(KeyboardShortcut.fromString("ctrl RIGHT")), "Tabby.InlineCompletion.Dismiss" to listOf(KeyboardShortcut.fromString("ESCAPE")), + "Tabby.Chat.ToggleChatToolWindow" to listOf(KeyboardShortcut.fromString("ctrl L")), ) } } \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt index 8eaf6e3daed6..7fa0b0fbcbc7 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/widgets/ChatToolWindowFactory.kt @@ -1,18 +1,22 @@ package com.tabbyml.intellijtabby.widgets +import com.intellij.openapi.components.service import com.intellij.openapi.project.DumbAware import com.intellij.openapi.project.Project import com.intellij.openapi.wm.ToolWindow import com.intellij.openapi.wm.ToolWindowFactory import com.intellij.ui.content.ContentFactory -import com.tabbyml.intellijtabby.chat.ChatBrowser +import com.tabbyml.intellijtabby.chat.ChatBrowserFactory class ChatToolWindowFactory : ToolWindowFactory, DumbAware { override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) { - val browser = ChatBrowser(project) - val content = ContentFactory.getInstance().createContent(browser.browserComponent, "", false) + val chatBrowserFactory = project.service() + val browser = chatBrowserFactory.createChatBrowser(toolWindow) + val content = ContentFactory.getInstance().createContent(browser.component, "", false) toolWindow.contentManager.addContent(content) } - override fun shouldBeAvailable(project: Project) = true + companion object { + const val TOOL_WINDOW_ID = "Tabby" + } } \ No newline at end of file diff --git a/clients/intellij/src/main/resources/META-INF/plugin.xml b/clients/intellij/src/main/resources/META-INF/plugin.xml index bdf39a3b3c47..fa6bf3f63254 100644 --- a/clients/intellij/src/main/resources/META-INF/plugin.xml +++ b/clients/intellij/src/main/resources/META-INF/plugin.xml @@ -58,14 +58,14 @@ id="Tabby" anchor="right" icon="com.tabbyml.intellijtabby.Icons.Chat" - factoryClass="com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory" /> + factoryClass="com.tabbyml.intellijtabby.widgets.ChatToolWindowFactory"/> - @@ -78,12 +78,6 @@ text="Accept Completion" description="Accept the shown suggestions and insert the text."> - - - - + + class="com.tabbyml.intellijtabby.actions.ToggleInlineCompletionTriggerMode" + text="Toggle Auto Inline Completion" + description="Enable or disable the automatic inline completion."> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file