diff --git a/CHANGELOG.md b/CHANGELOG.md
index 48a99f1a..e6171003 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Added Find bar (Ctrl+F / F3 / Shift+F3) to Markdown Preview
- Added automatic Markdown Preview tab when Claude writes a plan file (configurable in Settings → General)
- Added `show_markdown` MCP tool: displays markdown text in a NetBeans Markdown Preview tab
- Added `show_markdown_file` MCP tool: opens a live-updating Markdown Preview tab for a .md file on disk
diff --git a/pom.xml b/pom.xml
index fd149405..b34092da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.23.10-SNAPSHOT
+ 0.23.12-SNAPSHOT
nbm
Netbeans Plugin Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownDiffPanel.java b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownDiffPanel.java
index d4f5a915..296ce04f 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownDiffPanel.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownDiffPanel.java
@@ -1,15 +1,22 @@
package io.github.nbclaudecodegui.ui;
import io.github.nbclaudecodegui.ui.common.MarkdownRenderer;
+import io.github.nbclaudecodegui.ui.common.markdown.MarkdownFindBar;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.KeyEvent;
+import javax.swing.AbstractAction;
+import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
+import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import org.openide.util.NbPreferences;
@@ -66,7 +73,18 @@ public MarkdownDiffPanel(String before, String after) {
beforeScroll = new JScrollPane(mdBeforePane);
afterScroll = new JScrollPane(mdAfterPane);
- splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, beforeScroll, afterScroll);
+ MarkdownFindBar beforeFindBar = new MarkdownFindBar(mdBeforePane);
+ MarkdownFindBar afterFindBar = new MarkdownFindBar(mdAfterPane);
+
+ JPanel beforePanel = new JPanel(new BorderLayout());
+ beforePanel.add(beforeScroll, BorderLayout.CENTER);
+ beforePanel.add(beforeFindBar, BorderLayout.SOUTH);
+
+ JPanel afterPanel = new JPanel(new BorderLayout());
+ afterPanel.add(afterScroll, BorderLayout.CENTER);
+ afterPanel.add(afterFindBar, BorderLayout.SOUTH);
+
+ splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, beforePanel, afterPanel);
splitPane.setResizeWeight(0.5);
// restore divider
@@ -86,10 +104,16 @@ public MarkdownDiffPanel(String before, String after) {
add(splitPane, BorderLayout.CENTER);
wireScrollSync();
- // Add "Hide" and "Pin Preview" to the existing popup menus on both editor panes
- for (JEditorPane pane : new JEditorPane[]{mdBeforePane, mdAfterPane}) {
+ // Add "Find…", "Hide", and "Pin Preview" to the existing popup menus on both panes
+ for (int i = 0; i < 2; i++) {
+ JEditorPane pane = i == 0 ? mdBeforePane : mdAfterPane;
+ MarkdownFindBar fb = i == 0 ? beforeFindBar : afterFindBar;
JPopupMenu menu = pane.getComponentPopupMenu();
if (menu == null) { menu = new JPopupMenu(); pane.setComponentPopupMenu(menu); }
+ JMenuItem findItem = new JMenuItem("Find…");
+ findItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK));
+ findItem.addActionListener(e -> fb.gainFocus());
+ menu.add(findItem);
menu.addSeparator();
JMenuItem hideItem = new JMenuItem("Hide");
hideItem.addActionListener(e -> { if (onHide != null) onHide.run(); });
@@ -97,11 +121,35 @@ public MarkdownDiffPanel(String before, String after) {
pinItem.addActionListener(e -> { if (onPinPreview != null) onPinPreview.run(); });
menu.add(hideItem);
menu.add(pinItem);
+ bindFindBarKeys(pane, fb);
}
// Let right-click on the divider also show the menu
splitPane.setComponentPopupMenu(mdBeforePane.getComponentPopupMenu());
}
+ private static void bindFindBarKeys(JEditorPane pane, MarkdownFindBar fb) {
+ KeyStroke ctrlF = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK);
+ KeyStroke f3 = KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0);
+ KeyStroke shiftF3 = KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK);
+ pane.getInputMap(JComponent.WHEN_FOCUSED).put(ctrlF, "mddiff-find-show");
+ pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlF, "mddiff-find-show");
+ pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(f3, "mddiff-find-next");
+ pane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shiftF3, "mddiff-find-prev");
+ pane.getActionMap().put("mddiff-find-show", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) { fb.gainFocus(); }
+ });
+ pane.getActionMap().put("mddiff-find-next", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) {
+ if (fb.isVisible()) fb.findNext(); else fb.searchAndFindNext();
+ }
+ });
+ pane.getActionMap().put("mddiff-find-prev", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) {
+ if (fb.isVisible()) fb.findPrev(); else fb.searchAndFindPrev();
+ }
+ });
+ }
+
private void wireScrollSync() {
beforeScroll.getViewport().addChangeListener(e -> {
if (syncActive) return;
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java
index e26ba7b5..a7f2cbd6 100644
--- a/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java
+++ b/src/main/java/io/github/nbclaudecodegui/ui/MarkdownPreviewTab.java
@@ -3,6 +3,7 @@
import io.github.nbclaudecodegui.settings.ClaudeCodePreferences;
import io.github.nbclaudecodegui.ui.common.MarkdownRenderer;
import io.github.nbclaudecodegui.ui.common.UiUtils;
+import io.github.nbclaudecodegui.ui.common.markdown.MarkdownFindBar;
import java.awt.BorderLayout;
import java.awt.Toolkit;
import java.awt.datatransfer.StringSelection;
@@ -103,6 +104,9 @@ public class MarkdownPreviewTab extends TopComponent {
/** Snapshot of the href captured when the context menu opens; read by action listeners. */
String menuHref = null;
+ /** Find bar shown at the bottom of the preview. */
+ private MarkdownFindBar findBar;
+
/** File path saved by writeExternal; applied in componentOpened() after IDE restart. */
String savedFilePath = null;
@@ -156,10 +160,11 @@ public static void openLive(String filePath, String content, FileObject fo) {
tab.scrollPane = new JScrollPane(tab.pane);
tab.add(tab.scrollPane, BorderLayout.CENTER);
- // Attach hyperlink listener and context menu
+ // Attach hyperlink listener, context menu, and find bar
tab.attachHyperlinkListener();
tab.bindRefreshKey(tab.pane);
tab.pane.setComponentPopupMenu(tab.buildContextMenu());
+ tab.initFindBar(tab.pane);
if (fo != null) {
tab.fileListener = makeFileListener(tab, fo);
@@ -207,6 +212,43 @@ public void updateContent(String markdown) {
});
}
+ private void initFindBar(JEditorPane p) {
+ findBar = new MarkdownFindBar(p);
+ add(findBar, BorderLayout.SOUTH);
+ bindFindBarKeys(p);
+ }
+
+ private void bindFindBarKeys(JEditorPane p) {
+ KeyStroke ctrlF = KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK);
+ KeyStroke f3 = KeyStroke.getKeyStroke(KeyEvent.VK_F3, 0);
+ KeyStroke shiftF3 = KeyStroke.getKeyStroke(KeyEvent.VK_F3, InputEvent.SHIFT_DOWN_MASK);
+ // WHEN_FOCUSED overrides built-in JEditorPane Ctrl+F before NetBeans global action
+ p.getInputMap(JComponent.WHEN_FOCUSED).put(ctrlF, "md-find-show");
+ // WHEN_IN_FOCUSED_WINDOW covers F3/Shift+F3 from anywhere in the tab
+ p.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ctrlF, "md-find-show");
+ p.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(f3, "md-find-next");
+ p.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(shiftF3, "md-find-prev");
+ p.getActionMap().put("md-find-show", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) { showFindBar(); }
+ });
+ p.getActionMap().put("md-find-next", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) {
+ if (findBar != null && findBar.isVisible()) findBar.findNext();
+ else if (findBar != null) findBar.searchAndFindNext();
+ }
+ });
+ p.getActionMap().put("md-find-prev", new AbstractAction() {
+ @Override public void actionPerformed(ActionEvent e) {
+ if (findBar != null && findBar.isVisible()) findBar.findPrev();
+ else if (findBar != null) findBar.searchAndFindPrev();
+ }
+ });
+ }
+
+ void showFindBar() {
+ if (findBar != null) findBar.gainFocus();
+ }
+
@Override
public void componentClosed() {
super.componentClosed();
@@ -483,6 +525,10 @@ JPopupMenu buildContextMenu() {
refresh.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F5, 0));
refresh.addActionListener(e -> forceReload());
+ JMenuItem find = new JMenuItem("Find…");
+ find.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F, InputEvent.CTRL_DOWN_MASK));
+ find.addActionListener(e -> showFindBar());
+
JMenuItem selectAll = new JMenuItem("Select All");
selectAll.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_DOWN_MASK));
selectAll.addActionListener(e -> { if (pane != null) pane.selectAll(); });
@@ -508,6 +554,8 @@ JPopupMenu buildContextMenu() {
menu.add(forward);
menu.add(refresh);
menu.addSeparator();
+ menu.add(find);
+ menu.addSeparator();
menu.add(selectAll);
menu.add(copy);
@@ -604,6 +652,7 @@ protected void componentOpened() {
attachHyperlinkListener();
bindRefreshKey(pane);
pane.setComponentPopupMenu(buildContextMenu());
+ initFindBar(pane);
}
filePath = path;
diff --git a/src/main/java/io/github/nbclaudecodegui/ui/common/markdown/MarkdownFindBar.java b/src/main/java/io/github/nbclaudecodegui/ui/common/markdown/MarkdownFindBar.java
new file mode 100644
index 00000000..68ccb1d6
--- /dev/null
+++ b/src/main/java/io/github/nbclaudecodegui/ui/common/markdown/MarkdownFindBar.java
@@ -0,0 +1,561 @@
+package io.github.nbclaudecodegui.ui.common.markdown;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Insets;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import javax.swing.AbstractAction;
+import javax.swing.AbstractButton;
+import javax.swing.Box;
+import javax.swing.BoxLayout;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JToggleButton;
+import javax.swing.KeyStroke;
+import javax.swing.MutableComboBoxModel;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+import javax.swing.text.BadLocationException;
+import javax.swing.text.DefaultHighlighter;
+import javax.swing.text.Highlighter;
+import javax.swing.text.JTextComponent;
+import org.openide.awt.CloseButtonFactory;
+import org.openide.util.ImageUtilities;
+import org.openide.util.NbPreferences;
+
+/**
+ * Find bar for Markdown Preview, mirroring the NetBeans SearchBar UI layout and icons.
+ * Icons are loaded from the {@code org-netbeans-modules-editor-search} module JAR at runtime.
+ */
+public class MarkdownFindBar extends JPanel {
+
+ private static final String ICON_BASE = "org/netbeans/modules/editor/search/resources/";
+ private static final Insets BTN_INSETS = new Insets(2, 1, 0, 1);
+ private static final int COMBO_MIN_WIDTH = 200;
+ private static final int COMBO_MAX_WIDTH = 350;
+
+ private static final Color HIGHLIGHT_ALL_COLOR = new Color(255, 255, 160);
+ private static final Color HIGHLIGHT_CURRENT_COLOR = new Color(255, 165, 0);
+
+ private final JTextComponent targetPane;
+
+ final JLabel findLabel;
+ final JComboBox incSearchComboBox;
+ final JButton findPreviousButton;
+ final JButton findNextButton;
+ final JButton selectAllButton;
+ final JToggleButton matchCase;
+ final JToggleButton wholeWords;
+ final JToggleButton regexp;
+ final JToggleButton highlight;
+ final JToggleButton wrapAround;
+ final JLabel matches;
+ final JButton closeButton;
+
+ private final Highlighter.HighlightPainter allPainter =
+ new DefaultHighlighter.DefaultHighlightPainter(HIGHLIGHT_ALL_COLOR);
+ private final Highlighter.HighlightPainter currentPainter =
+ new DefaultHighlighter.DefaultHighlightPainter(HIGHLIGHT_CURRENT_COLOR);
+
+ private final List matchPositions = new ArrayList<>();
+ private int currentMatchIndex = -1;
+
+ private final List