Skip to content

Commit 0d3bfc7

Browse files
committed
feat: dnd files and folders from project window (closes #1124)
1 parent b440b03 commit 0d3bfc7

File tree

4 files changed

+170
-7
lines changed

4 files changed

+170
-7
lines changed

src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -520,4 +520,4 @@ private JPanel createRootPanel() {
520520
rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH);
521521
return rootPanel;
522522
}
523-
}
523+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package ee.carlrobert.codegpt.ui.dnd
2+
3+
import com.intellij.openapi.application.ApplicationManager
4+
import com.intellij.openapi.vfs.LocalFileSystem
5+
import com.intellij.openapi.vfs.VirtualFile
6+
import com.intellij.util.ui.JBUI
7+
import ee.carlrobert.codegpt.ui.textarea.UserInputPanel
8+
import java.awt.GraphicsEnvironment
9+
import java.awt.datatransfer.DataFlavor
10+
import java.awt.dnd.*
11+
import java.io.File
12+
import java.net.URI
13+
import java.net.URLDecoder
14+
import java.nio.charset.StandardCharsets
15+
import javax.swing.JComponent
16+
17+
object FileDragAndDrop {
18+
fun install(component: JComponent, onFilesDropped: (List<VirtualFile>) -> Unit) {
19+
install(component, component, onFilesDropped)
20+
}
21+
22+
fun install(
23+
component: JComponent,
24+
highlightTarget: JComponent,
25+
onFilesDropped: (List<VirtualFile>) -> Unit
26+
) {
27+
val appHeadless = try {
28+
ApplicationManager.getApplication()?.isHeadlessEnvironment == true
29+
} catch (_: Throwable) {
30+
false
31+
}
32+
if (GraphicsEnvironment.isHeadless() || appHeadless) return
33+
DropTarget(component, DnDConstants.ACTION_COPY, object : DropTargetAdapter() {
34+
override fun dragEnter(dragEvent: DropTargetDragEvent) {
35+
if (canImport(dragEvent.currentDataFlavors)) {
36+
dragEvent.acceptDrag(DnDConstants.ACTION_COPY)
37+
setHighlight(highlightTarget, true)
38+
} else dragEvent.rejectDrag()
39+
}
40+
41+
override fun drop(dropEvent: DropTargetDropEvent) {
42+
val files = extractVirtualFiles(dropEvent.transferable)
43+
if (files.isNotEmpty()) {
44+
dropEvent.acceptDrop(DnDConstants.ACTION_COPY)
45+
onFilesDropped(files)
46+
dropEvent.dropComplete(true)
47+
} else dropEvent.rejectDrop()
48+
setHighlight(highlightTarget, false)
49+
}
50+
51+
override fun dragExit(dte: DropTargetEvent) {
52+
setHighlight(highlightTarget, false)
53+
}
54+
}, true)
55+
}
56+
57+
private fun canImport(flavors: Array<DataFlavor>): Boolean {
58+
return flavors.any {
59+
it == DataFlavor.javaFileListFlavor
60+
|| it == DataFlavor.stringFlavor
61+
|| isUriListFlavor(it)
62+
}
63+
}
64+
65+
private fun isUriListFlavor(flavor: DataFlavor): Boolean {
66+
return flavor.primaryType.equals("text", true) && flavor.subType.equals("uri-list", true)
67+
}
68+
69+
private fun extractVirtualFiles(transferable: java.awt.datatransfer.Transferable): List<VirtualFile> {
70+
val out = mutableListOf<VirtualFile>()
71+
try {
72+
if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
73+
val list = transferable.getTransferData(DataFlavor.javaFileListFlavor) as? List<*>
74+
list?.mapNotNull { it as? File }?.forEach { addIfExists(out, it) }
75+
}
76+
} catch (_: Exception) {
77+
}
78+
try {
79+
if (transferable.isDataFlavorSupported(DataFlavor.stringFlavor)) {
80+
val s = transferable.getTransferData(DataFlavor.stringFlavor) as? String
81+
if (!s.isNullOrBlank()) parseUriList(s).forEach { addIfExists(out, it) }
82+
}
83+
} catch (_: Exception) {
84+
}
85+
return out.distinct()
86+
}
87+
88+
private fun parseUriList(data: String): List<File> {
89+
return data.lineSequence()
90+
.map { it.trim() }
91+
.filter { it.isNotEmpty() && !it.startsWith("#") }
92+
.mapNotNull {
93+
try {
94+
if (it.startsWith("file:")) File(
95+
URLDecoder.decode(
96+
URI.create(it).path,
97+
StandardCharsets.UTF_8
98+
)
99+
) else File(it)
100+
} catch (_: Exception) {
101+
null
102+
}
103+
}
104+
.toList()
105+
}
106+
107+
private fun addIfExists(out: MutableList<VirtualFile>, file: File) {
108+
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file)?.let { out += it }
109+
}
110+
111+
private fun setHighlight(component: JComponent, enabled: Boolean) {
112+
if (component is UserInputPanel) {
113+
component.setDragActive(enabled)
114+
return
115+
}
116+
val key = "codegpt.dnd.prev.border"
117+
if (enabled) {
118+
if (component.getClientProperty(key) == null) component.putClientProperty(
119+
key,
120+
component.border
121+
)
122+
val focusColor = JBUI.CurrentTheme.Focus.defaultButtonColor()
123+
val overlay = JBUI.Borders.customLine(focusColor, 1)
124+
val base = component.getClientProperty(key) as? javax.swing.border.Border
125+
component.border = JBUI.Borders.merge(base, overlay, true)
126+
} else {
127+
val prev = component.getClientProperty(key) as? javax.swing.border.Border
128+
if (prev != null) {
129+
component.border = prev
130+
component.putClientProperty(key, null)
131+
}
132+
}
133+
component.revalidate()
134+
component.repaint()
135+
}
136+
}

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.intellij.openapi.editor.ex.EditorEx
1717
import com.intellij.openapi.fileTypes.FileTypes
1818
import com.intellij.openapi.project.Project
1919
import com.intellij.openapi.wm.ToolWindowManager
20+
import com.intellij.openapi.vfs.VirtualFile
2021
import com.intellij.ui.EditorTextField
2122
import com.intellij.util.ui.JBUI
2223
import ee.carlrobert.codegpt.CodeGPTBundle
@@ -25,6 +26,7 @@ import ee.carlrobert.codegpt.ui.textarea.header.tag.TagManager
2526
import ee.carlrobert.codegpt.ui.textarea.lookup.DynamicLookupGroupItem
2627
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
2728
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupGroupItem
29+
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
2830
import kotlinx.coroutines.*
2931
import java.awt.Dimension
3032
import java.util.*
@@ -36,6 +38,7 @@ class PromptTextField(
3638
private val onBackSpace: () -> Unit,
3739
private val onLookupAdded: (LookupActionItem) -> Unit,
3840
private val onSubmit: (String) -> Unit,
41+
private val onFilesDropped: (List<VirtualFile>) -> Unit = {},
3942
) : EditorTextField(project, FileTypes.PLAIN_TEXT), Disposable {
4043

4144
companion object {
@@ -72,6 +75,8 @@ class PromptTextField(
7275
},
7376
this
7477
)
78+
val highlightTarget = (this.parent as? javax.swing.JComponent) ?: this
79+
FileDragAndDrop.install(editor.contentComponent as javax.swing.JComponent, highlightTarget) { onFilesDropped(it) }
7580
}
7681

