Merge pull request #3865 from techee/none_crash
[geany-mirror.git] / src / search.c
bloba4d6babd67fccdd9ebd1cb457d40ecc1e1c0ba9f
1 /*
2 * search.c - this file is part of Geany, a fast and lightweight IDE
4 * Copyright 2006 The Geany contributors
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
22 * Find, Replace, Find in Files dialog related functions.
23 * Note that the basic text find functions are in document.c.
26 #ifdef HAVE_CONFIG_H
27 # include "config.h"
28 #endif
30 #include "search.h"
32 #include "app.h"
33 #include "dialogs.h"
34 #include "document.h"
35 #include "encodings.h"
36 #include "encodingsprivate.h"
37 #include "keyfile.h"
38 #include "msgwindow.h"
39 #include "prefs.h"
40 #include "sciwrappers.h"
41 #include "spawn.h"
42 #include "stash.h"
43 #include "support.h"
44 #include "toolbar.h"
45 #include "ui_utils.h"
46 #include "utils.h"
48 #include <unistd.h>
49 #include <string.h>
50 #include <ctype.h>
52 #include <gtk/gtk.h>
53 #include <gdk/gdkkeysyms.h>
55 enum
57 GEANY_RESPONSE_FIND = 1,
58 GEANY_RESPONSE_FIND_PREVIOUS,
59 GEANY_RESPONSE_FIND_IN_FILE,
60 GEANY_RESPONSE_FIND_IN_SESSION,
61 GEANY_RESPONSE_MARK,
62 GEANY_RESPONSE_REPLACE,
63 GEANY_RESPONSE_REPLACE_AND_FIND,
64 GEANY_RESPONSE_REPLACE_IN_SESSION,
65 GEANY_RESPONSE_REPLACE_IN_FILE,
66 GEANY_RESPONSE_REPLACE_IN_SEL
70 enum
72 FILES_MODE_ALL,
73 FILES_MODE_PROJECT,
74 FILES_MODE_CUSTOM
78 GeanySearchData search_data;
79 GeanySearchPrefs search_prefs;
82 static struct
84 gboolean fif_regexp;
85 gboolean fif_case_sensitive;
86 gboolean fif_match_whole_word;
87 gboolean fif_invert_results;
88 gboolean fif_recursive;
89 gboolean fif_use_extra_options;
90 gchar *fif_extra_options;
91 gint fif_files_mode;
92 gchar *fif_files;
93 gboolean find_regexp;
94 gboolean find_regexp_multiline;
95 gboolean find_escape_sequences;
96 gboolean find_case_sensitive;
97 gboolean find_match_whole_word;
98 gboolean find_match_word_start;
99 gboolean find_close_dialog;
100 gboolean replace_regexp;
101 gboolean replace_regexp_multiline;
102 gboolean replace_escape_sequences;
103 gboolean replace_case_sensitive;
104 gboolean replace_match_whole_word;
105 gboolean replace_match_word_start;
106 gboolean replace_search_backwards;
107 gboolean replace_close_dialog;
109 settings;
111 static StashGroup *fif_prefs = NULL;
112 static StashGroup *find_prefs = NULL;
113 static StashGroup *replace_prefs = NULL;
116 static struct
118 GtkWidget *dialog;
119 GtkWidget *entry;
120 gboolean all_expanded;
121 gint position[2]; /* x, y */
123 find_dlg = {NULL, NULL, FALSE, {0, 0}};
125 static struct
127 GtkWidget *dialog;
128 GtkWidget *find_combobox;
129 GtkWidget *find_entry;
130 GtkWidget *replace_combobox;
131 GtkWidget *replace_entry;
132 gboolean all_expanded;
133 gint position[2]; /* x, y */
135 replace_dlg = {NULL, NULL, NULL, NULL, NULL, FALSE, {0, 0}};
137 static struct
139 GtkWidget *dialog;
140 GtkWidget *dir_combo;
141 GtkWidget *files_combo;
142 GtkWidget *search_combo;
143 GtkWidget *encoding_combo;
144 GtkWidget *files_mode_combo;
145 gint position[2]; /* x, y */
147 fif_dlg = {NULL, NULL, NULL, NULL, NULL, NULL, {0, 0}};
150 static void search_read_io(GString *string, GIOCondition condition, gpointer data);
151 static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data);
153 static void search_finished(GPid child_pid, gint status, gpointer user_data);
155 static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir);
157 static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags);
160 static void
161 on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data);
163 static void
164 on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
166 static void
167 on_find_entry_activate(GtkEntry *entry, gpointer user_data);
169 static void
170 on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data);
172 static void
173 on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
175 static void
176 on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data);
178 static void
179 on_replace_entry_activate(GtkEntry *entry, gpointer user_data);
181 static void
182 on_find_in_files_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
184 static gboolean
185 search_find_in_files(const gchar *utf8_search_text, const gchar *dir, const gchar *opts,
186 const gchar *enc);
189 static void init_prefs(void)
191 StashGroup *group;
193 group = stash_group_new("search");
194 configuration_add_pref_group(group, TRUE);
195 stash_group_add_toggle_button(group, &search_prefs.always_wrap,
196 "pref_search_hide_find_dialog", FALSE, "check_always_wrap_search");
197 stash_group_add_toggle_button(group, &search_prefs.hide_find_dialog,
198 "pref_search_always_wrap", FALSE, "check_hide_find_dialog");
199 stash_group_add_toggle_button(group, &search_prefs.use_current_file_dir,
200 "pref_search_current_file_dir", TRUE, "check_fif_current_dir");
202 /* dialog layout & positions */
203 group = stash_group_new("search");
204 configuration_add_session_group(group, FALSE);
205 stash_group_add_boolean(group, &find_dlg.all_expanded, "find_all_expanded", FALSE);
206 stash_group_add_boolean(group, &replace_dlg.all_expanded, "replace_all_expanded", FALSE);
207 stash_group_add_integer(group, &find_dlg.position[0], "position_find_x", -1);
208 stash_group_add_integer(group, &find_dlg.position[1], "position_find_y", -1);
209 stash_group_add_integer(group, &replace_dlg.position[0], "position_replace_x", -1);
210 stash_group_add_integer(group, &replace_dlg.position[1], "position_replace_y", -1);
211 stash_group_add_integer(group, &fif_dlg.position[0], "position_fif_x", -1);
212 stash_group_add_integer(group, &fif_dlg.position[1], "position_fif_y", -1);
214 memset(&settings, '\0', sizeof(settings));
216 group = stash_group_new("search");
217 fif_prefs = group;
218 configuration_add_pref_group(group, FALSE);
219 stash_group_add_toggle_button(group, &settings.fif_regexp,
220 "fif_regexp", FALSE, "check_regexp");
221 stash_group_add_toggle_button(group, &settings.fif_case_sensitive,
222 "fif_case_sensitive", TRUE, "check_case");
223 stash_group_add_toggle_button(group, &settings.fif_match_whole_word,
224 "fif_match_whole_word", FALSE, "check_wholeword");
225 stash_group_add_toggle_button(group, &settings.fif_invert_results,
226 "fif_invert_results", FALSE, "check_invert");
227 stash_group_add_toggle_button(group, &settings.fif_recursive,
228 "fif_recursive", FALSE, "check_recursive");
229 stash_group_add_entry(group, &settings.fif_extra_options,
230 "fif_extra_options", "", "entry_extra");
231 stash_group_add_toggle_button(group, &settings.fif_use_extra_options,
232 "fif_use_extra_options", FALSE, "check_extra");
233 stash_group_add_entry(group, &settings.fif_files,
234 "fif_files", "", "entry_files");
235 stash_group_add_combo_box(group, &settings.fif_files_mode,
236 "fif_files_mode", FILES_MODE_ALL, "combo_files_mode");
238 group = stash_group_new("search");
239 find_prefs = group;
240 configuration_add_pref_group(group, FALSE);
241 stash_group_add_toggle_button(group, &settings.find_regexp,
242 "find_regexp", FALSE, "check_regexp");
243 stash_group_add_toggle_button(group, &settings.find_regexp_multiline,
244 "find_regexp_multiline", FALSE, "check_multiline");
245 stash_group_add_toggle_button(group, &settings.find_case_sensitive,
246 "find_case_sensitive", FALSE, "check_case");
247 stash_group_add_toggle_button(group, &settings.find_escape_sequences,
248 "find_escape_sequences", FALSE, "check_escape");
249 stash_group_add_toggle_button(group, &settings.find_match_whole_word,
250 "find_match_whole_word", FALSE, "check_word");
251 stash_group_add_toggle_button(group, &settings.find_match_word_start,
252 "find_match_word_start", FALSE, "check_wordstart");
253 stash_group_add_toggle_button(group, &settings.find_close_dialog,
254 "find_close_dialog", TRUE, "check_close");
256 group = stash_group_new("search");
257 replace_prefs = group;
258 configuration_add_pref_group(group, FALSE);
259 stash_group_add_toggle_button(group, &settings.replace_regexp,
260 "replace_regexp", FALSE, "check_regexp");
261 stash_group_add_toggle_button(group, &settings.replace_regexp_multiline,
262 "replace_regexp_multiline", FALSE, "check_multiline");
263 stash_group_add_toggle_button(group, &settings.replace_case_sensitive,
264 "replace_case_sensitive", FALSE, "check_case");
265 stash_group_add_toggle_button(group, &settings.replace_escape_sequences,
266 "replace_escape_sequences", FALSE, "check_escape");
267 stash_group_add_toggle_button(group, &settings.replace_match_whole_word,
268 "replace_match_whole_word", FALSE, "check_word");
269 stash_group_add_toggle_button(group, &settings.replace_match_word_start,
270 "replace_match_word_start", FALSE, "check_wordstart");
271 stash_group_add_toggle_button(group, &settings.replace_search_backwards,
272 "replace_search_backwards", FALSE, "check_back");
273 stash_group_add_toggle_button(group, &settings.replace_close_dialog,
274 "replace_close_dialog", TRUE, "check_close");
278 void search_init(void)
280 search_data.text = NULL;
281 search_data.original_text = NULL;
282 init_prefs();
286 #define FREE_WIDGET(wid) \
287 if (wid && GTK_IS_WIDGET(wid)) gtk_widget_destroy(wid);
289 void search_finalize(void)
291 FREE_WIDGET(find_dlg.dialog);
292 FREE_WIDGET(replace_dlg.dialog);
293 FREE_WIDGET(fif_dlg.dialog);
294 g_free(search_data.text);
295 g_free(search_data.original_text);
299 static void on_widget_toggled_set_insensitive(
300 GtkToggleButton *togglebutton, gpointer user_data)
302 gtk_widget_set_sensitive(GTK_WIDGET(user_data),
303 !gtk_toggle_button_get_active(togglebutton));
307 static GtkWidget *add_find_checkboxes(GtkDialog *dialog)
309 GtkWidget *checkbox1, *checkbox2, *check_regexp, *checkbox5,
310 *checkbox7, *check_multiline, *hbox, *fbox, *mbox;
312 check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions"));
313 ui_hookup_widget(dialog, check_regexp, "check_regexp");
314 gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE);
315 gtk_widget_set_tooltip_text(check_regexp, _("Use Perl-like regular expressions. "
316 "For detailed information about using regular expressions, please refer to the manual."));
317 g_signal_connect(check_regexp, "toggled",
318 G_CALLBACK(on_find_replace_checkbutton_toggled), dialog);
320 checkbox7 = gtk_check_button_new_with_mnemonic(_("Use _escape sequences"));
321 ui_hookup_widget(dialog, checkbox7, "check_escape");
322 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox7), FALSE);
323 gtk_widget_set_tooltip_text(checkbox7,
324 _("Replace \\\\, \\t, \\n, \\r and \\uXXXX (Unicode characters) with the "
325 "corresponding control characters"));
327 check_multiline = gtk_check_button_new_with_mnemonic(_("Use multi-line matchin_g"));
328 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_multiline), FALSE);
329 gtk_widget_set_sensitive(check_multiline, FALSE);
330 ui_hookup_widget(dialog, check_multiline, "check_multiline");
331 gtk_button_set_focus_on_click(GTK_BUTTON(check_multiline), FALSE);
332 gtk_widget_set_tooltip_text(check_multiline, _("Perform regular expression "
333 "matching on the whole buffer at once rather than line by line, allowing "
334 "matches to span multiple lines. In this mode, newline characters are part "
335 "of the input and can be captured as normal characters by the pattern."));
337 /* Search features */
338 fbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
339 gtk_box_pack_start(GTK_BOX(fbox), check_regexp, FALSE, FALSE, 0);
340 gtk_box_pack_start(GTK_BOX(fbox), check_multiline, FALSE, FALSE, 0);
341 gtk_box_pack_start(GTK_BOX(fbox), checkbox7, FALSE, FALSE, 0);
343 if (dialog != GTK_DIALOG(find_dlg.dialog))
345 GtkWidget *check_back = gtk_check_button_new_with_mnemonic(_("Search _backwards"));
346 ui_hookup_widget(dialog, check_back, "check_back");
347 gtk_button_set_focus_on_click(GTK_BUTTON(check_back), FALSE);
348 gtk_container_add(GTK_CONTAINER(fbox), check_back);
351 checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive"));
352 ui_hookup_widget(dialog, checkbox1, "check_case");
353 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE);
355 checkbox2 = gtk_check_button_new_with_mnemonic(_("Match only a _whole word"));
356 ui_hookup_widget(dialog, checkbox2, "check_word");
357 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE);
359 checkbox5 = gtk_check_button_new_with_mnemonic(_("Match from s_tart of word"));
360 ui_hookup_widget(dialog, checkbox5, "check_wordstart");
361 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox5), FALSE);
363 /* disable wordstart when wholeword is checked */
364 g_signal_connect(checkbox2, "toggled",
365 G_CALLBACK(on_widget_toggled_set_insensitive), checkbox5);
367 /* Matching options */
368 mbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
369 gtk_box_pack_start(GTK_BOX(mbox), checkbox1, FALSE, FALSE, 0);
370 gtk_box_pack_start(GTK_BOX(mbox), checkbox2, FALSE, FALSE, 0);
371 gtk_box_pack_start(GTK_BOX(mbox), checkbox5, FALSE, FALSE, 0);
373 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
374 gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
375 gtk_container_add(GTK_CONTAINER(hbox), fbox);
376 gtk_container_add(GTK_CONTAINER(hbox), mbox);
377 return hbox;
381 static void send_find_dialog_response(GtkButton *button, gpointer user_data)
383 gtk_dialog_response(GTK_DIALOG(find_dlg.dialog), GPOINTER_TO_INT(user_data));
387 /* store text, clear search flags so we can use Search->Find Next/Previous */
388 static void setup_find_next(const gchar *text)
390 g_free(search_data.text);
391 g_free(search_data.original_text);
392 search_data.text = g_strdup(text);
393 search_data.original_text = g_strdup(text);
394 search_data.flags = 0;
395 search_data.backwards = FALSE;
396 search_data.search_bar = FALSE;
400 /* Search for next match of the current "selection".
401 * Optionally for X11 based systems, this will try to use the system-wide
402 * x-selection first.
403 * If it doesn't find a suitable search string it will try to use
404 * the current word instead, or just repeat the last search.
405 * Search flags are always zero.
407 void search_find_selection(GeanyDocument *doc, gboolean search_backwards)
409 gchar *s = NULL;
411 g_return_if_fail(DOC_VALID(doc));
413 #ifdef G_OS_UNIX
414 if (search_prefs.find_selection_type == GEANY_FIND_SEL_X)
416 GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
418 s = gtk_clipboard_wait_for_text(clipboard);
419 if (s && (strchr(s,'\n') || strchr(s, '\r')))
421 g_free(s);
422 s = NULL;
425 #endif
427 if (!s && sci_has_selection(doc->editor->sci))
428 s = sci_get_selection_contents(doc->editor->sci);
430 if (!s && search_prefs.find_selection_type != GEANY_FIND_SEL_AGAIN)
432 /* get the current word */
433 s = editor_get_default_selection(doc->editor, TRUE, NULL);
436 if (s)
438 setup_find_next(s); /* allow find next/prev */
440 if (document_find_text(doc, s, NULL, 0, search_backwards, NULL, FALSE, NULL) > -1)
441 editor_display_current_line(doc->editor, 0.3F);
442 g_free(s);
444 else if (search_prefs.find_selection_type == GEANY_FIND_SEL_AGAIN)
446 /* Repeat last search (in case selection was lost) */
447 search_find_again(search_backwards);
449 else
451 utils_beep();
456 static void on_expander_activated(GtkExpander *exp, gpointer data)
458 gboolean *setting = data;
460 *setting = gtk_expander_get_expanded(exp);
464 static void create_find_dialog(void)
466 GtkWidget *label, *entry, *sbox, *vbox;
467 GtkWidget *exp, *bbox, *button, *check_close;
469 find_dlg.dialog = gtk_dialog_new_with_buttons(_("Find"),
470 GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
471 GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL);
472 vbox = ui_dialog_vbox_new(GTK_DIALOG(find_dlg.dialog));
473 gtk_widget_set_name(find_dlg.dialog, "GeanyDialogSearch");
474 gtk_box_set_spacing(GTK_BOX(vbox), 9);
476 button = ui_button_new_with_image(GTK_STOCK_GO_BACK, _("_Previous"));
477 gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button,
478 GEANY_RESPONSE_FIND_PREVIOUS);
479 ui_hookup_widget(find_dlg.dialog, button, "btn_previous");
481 button = ui_button_new_with_image(GTK_STOCK_GO_FORWARD, _("_Next"));
482 gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button,
483 GEANY_RESPONSE_FIND);
485 label = gtk_label_new_with_mnemonic(_("_Search for:"));
486 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
488 entry = gtk_combo_box_text_new_with_entry();
489 ui_entry_add_clear_icon(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))));
490 gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry);
491 gtk_entry_set_width_chars(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))), 50);
492 find_dlg.entry = gtk_bin_get_child(GTK_BIN(entry));
494 g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate",
495 G_CALLBACK(on_find_entry_activate), entry);
496 ui_entry_add_activate_backward_signal(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))));
497 g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate-backward",
498 G_CALLBACK(on_find_entry_activate_backward), entry);
499 g_signal_connect(find_dlg.dialog, "response",
500 G_CALLBACK(on_find_dialog_response), entry);
501 g_signal_connect(find_dlg.dialog, "delete-event",
502 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
504 sbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
505 gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0);
506 gtk_box_pack_start(GTK_BOX(sbox), entry, TRUE, TRUE, 0);
507 gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0);
509 gtk_container_add(GTK_CONTAINER(vbox),
510 add_find_checkboxes(GTK_DIALOG(find_dlg.dialog)));
512 /* Now add the multiple match options */
513 exp = gtk_expander_new_with_mnemonic(_("_Find All"));
514 gtk_expander_set_expanded(GTK_EXPANDER(exp), find_dlg.all_expanded);
515 g_signal_connect_after(exp, "activate",
516 G_CALLBACK(on_expander_activated), &find_dlg.all_expanded);
518 bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
520 button = gtk_button_new_with_mnemonic(_("_Mark"));
521 gtk_widget_set_tooltip_text(button,
522 _("Mark all matches in the current document"));
523 gtk_container_add(GTK_CONTAINER(bbox), button);
524 g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
525 GINT_TO_POINTER(GEANY_RESPONSE_MARK));
527 button = gtk_button_new_with_mnemonic(_("In Sessi_on"));
528 gtk_container_add(GTK_CONTAINER(bbox), button);
529 g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
530 GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_SESSION));
532 button = gtk_button_new_with_mnemonic(_("_In Document"));
533 gtk_container_add(GTK_CONTAINER(bbox), button);
534 g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
535 GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_FILE));
537 /* close window checkbox */
538 check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog"));
539 ui_hookup_widget(find_dlg.dialog, check_close, "check_close");
540 gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE);
541 gtk_widget_set_tooltip_text(check_close,
542 _("Disable this option to keep the dialog open"));
543 gtk_container_add(GTK_CONTAINER(bbox), check_close);
544 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE);
546 ui_hbutton_box_copy_layout(
547 GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(find_dlg.dialog))),
548 GTK_BUTTON_BOX(bbox));
549 gtk_container_add(GTK_CONTAINER(exp), bbox);
550 gtk_container_add(GTK_CONTAINER(vbox), exp);
554 static void set_dialog_position(GtkWidget *dialog, gint *position)
556 if (position[0] >= 0)
557 gtk_window_move(GTK_WINDOW(dialog), position[0], position[1]);
561 void search_show_find_dialog(void)
563 GeanyDocument *doc = document_get_current();
564 gchar *sel = NULL;
566 g_return_if_fail(doc != NULL);
568 sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
570 if (find_dlg.dialog == NULL)
572 create_find_dialog();
573 stash_group_display(find_prefs, find_dlg.dialog);
574 if (sel)
575 gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel);
577 set_dialog_position(find_dlg.dialog, find_dlg.position);
578 gtk_widget_show_all(find_dlg.dialog);
580 else
582 if (sel != NULL)
584 /* update the search text from current selection */
585 gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel);
586 /* reset the entry widget's background colour */
587 ui_set_search_entry_background(find_dlg.entry, TRUE);
589 gtk_widget_grab_focus(find_dlg.entry);
590 set_dialog_position(find_dlg.dialog, find_dlg.position);
591 gtk_widget_show(find_dlg.dialog);
592 /* bring the dialog back in the foreground in case it is already open but the focus is away */
593 gtk_window_present(GTK_WINDOW(find_dlg.dialog));
596 g_free(sel);
600 static void send_replace_dialog_response(GtkButton *button, gpointer user_data)
602 gtk_dialog_response(GTK_DIALOG(replace_dlg.dialog), GPOINTER_TO_INT(user_data));
606 static gboolean
607 on_widget_key_pressed_set_focus(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
609 if (event->keyval == GDK_KEY_Tab)
611 gtk_widget_grab_focus(GTK_WIDGET(user_data));
612 return TRUE;
614 return FALSE;
618 static void create_replace_dialog(void)
620 GtkWidget *label_find, *label_replace,
621 *check_close, *button, *rbox, *fbox, *vbox, *exp, *bbox;
622 GtkSizeGroup *label_size;
624 replace_dlg.dialog = gtk_dialog_new_with_buttons(_("Replace"),
625 GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
626 GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL);
627 vbox = ui_dialog_vbox_new(GTK_DIALOG(replace_dlg.dialog));
628 gtk_box_set_spacing(GTK_BOX(vbox), 9);
629 gtk_widget_set_name(replace_dlg.dialog, "GeanyDialogSearch");
631 button = gtk_button_new_from_stock(GTK_STOCK_FIND);
632 gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
633 GEANY_RESPONSE_FIND);
634 button = gtk_button_new_with_mnemonic(_("_Replace"));
635 gtk_button_set_image(GTK_BUTTON(button),
636 gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON));
637 gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
638 GEANY_RESPONSE_REPLACE);
639 button = gtk_button_new_with_mnemonic(_("Replace & Fi_nd"));
640 gtk_button_set_image(GTK_BUTTON(button),
641 gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON));
642 gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
643 GEANY_RESPONSE_REPLACE_AND_FIND);
645 label_find = gtk_label_new_with_mnemonic(_("_Search for:"));
646 gtk_misc_set_alignment(GTK_MISC(label_find), 0, 0.5);
648 label_replace = gtk_label_new_with_mnemonic(_("Replace wit_h:"));
649 gtk_misc_set_alignment(GTK_MISC(label_replace), 0, 0.5);
651 replace_dlg.find_combobox = gtk_combo_box_text_new_with_entry();
652 replace_dlg.find_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.find_combobox));
653 ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.find_entry));
654 gtk_label_set_mnemonic_widget(GTK_LABEL(label_find), replace_dlg.find_combobox);
655 gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.find_entry), 50);
656 ui_hookup_widget(replace_dlg.dialog, replace_dlg.find_combobox, "entry_find");
658 replace_dlg.replace_combobox = gtk_combo_box_text_new_with_entry();
659 replace_dlg.replace_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.replace_combobox));
660 ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.replace_entry));
661 gtk_label_set_mnemonic_widget(GTK_LABEL(label_replace), replace_dlg.replace_combobox);
662 gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.replace_entry), 50);
663 ui_hookup_widget(replace_dlg.dialog, replace_dlg.replace_combobox, "entry_replace");
665 /* tab from find to the replace entry */
666 g_signal_connect(replace_dlg.find_entry,
667 "key-press-event", G_CALLBACK(on_widget_key_pressed_set_focus),
668 replace_dlg.replace_entry);
669 g_signal_connect(replace_dlg.find_entry, "activate",
670 G_CALLBACK(on_replace_find_entry_activate), NULL);
671 g_signal_connect(replace_dlg.replace_entry, "activate",
672 G_CALLBACK(on_replace_entry_activate), NULL);
673 g_signal_connect(replace_dlg.dialog, "response",
674 G_CALLBACK(on_replace_dialog_response), NULL);
675 g_signal_connect(replace_dlg.dialog, "delete-event",
676 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
678 fbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
679 gtk_box_pack_start(GTK_BOX(fbox), label_find, FALSE, FALSE, 0);
680 gtk_box_pack_start(GTK_BOX(fbox), replace_dlg.find_combobox, TRUE, TRUE, 0);
682 rbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
683 gtk_box_pack_start(GTK_BOX(rbox), label_replace, FALSE, FALSE, 0);
684 gtk_box_pack_start(GTK_BOX(rbox), replace_dlg.replace_combobox, TRUE, TRUE, 0);
686 label_size = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
687 gtk_size_group_add_widget(label_size, label_find);
688 gtk_size_group_add_widget(label_size, label_replace);
689 g_object_unref(G_OBJECT(label_size)); /* auto destroy the size group */
691 gtk_box_pack_start(GTK_BOX(vbox), fbox, TRUE, FALSE, 0);
692 gtk_box_pack_start(GTK_BOX(vbox), rbox, TRUE, FALSE, 0);
693 gtk_container_add(GTK_CONTAINER(vbox),
694 add_find_checkboxes(GTK_DIALOG(replace_dlg.dialog)));
696 /* Now add the multiple replace options */
697 exp = gtk_expander_new_with_mnemonic(_("Re_place All"));
698 gtk_expander_set_expanded(GTK_EXPANDER(exp), replace_dlg.all_expanded);
699 g_signal_connect_after(exp, "activate",
700 G_CALLBACK(on_expander_activated), &replace_dlg.all_expanded);
702 bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
704 button = gtk_button_new_with_mnemonic(_("In Sessi_on"));
705 gtk_container_add(GTK_CONTAINER(bbox), button);
706 g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
707 GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SESSION));
709 button = gtk_button_new_with_mnemonic(_("_In Document"));
710 gtk_container_add(GTK_CONTAINER(bbox), button);
711 g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
712 GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_FILE));
714 button = gtk_button_new_with_mnemonic(_("In Se_lection"));
715 gtk_widget_set_tooltip_text(button,
716 _("Replace all matches found in the currently selected text"));
717 gtk_container_add(GTK_CONTAINER(bbox), button);
718 g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
719 GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SEL));
721 /* close window checkbox */
722 check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog"));
723 ui_hookup_widget(replace_dlg.dialog, check_close, "check_close");
724 gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE);
725 gtk_widget_set_tooltip_text(check_close,
726 _("Disable this option to keep the dialog open"));
727 gtk_container_add(GTK_CONTAINER(bbox), check_close);
728 gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE);
730 ui_hbutton_box_copy_layout(
731 GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(replace_dlg.dialog))),
732 GTK_BUTTON_BOX(bbox));
733 gtk_container_add(GTK_CONTAINER(exp), bbox);
734 gtk_container_add(GTK_CONTAINER(vbox), exp);
738 void search_show_replace_dialog(void)
740 GeanyDocument *doc = document_get_current();
741 gchar *sel = NULL;
743 if (doc == NULL)
744 return;
746 sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
748 if (replace_dlg.dialog == NULL)
750 create_replace_dialog();
751 stash_group_display(replace_prefs, replace_dlg.dialog);
752 if (sel)
753 gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel);
755 set_dialog_position(replace_dlg.dialog, replace_dlg.position);
756 gtk_widget_show_all(replace_dlg.dialog);
758 else
760 if (sel != NULL)
762 /* update the search text from current selection */
763 gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel);
764 /* reset the entry widget's background colour */
765 ui_set_search_entry_background(replace_dlg.find_entry, TRUE);
767 gtk_widget_grab_focus(replace_dlg.find_entry);
768 set_dialog_position(replace_dlg.dialog, replace_dlg.position);
769 gtk_widget_show(replace_dlg.dialog);
770 /* bring the dialog back in the foreground in case it is already open but the focus is away */
771 gtk_window_present(GTK_WINDOW(replace_dlg.dialog));
774 g_free(sel);
778 static void on_widget_toggled_set_sensitive(GtkToggleButton *togglebutton, gpointer user_data)
780 /* disable extra option entry when checkbutton not checked */
781 gtk_widget_set_sensitive(GTK_WIDGET(user_data),
782 gtk_toggle_button_get_active(togglebutton));
786 static void update_file_patterns(GtkWidget *mode_combo, GtkWidget *fcombo)
788 gint selection;
789 GtkWidget *entry;
791 entry = gtk_bin_get_child(GTK_BIN(fcombo));
793 selection = gtk_combo_box_get_active(GTK_COMBO_BOX(mode_combo));
795 if (selection == FILES_MODE_ALL)
797 gtk_entry_set_text(GTK_ENTRY(entry), "");
798 gtk_widget_set_sensitive(fcombo, FALSE);
800 else if (selection == FILES_MODE_CUSTOM)
802 gtk_widget_set_sensitive(fcombo, TRUE);
804 else if (selection == FILES_MODE_PROJECT)
806 if (app->project && !EMPTY(app->project->file_patterns))
808 gchar *patterns;
810 patterns = g_strjoinv(" ", app->project->file_patterns);
811 gtk_entry_set_text(GTK_ENTRY(entry), patterns);
812 g_free(patterns);
814 else
816 gtk_entry_set_text(GTK_ENTRY(entry), "");
819 gtk_widget_set_sensitive(fcombo, FALSE);
824 /* creates the combo to choose which files include in the search */
825 static GtkWidget *create_fif_file_mode_combo(void)
827 GtkWidget *combo;
828 GtkCellRenderer *renderer;
829 GtkListStore *store;
830 GtkTreeIter iter;
832 /* text/sensitive */
833 store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_BOOLEAN);
834 gtk_list_store_append(store, &iter);
835 gtk_list_store_set(store, &iter, 0, _("all"), 1, TRUE, -1);
836 gtk_list_store_append(store, &iter);
837 gtk_list_store_set(store, &iter, 0, _("project"), 1, app->project != NULL, -1);
838 gtk_list_store_append(store, &iter);
839 gtk_list_store_set(store, &iter, 0, _("custom"), 1, TRUE, -1);
841 combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
842 g_object_unref(store);
843 gtk_widget_set_tooltip_text(combo, _("All: search all files in the directory\n"
844 "Project: use file patterns defined in the project settings\n"
845 "Custom: specify file patterns manually"));
847 renderer = gtk_cell_renderer_text_new();
848 gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
849 gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", 0, "sensitive", 1, NULL);
851 return combo;
855 /* updates the sensitivity of the project combo item */
856 static void update_fif_file_mode_combo(void)
858 GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(fif_dlg.files_mode_combo));
859 GtkTreeIter iter;
861 /* "1" refers to the second list entry, project */
862 if (gtk_tree_model_get_iter_from_string(model, &iter, "1"))
863 gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, app->project != NULL, -1);
867 static void create_fif_dialog(void)
869 GtkWidget *dir_combo, *combo, *fcombo, *e_combo, *entry;
870 GtkWidget *label, *label1, *label2, *label3, *checkbox1, *checkbox2, *check_wholeword,
871 *check_recursive, *check_extra, *entry_extra, *check_regexp, *combo_files_mode;
872 GtkWidget *dbox, *sbox, *lbox, *rbox, *hbox, *vbox, *ebox;
873 GtkSizeGroup *size_group;
875 fif_dlg.dialog = gtk_dialog_new_with_buttons(
876 _("Find in Files"), GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
877 GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL);
878 vbox = ui_dialog_vbox_new(GTK_DIALOG(fif_dlg.dialog));
879 gtk_box_set_spacing(GTK_BOX(vbox), 9);
880 gtk_widget_set_name(fif_dlg.dialog, "GeanyDialogSearch");
882 gtk_dialog_add_button(GTK_DIALOG(fif_dlg.dialog), GTK_STOCK_FIND, GTK_RESPONSE_ACCEPT);
883 gtk_dialog_set_default_response(GTK_DIALOG(fif_dlg.dialog),
884 GTK_RESPONSE_ACCEPT);
886 label = gtk_label_new_with_mnemonic(_("_Search for:"));
887 gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
889 combo = gtk_combo_box_text_new_with_entry();
890 entry = gtk_bin_get_child(GTK_BIN(combo));
891 ui_entry_add_clear_icon(GTK_ENTRY(entry));
892 gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry);
893 gtk_entry_set_width_chars(GTK_ENTRY(entry), 50);
894 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
895 fif_dlg.search_combo = combo;
897 sbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
898 gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0);
899 gtk_box_pack_start(GTK_BOX(sbox), combo, TRUE, TRUE, 0);
901 /* make labels same width */
902 size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
903 gtk_size_group_add_widget(size_group, label);
905 label3 = gtk_label_new_with_mnemonic(_("File _patterns:"));
906 gtk_misc_set_alignment(GTK_MISC(label3), 0, 0.5);
908 combo_files_mode = create_fif_file_mode_combo();
909 gtk_label_set_mnemonic_widget(GTK_LABEL(label3), combo_files_mode);
910 ui_hookup_widget(fif_dlg.dialog, combo_files_mode, "combo_files_mode");
911 fif_dlg.files_mode_combo = combo_files_mode;
913 fcombo = gtk_combo_box_text_new_with_entry();
914 entry = gtk_bin_get_child(GTK_BIN(fcombo));
915 ui_entry_add_clear_icon(GTK_ENTRY(entry));
916 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
917 gtk_widget_set_tooltip_text(entry,
918 _("Space separated list of file patterns (e.g. *.c *.h)"));
919 ui_hookup_widget(fif_dlg.dialog, entry, "entry_files");
920 fif_dlg.files_combo = fcombo;
922 /* update the entry when selection is changed */
923 g_signal_connect(combo_files_mode, "changed", G_CALLBACK(update_file_patterns), fcombo);
925 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
926 gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 0);
927 gtk_box_pack_start(GTK_BOX(hbox), combo_files_mode, FALSE, FALSE, 0);
928 gtk_box_pack_start(GTK_BOX(hbox), fcombo, TRUE, TRUE, 0);
930 label1 = gtk_label_new_with_mnemonic(_("_Directory:"));
931 gtk_misc_set_alignment(GTK_MISC(label1), 0, 0.5);
933 dir_combo = gtk_combo_box_text_new_with_entry();
934 entry = gtk_bin_get_child(GTK_BIN(dir_combo));
935 ui_entry_add_clear_icon(GTK_ENTRY(entry));
936 gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
937 gtk_label_set_mnemonic_widget(GTK_LABEL(label1), entry);
938 gtk_entry_set_width_chars(GTK_ENTRY(entry), 50);
939 fif_dlg.dir_combo = dir_combo;
941 /* tab from files to the dir entry */
942 g_signal_connect(gtk_bin_get_child(GTK_BIN(fcombo)), "key-press-event",
943 G_CALLBACK(on_widget_key_pressed_set_focus), entry);
945 dbox = ui_path_box_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
946 GTK_ENTRY(entry));
947 gtk_box_pack_start(GTK_BOX(dbox), label1, FALSE, FALSE, 0);
949 label2 = gtk_label_new_with_mnemonic(_("E_ncoding:"));
950 gtk_misc_set_alignment(GTK_MISC(label2), 0, 0.5);
952 e_combo = ui_create_encodings_combo_box(FALSE, GEANY_ENCODING_UTF_8);
953 gtk_label_set_mnemonic_widget(GTK_LABEL(label2), e_combo);
954 fif_dlg.encoding_combo = e_combo;
956 ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
957 gtk_box_pack_start(GTK_BOX(ebox), label2, FALSE, FALSE, 0);
958 gtk_box_pack_start(GTK_BOX(ebox), e_combo, TRUE, TRUE, 0);
960 gtk_size_group_add_widget(size_group, label1);
961 gtk_size_group_add_widget(size_group, label2);
962 gtk_size_group_add_widget(size_group, label3);
963 g_object_unref(G_OBJECT(size_group)); /* auto destroy the size group */
965 gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0);
966 gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, FALSE, 0);
967 gtk_box_pack_start(GTK_BOX(vbox), dbox, TRUE, FALSE, 0);
968 gtk_box_pack_start(GTK_BOX(vbox), ebox, TRUE, FALSE, 0);
970 check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions"));
971 ui_hookup_widget(fif_dlg.dialog, check_regexp, "check_regexp");
972 gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE);
973 gtk_widget_set_tooltip_text(check_regexp, _("See grep's manual page for more information"));
975 check_recursive = gtk_check_button_new_with_mnemonic(_("_Recurse in subfolders"));
976 ui_hookup_widget(fif_dlg.dialog, check_recursive, "check_recursive");
977 gtk_button_set_focus_on_click(GTK_BUTTON(check_recursive), FALSE);
979 checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive"));
980 ui_hookup_widget(fif_dlg.dialog, checkbox1, "check_case");
981 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE);
982 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox1), TRUE);
984 check_wholeword = gtk_check_button_new_with_mnemonic(_("Match only a _whole word"));
985 ui_hookup_widget(fif_dlg.dialog, check_wholeword, "check_wholeword");
986 gtk_button_set_focus_on_click(GTK_BUTTON(check_wholeword), FALSE);
988 checkbox2 = gtk_check_button_new_with_mnemonic(_("_Invert search results"));
989 ui_hookup_widget(fif_dlg.dialog, checkbox2, "check_invert");
990 gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE);
991 gtk_widget_set_tooltip_text(checkbox2,
992 _("Invert the sense of matching, to select non-matching lines"));
994 lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
995 gtk_container_add(GTK_CONTAINER(lbox), check_regexp);
996 gtk_container_add(GTK_CONTAINER(lbox), checkbox2);
997 gtk_container_add(GTK_CONTAINER(lbox), check_recursive);
999 rbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
1000 gtk_container_add(GTK_CONTAINER(rbox), checkbox1);
1001 gtk_container_add(GTK_CONTAINER(rbox), check_wholeword);
1002 gtk_container_add(GTK_CONTAINER(rbox), gtk_label_new(NULL));
1004 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1005 gtk_container_add(GTK_CONTAINER(hbox), lbox);
1006 gtk_container_add(GTK_CONTAINER(hbox), rbox);
1007 gtk_container_add(GTK_CONTAINER(vbox), hbox);
1009 check_extra = gtk_check_button_new_with_mnemonic(_("E_xtra options:"));
1010 ui_hookup_widget(fif_dlg.dialog, check_extra, "check_extra");
1011 gtk_button_set_focus_on_click(GTK_BUTTON(check_extra), FALSE);
1013 entry_extra = gtk_entry_new();
1014 ui_entry_add_clear_icon(GTK_ENTRY(entry_extra));
1015 gtk_entry_set_activates_default(GTK_ENTRY(entry_extra), TRUE);
1016 gtk_widget_set_sensitive(entry_extra, FALSE);
1017 gtk_widget_set_tooltip_text(entry_extra, _("Other options to pass to Grep"));
1018 ui_hookup_widget(fif_dlg.dialog, entry_extra, "entry_extra");
1020 /* enable entry_extra when check_extra is checked */
1021 g_signal_connect(check_extra, "toggled",
1022 G_CALLBACK(on_widget_toggled_set_sensitive), entry_extra);
1024 hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1025 gtk_box_pack_start(GTK_BOX(hbox), check_extra, FALSE, FALSE, 0);
1026 gtk_box_pack_start(GTK_BOX(hbox), entry_extra, TRUE, TRUE, 0);
1027 gtk_container_add(GTK_CONTAINER(vbox), hbox);
1029 g_signal_connect(fif_dlg.dialog, "response",
1030 G_CALLBACK(on_find_in_files_dialog_response), NULL);
1031 g_signal_connect(fif_dlg.dialog, "delete-event",
1032 G_CALLBACK(gtk_widget_hide_on_delete), NULL);
1037 * Shows the Find in Files dialog.
1039 * @param dir The directory to search in (UTF-8 encoding). May be @c NULL
1040 * to determine it the usual way by using the current document's path.
1042 * @since 0.14, plugin API 53
1044 GEANY_API_SYMBOL
1045 void search_show_find_in_files_dialog(const gchar *dir)
1047 search_show_find_in_files_dialog_full(NULL, dir);
1051 void search_show_find_in_files_dialog_full(const gchar *text, const gchar *dir)
1053 GtkWidget *entry; /* for child GtkEntry of a GtkComboBoxEntry */
1054 GeanyDocument *doc = document_get_current();
1055 gchar *sel = NULL;
1056 gchar *cur_dir = NULL;
1057 GeanyEncodingIndex enc_idx = GEANY_ENCODING_UTF_8;
1059 if (fif_dlg.dialog == NULL)
1061 create_fif_dialog();
1062 gtk_widget_show_all(fif_dlg.dialog);
1063 if (doc && !text)
1064 sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
1066 stash_group_display(fif_prefs, fif_dlg.dialog);
1068 if (!text)
1070 if (doc && ! sel)
1071 sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
1072 text = sel;
1074 entry = gtk_bin_get_child(GTK_BIN(fif_dlg.search_combo));
1075 if (text)
1076 gtk_entry_set_text(GTK_ENTRY(entry), text);
1077 g_free(sel);
1079 /* add project's base path directory to the dir list, we do this here once
1080 * (in create_fif_dialog() it would fail if a project is opened after dialog creation) */
1081 if (app->project != NULL && !EMPTY(app->project->base_path))
1083 ui_combo_box_prepend_text_once(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo),
1084 app->project->base_path);
1087 entry = gtk_bin_get_child(GTK_BIN(fif_dlg.dir_combo));
1088 if (!EMPTY(dir))
1089 cur_dir = g_strdup(dir); /* custom directory argument passed */
1090 else
1092 if (search_prefs.use_current_file_dir)
1094 static gchar *last_cur_dir = NULL;
1095 static GeanyDocument *last_doc = NULL;
1097 /* Only set the directory entry once for the current document */
1098 cur_dir = utils_get_current_file_dir_utf8();
1099 if (doc == last_doc && cur_dir && utils_str_equal(cur_dir, last_cur_dir))
1101 /* in case the user now wants the current directory, add it to history */
1102 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo), cur_dir, 0);
1103 SETPTR(cur_dir, NULL);
1105 else
1106 SETPTR(last_cur_dir, g_strdup(cur_dir));
1108 last_doc = doc;
1110 if (!cur_dir && EMPTY(gtk_entry_get_text(GTK_ENTRY(entry))))
1112 /* use default_open_path if no directory could be determined
1113 * (e.g. when no files are open) */
1114 if (!cur_dir)
1115 cur_dir = g_strdup(utils_get_default_dir_utf8());
1116 if (!cur_dir)
1117 cur_dir = g_get_current_dir();
1120 if (cur_dir)
1122 gtk_entry_set_text(GTK_ENTRY(entry), cur_dir);
1123 g_free(cur_dir);
1126 ui_set_search_entry_background(fif_dlg.search_combo, TRUE);
1127 ui_set_search_entry_background(fif_dlg.dir_combo, TRUE);
1128 update_fif_file_mode_combo();
1129 update_file_patterns(fif_dlg.files_mode_combo, fif_dlg.files_combo);
1131 /* set the encoding of the current file */
1132 if (doc != NULL)
1133 enc_idx = encodings_get_idx_from_charset(doc->encoding);
1134 ui_encodings_combo_box_set_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo), enc_idx);
1136 /* put the focus to the directory entry if it is empty */
1137 if (utils_str_equal(gtk_entry_get_text(GTK_ENTRY(entry)), ""))
1138 gtk_widget_grab_focus(fif_dlg.dir_combo);
1139 else
1140 gtk_widget_grab_focus(fif_dlg.search_combo);
1142 /* set dialog window position */
1143 set_dialog_position(fif_dlg.dialog, fif_dlg.position);
1145 gtk_widget_show(fif_dlg.dialog);
1146 /* bring the dialog back in the foreground in case it is already open but the focus is away */
1147 gtk_window_present(GTK_WINDOW(fif_dlg.dialog));
1151 static void
1152 on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data)
1154 GtkWidget *dialog = GTK_WIDGET(user_data);
1155 GtkToggleButton *chk_regexp = GTK_TOGGLE_BUTTON(
1156 ui_lookup_widget(dialog, "check_regexp"));
1158 if (togglebutton == chk_regexp)
1160 gboolean regex_set = gtk_toggle_button_get_active(chk_regexp);
1161 GtkWidget *check_word = ui_lookup_widget(dialog, "check_word");
1162 GtkWidget *check_wordstart = ui_lookup_widget(dialog, "check_wordstart");
1163 GtkWidget *check_escape = ui_lookup_widget(dialog, "check_escape");
1164 GtkWidget *check_multiline = ui_lookup_widget(dialog, "check_multiline");
1165 gboolean replace = (dialog != find_dlg.dialog);
1166 const char *back_button[2] = { "btn_previous" , "check_back" };
1168 /* hide options that don't apply to regex searches */
1169 gtk_widget_set_sensitive(check_escape, ! regex_set);
1170 gtk_widget_set_sensitive(ui_lookup_widget(dialog, back_button[replace]), ! regex_set);
1171 gtk_widget_set_sensitive(check_word, ! regex_set);
1172 gtk_widget_set_sensitive(check_wordstart, ! regex_set);
1173 gtk_widget_set_sensitive(check_multiline, regex_set);
1178 static GeanyMatchInfo *match_info_new(GeanyFindFlags flags, gint start, gint end)
1180 GeanyMatchInfo *info = g_slice_alloc(sizeof *info);
1182 info->flags = flags;
1183 info->start = start;
1184 info->end = end;
1185 info->match_text = NULL;
1187 return info;
1190 void geany_match_info_free(GeanyMatchInfo *info)
1192 g_free(info->match_text);
1193 g_slice_free1(sizeof *info, info);
1197 /* find all in the given range.
1198 * Returns a list of allocated GeanyMatchInfo, should be freed using:
1200 * foreach_slist(node, matches)
1201 * geany_match_info_free(node->data);
1202 * g_slist_free(matches); */
1203 static GSList *find_range(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf)
1205 GSList *matches = NULL;
1206 GeanyMatchInfo *info;
1208 g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL, NULL);
1209 if (! *ttf->lpstrText)
1210 return NULL;
1212 while (search_find_text(sci, flags, ttf, &info) != -1)
1214 if (ttf->chrgText.cpMax > ttf->chrg.cpMax)
1216 /* found text is partially out of range */
1217 geany_match_info_free(info);
1218 break;
1221 matches = g_slist_prepend(matches, info);
1222 ttf->chrg.cpMin = ttf->chrgText.cpMax;
1224 /* avoid rematching with empty matches like "(?=[a-z])" or "^$".
1225 * note we cannot assume a match will always be empty or not and then break out, since
1226 * matches like "a?(?=b)" will sometimes be empty and sometimes not */
1227 if (ttf->chrgText.cpMax == ttf->chrgText.cpMin)
1228 ttf->chrg.cpMin ++;
1231 return g_slist_reverse(matches);
1235 /* Clears markers if text is null/empty.
1236 * @return Number of matches marked. */
1237 gint search_mark_all(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags)
1239 gint count = 0;
1240 struct Sci_TextToFind ttf;
1241 GSList *match, *matches;
1243 g_return_val_if_fail(DOC_VALID(doc), 0);
1245 /* clear previous search indicators */
1246 editor_indicator_clear(doc->editor, GEANY_INDICATOR_SEARCH);
1248 if (G_UNLIKELY(EMPTY(search_text)))
1249 return 0;
1251 ttf.chrg.cpMin = 0;
1252 ttf.chrg.cpMax = sci_get_length(doc->editor->sci);
1253 ttf.lpstrText = (gchar *)search_text;
1255 matches = find_range(doc->editor->sci, flags, &ttf);
1256 foreach_slist (match, matches)
1258 GeanyMatchInfo *info = match->data;
1260 if (info->end != info->start)
1261 editor_indicator_set_on_range(doc->editor, GEANY_INDICATOR_SEARCH, info->start, info->end);
1262 count++;
1264 geany_match_info_free(info);
1266 g_slist_free(matches);
1268 return count;
1272 static void
1273 on_find_entry_activate(GtkEntry *entry, gpointer user_data)
1275 on_find_dialog_response(NULL, GEANY_RESPONSE_FIND, user_data);
1279 static void
1280 on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data)
1282 /* can't search backwards with a regexp */
1283 if (search_data.flags & GEANY_FIND_REGEXP)
1284 utils_beep();
1285 else
1286 on_find_dialog_response(NULL, GEANY_RESPONSE_FIND_PREVIOUS, user_data);
1290 static GeanyFindFlags int_search_flags(gint match_case, gint whole_word, gint regexp, gint multiline, gint word_start)
1292 return (match_case ? GEANY_FIND_MATCHCASE : 0) |
1293 (regexp ? GEANY_FIND_REGEXP : 0) |
1294 (whole_word ? GEANY_FIND_WHOLEWORD : 0) |
1295 (multiline ? GEANY_FIND_MULTILINE : 0) |
1296 /* SCFIND_WORDSTART overrides SCFIND_WHOLEWORD, but we want the opposite */
1297 (word_start && !whole_word ? GEANY_FIND_WORDSTART : 0);
1301 static void
1302 on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data)
1304 gtk_window_get_position(GTK_WINDOW(find_dlg.dialog),
1305 &find_dlg.position[0], &find_dlg.position[1]);
1307 stash_group_update(find_prefs, find_dlg.dialog);
1309 if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT)
1310 gtk_widget_hide(find_dlg.dialog);
1311 else
1313 GeanyDocument *doc = document_get_current();
1314 gboolean check_close = settings.find_close_dialog;
1316 if (doc == NULL)
1317 return;
1319 search_data.backwards = FALSE;
1320 search_data.search_bar = FALSE;
1322 g_free(search_data.text);
1323 g_free(search_data.original_text);
1324 search_data.text = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(user_data)))));
1325 search_data.original_text = g_strdup(search_data.text);
1326 search_data.flags = int_search_flags(settings.find_case_sensitive,
1327 settings.find_match_whole_word, settings.find_regexp, settings.find_regexp_multiline,
1328 settings.find_match_word_start);
1330 if (EMPTY(search_data.text))
1332 fail:
1333 utils_beep();
1334 gtk_widget_grab_focus(find_dlg.entry);
1335 return;
1337 if (search_data.flags & GEANY_FIND_REGEXP)
1339 GRegex *regex = compile_regex(search_data.text, search_data.flags);
1340 if (!regex)
1341 goto fail;
1342 else
1343 g_regex_unref(regex);
1345 else if (settings.find_escape_sequences)
1347 if (! utils_str_replace_escape(search_data.text, FALSE))
1348 goto fail;
1350 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(user_data), search_data.original_text, 0);
1352 switch (response)
1354 case GEANY_RESPONSE_FIND:
1355 case GEANY_RESPONSE_FIND_PREVIOUS:
1357 gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags,
1358 (response == GEANY_RESPONSE_FIND_PREVIOUS), NULL, TRUE, GTK_WIDGET(find_dlg.dialog));
1359 ui_set_search_entry_background(find_dlg.entry, (result > -1));
1360 check_close = search_prefs.hide_find_dialog;
1361 break;
1363 case GEANY_RESPONSE_FIND_IN_FILE:
1364 search_find_usage(search_data.text, search_data.original_text, search_data.flags, FALSE);
1365 break;
1367 case GEANY_RESPONSE_FIND_IN_SESSION:
1368 search_find_usage(search_data.text, search_data.original_text, search_data.flags, TRUE);
1369 break;
1371 case GEANY_RESPONSE_MARK:
1373 gint count = search_mark_all(doc, search_data.text, search_data.flags);
1375 if (count == 0)
1376 ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), search_data.original_text);
1377 else
1378 ui_set_statusbar(FALSE,
1379 ngettext("Found %d match for \"%s\".",
1380 "Found %d matches for \"%s\".", count),
1381 count, search_data.original_text);
1383 break;
1385 if (check_close)
1386 gtk_widget_hide(find_dlg.dialog);
1391 static void
1392 on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data)
1394 on_replace_dialog_response(NULL, GEANY_RESPONSE_FIND, NULL);
1398 static void
1399 on_replace_entry_activate(GtkEntry *entry, gpointer user_data)
1401 on_replace_dialog_response(NULL,
1402 search_prefs.replace_and_find_by_default ? GEANY_RESPONSE_REPLACE_AND_FIND : GEANY_RESPONSE_REPLACE,
1403 NULL);
1407 static void replace_in_session(GeanyDocument *doc,
1408 GeanyFindFlags search_flags_re, gboolean search_replace_escape_re,
1409 const gchar *find, const gchar *replace,
1410 const gchar *original_find, const gchar *original_replace)
1412 guint n, page_count, rep_count = 0, file_count = 0;
1414 /* replace in all documents following notebook tab order */
1415 page_count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(main_widgets.notebook));
1416 for (n = 0; n < page_count; n++)
1418 GeanyDocument *tmp_doc = document_get_from_page(n);
1419 gint reps = 0;
1421 reps = document_replace_all(tmp_doc, find, replace, original_find, original_replace, search_flags_re);
1422 rep_count += reps;
1423 if (reps)
1424 file_count++;
1426 if (file_count == 0)
1428 utils_beep();
1429 ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_find);
1430 return;
1432 /* if only one file was changed, don't override that document's status message
1433 * so we don't have to translate 4 messages for ngettext */
1434 if (file_count > 1)
1435 ui_set_statusbar(FALSE, _("Replaced %u matches in %u documents."),
1436 rep_count, file_count);
1438 /* show which docs had replacements: */
1439 gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_STATUS);
1441 ui_save_buttons_toggle(doc->changed); /* update save all */
1445 static void
1446 on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data)
1448 GeanyDocument *doc = document_get_current();
1449 GeanyFindFlags search_flags_re;
1450 gboolean search_backwards_re, search_replace_escape_re;
1451 gchar *find, *replace, *original_find = NULL, *original_replace = NULL;
1453 gtk_window_get_position(GTK_WINDOW(replace_dlg.dialog),
1454 &replace_dlg.position[0], &replace_dlg.position[1]);
1456 stash_group_update(replace_prefs, replace_dlg.dialog);
1458 if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT)
1460 gtk_widget_hide(replace_dlg.dialog);
1461 return;
1464 if (response == GEANY_RESPONSE_REPLACE_IN_SESSION) {
1465 if (!search_prefs.skip_confirmation_for_replace_in_session &&
1466 ! dialogs_show_question_full(replace_dlg.dialog, NULL, NULL,
1467 _("This operation will modify all open files which contain the text to replace."),
1468 _("Are you sure to replace in the whole session?"))) {
1469 return;
1473 search_backwards_re = settings.replace_search_backwards;
1474 search_replace_escape_re = settings.replace_escape_sequences;
1475 find = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.find_entry)));
1476 replace = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.replace_entry)));
1478 search_flags_re = int_search_flags(settings.replace_case_sensitive,
1479 settings.replace_match_whole_word, settings.replace_regexp,
1480 settings.replace_regexp_multiline, settings.replace_match_word_start);
1482 if ((response != GEANY_RESPONSE_FIND) && (search_flags_re & GEANY_FIND_MATCHCASE)
1483 && (strcmp(find, replace) == 0))
1484 goto fail;
1486 original_find = g_strdup(find);
1487 original_replace = g_strdup(replace);
1489 if (search_flags_re & GEANY_FIND_REGEXP)
1491 GRegex *regex = compile_regex(find, search_flags_re);
1492 if (regex)
1493 g_regex_unref(regex);
1494 /* find escapes will be handled by GRegex */
1495 if (!regex || !utils_str_replace_escape(replace, TRUE))
1496 goto fail;
1498 else if (search_replace_escape_re)
1500 if (! utils_str_replace_escape(find, FALSE) ||
1501 ! utils_str_replace_escape(replace, FALSE))
1502 goto fail;
1505 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.find_combobox), original_find, 0);
1506 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.replace_combobox), original_replace, 0);
1508 switch (response)
1510 case GEANY_RESPONSE_REPLACE_AND_FIND:
1512 gint rep = document_replace_text(doc, find, original_find, replace, search_flags_re,
1513 search_backwards_re);
1514 if (rep != -1)
1515 document_find_text(doc, find, original_find, search_flags_re, search_backwards_re,
1516 NULL, TRUE, NULL);
1517 break;
1519 case GEANY_RESPONSE_REPLACE:
1520 document_replace_text(doc, find, original_find, replace, search_flags_re,
1521 search_backwards_re);
1522 break;
1524 case GEANY_RESPONSE_FIND:
1526 gint result = document_find_text(doc, find, original_find, search_flags_re,
1527 search_backwards_re, NULL, TRUE, GTK_WIDGET(dialog));
1528 ui_set_search_entry_background(replace_dlg.find_entry, (result > -1));
1529 break;
1531 case GEANY_RESPONSE_REPLACE_IN_FILE:
1532 if (! document_replace_all(doc, find, replace, original_find, original_replace, search_flags_re))
1533 utils_beep();
1534 break;
1536 case GEANY_RESPONSE_REPLACE_IN_SESSION:
1537 replace_in_session(doc, search_flags_re, search_replace_escape_re, find, replace, original_find, original_replace);
1538 break;
1540 case GEANY_RESPONSE_REPLACE_IN_SEL:
1541 document_replace_sel(doc, find, replace, original_find, original_replace, search_flags_re);
1542 break;
1544 switch (response)
1546 case GEANY_RESPONSE_REPLACE_IN_SEL:
1547 case GEANY_RESPONSE_REPLACE_IN_FILE:
1548 case GEANY_RESPONSE_REPLACE_IN_SESSION:
1549 if (settings.replace_close_dialog)
1550 gtk_widget_hide(replace_dlg.dialog);
1552 g_free(find);
1553 g_free(replace);
1554 g_free(original_find);
1555 g_free(original_replace);
1556 return;
1558 fail:
1559 utils_beep();
1560 gtk_widget_grab_focus(replace_dlg.find_entry);
1561 g_free(find);
1562 g_free(replace);
1563 g_free(original_find);
1564 g_free(original_replace);
1568 static void reset_msgwin(void)
1570 gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE);
1571 gtk_list_store_clear(msgwindow.store_msg);
1572 // reset width after any long messages
1573 gtk_tree_view_columns_autosize(GTK_TREE_VIEW(msgwindow.tree_msg));
1577 static GString *get_grep_options(void)
1579 GString *gstr = g_string_new("-nHI"); /* line numbers, filenames, ignore binaries */
1581 if (settings.fif_invert_results)
1582 g_string_append_c(gstr, 'v');
1583 if (! settings.fif_case_sensitive)
1584 g_string_append_c(gstr, 'i');
1585 if (settings.fif_match_whole_word)
1586 g_string_append_c(gstr, 'w');
1587 if (settings.fif_recursive)
1588 g_string_append_c(gstr, 'r');
1590 if (!settings.fif_regexp)
1591 g_string_append_c(gstr, 'F');
1592 else
1593 g_string_append_c(gstr, 'E');
1595 if (settings.fif_use_extra_options)
1597 g_strstrip(settings.fif_extra_options);
1599 if (*settings.fif_extra_options != 0)
1601 g_string_append_c(gstr, ' ');
1602 g_string_append(gstr, settings.fif_extra_options);
1605 g_strstrip(settings.fif_files);
1606 if (settings.fif_files_mode != FILES_MODE_ALL && *settings.fif_files)
1608 GString *tmp;
1610 /* put --include= before each pattern */
1611 tmp = g_string_new(settings.fif_files);
1612 do {} while (utils_string_replace_all(tmp, " ", " "));
1613 g_string_prepend_c(tmp, ' ');
1614 utils_string_replace_all(tmp, " ", " --include=");
1615 g_string_append(gstr, tmp->str);
1616 g_string_free(tmp, TRUE);
1618 return gstr;
1622 static void
1623 on_find_in_files_dialog_response(GtkDialog *dialog, gint response,
1624 G_GNUC_UNUSED gpointer user_data)
1626 gtk_window_get_position(GTK_WINDOW(fif_dlg.dialog), &fif_dlg.position[0], &fif_dlg.position[1]);
1628 stash_group_update(fif_prefs, fif_dlg.dialog);
1630 if (response == GTK_RESPONSE_ACCEPT)
1632 GtkWidget *search_combo = fif_dlg.search_combo;
1633 const gchar *search_text =
1634 gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(search_combo))));
1635 GtkWidget *dir_combo = fif_dlg.dir_combo;
1636 const gchar *utf8_dir =
1637 gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(dir_combo))));
1638 gchar *locale_dir = utils_get_locale_from_utf8(utf8_dir);
1639 GeanyEncodingIndex enc_idx =
1640 ui_encodings_combo_box_get_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo));
1642 if (!g_file_test(locale_dir, G_FILE_TEST_IS_DIR))
1644 ui_set_statusbar(FALSE, _("Invalid directory for Find in Files."));
1645 ui_set_search_entry_background(dir_combo, FALSE);
1647 else if (EMPTY(search_text))
1649 ui_set_statusbar(FALSE, _("No text to find."));
1650 ui_set_search_entry_background(search_combo, FALSE);
1652 else
1654 GString *opts = get_grep_options();
1655 const gchar *enc = (enc_idx == GEANY_ENCODING_UTF_8) ? NULL :
1656 encodings_get_charset_from_index(enc_idx);
1658 if (search_find_in_files(search_text, utf8_dir, opts->str, enc))
1660 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(search_combo), search_text, 0);
1661 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.files_combo), NULL, 0);
1662 ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(dir_combo), utf8_dir, 0);
1663 gtk_widget_hide(fif_dlg.dialog);
1665 g_string_free(opts, TRUE);
1667 g_free(locale_dir);
1669 else
1670 gtk_widget_hide(fif_dlg.dialog);
1674 static gboolean
1675 search_find_in_files(const gchar *utf8_search_text, const gchar *utf8_dir, const gchar *opts,
1676 const gchar *enc)
1678 gchar **argv_prefix, **argv;
1679 gchar *command_grep;
1680 gchar *command_line, *dir;
1681 gchar *search_text = NULL;
1682 GError *error = NULL;
1683 gboolean ret = FALSE;
1684 gssize utf8_text_len;
1686 if (EMPTY(utf8_search_text) || ! utf8_dir) return TRUE;
1688 command_grep = g_find_program_in_path(tool_prefs.grep_cmd);
1689 if (command_grep == NULL)
1690 command_line = g_strdup_printf("%s %s --", tool_prefs.grep_cmd, opts);
1691 else
1693 command_line = g_strdup_printf("\"%s\" %s --", command_grep, opts);
1694 g_free(command_grep);
1697 /* convert the search text in the preferred encoding (if the text is not valid UTF-8. assume
1698 * it is already in the preferred encoding) */
1699 utf8_text_len = strlen(utf8_search_text);
1700 if (enc != NULL && g_utf8_validate(utf8_search_text, utf8_text_len, NULL))
1702 search_text = g_convert(utf8_search_text, utf8_text_len, enc, "UTF-8", NULL, NULL, NULL);
1704 if (search_text == NULL)
1705 search_text = g_strdup(utf8_search_text);
1707 argv_prefix = g_new(gchar*, 3);
1708 argv_prefix[0] = search_text;
1709 dir = utils_get_locale_from_utf8(utf8_dir);
1711 /* finally add the arguments(files to be searched) */
1712 if (settings.fif_recursive) /* recursive option set */
1714 /* Use '.' so we get relative paths in the output */
1715 argv_prefix[1] = g_strdup(".");
1716 argv_prefix[2] = NULL;
1717 argv = argv_prefix;
1719 else
1721 argv_prefix[1] = NULL;
1722 argv = search_get_argv((const gchar**)argv_prefix, dir);
1723 g_strfreev(argv_prefix);
1725 if (argv == NULL) /* no files */
1727 g_free(command_line);
1728 return FALSE;
1731 reset_msgwin();
1733 /* we can pass 'enc' without strdup'ing it here because it's a global const string and
1734 * always exits longer than the lifetime of this function */
1735 if (spawn_with_callbacks(dir, command_line, argv, NULL, 0, NULL, NULL, search_read_io,
1736 (gpointer) enc, 0, search_read_io_stderr, (gpointer) enc, 0, search_finished, NULL,
1737 NULL, &error))
1739 gchar *utf8_str;
1741 ui_progress_bar_start(_("Searching..."));
1742 msgwin_set_messages_dir(dir);
1743 utf8_str = g_strdup_printf(_("%s %s -- %s (in directory: %s)"),
1744 tool_prefs.grep_cmd, opts, utf8_search_text, utf8_dir);
1745 msgwin_msg_add_string(COLOR_BLUE, -1, NULL, utf8_str);
1746 g_free(utf8_str);
1747 ret = TRUE;
1749 else
1751 ui_set_statusbar(TRUE, _("Cannot execute grep tool \"%s\": %s. "
1752 "Check the path setting in Preferences."), tool_prefs.grep_cmd, error->message);
1753 g_error_free(error);
1756 utils_free_pointers(2, dir, command_line, NULL);
1757 g_strfreev(argv);
1758 return ret;
1762 static gboolean pattern_list_match(GSList *patterns, const gchar *str)
1764 GSList *item;
1766 foreach_slist(item, patterns)
1768 if (g_pattern_match_string(item->data, str))
1769 return TRUE;
1771 return FALSE;
1775 /* Creates an argument vector of strings, copying argv_prefix[] values for
1776 * the first arguments, then followed by filenames found in dir.
1777 * Returns NULL if no files were found, otherwise returned vector should be fully freed. */
1778 static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir)
1780 guint prefix_len, list_len, i, j;
1781 gchar **argv;
1782 GSList *list, *item, *patterns = NULL;
1783 GError *error = NULL;
1785 g_return_val_if_fail(dir != NULL, NULL);
1787 prefix_len = g_strv_length((gchar**)argv_prefix);
1788 list = utils_get_file_list(dir, &list_len, &error);
1789 if (error)
1791 ui_set_statusbar(TRUE, _("Could not open directory (%s)"), error->message);
1792 g_error_free(error);
1793 return NULL;
1795 if (list == NULL)
1796 return NULL;
1798 argv = g_new(gchar*, prefix_len + list_len + 1);
1800 for (i = 0, j = 0; i < prefix_len; i++)
1802 if (g_str_has_prefix(argv_prefix[i], "--include="))
1804 const gchar *pat = &(argv_prefix[i][10]); /* the pattern part of the argument */
1806 patterns = g_slist_prepend(patterns, g_pattern_spec_new(pat));
1808 else
1809 argv[j++] = g_strdup(argv_prefix[i]);
1812 if (patterns)
1814 GSList *pat;
1816 foreach_slist(item, list)
1818 if (pattern_list_match(patterns, item->data))
1819 argv[j++] = item->data;
1820 else
1821 g_free(item->data);
1823 foreach_slist(pat, patterns)
1824 g_pattern_spec_free(pat->data);
1825 g_slist_free(patterns);
1827 else
1829 foreach_slist(item, list)
1830 argv[j++] = item->data;
1833 argv[j] = NULL;
1834 g_slist_free(list);
1835 return argv;
1839 static void read_fif_io(gchar *msg, GIOCondition condition, gchar *enc, gint msg_color)
1841 if (condition & (G_IO_IN | G_IO_PRI))
1843 gchar *utf8_msg = NULL;
1845 g_strstrip(msg);
1846 /* enc is NULL when encoding is set to UTF-8, so we can skip any conversion */
1847 if (enc != NULL)
1849 if (! g_utf8_validate(msg, -1, NULL))
1851 utf8_msg = g_convert(msg, -1, "UTF-8", enc, NULL, NULL, NULL);
1853 if (utf8_msg == NULL)
1854 utf8_msg = msg;
1856 else
1857 utf8_msg = msg;
1859 msgwin_msg_add_string(msg_color, -1, NULL, utf8_msg);
1861 if (utf8_msg != msg)
1862 g_free(utf8_msg);
1867 static void search_read_io(GString *string, GIOCondition condition, gpointer data)
1869 read_fif_io(string->str, condition, data, COLOR_BLACK);
1873 static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data)
1875 read_fif_io(string->str, condition, data, COLOR_DARK_RED);
1879 static void search_finished(GPid child_pid, gint status, gpointer user_data)
1881 const gchar *msg = _("Search failed.");
1882 gint exit_status;
1884 if (SPAWN_WIFEXITED(status))
1886 exit_status = SPAWN_WEXITSTATUS(status);
1888 else if (SPAWN_WIFSIGNALED(status))
1890 exit_status = -1;
1891 g_warning("Find in Files: The command failed unexpectedly (signal received).");
1893 else
1895 exit_status = 1;
1898 switch (exit_status)
1900 case 0:
1902 gint count = gtk_tree_model_iter_n_children(
1903 GTK_TREE_MODEL(msgwindow.store_msg), NULL) - 1;
1904 gchar *text = g_strdup_printf(ngettext(
1905 "Search completed with %d match.",
1906 "Search completed with %d matches.", count),
1907 count);
1909 msgwin_msg_add_string(COLOR_BLUE, -1, NULL, text);
1910 ui_set_statusbar(FALSE, "%s", text);
1911 g_free(text);
1912 break;
1914 case 1:
1915 msg = _("No matches found.");
1916 /* fall through */
1917 default:
1918 msgwin_msg_add_string(COLOR_BLUE, -1, NULL, msg);
1919 ui_set_statusbar(FALSE, "%s", msg);
1920 break;
1922 utils_beep();
1923 ui_progress_bar_stop();
1927 static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags)
1929 GRegex *regex;
1930 GError *error = NULL;
1931 gint rflags = 0;
1933 if (sflags & GEANY_FIND_MULTILINE)
1934 rflags |= G_REGEX_MULTILINE;
1935 if (~sflags & GEANY_FIND_MATCHCASE)
1936 rflags |= G_REGEX_CASELESS;
1937 if (sflags & (GEANY_FIND_WHOLEWORD | GEANY_FIND_WORDSTART))
1939 geany_debug("%s: Unsupported regex flags found!", G_STRFUNC);
1942 regex = g_regex_new(str, rflags, 0, &error);
1943 if (!regex)
1945 ui_set_statusbar(FALSE, _("Bad regex: %s"), error->message);
1946 g_error_free(error);
1948 return regex;
1952 /* groups that don't exist are handled OK as len = end - start = (-1) - (-1) = 0 */
1953 static gchar *get_regex_match_string(const gchar *text, const GeanyMatchInfo *match, guint nth)
1955 const gint start = match->matches[nth].start;
1956 const gint end = match->matches[nth].end;
1957 return g_strndup(&text[start], end - start);
1961 static gint find_regex(ScintillaObject *sci, guint pos, GRegex *regex, gboolean multiline, GeanyMatchInfo *match)
1963 const gchar *text;
1964 GMatchInfo *minfo;
1965 guint document_length;
1966 gint ret = -1;
1967 gint offset = 0;
1969 document_length = (guint)sci_get_length(sci);
1970 if (document_length <= 0)
1971 return -1; /* skip empty documents */
1973 g_return_val_if_fail(pos <= document_length, -1);
1975 if (multiline)
1977 /* Warning: any SCI calls will invalidate 'text' after calling SCI_GETCHARACTERPOINTER */
1978 text = (void*)SSM(sci, SCI_GETCHARACTERPOINTER, 0, 0);
1979 g_regex_match_full(regex, text, -1, pos, 0, &minfo, NULL);
1981 else /* single-line mode, manually match against each line */
1983 gint line = sci_get_line_from_position(sci, pos);
1985 for (;;)
1987 gint start = sci_get_position_from_line(sci, line);
1988 gint end = sci_get_line_end_position(sci, line);
1990 text = (void*)SSM(sci, SCI_GETRANGEPOINTER, start, end - start);
1991 if (g_regex_match_full(regex, text, end - start, pos - start, 0, &minfo, NULL))
1993 offset = start;
1994 break;
1996 else /* not found, try next line */
1998 line ++;
1999 if (line >= sci_get_line_count(sci))
2000 break;
2001 pos = sci_get_position_from_line(sci, line);
2002 /* don't free last info, it's freed below */
2003 g_match_info_free(minfo);
2008 /* Warning: minfo will become invalid when 'text' does! */
2009 if (g_match_info_matches(minfo))
2011 guint i;
2013 /* copy whole match text and offsets before they become invalid */
2014 SETPTR(match->match_text, g_match_info_fetch(minfo, 0));
2016 foreach_range(i, G_N_ELEMENTS(match->matches))
2018 gint start = -1, end = -1;
2020 g_match_info_fetch_pos(minfo, (gint)i, &start, &end);
2021 match->matches[i].start = offset + start;
2022 match->matches[i].end = offset + end;
2024 match->start = match->matches[0].start;
2025 match->end = match->matches[0].end;
2026 ret = match->start;
2028 g_match_info_free(minfo);
2029 return ret;
2033 static gint geany_find_flags_to_sci_flags(GeanyFindFlags flags)
2035 g_warn_if_fail(! (flags & GEANY_FIND_REGEXP) || ! (flags & GEANY_FIND_MULTILINE));
2037 return ((flags & GEANY_FIND_MATCHCASE) ? SCFIND_MATCHCASE : 0) |
2038 ((flags & GEANY_FIND_WHOLEWORD) ? SCFIND_WHOLEWORD : 0) |
2039 ((flags & GEANY_FIND_REGEXP) ? SCFIND_REGEXP | SCFIND_POSIX : 0) |
2040 ((flags & GEANY_FIND_WORDSTART) ? SCFIND_WORDSTART : 0);
2044 gint search_find_prev(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_)
2046 gint ret;
2048 g_return_val_if_fail(! (flags & GEANY_FIND_REGEXP), -1);
2050 ret = sci_search_prev(sci, geany_find_flags_to_sci_flags(flags), str);
2051 if (ret != -1 && match_)
2052 *match_ = match_info_new(flags, ret, ret + strlen(str));
2053 return ret;
2057 gint search_find_next(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_)
2059 GeanyMatchInfo *match;
2060 GRegex *regex;
2061 gint ret = -1;
2062 gint pos;
2064 if (~flags & GEANY_FIND_REGEXP)
2066 ret = sci_search_next(sci, geany_find_flags_to_sci_flags(flags), str);
2067 if (ret != -1 && match_)
2068 *match_ = match_info_new(flags, ret, ret + strlen(str));
2069 return ret;
2072 regex = compile_regex(str, flags);
2073 if (!regex)
2074 return -1;
2076 match = match_info_new(flags, 0, 0);
2078 pos = sci_get_current_position(sci);
2079 ret = find_regex(sci, pos, regex, flags & GEANY_FIND_MULTILINE, match);
2080 /* avoid re-matching the same position in case of empty matches */
2081 if (ret == pos && match->matches[0].start == match->matches[0].end)
2082 ret = find_regex(sci, pos + 1, regex, flags & GEANY_FIND_MULTILINE, match);
2083 if (ret >= 0)
2084 sci_set_selection(sci, match->start, match->end);
2086 if (ret != -1 && match_)
2087 *match_ = match;
2088 else
2089 geany_match_info_free(match);
2091 g_regex_unref(regex);
2092 return ret;
2096 gint search_replace_match(ScintillaObject *sci, const GeanyMatchInfo *match, const gchar *replace_text)
2098 GString *str;
2099 gint ret = 0;
2100 gint i = 0;
2102 sci_set_target_start(sci, match->start);
2103 sci_set_target_end(sci, match->end);
2105 if (! (match->flags & GEANY_FIND_REGEXP))
2106 return sci_replace_target(sci, replace_text, FALSE);
2108 str = g_string_new(replace_text);
2109 while (str->str[i])
2111 gchar *ptr = &str->str[i];
2112 gchar *grp;
2113 gchar c;
2115 if (ptr[0] != '\\')
2117 i++;
2118 continue;
2120 c = ptr[1];
2121 /* backslash or unnecessary escape */
2122 if (c == '\\' || !isdigit(c))
2124 g_string_erase(str, i, 1);
2125 i++;
2126 continue;
2128 /* digit escape */
2129 g_string_erase(str, i, 2);
2130 /* fix match offsets by subtracting index of whole match start from the string */
2131 grp = get_regex_match_string(match->match_text - match->matches[0].start, match, c - '0');
2132 g_string_insert(str, i, grp);
2133 i += strlen(grp);
2134 g_free(grp);
2136 ret = sci_replace_target(sci, str->str, FALSE);
2137 g_string_free(str, TRUE);
2138 return ret;
2142 gint search_find_text(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf, GeanyMatchInfo **match_)
2144 GeanyMatchInfo *match = NULL;
2145 GRegex *regex;
2146 gint ret;
2148 if (~flags & GEANY_FIND_REGEXP)
2150 ret = sci_find_text(sci, geany_find_flags_to_sci_flags(flags), ttf);
2151 if (ret != -1 && match_)
2152 *match_ = match_info_new(flags, ttf->chrgText.cpMin, ttf->chrgText.cpMax);
2153 return ret;
2156 regex = compile_regex(ttf->lpstrText, flags);
2157 if (!regex)
2158 return -1;
2160 match = match_info_new(flags, 0, 0);
2162 ret = find_regex(sci, ttf->chrg.cpMin, regex, flags & GEANY_FIND_MULTILINE, match);
2163 if (ret >= ttf->chrg.cpMax)
2164 ret = -1;
2165 else if (ret >= 0)
2167 ttf->chrgText.cpMin = match->start;
2168 ttf->chrgText.cpMax = match->end;
2171 if (ret != -1 && match_)
2172 *match_ = match;
2173 else
2174 geany_match_info_free(match);
2176 g_regex_unref(regex);
2177 return ret;
2181 static gint find_document_usage(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags)
2183 gchar *buffer, *short_file_name;
2184 struct Sci_TextToFind ttf;
2185 gint count = 0;
2186 gint prev_line = -1;
2187 GSList *match, *matches;
2189 g_return_val_if_fail(DOC_VALID(doc), 0);
2191 short_file_name = g_path_get_basename(DOC_FILENAME(doc));
2193 ttf.chrg.cpMin = 0;
2194 ttf.chrg.cpMax = sci_get_length(doc->editor->sci);
2195 ttf.lpstrText = (gchar *)search_text;
2197 matches = find_range(doc->editor->sci, flags, &ttf);
2198 foreach_slist (match, matches)
2200 GeanyMatchInfo *info = match->data;
2201 gint line = sci_get_line_from_position(doc->editor->sci, info->start);
2203 if (line != prev_line)
2205 buffer = sci_get_line(doc->editor->sci, line);
2206 msgwin_msg_add(COLOR_BLACK, line + 1, doc,
2207 "%s:%d: %s", short_file_name, line + 1, g_strstrip(buffer));
2208 g_free(buffer);
2209 prev_line = line;
2211 count++;
2213 geany_match_info_free(info);
2215 g_slist_free(matches);
2216 g_free(short_file_name);
2217 return count;
2221 void search_find_usage(const gchar *search_text, const gchar *original_search_text,
2222 GeanyFindFlags flags, gboolean in_session)
2224 GeanyDocument *doc;
2225 gint count = 0;
2227 doc = document_get_current();
2228 g_return_if_fail(doc != NULL);
2230 if (G_UNLIKELY(EMPTY(search_text)))
2232 utils_beep();
2233 return;
2235 reset_msgwin();
2237 if (! in_session)
2238 { /* use current document */
2239 count = find_document_usage(doc, search_text, flags);
2241 else
2243 guint i;
2244 for (i = 0; i < documents_array->len; i++)
2246 if (documents[i]->is_valid)
2248 count += find_document_usage(documents[i], search_text, flags);
2253 if (count == 0) /* no matches were found */
2255 ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_search_text);
2256 msgwin_msg_add(COLOR_BLUE, -1, NULL, _("No matches found for \"%s\"."), original_search_text);
2258 else
2260 ui_set_statusbar(FALSE, ngettext(
2261 "Found %d match for \"%s\".", "Found %d matches for \"%s\".", count),
2262 count, original_search_text);
2263 msgwin_msg_add(COLOR_BLUE, -1, NULL, ngettext(
2264 "Found %d match for \"%s\".", "Found %d matches for \"%s\".", count),
2265 count, original_search_text);
2270 /* ttf is updated to include the last match position (ttf->chrg.cpMin) and
2271 * the new search range end (ttf->chrg.cpMax).
2272 * Note: Normally you would call sci_start/end_undo_action() around this call. */
2273 guint search_replace_range(ScintillaObject *sci, struct Sci_TextToFind *ttf,
2274 GeanyFindFlags flags, const gchar *replace_text)
2276 gint count = 0;
2277 gint offset = 0; /* difference between search pos and replace pos */
2278 GSList *match, *matches;
2280 g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL && replace_text != NULL, 0);
2281 if (! *ttf->lpstrText)
2282 return 0;
2284 matches = find_range(sci, flags, ttf);
2285 foreach_slist (match, matches)
2287 GeanyMatchInfo *info = match->data;
2288 gint replace_len;
2290 info->start += offset;
2291 info->end += offset;
2293 replace_len = search_replace_match(sci, info, replace_text);
2294 offset += replace_len - (info->end - info->start);
2295 count ++;
2297 /* on last match, update the last match/new range end */
2298 if (! match->next)
2300 ttf->chrg.cpMin = info->start;
2301 ttf->chrg.cpMax += offset;
2304 geany_match_info_free(info);
2306 g_slist_free(matches);
2308 return count;
2312 void search_find_again(gboolean change_direction)
2314 GeanyDocument *doc = document_get_current();
2316 g_return_if_fail(doc != NULL);
2318 if (search_data.text)
2320 gboolean forward = ! search_data.backwards;
2321 gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags,
2322 change_direction ? forward : !forward, NULL, FALSE, NULL);
2324 if (result > -1)
2325 editor_display_current_line(doc->editor, 0.3F);
2327 if (search_data.search_bar)
2328 ui_set_search_entry_background(
2329 toolbar_get_widget_child_by_name("SearchEntry"), (result > -1));