Skip to content

Commit 2c91212

Browse files
committed
implement search in files popover
1 parent 83b4b34 commit 2c91212

1 file changed

Lines changed: 362 additions & 0 deletions

File tree

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
package gr.sqlbrowserfx.nodes;
2+
3+
import java.io.File;
4+
import java.io.IOException;
5+
import java.util.ArrayList;
6+
import java.util.concurrent.Executors;
7+
import java.util.concurrent.ScheduledExecutorService;
8+
import java.util.concurrent.TimeUnit;
9+
import java.util.regex.Pattern;
10+
11+
import org.fxmisc.flowless.VirtualizedScrollPane;
12+
import org.fxmisc.richtext.CodeArea;
13+
14+
import gr.sqlbrowserfx.nodes.codeareas.FileCodeArea;
15+
import gr.sqlbrowserfx.nodes.codeareas.SimpleFileCodeArea;
16+
import gr.sqlbrowserfx.nodes.codeareas.java.FileJavaCodeArea;
17+
import gr.sqlbrowserfx.nodes.codeareas.sql.FileSqlCodeArea;
18+
import gr.sqlbrowserfx.nodes.codeareas.typescript.FileTypeScriptCodeArea;
19+
import gr.sqlbrowserfx.nodes.sqlpane.CustomPopOver;
20+
import gr.sqlbrowserfx.utils.FileInfo;
21+
import gr.sqlbrowserfx.utils.FilesUtils;
22+
import gr.sqlbrowserfx.utils.JavaFXUtils;
23+
import gr.sqlbrowserfx.utils.LineMatch;
24+
import gr.sqlbrowserfx.utils.PropertiesLoader;
25+
import javafx.application.Platform;
26+
import javafx.beans.property.SimpleStringProperty;
27+
import javafx.collections.FXCollections;
28+
import javafx.geometry.Orientation;
29+
import javafx.scene.control.Button;
30+
import javafx.scene.control.CheckBox;
31+
import javafx.scene.control.ContextMenu;
32+
import javafx.scene.control.Label;
33+
import javafx.scene.control.ListView;
34+
import javafx.scene.control.MenuItem;
35+
import javafx.scene.control.SplitPane;
36+
import javafx.scene.control.TableColumn;
37+
import javafx.scene.control.TableView;
38+
import javafx.scene.control.TextField;
39+
import javafx.scene.control.Tooltip;
40+
import javafx.scene.input.Clipboard;
41+
import javafx.scene.input.ClipboardContent;
42+
import javafx.scene.input.KeyCode;
43+
import javafx.scene.input.MouseButton;
44+
import javafx.scene.layout.BorderPane;
45+
import javafx.scene.layout.Priority;
46+
import javafx.scene.layout.VBox;
47+
import javafx.stage.DirectoryChooser;
48+
49+
public class SearchInFilesPopOver extends CustomPopOver {
50+
51+
private String rootPath = ((String) PropertiesLoader.getProperty("sqlbrowserfx.root.path", String.class, "~/"))
52+
.replaceAll("\"", "");
53+
54+
private CodeArea codeArea = new CodeArea();
55+
TextField searchField;
56+
private TextField extensionField;
57+
private TableView<FileInfo> filesTableView;
58+
private ListView<LineMatch> linesListView;
59+
private CheckBox wholeWordCheckBox;
60+
private CheckBox caseInsensitiveCheckBox;
61+
private SplitPane vSplit;
62+
63+
private ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor();
64+
65+
66+
public SearchInFilesPopOver() {
67+
68+
var fileSearchBox = this.createFileSearchBox();
69+
var linesListBox = this.createLinesListBox();
70+
var hSplit = new SplitPane(fileSearchBox, new CustomVBox(new Label("Lines Matches"),linesListBox));
71+
hSplit.setOrientation(Orientation.HORIZONTAL);
72+
hSplit.setDividerPositions(0.7f, 0.3f);
73+
74+
75+
vSplit = new SplitPane(hSplit, new VirtualizedScrollPane<CodeArea>(codeArea));
76+
vSplit.setOrientation(Orientation.VERTICAL);
77+
78+
var borderPane = new BorderPane(vSplit);
79+
this.setContentNode(borderPane);
80+
this.setPrefSize(1000, 800);
81+
// Add ESC key handler to close the stage
82+
borderPane.setOnKeyPressed(event -> {
83+
if (event.getCode() == KeyCode.ESCAPE) {
84+
PropertiesLoader.storeProperty("./sqlbrowserfx.properties", "sqlbrowserfx.root.path", this.rootPath);
85+
this.hide();
86+
}
87+
});
88+
}
89+
90+
91+
private VBox createLinesListBox() {
92+
this.createLinesListView();
93+
94+
var nextBtn = new Button("Next >");
95+
nextBtn.setOnAction(evetn -> {
96+
var idx = linesListView.getSelectionModel().getSelectedIndex();
97+
if (idx < linesListView.getItems().size() - 1) {
98+
idx++;
99+
}
100+
101+
linesListView.getSelectionModel().select(idx);
102+
selectLineMatch(linesListView.getSelectionModel().getSelectedItem());
103+
});
104+
105+
var prevBtn = new Button("< Prev");
106+
prevBtn.setOnAction(evetn -> {
107+
var idx = linesListView.getSelectionModel().getSelectedIndex();
108+
if (idx > 0) {
109+
idx--;
110+
}
111+
112+
linesListView.getSelectionModel().select(idx);
113+
selectLineMatch(linesListView.getSelectionModel().getSelectedItem());
114+
});
115+
116+
return new VBox(new CustomHBox(prevBtn, nextBtn), linesListView);
117+
}
118+
119+
private VBox createFileSearchBox() {
120+
var vbox = new CustomVBox();
121+
var openButton = new Button("", JavaFXUtils.createIcon("/icons/code-file.png"));
122+
openButton.setTooltip(new Tooltip("Open file"));
123+
124+
extensionField = new TextField();
125+
extensionField.setPromptText("File Extension...");
126+
searchField = new TextField();
127+
// searchField.setPrefWidth(576);
128+
searchField.setPromptText("Pattern...");
129+
searchField.setOnKeyPressed(keyEvent -> {
130+
if (keyEvent.getCode() == KeyCode.ENTER) {
131+
search();
132+
}
133+
134+
if (keyEvent.getCode() != KeyCode.ESCAPE) {
135+
keyEvent.consume();
136+
}
137+
});
138+
wholeWordCheckBox = new CheckBox("ww");
139+
wholeWordCheckBox.setTooltip(new Tooltip("Whole Word"));
140+
wholeWordCheckBox.setFocusTraversable(false);
141+
caseInsensitiveCheckBox = new CheckBox("ci");
142+
caseInsensitiveCheckBox.setTooltip(new Tooltip("Case Insensitive"));
143+
caseInsensitiveCheckBox.setFocusTraversable(false);
144+
145+
var descLabel = new Label("File Search in: " + rootPath);
146+
147+
var settingsButton = new Button("", JavaFXUtils.createIcon("/icons/settings.png"));
148+
settingsButton.setOnMouseClicked(event -> {
149+
var dirChooser = new DirectoryChooser();
150+
File initialDir = new File(this.rootPath);
151+
dirChooser.setInitialDirectory(initialDir);
152+
var selectedDir = dirChooser.showDialog(vbox.getScene().getWindow());
153+
if (selectedDir != null) {
154+
this.rootPath = selectedDir.getAbsolutePath();
155+
descLabel.setText("File Search in: " + rootPath);
156+
}
157+
});
158+
settingsButton.setTooltip(new Tooltip("Click to change root path"));
159+
160+
this.createFilesTableView();
161+
162+
var searchButton = new Button("Search", JavaFXUtils.createIcon("/icons/magnify.png"));
163+
searchButton.setOnAction(event -> this.search());
164+
165+
vbox.getChildren().addAll(
166+
new CustomHBox(settingsButton, descLabel),
167+
new CustomHBox(
168+
searchField,
169+
caseInsensitiveCheckBox,
170+
wholeWordCheckBox,
171+
extensionField,
172+
searchButton
173+
),
174+
filesTableView);
175+
return vbox;
176+
}
177+
178+
private void createLinesListView() {
179+
linesListView = new ListView<>();
180+
linesListView.setOnMouseClicked(event -> {
181+
if (event.getClickCount() == 1) {
182+
var selectedItem = linesListView.getSelectionModel().getSelectedItem();
183+
this.selectLineMatch(selectedItem);
184+
}
185+
});
186+
187+
188+
linesListView.setOnKeyPressed(keyEvent -> {
189+
if (keyEvent.getCode() == KeyCode.ENTER
190+
|| keyEvent.getCode() == KeyCode.UP
191+
|| keyEvent.getCode() == KeyCode.DOWN) {
192+
var selectedItem = linesListView.getSelectionModel().getSelectedItem();
193+
this.selectLineMatch(selectedItem);
194+
}
195+
});
196+
197+
VBox.setVgrow(linesListView, Priority.ALWAYS);
198+
}
199+
200+
private void createFilesTableView() {
201+
filesTableView = new TableView<>();
202+
var nameColumn = new TableColumn<FileInfo, String>("File");
203+
nameColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getName()));
204+
filesTableView.getColumns().add(nameColumn);
205+
nameColumn.setPrefWidth(400);
206+
207+
var countColumn = new TableColumn<FileInfo, String>("Matches");
208+
countColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getMatchCount().toString()));
209+
filesTableView.getColumns().add(countColumn);
210+
211+
var pathColumn = new TableColumn<FileInfo, String>("Relative Path");
212+
pathColumn.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().getRelativePath()));
213+
filesTableView.getColumns().add(pathColumn);
214+
pathColumn.setPrefWidth(400);
215+
filesTableView.setMaxHeight(Double.MAX_VALUE);
216+
VBox.setVgrow(filesTableView, Priority.ALWAYS);
217+
218+
filesTableView.setOnKeyPressed(keyEvent -> {
219+
if (keyEvent.getCode() == KeyCode.ENTER
220+
|| keyEvent.getCode() == KeyCode.UP
221+
|| keyEvent.getCode() == KeyCode.DOWN) {
222+
this.selectFile();
223+
}
224+
});
225+
filesTableView.setOnMouseClicked(mouseEvent -> {
226+
if (mouseEvent.getButton() == MouseButton.PRIMARY) {
227+
this.selectFile();
228+
}
229+
});
230+
231+
var menuItemCopy = new MenuItem("Copy Absolute Path", JavaFXUtils.createIcon("/icons/copy.png"));
232+
menuItemCopy.setOnAction(event -> {
233+
var selectedItem = filesTableView.getSelectionModel().getSelectedItem();
234+
if (selectedItem != null) {
235+
var content = new ClipboardContent();
236+
content.putString(selectedItem.getAbsolutePath());
237+
Clipboard.getSystemClipboard().setContent(content);
238+
}
239+
});
240+
241+
var openInVSCode = new MenuItem("Open in VS Code", JavaFXUtils.createIcon("/icons/code-file.png"));
242+
openInVSCode.setOnAction(e -> {
243+
var selected = filesTableView.getSelectionModel().getSelectedItem();
244+
if (selected != null) {
245+
openInVSCode(selected.getAbsolutePath(), false);
246+
}
247+
});
248+
var openInActiveVSCode = new MenuItem("Open in Active VS Code", JavaFXUtils.createIcon("/icons/code-file.png"));
249+
openInActiveVSCode.setOnAction(e -> {
250+
var selected = filesTableView.getSelectionModel().getSelectedItem();
251+
if (selected != null) {
252+
openInVSCode(selected.getAbsolutePath(), true);
253+
}
254+
});
255+
256+
filesTableView.setContextMenu(new ContextMenu(menuItemCopy, openInVSCode, openInActiveVSCode));
257+
}
258+
259+
private void selectFile() {
260+
var fileInfo = filesTableView.getSelectionModel().getSelectedItem();
261+
if (fileInfo == null) {
262+
return;
263+
}
264+
this.openFile(fileInfo.getAbsolutePath());
265+
this.linesListView.setItems(FXCollections.observableArrayList(fileInfo.getLineMatches()));
266+
this.selectLineMatch(this.linesListView.getItems().get(0));
267+
}
268+
269+
private void selectLineMatch(LineMatch selectedItem) {
270+
if (selectedItem == null) {
271+
return;
272+
}
273+
int line = selectedItem.getLineNumber() - 1; // CodeArea lines are 0-based
274+
int position = codeArea.position(line, 0).toOffset();
275+
codeArea.moveTo(position);
276+
277+
int lineEnd = codeArea.position(line + 1, 0).toOffset();
278+
codeArea.selectRange(position, lineEnd);
279+
280+
codeArea.requestFollowCaret();
281+
}
282+
283+
284+
private void openFile(String absolutePath) {
285+
if (codeArea instanceof FileCodeArea && absolutePath.equals(((FileCodeArea) this.codeArea).getPath())) {
286+
return;
287+
}
288+
289+
var file = new File(absolutePath);
290+
if (file.getName().endsWith(".java")) {
291+
this.codeArea = new FileJavaCodeArea(file);
292+
} else if (file.getName().endsWith(".sql")) {
293+
this.codeArea = new FileSqlCodeArea(file);
294+
} else if (file.getName().endsWith(".ts")) {
295+
this.codeArea = new FileTypeScriptCodeArea(file);
296+
} else if (file.getName().endsWith(".html")) {
297+
this.codeArea = new FileTypeScriptCodeArea(file);
298+
}
299+
else {
300+
this.codeArea = new SimpleFileCodeArea(file);
301+
}
302+
303+
this.vSplit.getItems().remove(1);
304+
this.vSplit.getItems().add(new VirtualizedScrollPane<CodeArea>(codeArea));
305+
}
306+
307+
private void openInVSCode(String filePath, boolean reuseWindow) {
308+
try {
309+
var command = new ArrayList<String>();
310+
command.add("codium");
311+
312+
if (reuseWindow) {
313+
command.add("-r"); // reuse active window
314+
}
315+
316+
command.add(filePath);
317+
318+
new ProcessBuilder(command)
319+
.redirectErrorStream(true)
320+
.start();
321+
322+
} catch (IOException ex) {
323+
System.err.println("Failed to open file in VS Code: " + ex.getMessage());
324+
}
325+
}
326+
327+
private void search() {
328+
searchField.setDisable(true);
329+
filesTableView.setDisable(true);
330+
331+
var rawPattern = searchField.getText();
332+
if (rawPattern.isEmpty()) {
333+
searchField.setDisable(false);
334+
filesTableView.setDisable(false);
335+
return;
336+
}
337+
338+
// Build regex safely
339+
var pattern = Pattern.quote(rawPattern);
340+
if (wholeWordCheckBox.isSelected() && rawPattern.matches("\\w+")) {
341+
pattern = "\\b" + pattern + "\\b";
342+
}
343+
344+
if (caseInsensitiveCheckBox.isSelected()) {
345+
pattern = "(?i)" + pattern;
346+
}
347+
348+
final var finalPattern = pattern;
349+
final var extension = extensionField.getText();
350+
351+
executor.schedule(() -> {
352+
var searchResults = FilesUtils.walkContentsWithLines(rootPath, finalPattern, extension, 10);
353+
354+
Platform.runLater(() -> {
355+
filesTableView.setItems(FXCollections.observableArrayList(searchResults));
356+
searchField.setDisable(false);
357+
filesTableView.setDisable(false);
358+
});
359+
}, 0, TimeUnit.SECONDS);
360+
}
361+
362+
}

0 commit comments

Comments
 (0)