added new options to file search

This commit is contained in:
Radek Davidek 2026-02-10 16:50:04 +01:00
parent 474f1fd38f
commit 461bb74503
3 changed files with 154 additions and 37 deletions

View File

@ -656,7 +656,7 @@ public class FileOperations {
* Search files by filename pattern and/or content text.
* If both are provided, both must match.
*/
public static void search(File directory, String filenamePattern, String contentText, boolean recursive, boolean searchArchives, SearchCallback callback) throws IOException {
public static void search(File directory, String filenamePattern, String contentText, boolean recursive, boolean searchArchives, boolean wholeWord, boolean caseSensitive, SearchCallback callback) throws IOException {
Pattern filenameRegex = null;
String filenameLower = null;
if (filenamePattern != null && !filenamePattern.isEmpty()) {
@ -672,7 +672,13 @@ public class FileOperations {
Pattern contentPattern = null;
if (contentText != null && !contentText.isEmpty()) {
contentPattern = Pattern.compile(Pattern.quote(contentText), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
String quote = Pattern.quote(contentText);
String regex = wholeWord ? "(?<!\\w)" + quote + "(?!\\w)" : quote;
int flags = Pattern.UNICODE_CASE;
if (!caseSensitive) {
flags |= Pattern.CASE_INSENSITIVE;
}
contentPattern = Pattern.compile(regex, flags);
}
searchRecursive(directory.toPath(), filenameLower, filenameRegex, contentPattern, recursive, searchArchives, callback);

View File

@ -11,6 +11,8 @@ import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
/**
* Internal file editor/viewer
@ -49,12 +51,22 @@ public class FileEditor extends JFrame {
// Search support
private JPanel searchPanel;
private JTextField searchField;
private JCheckBox wholeWordCheckBox;
private JCheckBox caseSensitiveCheckBox;
private JLabel searchStatusLabel;
private static String lastSearchValue = "";
private static boolean lastWholeWord = false;
private static boolean lastCaseSensitive = false;
public static void setLastSearchValue(String value) {
lastSearchValue = value;
}
public static void setLastSearchOptions(String value, boolean wholeWord, boolean caseSensitive) {
lastSearchValue = value;
lastWholeWord = wholeWord;
lastCaseSensitive = caseSensitive;
}
public FileEditor(Window parent, File file, AppConfig config, boolean readOnly) {
this(parent, file, null, config, readOnly);
@ -104,6 +116,9 @@ public class FileEditor extends JFrame {
searchField = new JTextField(20);
searchField.addActionListener(e -> findNext());
wholeWordCheckBox = new JCheckBox("Whole word");
caseSensitiveCheckBox = new JCheckBox("Case sensitive");
JButton nextBtn = new JButton("Next");
nextBtn.addActionListener(e -> findNext());
@ -118,6 +133,8 @@ public class FileEditor extends JFrame {
searchPanel.add(new JLabel("Search:"));
searchPanel.add(searchField);
searchPanel.add(wholeWordCheckBox);
searchPanel.add(caseSensitiveCheckBox);
searchPanel.add(nextBtn);
searchPanel.add(prevBtn);
searchPanel.add(closeBtn);
@ -128,6 +145,8 @@ public class FileEditor extends JFrame {
private void showSearchPanel(boolean focusField) {
searchPanel.setVisible(true);
wholeWordCheckBox.setSelected(lastWholeWord);
caseSensitiveCheckBox.setSelected(lastCaseSensitive);
String selection = textArea.getSelectedText();
if (selection != null && !selection.isEmpty() && !selection.contains("\n")) {
searchField.setText(selection);
@ -171,7 +190,10 @@ public class FileEditor extends JFrame {
String text = searchField.getText();
if (text.isEmpty()) text = lastSearchValue;
if (text == null || text.isEmpty()) return;
lastSearchValue = text;
lastWholeWord = wholeWordCheckBox.isSelected();
lastCaseSensitive = caseSensitiveCheckBox.isSelected();
updateSearchHistory(text);
if (searchField.getText().isEmpty()) searchField.setText(text);
@ -181,64 +203,132 @@ public class FileEditor extends JFrame {
// If we have a selection that matches, start after it
if (textArea.getSelectionEnd() > textArea.getSelectionStart()) {
if (content.substring(textArea.getSelectionStart(), textArea.getSelectionEnd()).equalsIgnoreCase(text)) {
String selected = content.substring(textArea.getSelectionStart(), textArea.getSelectionEnd());
boolean match;
if (lastCaseSensitive) {
match = selected.equals(text);
} else {
match = selected.equalsIgnoreCase(text);
}
if (match) {
start = textArea.getSelectionEnd();
}
}
int idx = content.toLowerCase().indexOf(text.toLowerCase(), start);
if (idx == -1) {
// wrap around
idx = content.toLowerCase().indexOf(text.toLowerCase(), 0);
String quote = Pattern.quote(text);
String regex = lastWholeWord ? "(?<!\\w)" + quote + "(?!\\w)" : quote;
int flags = 0;
if (!lastCaseSensitive) {
flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
}
Pattern pattern = Pattern.compile(regex, flags);
Matcher matcher = pattern.matcher(content);
if (idx != -1) {
textArea.setSelectionStart(idx);
textArea.setSelectionEnd(idx + text.length());
textArea.getCaret().setSelectionVisible(true);
try {
Rectangle rect = textArea.modelToView2D(idx).getBounds();
textArea.scrollRectToVisible(rect);
} catch (Exception ignore) {}
searchStatusLabel.setText("");
if (matcher.find(start)) {
selectSearchResult(matcher.start(), matcher.end());
} else {
searchStatusLabel.setText("Not found");
// wrap around
if (matcher.find(0)) {
int res = JOptionPane.showConfirmDialog(this,
"Reached the end of the file. Start searching from the beginning?",
"Search",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (res == JOptionPane.YES_OPTION) {
selectSearchResult(matcher.start(), matcher.end());
}
} else {
showNotFoundDialog();
}
}
}
private void showNotFoundDialog() {
JOptionPane pane = new JOptionPane("Text not found", JOptionPane.INFORMATION_MESSAGE);
JDialog dialog = pane.createDialog(this, "Search");
// Escape to close
dialog.getRootPane().registerKeyboardAction(e -> {
dialog.dispose();
}, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW);
dialog.setVisible(true);
}
private void findPrevious() {
String text = searchField.getText();
if (text.isEmpty()) text = lastSearchValue;
if (text == null || text.isEmpty()) return;
lastSearchValue = text;
lastWholeWord = wholeWordCheckBox.isSelected();
lastCaseSensitive = caseSensitiveCheckBox.isSelected();
updateSearchHistory(text);
if (searchField.getText().isEmpty()) searchField.setText(text);
String content = textArea.getText();
int start = textArea.getSelectionStart() - 1;
if (start < 0) start = content.length() - 1;
int start = textArea.getSelectionStart();
if (start < 0) start = content.length();
int idx = content.toLowerCase().lastIndexOf(text.toLowerCase(), start);
if (idx == -1) {
// wrap around
idx = content.toLowerCase().lastIndexOf(text.toLowerCase(), content.length() - 1);
String quote = Pattern.quote(text);
String regex = lastWholeWord ? "(?<!\\w)" + quote + "(?!\\w)" : quote;
int flags = 0;
if (!lastCaseSensitive) {
flags |= Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE;
}
Pattern pattern = Pattern.compile(regex, flags);
Matcher matcher = pattern.matcher(content);
int lastMatchStart = -1;
int lastMatchEnd = -1;
int searchIdx = 0;
while (matcher.find(searchIdx)) {
if (matcher.start() < start) {
lastMatchStart = matcher.start();
lastMatchEnd = matcher.end();
searchIdx = matcher.start() + 1;
} else {
break;
}
}
if (idx != -1) {
textArea.setSelectionStart(idx);
textArea.setSelectionEnd(idx + text.length());
textArea.getCaret().setSelectionVisible(true);
try {
Rectangle rect = textArea.modelToView2D(idx).getBounds();
textArea.scrollRectToVisible(rect);
} catch (Exception ignore) {}
searchStatusLabel.setText("");
if (lastMatchStart != -1) {
selectSearchResult(lastMatchStart, lastMatchEnd);
} else {
searchStatusLabel.setText("Not found");
// wrap around to the very last match in the file
searchIdx = 0;
while (matcher.find(searchIdx)) {
lastMatchStart = matcher.start();
lastMatchEnd = matcher.end();
searchIdx = matcher.start() + 1;
}
if (lastMatchStart != -1) {
int res = JOptionPane.showConfirmDialog(this,
"Reached the beginning of the file. Continue from the end?",
"Search",
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE);
if (res == JOptionPane.YES_OPTION) {
selectSearchResult(lastMatchStart, lastMatchEnd);
}
} else {
showNotFoundDialog();
}
}
}
private void selectSearchResult(int start, int end) {
textArea.setSelectionStart(start);
textArea.setSelectionEnd(end);
textArea.getCaret().setSelectionVisible(true);
try {
Rectangle rect = textArea.modelToView2D(start).getBounds();
textArea.scrollRectToVisible(rect);
} catch (Exception ignore) {}
searchStatusLabel.setText("");
}
private void updateSearchHistory(String text) {
if (config == null || text == null || text.isEmpty()) return;
java.util.List<String> hist = new java.util.ArrayList<>(config.getContentSearchHistory());

View File

@ -23,6 +23,8 @@ public class SearchDialog extends JDialog {
private JCheckBox recursiveCheckBox;
private JCheckBox contentSearchCheckBox;
private JCheckBox archiveSearchCheckBox;
private JCheckBox wholeWordCheckBox;
private JCheckBox caseSensitiveCheckBox;
private JTable resultsTable;
private ResultsTableModel tableModel;
private JButton searchButton;
@ -175,17 +177,30 @@ public class SearchDialog extends JDialog {
contentPatternCombo.setToolTipText("Text to search inside files");
searchPanel.add(contentPatternCombo, gbc);
gbc.gridx = 0;
gbc.gridy = 3;
gbc.gridwidth = 2;
contentSearchCheckBox = new JCheckBox("Search inside file contents", false);
searchPanel.add(contentSearchCheckBox, gbc);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 1;
wholeWordCheckBox = new JCheckBox("Whole word only", false);
searchPanel.add(wholeWordCheckBox, gbc);
gbc.gridx = 1;
caseSensitiveCheckBox = new JCheckBox("Case sensitive", false);
searchPanel.add(caseSensitiveCheckBox, gbc);
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridwidth = 2;
archiveSearchCheckBox = new JCheckBox("Search inside archives", false);
archiveSearchCheckBox.setMnemonic(KeyEvent.VK_R);
searchPanel.add(archiveSearchCheckBox, gbc);
gbc.gridy = 5;
gbc.gridy = 6;
JLabel pathLabel = new JLabel("Directory: " + searchDirectory.getAbsolutePath());
pathLabel.setFont(pathLabel.getFont().deriveFont(Font.ITALIC));
searchPanel.add(pathLabel, gbc);
@ -543,6 +558,8 @@ public class SearchDialog extends JDialog {
final String finalNamePat = namePat;
final String finalContentPat = isContentSearch ? contentPat : null;
final boolean searchArchives = archiveSearchCheckBox != null && archiveSearchCheckBox.isSelected();
final boolean wholeWord = wholeWordCheckBox != null && wholeWordCheckBox.isSelected();
final boolean caseSensitive = caseSensitiveCheckBox != null && caseSensitiveCheckBox.isSelected();
// Reset and show status
foundCount = 0;
@ -556,7 +573,7 @@ public class SearchDialog extends JDialog {
@Override
protected Void doInBackground() throws Exception {
FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, new FileOperations.SearchCallback() {
FileOperations.search(searchDirectory, finalNamePat, finalContentPat, recursiveCheckBox.isSelected(), searchArchives, wholeWord, caseSensitive, new FileOperations.SearchCallback() {
@Override
public void onFileFound(File file, String virtualPath) {
publish(new Object[]{"file", file, virtualPath});
@ -701,7 +718,9 @@ public class SearchDialog extends JDialog {
contentPat = cit != null ? cit.toString().trim() : "";
} catch (Exception ex) {}
if (!contentPat.isEmpty()) {
FileEditor.setLastSearchValue(contentPat);
FileEditor.setLastSearchOptions(contentPat,
wholeWordCheckBox != null && wholeWordCheckBox.isSelected(),
caseSensitiveCheckBox != null && caseSensitiveCheckBox.isSelected());
}
FileEditor viewer = new FileEditor(owner, item.getFile(), vPath, config, true);
@ -745,7 +764,9 @@ public class SearchDialog extends JDialog {
contentPat = cit != null ? cit.toString().trim() : "";
} catch (Exception ex) {}
if (!contentPat.isEmpty()) {
FileEditor.setLastSearchValue(contentPat);
FileEditor.setLastSearchOptions(contentPat,
wholeWordCheckBox != null && wholeWordCheckBox.isSelected(),
caseSensitiveCheckBox != null && caseSensitiveCheckBox.isSelected());
}
FileEditor editor = new FileEditor(owner, item.getFile(), config, false);