From 0a97d1213d3aca8f7ac1bc283c096b87f7b26c8e Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Mon, 6 Jan 2025 23:01:52 +0800 Subject: [PATCH] fix(intellij): update chat panel api version 0.5.0 (#3644) * fix(intellij): update chat panel api version 0.5.0. * fix: lint. --- .../tabbyml/intellijtabby/chat/ChatBrowser.kt | 312 ++++++++++-------- .../intellijtabby/chat/ChatPanelData.kt | 113 +++++++ .../events/EditorFactoryListener.kt | 10 + .../intellijtabby/events/SelectionListener.kt | 14 + 4 files changed, 309 insertions(+), 140 deletions(-) create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt create mode 100644 clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt 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 a94930636410..e1e2ad640dc8 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 @@ -1,7 +1,6 @@ package com.tabbyml.intellijtabby.chat import com.google.gson.Gson -import com.google.gson.annotations.SerializedName import com.google.gson.reflect.TypeToken import com.intellij.ide.BrowserUtil import com.intellij.ide.plugins.PluginManagerCore @@ -11,12 +10,17 @@ import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.service import com.intellij.openapi.components.serviceOrNull import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.colors.EditorColors 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.editor.event.SelectionEvent import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener import com.intellij.openapi.fileEditor.OpenFileDescriptor import com.intellij.openapi.progress.util.BackgroundTaskUtil import com.intellij.openapi.project.Project @@ -26,10 +30,12 @@ import com.intellij.ui.jcef.JBCefBrowser import com.intellij.ui.jcef.JBCefBrowserBase import com.intellij.ui.jcef.JBCefJSQuery import com.tabbyml.intellijtabby.events.CombinedState +import com.tabbyml.intellijtabby.events.SelectionListener import com.tabbyml.intellijtabby.findVirtualFile import com.tabbyml.intellijtabby.git.GitProvider import com.tabbyml.intellijtabby.lsp.ConnectionService import com.tabbyml.intellijtabby.lsp.ConnectionService.InitializationException +import com.tabbyml.intellijtabby.lsp.positionInDocument import com.tabbyml.intellijtabby.lsp.protocol.Config import com.tabbyml.intellijtabby.lsp.protocol.StatusInfo import com.tabbyml.intellijtabby.lsp.protocol.StatusRequestParams @@ -37,9 +43,7 @@ import io.github.z4kn4fein.semver.Version import io.github.z4kn4fein.semver.constraints.Constraint import io.github.z4kn4fein.semver.constraints.satisfiedBy import io.ktor.http.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import kotlinx.coroutines.* import org.cef.browser.CefBrowser import org.cef.handler.CefLoadHandlerAdapter import java.awt.Color @@ -66,29 +70,18 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( private val gson = Gson() private val combinedState = project.service() private val gitProvider = project.service() + private val fileEditorManager = FileEditorManager.getInstance(project) private val messageBusConnection = project.messageBus.connect() private val scope = CoroutineScope(Dispatchers.IO) + private var syncChatPanelActiveSelectionJob: Job? = null + private suspend fun getServer() = project.serviceOrNull()?.getServerAsync() private var currentConfig: Config.ServerConfig? = null private var isChatPanelLoaded = false private val pendingScripts: MutableList = mutableListOf() - private data class FileContext( - val kind: String = "file", - val range: LineRange, - val filepath: String, - val content: String, - @SerializedName("git_url") - val gitUrl: String, - ) { - data class LineRange( - val start: Int, - val end: Int, - ) - } - init { component.isVisible = false val bgColor = calcComponentBgColor() @@ -118,6 +111,32 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( } }) + messageBusConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + syncChatPanelActiveSelectionJob?.cancel() + syncChatPanelActiveSelectionJob = scope.launch { + BackgroundTaskUtil.executeOnPooledThread(this@ChatBrowser) { + val context = getActiveEditorFileContext() + chatPanelUpdateActiveSelection(context) + } + } + } + }) + + messageBusConnection.subscribe(SelectionListener.TOPIC, object : SelectionListener { + override fun selectionChanged(editor: Editor, event: SelectionEvent) { + if (editor == fileEditorManager.selectedTextEditor) { + syncChatPanelActiveSelectionJob?.cancel() + syncChatPanelActiveSelectionJob = scope.launch { + delay(100) + BackgroundTaskUtil.executeOnPooledThread(this@ChatBrowser) { + val context = getActiveEditorFileContext() + chatPanelUpdateActiveSelection(context) + } + } + } + } + }) messageBusConnection.subscribe(EditorColorsManager.TOPIC, EditorColorsListener { BackgroundTaskUtil.executeOnPooledThread(this) { logger.debug("EditorColorsManager globalSchemeChange received, updating style.") @@ -130,94 +149,128 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( fun explainSelectedText() { BackgroundTaskUtil.executeOnPooledThread(this) { - val context = getActiveFileContext() - chatPanelSendMessage(PROMPT_EXPLAIN, context) + chatPanelExecuteCommand(ChatCommand.EXPLAIN) } } fun fixSelectedText() { BackgroundTaskUtil.executeOnPooledThread(this) { // FIXME(@icycodes): collect the diagnostic message provided by IDE - val context = getActiveFileContext() - chatPanelSendMessage(PROMPT_FIX, context) + chatPanelExecuteCommand(ChatCommand.FIX) } } fun generateDocsForSelectedText() { BackgroundTaskUtil.executeOnPooledThread(this) { - val context = getActiveFileContext() - chatPanelSendMessage(PROMPT_GENERATE_DOCS, context) + chatPanelExecuteCommand(ChatCommand.GENERATE_DOCS) } } fun generateTestsForSelectedText() { BackgroundTaskUtil.executeOnPooledThread(this) { - val context = getActiveFileContext() - chatPanelSendMessage(PROMPT_GENERATE_TESTS, context) + chatPanelExecuteCommand(ChatCommand.GENERATE_TESTS) } } fun addActiveEditorAsContext(useSelectedText: Boolean) { BackgroundTaskUtil.executeOnPooledThread(this) { - val context = getActiveFileContext(useSelectedText) ?: return@executeOnPooledThread + val context = getActiveEditorFileContext(useSelectedText) ?: return@executeOnPooledThread chatPanelAddRelevantContext(context) } } - private fun getActiveFileContext(useSelectedText: Boolean = true): FileContext? { - return FileEditorManager.getInstance(project).selectedTextEditor?.let { editor -> - runReadAction { - val document = editor.document - if (useSelectedText) { - val selectionModel = editor.selectionModel - val text = selectionModel.selectedText.takeUnless { it.isNullOrBlank() } ?: return@runReadAction null - Triple( - text, - document.getLineNumber(selectionModel.selectionStart) + 1, - document.getLineNumber(selectionModel.selectionEnd) + 1, - ) - } else { - val text = document.text.takeUnless { it.isBlank() } ?: return@runReadAction null - Triple( - text, - 1, - document.lineCount, + private fun getActiveEditorFileContext(useSelectedText: Boolean = true): EditorFileContext? { + val editor = fileEditorManager.selectedTextEditor ?: return null + val uri = editor.virtualFile?.url ?: return null + + val context = runReadAction { + val document = editor.document + if (useSelectedText) { + val selectionModel = editor.selectionModel + val text = selectionModel.selectedText.takeUnless { it.isNullOrBlank() } ?: return@runReadAction null + Pair( + text, + PositionRange( + positionOneBasedInDocument(document, selectionModel.selectionStart), + positionOneBasedInDocument(document, selectionModel.selectionEnd), ) - } - }?.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?.let { getDefaultRemoteUrl(it) } ?: "", + ) + } else { + val text = document.text.takeUnless { it.isBlank() } ?: return@runReadAction null + Pair( + text, + null, ) } + } ?: return null + + val gitRepo = gitProvider.getRepository(uri) + val gitUrl = gitRepo?.let { getDefaultRemoteUrl(it) } + + val filepath = if (gitUrl != null && uri.startsWith(gitRepo.root)) { + val relativePath = uri.substringAfter(gitRepo.root).trimStart(File.separatorChar) + FilepathInGitRepository( + filepath = relativePath, + gitUrl = gitUrl, + ) + } else { + FilepathUri(uri = uri) } + + val editorFileContext = EditorFileContext( + filepath = filepath, + range = context.second, + content = context.first, + ) + + logger.debug("Collected active editor file context: $editorFileContext") + return editorFileContext } - private fun navigateToFileContext(fileContext: FileContext) { - val virtualFile = project.findVirtualFile(fileContext.filepath) - ?: gitRemoteUrlToLocalRoot[fileContext.gitUrl]?.let { project.findVirtualFile(it.appendUrlPathSegments(fileContext.filepath)) } - ?: project.guessProjectDir()?.url?.let { project.findVirtualFile(it.appendUrlPathSegments(fileContext.filepath)) } - ?: return + private fun openInEditor(fileLocation: FileLocation): Boolean { + val filepath = fileLocation.filepath + val virtualFile = when (filepath.kind) { + Filepath.Kind.URI -> { + val filepathUri = filepath as FilepathUri + project.findVirtualFile(filepathUri.uri) + } + + Filepath.Kind.GIT -> { + val filepathInGit = filepath as FilepathInGitRepository + val gitLocalRoot = gitRemoteUrlToLocalRoot[filepathInGit.gitUrl] + gitLocalRoot?.let { + project.findVirtualFile(it.appendUrlPathSegments(filepathInGit.filepath)) + } + } + + else -> { + null + } + } ?: return false + + val location = fileLocation.location + val position = if (location is Number) { + Position(location.toInt() - 1, 0) + } else if (location is Position) { + location + } else if (location is LineRange) { + Position(location.start - 1, 0) + } else if (location is PositionRange) { + location.start + } else { + null + } ?: return false + invokeLater { - val lineNumber = (fileContext.range.start - 1).coerceAtLeast(0) - val descriptor = OpenFileDescriptor(project, virtualFile, lineNumber, 0) - FileEditorManager.getInstance(project).openTextEditor(descriptor, true) + val descriptor = OpenFileDescriptor( + project, + virtualFile, + position.line.coerceAtLeast(0), + position.character.coerceAtLeast(0) + ) + fileEditorManager.openTextEditor(descriptor, true) } + return true } private fun handleLoaded() { @@ -337,26 +390,13 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( jsChatPanelClientInvoke("init", params) } - private fun chatPanelSendMessage( - message: String, - selectContext: FileContext? = null, - relevantContext: List? = null, - activeContext: FileContext? = null, - ) { - val params = - listOf( - mapOf( - "message" to message, - "selectContext" to selectContext, - "relevantContext" to relevantContext, - "activeContext" to activeContext, - ) - ) - logger.debug("chatPanelSendMessage: $params") - jsChatPanelClientInvoke("sendMessage", params) + private fun chatPanelExecuteCommand(command: String) { + val params = listOf(command) + logger.debug("chatPanelExecuteCommand: $params") + jsChatPanelClientInvoke("executeCommand", params) } - private fun chatPanelAddRelevantContext(context: FileContext) { + private fun chatPanelAddRelevantContext(context: EditorFileContext) { val params = listOf(context) logger.debug("chatPanelAddRelevantContext: $params") @@ -373,7 +413,7 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( jsChatPanelClientInvoke("updateTheme", params) } - private fun chatPanelUpdateActiveSelection(context: FileContext?) { + private fun chatPanelUpdateActiveSelection(context: EditorFileContext?) { val params = listOf(context) logger.debug("chatPanelUpdateActiveSelection: $params") jsChatPanelClientInvoke("updateActiveSelection", params) @@ -406,40 +446,15 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( private val jsReloadContent = createJsFunction { reloadContent(true) } - private val jsHandleChatPanelNavigate = createJsFunction { params -> - logger.debug("navigate: $params") - val context = params.getOrNull(0)?.let { - gson.fromJson(gson.toJson(it), FileContext::class.java) - } ?: return@createJsFunction Unit - val options = params.getOrNull(1) as Map<*, *>? - if (options?.get("openInEditor") == true) { - navigateToFileContext(context) - } else { - currentConfig?.let { buildCodeBrowserUrl(it, context) }?.let { BrowserUtil.browse(it) } - } - } - private val jsHandleChatPanelRefresh = createJsFunction { logger.debug("refresh") reloadContent(true) } - private val jsHandleChatPanelOnSubmitMessage = createJsFunction { params -> - logger.debug("onSubmitMessage: $params") - if (params.isNotEmpty()) { - val message = params[0] as String - val relevantContext: List? = params.getOrNull(1)?.let { - gson.fromJson(gson.toJson(it), object : TypeToken?>() {}.type) - } - val activeContext = getActiveFileContext() - chatPanelSendMessage(message, null, relevantContext, activeContext) - } - } - private val jsHandleChatPanelOnApplyInEditor = createJsFunction { params -> logger.debug("onApplyInEditor: $params") val content = params.getOrNull(0) as String? ?: return@createJsFunction Unit - val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return@createJsFunction Unit + val editor = fileEditorManager.selectedTextEditor ?: return@createJsFunction Unit invokeLater { WriteCommandAction.runWriteCommandAction(project) { val start = editor.selectionModel.selectionStart @@ -462,11 +477,11 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( } } isChatPanelLoaded = true + pendingScripts.forEach { executeJs(it) } + pendingScripts.clear() chatPanelInit() chatPanelUpdateTheme() showContent() - pendingScripts.forEach { executeJs(it) } - pendingScripts.clear() } private val jsHandleChatPanelOnCopy = createJsFunction { params -> @@ -484,8 +499,32 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( private val jsHandleChatPanelOpenInEditor = createJsFunction { params -> logger.debug("openInEditor: request: $params") - //FIXME(@icycodes): not implemented - return@createJsFunction false + val fileLocation = params.getOrNull(0)?.asFileLocation() ?: return@createJsFunction false + return@createJsFunction openInEditor(fileLocation) + } + + private val jsHandleChatPanelOpenExternal = createJsFunction { params -> + logger.debug("openExternal: request: $params") + val url = params.getOrNull(0) as String? ?: return@createJsFunction Unit + BrowserUtil.browse(url) + } + + private val jsHandleChatPanelReadWorkspaceGitRepositories = createJsFunction { params -> + logger.debug("readWorkspaceGitRepositories: request: $params") + val activeTextEditorUri = fileEditorManager.selectedTextEditor?.virtualFile?.url + val projectDir = project.guessProjectDir()?.url + val pathToCheck = activeTextEditorUri ?: projectDir ?: return@createJsFunction null + val gitRepo = gitProvider.getRepository(pathToCheck)?.let { + getDefaultRemoteUrl(it) + }?.let { + GitRepository(it) + } + return@createJsFunction listOfNotNull(gitRepo) + } + + private val jsHandleChatPanelGetActiveEditorSelection = createJsFunction { params -> + logger.debug("getActiveEditorSelection: request: $params") + return@createJsFunction getActiveEditorFileContext() } // functions to execute js scripts @@ -508,14 +547,15 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( if (!window.tabbyChatPanelClient) { window.tabbyChatPanelClient = TabbyThreads.createThreadFromIframe(getChatPanel(), { expose: { - navigate: $jsHandleChatPanelNavigate, refresh: $jsHandleChatPanelRefresh, - onSubmitMessage: $jsHandleChatPanelOnSubmitMessage, onApplyInEditor: $jsHandleChatPanelOnApplyInEditor, onLoaded: $jsHandleChatPanelOnLoaded, onCopy: $jsHandleChatPanelOnCopy, onKeyboardEvent: $jsHandleChatPanelOnKeyboardEvent, openInEditor: $jsHandleChatPanelOpenInEditor, + openExternal: $jsHandleChatPanelOpenExternal, + readWorkspaceGitRepositories: $jsHandleChatPanelReadWorkspaceGitRepositories, + getActiveEditorSelection: $jsHandleChatPanelGetActiveEditorSelection, } }) } @@ -592,7 +632,7 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( } })() """.trimIndent().trimStart() - + logger.debug("Request to chat panel: $uuid, $method, $paramsJson") if (isChatPanelLoaded) { executeJs(script) @@ -663,6 +703,11 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( .replace("\b", "\\b") } + private fun positionOneBasedInDocument(document: Document, offset: Int): Position { + val position = positionInDocument(document, offset) + return Position(position.line + 1, position.character + 1) + } + private fun Color.toHsl(): String { val r = red / 255.0 val g = green / 255.0 @@ -694,6 +739,7 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( return String.format("%.0f, %.0f%%, %.0f%%", h, s, l) } + // FIXME: extract this to git provider private val gitRemoteUrlToLocalRoot = mutableMapOf() private fun getDefaultRemoteUrl(repo: GitProvider.Repository): String? { @@ -713,23 +759,9 @@ class ChatBrowser(private val project: Project) : JBCefBrowser( return URLBuilder(this).appendPathSegments(path).toString() } - private fun buildCodeBrowserUrl(config: Config.ServerConfig, context: FileContext): String { - return URLBuilder(config.endpoint).apply { - appendPathSegments("files") - parameters.append("redirect_git_url", context.gitUrl) - parameters.append("redirect_filepath", context.filepath) - fragment = "L${context.range.start}-L${context.range.end}" - }.buildString() - } - - private const val TABBY_CHAT_PANEL_API_VERSION_RANGE = "~0.4.0" + private const val TABBY_CHAT_PANEL_API_VERSION_RANGE = "~0.5.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 fun loadTabbyThreadsScript(): String { val script = PluginManagerCore.getPlugin(PluginId.getId("com.tabbyml.intellij-tabby")) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt new file mode 100644 index 000000000000..c34dc6a65b24 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/chat/ChatPanelData.kt @@ -0,0 +1,113 @@ +package com.tabbyml.intellijtabby.chat + +import com.google.gson.Gson + +abstract class Filepath( + val kind: String +) { + sealed class Kind { + companion object { + const val GIT = "git" + const val URI = "uri" + } + } +} + +data class FilepathInGitRepository( + val filepath: String, + val gitUrl: String, + val revision: String? = null, +) : Filepath(Kind.GIT) + +data class FilepathUri( + val uri: String, +) : Filepath(Kind.URI) + +data class Position( + // 1-based + val line: Int, + val character: Int, +) + +abstract class Range +data class LineRange( + // 1-based + val start: Int, + val end: Int, +) : Range() + +data class PositionRange( + val start: Position, + val end: Position, +) : Range() + +data class EditorFileContext( + val kind: String = "file", + val filepath: Filepath, + // Range or Range + val range: Range?, + val content: String, +) + +sealed class ChatCommand { + companion object { + const val EXPLAIN = "explain" + const val FIX = "fix" + const val GENERATE_DOCS = "generate-docs" + const val GENERATE_TESTS = "generate-tests" + } +} + +data class FileLocation( + val filepath: Filepath, + // Int, LineRange, Position, or PositionRange + val location: Any?, +) + +data class GitRepository( + val url: String, +) + +private val gson = Gson() + +fun Any.asFileLocation(): FileLocation? { + val filepath = if (this is Map<*, *> && this.containsKey("filepath")) { + val filepathValue = this["filepath"] + if (filepathValue is Map<*, *> && filepathValue.containsKey("kind")) { + if (filepathValue["kind"] == Filepath.Kind.GIT) { + gson.fromJson(gson.toJson(filepathValue), FilepathInGitRepository::class.java) + } else if (filepathValue["kind"] == Filepath.Kind.URI) { + gson.fromJson(gson.toJson(filepathValue), FilepathUri::class.java) + } else { + null + } + } else { + null + } + } else { + null + } ?: return null + + val location = if (this is Map<*, *> && containsKey("location")) { + val locationValue = this["location"] + if (locationValue is Number) { + locationValue + } else if (locationValue is Map<*, *>) { + if (locationValue.containsKey("line")) { + gson.fromJson(gson.toJson(locationValue), Position::class.java) + } else if (locationValue.containsKey("start") && locationValue["start"] is Number) { + gson.fromJson(gson.toJson(locationValue), LineRange::class.java) + } else if (locationValue.containsKey("start") && locationValue["start"] is Map<*, *>) { + gson.fromJson(gson.toJson(locationValue), PositionRange::class.java) + } else { + null + } + } else { + null + } + } else { + null + } + + return FileLocation(filepath, location) +} diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt index d81bf220da53..be45f95d7e54 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/EditorFactoryListener.kt @@ -6,6 +6,7 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.event.CaretEvent import com.intellij.openapi.editor.event.DocumentEvent import com.intellij.openapi.editor.event.EditorFactoryEvent +import com.intellij.openapi.editor.event.SelectionEvent import com.tabbyml.intellijtabby.safeSyncPublisher class EditorFactoryListener : com.intellij.openapi.editor.event.EditorFactoryListener { @@ -25,6 +26,13 @@ class EditorFactoryListener : com.intellij.openapi.editor.event.EditorFactoryLis } } + val selectionListener = object : com.intellij.openapi.editor.event.SelectionListener { + override fun selectionChanged(event: SelectionEvent) { + logger.debug("SelectionListener: selectionChanged $editor $event") + project.safeSyncPublisher(SelectionListener.TOPIC)?.selectionChanged(editor, event) + } + } + val documentListener = object : com.intellij.openapi.editor.event.DocumentListener { override fun documentChanged(event: DocumentEvent) { logger.debug("DocumentListener: documentChanged $editor $event") @@ -33,10 +41,12 @@ class EditorFactoryListener : com.intellij.openapi.editor.event.EditorFactoryLis } editor.caretModel.addCaretListener(caretListener) + editor.selectionModel.addSelectionListener(selectionListener) editor.document.addDocumentListener(documentListener) listeners[editor] = Disposable { editor.caretModel.removeCaretListener(caretListener) + editor.selectionModel.removeSelectionListener(selectionListener) editor.document.removeDocumentListener(documentListener) } } diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt new file mode 100644 index 000000000000..179ccd12cc28 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/events/SelectionListener.kt @@ -0,0 +1,14 @@ +package com.tabbyml.intellijtabby.events + +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.util.messages.Topic + +interface SelectionListener { + fun selectionChanged(editor: Editor, event: SelectionEvent) {} + + companion object { + @Topic.ProjectLevel + val TOPIC = Topic(SelectionListener::class.java, Topic.BroadcastDirection.NONE) + } +} \ No newline at end of file