mirror of
https://github.com/carlrobertoh/ProxyAI.git
synced 2026-05-20 09:24:08 +00:00
feat: dnd files and folders from project window (closes #1124)
This commit is contained in:
parent
807d039797
commit
c42ebeb691
4 changed files with 170 additions and 7 deletions
|
|
@ -520,4 +520,4 @@ public class ChatToolWindowTabPanel implements Disposable {
|
|||
rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH);
|
||||
return rootPanel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
136
src/main/kotlin/ee/carlrobert/codegpt/ui/dnd/FileDragAndDrop.kt
Normal file
136
src/main/kotlin/ee/carlrobert/codegpt/ui/dnd/FileDragAndDrop.kt
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package ee.carlrobert.codegpt.ui.dnd
|
||||
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.vfs.LocalFileSystem
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.awt.dnd.*
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import javax.swing.JComponent
|
||||
|
||||
object FileDragAndDrop {
|
||||
fun install(component: JComponent, onFilesDropped: (List<VirtualFile>) -> Unit) {
|
||||
install(component, component, onFilesDropped)
|
||||
}
|
||||
|
||||
fun install(
|
||||
component: JComponent,
|
||||
highlightTarget: JComponent,
|
||||
onFilesDropped: (List<VirtualFile>) -> Unit
|
||||
) {
|
||||
val appHeadless = try {
|
||||
ApplicationManager.getApplication()?.isHeadlessEnvironment == true
|
||||
} catch (_: Throwable) {
|
||||
false
|
||||
}
|
||||
if (GraphicsEnvironment.isHeadless() || appHeadless) return
|
||||
DropTarget(component, DnDConstants.ACTION_COPY, object : DropTargetAdapter() {
|
||||
override fun dragEnter(dragEvent: DropTargetDragEvent) {
|
||||
if (canImport(dragEvent.currentDataFlavors)) {
|
||||
dragEvent.acceptDrag(DnDConstants.ACTION_COPY)
|
||||
setHighlight(highlightTarget, true)
|
||||
} else dragEvent.rejectDrag()
|
||||
}
|
||||
|
||||
override fun drop(dropEvent: DropTargetDropEvent) {
|
||||
val files = extractVirtualFiles(dropEvent.transferable)
|
||||
if (files.isNotEmpty()) {
|
||||
dropEvent.acceptDrop(DnDConstants.ACTION_COPY)
|
||||
onFilesDropped(files)
|
||||
dropEvent.dropComplete(true)
|
||||
} else dropEvent.rejectDrop()
|
||||
setHighlight(highlightTarget, false)
|
||||
}
|
||||
|
||||
override fun dragExit(dte: DropTargetEvent) {
|
||||
setHighlight(highlightTarget, false)
|
||||
}
|
||||
}, true)
|
||||
}
|
||||
|
||||
private fun canImport(flavors: Array<DataFlavor>): Boolean {
|
||||
return flavors.any {
|
||||
it == DataFlavor.javaFileListFlavor
|
||||
|| it == DataFlavor.stringFlavor
|
||||
|| isUriListFlavor(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isUriListFlavor(flavor: DataFlavor): Boolean {
|
||||
return flavor.primaryType.equals("text", true) && flavor.subType.equals("uri-list", true)
|
||||
}
|
||||
|
||||
private fun extractVirtualFiles(transferable: java.awt.datatransfer.Transferable): List<VirtualFile> {
|
||||
val out = mutableListOf<VirtualFile>()
|
||||
try {
|
||||
if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
|
||||
val list = transferable.getTransferData(DataFlavor.javaFileListFlavor) as? List<*>
|
||||
list?.mapNotNull { it as? File }?.forEach { addIfExists(out, it) }
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
try {
|
||||
if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
|
||||
val s = transferable.getTransferData(DataFlavor.stringFlavor) as? String
|
||||
if (!s.isNullOrBlank()) parseUriList(s).forEach { addIfExists(out, it) }
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
return out.distinct()
|
||||
}
|
||||
|
||||
private fun parseUriList(data: String): List<File> {
|
||||
return data.lineSequence()
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() && !it.startsWith("#") }
|
||||
.mapNotNull {
|
||||
try {
|
||||
if (it.startsWith("file:")) File(
|
||||
URLDecoder.decode(
|
||||
URI.create(it).path,
|
||||
StandardCharsets.UTF_8
|
||||
)
|
||||
) else File(it)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
|
||||
private fun addIfExists(out: MutableList<VirtualFile>, file: File) {
|
||||
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)?.let { out += it }
|
||||
}
|
||||
|
||||
private fun setHighlight(component: JComponent, enabled: Boolean) {
|
||||
if (component is UserInputPanel) {
|
||||
component.setDragActive(enabled)
|
||||
return
|
||||
}
|
||||
val key = "codegpt.dnd.prev.border"
|
||||
if (enabled) {
|
||||
if (component.getClientProperty(key) == null) component.putClientProperty(
|
||||
key,
|
||||
component.border
|
||||
)
|
||||
val focusColor = JBUI.CurrentTheme.Focus.defaultButtonColor()
|
||||
val overlay = JBUI.Borders.customLine(focusColor, 1)
|
||||
val base = component.getClientProperty(key) as? javax.swing.border.Border
|
||||
component.border = JBUI.Borders.merge(base, overlay, true)
|
||||
} else {
|
||||
val prev = component.getClientProperty(key) as? javax.swing.border.Border
|
||||
if (prev != null) {
|
||||
component.border = prev
|
||||
component.putClientProperty(key, null)
|
||||
}
|
||||
}
|
||||
component.revalidate()
|
||||
component.repaint()
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@ import com.intellij.openapi.editor.ex.EditorEx
|
|||
import com.intellij.openapi.fileTypes.FileTypes
|
||||
import com.intellij.openapi.project.Project
|
||||
import com.intellij.openapi.wm.ToolWindowManager
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.ui.EditorTextField
|
||||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
|
|
@ -25,6 +26,7 @@ import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
|
|||
import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
|
||||
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
|
||||
import kotlinx.coroutines.*
|
||||
import java.awt.Dimension
|
||||
import java.util.*
|
||||
|
|
@ -36,6 +38,7 @@ class PromptTextField(
|
|||
private val onBackSpace: () -> Unit,
|
||||
private val onLookupAdded: (LookupActionItem) -> Unit,
|
||||
private val onSubmit: (String) -> Unit,
|
||||
private val onFilesDropped: (List<VirtualFile>) -> Unit = {},
|
||||
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
|
||||
|
||||
companion object {
|
||||
|
|
@ -72,6 +75,8 @@ class PromptTextField(
|
|||
},
|
||||
this
|
||||
)
|
||||
val highlightTarget = (this.parent as? javax.swing.JComponent) ?: this
|
||||
FileDragAndDrop.install(editor.contentComponent as javax.swing.JComponent, highlightTarget) { onFilesDropped(it) }
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
|
|
@ -361,4 +366,4 @@ class PromptTextField(
|
|||
.getToolWindow("ProxyAI")?.component?.visibleRect?.height
|
||||
?: PromptTextFieldConstants.DEFAULT_TOOL_WINDOW_HEIGHT
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import com.intellij.util.IconUtil
|
|||
import com.intellij.util.ui.JBUI
|
||||
import ee.carlrobert.codegpt.CodeGPTBundle
|
||||
import ee.carlrobert.codegpt.Icons
|
||||
import ee.carlrobert.codegpt.conversations.Conversation
|
||||
import ee.carlrobert.codegpt.ReferencedFile
|
||||
import ee.carlrobert.codegpt.settings.configuration.ChatMode
|
||||
import ee.carlrobert.codegpt.settings.models.ModelRegistry
|
||||
import ee.carlrobert.codegpt.settings.service.FeatureType
|
||||
|
|
@ -30,6 +30,7 @@ import ee.carlrobert.codegpt.settings.service.ServiceType
|
|||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
|
||||
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
|
||||
import ee.carlrobert.codegpt.ui.IconActionButton
|
||||
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.UserInputHeaderPanel
|
||||
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
|
||||
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
|
||||
|
|
@ -64,7 +65,11 @@ class UserInputPanel(
|
|||
::updateUserTokens,
|
||||
::handleBackSpace,
|
||||
::handleLookupAdded,
|
||||
::handleSubmit
|
||||
::handleSubmit,
|
||||
onFilesDropped = { files ->
|
||||
includeFiles(files.toMutableList())
|
||||
totalTokensPanel.updateReferencedFilesTokens(files.map { ReferencedFile.from(it).fileContent() })
|
||||
}
|
||||
)
|
||||
private val userInputHeaderPanel =
|
||||
UserInputHeaderPanel(
|
||||
|
|
@ -112,6 +117,10 @@ class UserInputPanel(
|
|||
setupDisposables(parentDisposable)
|
||||
setupLayout()
|
||||
addSelectedEditorContent()
|
||||
FileDragAndDrop.install(this) { files ->
|
||||
includeFiles(files.toMutableList())
|
||||
totalTokensPanel.updateReferencedFilesTokens(files.map { ReferencedFile.from(it).fileContent() })
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDisposables(parentDisposable: Disposable) {
|
||||
|
|
@ -198,7 +207,13 @@ class UserInputPanel(
|
|||
}
|
||||
|
||||
fun includeFiles(referencedFiles: MutableList<VirtualFile>) {
|
||||
referencedFiles.forEach { userInputHeaderPanel.addTag(FileTagDetails(it)) }
|
||||
referencedFiles.forEach { vf ->
|
||||
if (vf.isDirectory) {
|
||||
userInputHeaderPanel.addTag(FolderTagDetails(vf))
|
||||
} else {
|
||||
userInputHeaderPanel.addTag(FileTagDetails(vf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestFocus() {
|
||||
|
|
@ -252,12 +267,19 @@ class UserInputPanel(
|
|||
|
||||
private fun drawRoundedBorder(g2: Graphics2D) {
|
||||
g2.color = JBUI.CurrentTheme.Focus.defaultButtonColor()
|
||||
if (promptTextField.isFocusOwner) {
|
||||
if (promptTextField.isFocusOwner || dragActive) {
|
||||
g2.stroke = BasicStroke(1.5F)
|
||||
}
|
||||
g2.drawRoundRect(0, 0, width - 1, height - 1, CORNER_RADIUS, CORNER_RADIUS)
|
||||
}
|
||||
|
||||
private var dragActive: Boolean = false
|
||||
|
||||
fun setDragActive(active: Boolean) {
|
||||
dragActive = active
|
||||
repaint()
|
||||
}
|
||||
|
||||
override fun getInsets(): Insets = JBUI.insets(4)
|
||||
|
||||
private fun handleSubmit(text: String) {
|
||||
|
|
@ -350,4 +372,4 @@ class UserInputPanel(
|
|||
ModelRegistry.CLAUDE_4_SONNET_THINKING
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue