Просмотр исходного кода

fixed multi-project bug for drag&drop and "Add to context"

paviko 2 месяцев назад
Родитель
Сommit
94c36c0106

+ 1 - 0
hosts/jetbrains-plugin/changelog.html

@@ -2,6 +2,7 @@
 
 <h3>2025.11.xx</h3>
 <ul>
+  <li>Fixed multi-project bug with drag&drop and "Add to context"</li>
   <li>UI improvements</li>
 </ul>
 

+ 2 - 1
hosts/jetbrains-plugin/gradlew

@@ -32,6 +32,7 @@ class EditorAddLinesToContextAction : AnAction("OpenCode: Add lines to context")
         } catch (_: Throwable) { null } ?: return
 
         val pathWithRange = "$basePath:$startLine-$endLine"
-        PathInserter.insertPaths(listOf(pathWithRange))
+        val project = e.project ?: return
+        PathInserter.insertPaths(project, listOf(pathWithRange))
     }
 }

+ 2 - 1
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/actions/EditorAddToContextAction.kt

@@ -17,8 +17,9 @@ class EditorAddToContextAction : AnAction("OpenCode: Add to context") {
         val path = try {
             if (file.isInLocalFileSystem) VfsUtilCore.virtualToIoFile(file).absolutePath else file.path
         } catch (_: Throwable) { null }
+        val project = e.project ?: return
         if (!path.isNullOrEmpty()) {
-            PathInserter.insertPaths(listOf(path))
+            PathInserter.insertPaths(project, listOf(path))
         }
     }
 }

+ 2 - 1
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/actions/ProjectAddToContextAction.kt

@@ -21,8 +21,9 @@ class ProjectAddToContextAction : AnAction("OpenCode: Add to context") {
         for (vf in files) {
             collectFilePaths(vf, paths)
         }
+        val project = e.project ?: return
         if (paths.isNotEmpty()) {
-            PathInserter.insertPaths(paths)
+            PathInserter.insertPaths(project, paths)
         }
     }
 

+ 2 - 1
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/actions/ProjectPastePathAction.kt

@@ -21,9 +21,10 @@ class ProjectPastePathAction : AnAction("OpenCode: paste path") {
         val files = e.getData(CommonDataKeys.VIRTUAL_FILE_ARRAY) ?: return
         val dirs = files.filter { it.isDirectory }
         if (dirs.isEmpty()) return
+        val project = e.project ?: return
         for (vf in dirs) {
             val p = asAbsolutePath(vf)
-            if (!p.isNullOrEmpty()) PathInserter.pastePath(p)
+            if (!p.isNullOrEmpty()) PathInserter.pastePath(project, p)
         }
     }
 

+ 5 - 3
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/ChatToolWindowFactory.kt

@@ -111,7 +111,8 @@ class ChatToolWindowFactory : ToolWindowFactory, DumbAware {
                                         val browser = JBCefBrowser(appUrl)
 
                                         // Store browser reference for path insertion (context actions)
-                                        PathInserter.setBrowser(browser)
+                                        // PathInserter.setBrowser(browser) - Removed, now stateless
+
 
                                         IdeBridge.install(browser, project)
 
@@ -156,7 +157,7 @@ class ChatToolWindowFactory : ToolWindowFactory, DumbAware {
 
                                         // Enable dropping files from the IDE onto the web UI via helper
                                         try {
-                                            DragAndDropInstaller.install(browser, logger)
+                                            DragAndDropInstaller.install(project, browser, logger)
                                         } catch (e: Exception) {
                                             logger.warn("Failed to set up drag and drop", e)
                                         }
@@ -228,7 +229,8 @@ class ChatToolWindowFactory : ToolWindowFactory, DumbAware {
             } catch (_: Throwable) {
             }
             // Clear browser references
-            PathInserter.clearBrowser()
+            IdeBridge.remove(project)
+
         }
     }
 

+ 3 - 3
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/DragAndDropInstaller.kt

