Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Fixed drag-and-drop into text fields: files are now inserted at the text caret position instead of where the mouse was dropped
- Fixed Accept/Decline and Yes/No button colors to respect the IDE Look and Feel theme (dark themes no longer show clashing hardcoded colors)
- Added session management when opening a session from the toolbar and when closing a tab via the Close button
- Added configurable session mode: choose to resume the last session or start a new one
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbclaudecodegui</groupId>
<artifactId>netbeans-claude-code-gui</artifactId>
<version>0.21.14-SNAPSHOT</version>
<version>0.21.18-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,20 @@ public boolean importData(TransferSupport support) {
LOG.fine("importData: component=" + support.getComponent().getClass().getSimpleName());
Transferable t = support.getTransferable();
JTextComponent textComponent = findTextComponent(support.getComponent());
return doImport(t, textComponent);
int insertPos = textComponent != null ? textComponent.getCaretPosition() : 0;
return doImport(t, textComponent, insertPos);
}

/**
* Called by our custom {@link java.awt.dnd.DropTargetAdapter} in place of the standard
* Swing drop pipeline. Because our adapter never calls {@code setDropLocation}, the caret
* stays at the position the user left it — so we can read it here and insert there.
*/
void handleDrop(java.awt.dnd.DropTargetDropEvent dtde, JTextComponent tc) {
dtde.acceptDrop(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
int insertPos = tc.getCaretPosition();
boolean ok = doImport(dtde.getTransferable(), tc, insertPos);
dtde.dropComplete(ok);
}

// -------------------------------------------------------------------------
Expand All @@ -223,27 +236,27 @@ public boolean importFromClipboard(JTextComponent textComponent) {
boolean hasText = t.isDataFlavorSupported(DataFlavor.stringFlavor);
LOG.fine("importFromClipboard: hasFiles=" + hasFiles + " hasImage=" + hasImage + " hasText=" + hasText);
if (!hasFiles && !hasImage && !hasText) return false;
return doImport(t, textComponent);
return doImport(t, textComponent, textComponent.getCaretPosition());
}

// -------------------------------------------------------------------------
// Shared import logic
// -------------------------------------------------------------------------

private boolean doImport(Transferable t, JTextComponent textComponent) {
private boolean doImport(Transferable t, JTextComponent textComponent, int insertPos) {
try {
Node[] nodes = extractNodes(t);
if (nodes != null && nodes.length > 0) {
LOG.fine("doImport: nodes branch, count=" + nodes.length);
return importNodes(nodes, textComponent);
return importNodes(nodes, textComponent, insertPos);
}
if (t.isDataFlavorSupported(DataFlavor.imageFlavor)) {
Image img = (Image) t.getTransferData(DataFlavor.imageFlavor);
if (img != null) {
LOG.fine("doImport: image branch");
File tmp = writeTempPng(img);
if (textComponent != null) {
insertAtCaret(textComponent, "@" + tmp.getAbsolutePath());
insertAtCaret(textComponent, "@" + tmp.getAbsolutePath(), insertPos);
}
return true;
}
Expand All @@ -265,14 +278,14 @@ private boolean doImport(Transferable t, JTextComponent textComponent) {
}

if (paths.length() > 0 && textComponent != null) {
insertAtCaret(textComponent, paths.toString());
insertAtCaret(textComponent, paths.toString(), insertPos);
}
return true;
}
if (t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
LOG.fine("doImport: string branch");
String s = (String) t.getTransferData(DataFlavor.stringFlavor);
if (s != null && textComponent != null) { insertAtCaret(textComponent, s); return true; }
if (s != null && textComponent != null) { insertAtCaret(textComponent, s, insertPos); return true; }
}
} catch (Exception ex) {
// Import failed; fall through to return false
Expand All @@ -297,6 +310,11 @@ private boolean doImport(Transferable t, JTextComponent textComponent) {
* </ul>
*/
boolean importNodes(Node[] nodes, JTextComponent textComponent) {
int pos = textComponent != null ? textComponent.getCaretPosition() : 0;
return importNodes(nodes, textComponent, pos);
}

private boolean importNodes(Node[] nodes, JTextComponent textComponent, int insertPos) {
LOG.fine("importNodes: entry, count=" + (nodes == null ? 0 : nodes.length));
if (nodes == null || nodes.length == 0) return false;
String wdStr = workingDirSupplier.get();
Expand Down Expand Up @@ -393,7 +411,7 @@ boolean importNodes(Node[] nodes, JTextComponent textComponent) {
}

if (inlineText.length() > 0 && textComponent != null) {
insertAtCaret(textComponent, inlineText.toString());
insertAtCaret(textComponent, inlineText.toString(), insertPos);
}
LOG.fine("importNodes: returning " + (inlineText.length() > 0) + ", text='" + inlineText + "'");
return inlineText.length() > 0;
Expand Down Expand Up @@ -438,9 +456,8 @@ private File writeTempPng(Image img) throws IOException {
return tmp;
}

private static void insertAtCaret(JTextComponent textComponent, String text) {
private static void insertAtCaret(JTextComponent textComponent, String text, int pos) {
javax.swing.SwingUtilities.invokeLater(() -> {
int pos = textComponent.getCaretPosition();
try {
String before = textComponent.getText(0, pos);
String toInsert = (!before.isEmpty() && !before.endsWith(" ") && !before.endsWith("\n"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,31 @@ public void setup() {
dropHandler = new FileDropHandler(wdSupplier, tc.getTransferHandler());
tc.setTransferHandler(dropHandler);

// Replace Swing's SwingDropTarget with our own that never calls setDropLocation,
// so the caret stays frozen at its original position during the entire drag.
tc.setDropTarget(new java.awt.dnd.DropTarget(tc,
java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE,
new java.awt.dnd.DropTargetAdapter() {
@Override
public void dragEnter(java.awt.dnd.DropTargetDragEvent e) {
if (dropHandler.canImportTransferable(e.getTransferable()))
e.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
else
e.rejectDrag();
}
@Override
public void dragOver(java.awt.dnd.DropTargetDragEvent e) {
if (dropHandler.canImportTransferable(e.getTransferable()))
e.acceptDrag(java.awt.dnd.DnDConstants.ACTION_COPY_OR_MOVE);
else
e.rejectDrag();
}
@Override
public void drop(java.awt.dnd.DropTargetDropEvent e) {
dropHandler.handleDrop(e, tc);
}
}, true));

// 2. @-completion
AtCompletionPopup.install(tc, wdSupplier);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,61 @@ void importNodesFilesWindowNodeInsideWorkDirInsertsRelativePath() throws Excepti
});
}

// -------------------------------------------------------------------------
// Tests: insert at explicit position (DnD caret-save fix)
// -------------------------------------------------------------------------

/**
* Verifies that doImport(Transferable, JTextComponent, int) inserts at the
* given position, NOT at the text component's current caret position.
* This is the core of the DnD caret-save fix: the drag-hover caret movement
* must be ignored; insertion must happen where the caret was before the drag.
*/
@Test
void dropFileInsertsAtExplicitPositionIgnoringCurrentCaret() throws Exception {
File inside = new File(workDir, "file.txt");
inside.createNewFile();

SwingUtilities.invokeAndWait(() -> {
area.setText("hello world");
// Simulate Swing moving the caret to the drop location (end of text)
area.setCaretPosition(11);
// Call doImport with insertPos=5 (the saved pre-drag caret position)
java.awt.datatransfer.Transferable t = fileListTransferable(List.of(inside));
callDoImportAt(handler, t, area, 5);
});

SwingUtilities.invokeAndWait(() -> {
String text = area.getText();
assertTrue(text.contains("file.txt"), "path must appear in text, got: '" + text + "'");
// The path must be inserted inside "hello world", not appended at the end
assertTrue(text.startsWith("hello"),
"text must start with 'hello' (insertion at pos 5), got: '" + text + "'");
int pathIdx = text.indexOf("file.txt");
assertTrue(pathIdx > 0 && pathIdx < text.indexOf("world"),
"path must be inserted between 'hello' and 'world', got: '" + text + "'");
});
}

/**
* Calls the package-private {@code doImport(Transferable, JTextComponent, int)} via reflection.
*/
private static boolean callDoImportAt(FileDropHandler handler,
java.awt.datatransfer.Transferable t,
JTextArea area, int insertPos) {
try {
java.lang.reflect.Method m = FileDropHandler.class.getDeclaredMethod(
"doImport",
java.awt.datatransfer.Transferable.class,
javax.swing.text.JTextComponent.class,
int.class);
m.setAccessible(true);
return (boolean) m.invoke(handler, t, area, insertPos);
} catch (Exception ex) {
throw new RuntimeException("Could not invoke FileDropHandler.doImport(t, tc, pos)", ex);
}
}

// -------------------------------------------------------------------------
// Tests: Cut/Copy — export delegation to original TransferHandler
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -845,7 +900,8 @@ void canImportBlankAbstractNodeReturnsFalse() throws Exception {
}

/**
* Calls the package-private {@code doImport(Transferable, JTextComponent)} via reflection.
* Calls the package-private {@code doImport(Transferable, JTextComponent, int)} via reflection,
* using the current caret position as the insert position.
*/
private static boolean callDoImport(FileDropHandler handler,
java.awt.datatransfer.Transferable t,
Expand All @@ -854,9 +910,10 @@ private static boolean callDoImport(FileDropHandler handler,
java.lang.reflect.Method m = FileDropHandler.class.getDeclaredMethod(
"doImport",
java.awt.datatransfer.Transferable.class,
javax.swing.text.JTextComponent.class);
javax.swing.text.JTextComponent.class,
int.class);
m.setAccessible(true);
return (boolean) m.invoke(handler, t, area);
return (boolean) m.invoke(handler, t, area, area.getCaretPosition());
} catch (Exception ex) {
throw new RuntimeException("Could not invoke FileDropHandler.doImport", ex);
}
Expand Down
Loading