7782
fun clear() {
@@ -361,4 +366,4 @@ class PromptTextField(
361366
.getToolWindow("ProxyAI")?.component?.visibleRect?.height
362367
?: PromptTextFieldConstants.DEFAULT_TOOL_WINDOW_HEIGHT
363368
}
364-
}
369+
}

src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import com.intellij.util.IconUtil
2121
import com.intellij.util.ui.JBUI
2222
import ee.carlrobert.codegpt.CodeGPTBundle
2323
import ee.carlrobert.codegpt.Icons
24-
import ee.carlrobert.codegpt.conversations.Conversation
24+
import ee.carlrobert.codegpt.ReferencedFile
2525
import ee.carlrobert.codegpt.settings.configuration.ChatMode
2626
import ee.carlrobert.codegpt.settings.models.ModelRegistry
2727
import ee.carlrobert.codegpt.settings.service.FeatureType
@@ -30,6 +30,7 @@ import ee.carlrobert.codegpt.settings.service.ServiceType
3030
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction
3131
import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel
3232
import ee.carlrobert.codegpt.ui.IconActionButton
33+
import ee.carlrobert.codegpt.ui.dnd.FileDragAndDrop
3334
import ee.carlrobert.codegpt.ui.textarea.header.UserInputHeaderPanel
3435
import ee.carlrobert.codegpt.ui.textarea.header.tag.*
3536
import ee.carlrobert.codegpt.ui.textarea.lookup.LookupActionItem
@@ -64,7 +65,11 @@ class UserInputPanel(
6465
::updateUserTokens,
6566
::handleBackSpace,
6667
::handleLookupAdded,
67-
::handleSubmit
68+
::handleSubmit,
69+
onFilesDropped = { files ->
70+
includeFiles(files.toMutableList())
71+
totalTokensPanel.updateReferencedFilesTokens(files.map { ReferencedFile.from(it).fileContent() })
72+
}
6873
)
6974
private val userInputHeaderPanel =
7075
UserInputHeaderPanel(
@@ -112,6 +117,10 @@ class UserInputPanel(
112117
setupDisposables(parentDisposable)
113118
setupLayout()
114119
addSelectedEditorContent()
120+
FileDragAndDrop.install(this) { files ->
121+
includeFiles(files.toMutableList())
122+
totalTokensPanel.updateReferencedFilesTokens(files.map { ReferencedFile.from(it).fileContent() })
123+
}
115124
}
116125

117126
private fun setupDisposables(parentDisposable: Disposable) {
@@ -198,7 +207,13 @@ class UserInputPanel(
198207
}
199208

200209
fun includeFiles(referencedFiles: MutableList<VirtualFile>) {
201-
referencedFiles.forEach { userInputHeaderPanel.addTag(FileTagDetails(it)) }
210+
referencedFiles.forEach { vf ->
211+
if (vf.isDirectory) {
212+
userInputHeaderPanel.addTag(FolderTagDetails(vf))
213+
} else {
214+
userInputHeaderPanel.addTag(FileTagDetails(vf))
215+
}
216+
}
202217
}
203218

204219
override fun requestFocus() {
@@ -252,12 +267,19 @@ class UserInputPanel(
252267

253268
private fun drawRoundedBorder(g2: Graphics2D) {
254269
g2.color = JBUI.CurrentTheme.Focus.defaultButtonColor()
255-
if (promptTextField.isFocusOwner) {
270+
if (promptTextField.isFocusOwner || dragActive) {
256271
g2.stroke = BasicStroke(1.5F)
257272
}
258273
g2.drawRoundRect(0, 0, width - 1, height - 1, CORNER_RADIUS, CORNER_RADIUS)
259274
}
260275

276+
private var dragActive: Boolean = false
277+
278+
fun setDragActive(active: Boolean) {
279+
dragActive = active
280+
repaint()
281+
}
282+
261283
override fun getInsets(): Insets = JBUI.insets(4)
262284

263285
private fun handleSubmit(text: String) {
@@ -350,4 +372,4 @@ class UserInputPanel(
350372
ModelRegistry.CLAUDE_4_SONNET_THINKING
351373
)
352374
}
353-
}
375+
}

0 commit comments

Comments
 (0)