@@ -11,7 +11,7 @@ import java.awt.dnd.DropTargetDropEvent
 
 object DragAndDropInstaller {
     private val mapper = jacksonObjectMapper()
-    fun install(browser: JBCefBrowser, logger: Logger) {
+    fun install(project: com.intellij.openapi.project.Project, browser: JBCefBrowser, logger: Logger) {
         val comp = browser.component
         val dt = DropTarget(comp, object : DropTargetAdapter() {
             override fun drop(dtde: DropTargetDropEvent) {
@@ -25,13 +25,13 @@ object DragAndDropInstaller {
                         // Only send regular files to insertPaths to avoid chips/segments for directories
                         val filePaths = files.filter { it.isFile }.map { it.absolutePath }
                         if (filePaths.isNotEmpty()) {
-                            IdeBridge.send("insertPaths", mapOf("paths" to filePaths))
+                            IdeBridge.send(project, "insertPaths", mapOf("paths" to filePaths))
                         }
                         // Additionally, send directories via pastePath (no chips/segments)
                         val dirPaths = files.filter { it.isDirectory }.map { it.absolutePath }
                         if (dirPaths.isNotEmpty()) {
                             for (dp in dirPaths) {
-                                IdeBridge.send("pastePath", mapOf("path" to dp))
+                                IdeBridge.send(project, "pastePath", mapOf("path" to dp))
                             }
                         }
                         // Proactively restore focus to the embedded browser after injecting paths

+ 40 - 30
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/IdeBridge.kt

@@ -17,15 +17,22 @@ import javax.swing.SwingUtilities
 object IdeBridge {
     private val logger = Logger.getInstance(IdeBridge::class.java)
     private val mapper = jacksonObjectMapper()
-    @Volatile private var browser: JBCefBrowser? = null
-    @Volatile private var query: JBCefJSQuery? = null
-    @Volatile private var ready = false
-    private val outbox: MutableList<Map<String, Any?>> = mutableListOf()
+    
+    private class ProjectState(
+        val browser: JBCefBrowser,
+        val query: JBCefJSQuery?,
+        val outbox: MutableList<Map<String, Any?>> = mutableListOf()
+    ) {
+        @Volatile var ready = false
+    }
+
+    private val states = java.util.concurrent.ConcurrentHashMap<Project, ProjectState>()
 
     fun install(browser: JBCefBrowser, project: Project) {
-        this.browser = browser
         val q = try { JBCefJSQuery.create(browser) } catch (_: Throwable) { null }
-        query = q
+        val state = ProjectState(browser, q)
+        states[project] = state
+        
         if (q != null) {
             try {
                 q.addHandler { payload ->
@@ -52,12 +59,16 @@ object IdeBridge {
                 "})();"
             )
             browser.cefBrowser.executeJavaScript(js, browser.cefBrowser.url, 0)
-            SwingUtilities.invokeLater { flushOutbox() }
+            SwingUtilities.invokeLater { flushOutbox(project) }
         } catch (t: Throwable) {
             logger.warn("Failed to inject ideBridge", t)
         }
     }
 
+    fun remove(project: Project) {
+        states.remove(project)
+    }
+
     private fun handleInbound(json: String, project: Project) {
         val obj = try { mapper.readTree(json) } catch (_: Throwable) { null } ?: return
         val id = obj.get("id")?.asText()
@@ -65,7 +76,7 @@ object IdeBridge {
         when (type) {
             "openFile" -> {
                 val payload = obj.get("payload")
-                val rawPath = payload?.get("path")?.asText() ?: return replyError(id, "missing path")
+                val rawPath = payload?.get("path")?.asText() ?: return replyError(project, id, "missing path")
                 val lineFromPayload1Based = payload.get("line")?.asInt() ?: -1
                 val rangeRegex = Regex(":(\\d+)(?:-(\\d+))?$")
                 val match = rangeRegex.find(rawPath)
@@ -84,19 +95,19 @@ object IdeBridge {
                 val endLine0Based = if (endLine1Based > 0) endLine1Based - 1 else -1
 
                 openFile(project, cleanedPath, startLine0Based, endLine0Based)
-                replyOk(id)
+                replyOk(project, id)
             }
             "openUrl" -> {
                 val payload = obj.get("payload")
-                val url = payload?.get("url")?.asText() ?: return replyError(id, "missing url")
+                val url = payload?.get("url")?.asText() ?: return replyError(project, id, "missing url")
                 try {
                     BrowserUtil.browse(url)
-                    replyOk(id)
+                    replyOk(project, id)
                 } catch (t: Throwable) {
-                    replyError(id, t.message ?: "Failed to open url")
+                    replyError(project, id, t.message ?: "Failed to open url")
                 }
             }
-            else -> replyOk(id)
+            else -> replyOk(project, id)
         }
     }
 
@@ -145,36 +156,35 @@ object IdeBridge {
         }
     }
 
-    private fun replyOk(replyTo: String?) { sendRaw(mapOf("replyTo" to replyTo, "ok" to true)) }
-    private fun replyError(replyTo: String?, error: String) { sendRaw(mapOf("replyTo" to replyTo, "ok" to false, "error" to error)) }
+    private fun replyOk(project: Project, replyTo: String?) { sendRaw(project, mapOf("replyTo" to replyTo, "ok" to true)) }
+    private fun replyError(project: Project, replyTo: String?, error: String) { sendRaw(project, mapOf("replyTo" to replyTo, "ok" to false, "error" to error)) }
 
-    fun send(type: String, payload: Map<String, Any?> = emptyMap()) {
+    fun send(project: Project, type: String, payload: Map<String, Any?> = emptyMap()) {
         val message = mutableMapOf<String, Any?>("type" to type, "timestamp" to System.currentTimeMillis())
         if (payload.isNotEmpty()) message["payload"] = payload
-        sendRaw(message)
+        sendRaw(project, message)
     }
 
-    private fun sendRaw(message: Map<String, Any?>) {
-        val b = browser
-        if (b == null) {
-            outbox.add(message)
-            return
-        }
+    private fun sendRaw(project: Project, message: Map<String, Any?>) {
+        val state = states[project] ?: return
+        val b = state.browser
+        
         val json = try { mapper.writeValueAsString(message) } catch (_: Throwable) { return }
         val script = "(function(){ try { if(window.__ideBridgeDeliver){ window.__ideBridgeDeliver(" + mapper.writeValueAsString(json) + "); } else { window.postMessage(" + json + ", '*'); } } catch(e){} })();"
         try {
             b.cefBrowser.executeJavaScript(script, b.cefBrowser.url, 0)
-            ready = true
+            state.ready = true
         } catch (t: Throwable) {
-            outbox.add(message)
+            state.outbox.add(message)
         }
     }
 
-    private fun flushOutbox() {
-        if (ready) {
-            val pending = ArrayList(outbox)
-            outbox.clear()
-            for (m in pending) sendRaw(m)
+    private fun flushOutbox(project: Project) {
+        val state = states[project] ?: return
+        if (state.ready) {
+            val pending = ArrayList(state.outbox)
+            state.outbox.clear()
+            for (m in pending) sendRaw(project, m)
         }
     }
 }

+ 1 - 1
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/IdeOpenFilesUpdater.kt

@@ -22,7 +22,7 @@ class IdeOpenFilesUpdater(private val project: Project, private val browser: JBC
             try {
                 val opened = fem.openFiles.mapNotNull { vf -> vfPath(vf) }
                 val current = fem.selectedEditor?.file?.let { vf -> vfPath(vf) }
-                IdeBridge.send("updateOpenedFiles", mapOf("openedFiles" to opened, "currentFile" to current))
+                IdeBridge.send(project, "updateOpenedFiles", mapOf("openedFiles" to opened, "currentFile" to current))
             } catch (e: Exception) {
                 e.printStackTrace()
             }

+ 5 - 21
hosts/jetbrains-plugin/src/main/kotlin/paviko/opencode/ui/PathInserter.kt

@@ -2,6 +2,7 @@ package paviko.opencode.ui
 
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import com.intellij.openapi.diagnostic.Logger
+import com.intellij.openapi.project.Project
 import com.intellij.ui.jcef.JBCefBrowser
 import javax.swing.SwingUtilities
 
@@ -11,39 +12,22 @@ import javax.swing.SwingUtilities
 object PathInserter {
     private val logger = Logger.getInstance(PathInserter::class.java)
     private val mapper = jacksonObjectMapper()
-    @Volatile private var browser: JBCefBrowser? = null
 
-    fun setBrowser(browser: JBCefBrowser) {
-        this.browser = browser
-    }
-
-    fun clearBrowser() {
-        this.browser = null
-    }
-
-    fun insertPaths(paths: List<String>) {
+    fun insertPaths(project: Project, paths: List<String>) {
         try {
-            val b = browser ?: run {
-                logger.warn("No browser available to insert paths")
-                return
-            }
             if (paths.isEmpty()) return
             
-            IdeBridge.send("insertPaths", mapOf("paths" to paths))
+            IdeBridge.send(project, "insertPaths", mapOf("paths" to paths))
         } catch (e: Exception) {
             logger.error("Unexpected error inserting paths", e)
         }
     }
 
-    fun pastePath(path: String) {
+    fun pastePath(project: Project, path: String) {
         try {
-            val b = browser ?: run {
-                logger.warn("No browser available to paste path")
-                return
-            }
             if (path.isEmpty()) return
             
-            IdeBridge.send("pastePath", mapOf("path" to path))
+            IdeBridge.send(project, "pastePath", mapOf("path" to path))
         } catch (e: Exception) {
             logger.error("Unexpected error pasting path", e)
         }