/* * search.c - this file is part of Geany, a fast and lightweight IDE * * Copyright 2006-2012 Enrico Tröger * Copyright 2006-2012 Nick Treleaven * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ /* * Find, Replace, Find in Files dialog related functions. * Note that the basic text find functions are in document.c. */ #ifdef HAVE_CONFIG_H # include "config.h" #endif #include "search.h" #include "app.h" #include "document.h" #include "encodings.h" #include "encodingsprivate.h" #include "keyfile.h" #include "msgwindow.h" #include "prefs.h" #include "sciwrappers.h" #include "spawn.h" #include "stash.h" #include "support.h" #include "toolbar.h" #include "ui_utils.h" #include "utils.h" #include "gtkcompat.h" #include #include #include #include enum { GEANY_RESPONSE_FIND = 1, GEANY_RESPONSE_FIND_PREVIOUS, GEANY_RESPONSE_FIND_IN_FILE, GEANY_RESPONSE_FIND_IN_SESSION, GEANY_RESPONSE_MARK, GEANY_RESPONSE_REPLACE, GEANY_RESPONSE_REPLACE_AND_FIND, GEANY_RESPONSE_REPLACE_IN_SESSION, GEANY_RESPONSE_REPLACE_IN_FILE, GEANY_RESPONSE_REPLACE_IN_SEL }; enum { FILES_MODE_ALL, FILES_MODE_PROJECT, FILES_MODE_CUSTOM }; GeanySearchData search_data; GeanySearchPrefs search_prefs; static struct { gboolean fif_regexp; gboolean fif_case_sensitive; gboolean fif_match_whole_word; gboolean fif_invert_results; gboolean fif_recursive; gboolean fif_use_extra_options; gchar *fif_extra_options; gint fif_files_mode; gchar *fif_files; gboolean find_regexp; gboolean find_regexp_multiline; gboolean find_escape_sequences; gboolean find_case_sensitive; gboolean find_match_whole_word; gboolean find_match_word_start; gboolean find_close_dialog; gboolean replace_regexp; gboolean replace_regexp_multiline; gboolean replace_escape_sequences; gboolean replace_case_sensitive; gboolean replace_match_whole_word; gboolean replace_match_word_start; gboolean replace_search_backwards; gboolean replace_close_dialog; } settings; static StashGroup *fif_prefs = NULL; static StashGroup *find_prefs = NULL; static StashGroup *replace_prefs = NULL; static struct { GtkWidget *dialog; GtkWidget *entry; gboolean all_expanded; gint position[2]; /* x, y */ } find_dlg = {NULL, NULL, FALSE, {0, 0}}; static struct { GtkWidget *dialog; GtkWidget *find_combobox; GtkWidget *find_entry; GtkWidget *replace_combobox; GtkWidget *replace_entry; gboolean all_expanded; gint position[2]; /* x, y */ } replace_dlg = {NULL, NULL, NULL, NULL, NULL, FALSE, {0, 0}}; static struct { GtkWidget *dialog; GtkWidget *dir_combo; GtkWidget *files_combo; GtkWidget *search_combo; GtkWidget *encoding_combo; GtkWidget *files_mode_combo; gint position[2]; /* x, y */ } fif_dlg = {NULL, NULL, NULL, NULL, NULL, NULL, {0, 0}}; static void search_read_io(GString *string, GIOCondition condition, gpointer data); static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data); static void search_finished(GPid child_pid, gint status, gpointer user_data); static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir); static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags); static void on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data); static void on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data); static void on_find_entry_activate(GtkEntry *entry, gpointer user_data); static void on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data); static void on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data); static void on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data); static void on_replace_entry_activate(GtkEntry *entry, gpointer user_data); static void on_find_in_files_dialog_response(GtkDialog *dialog, gint response, gpointer user_data); static gboolean search_find_in_files(const gchar *utf8_search_text, const gchar *dir, const gchar *opts, const gchar *enc); static void init_prefs(void) { StashGroup *group; group = stash_group_new("search"); configuration_add_pref_group(group, TRUE); stash_group_add_toggle_button(group, &search_prefs.always_wrap, "pref_search_hide_find_dialog", FALSE, "check_always_wrap_search"); stash_group_add_toggle_button(group, &search_prefs.hide_find_dialog, "pref_search_always_wrap", FALSE, "check_hide_find_dialog"); stash_group_add_toggle_button(group, &search_prefs.use_current_file_dir, "pref_search_current_file_dir", TRUE, "check_fif_current_dir"); stash_group_add_boolean(group, &find_dlg.all_expanded, "find_all_expanded", FALSE); stash_group_add_boolean(group, &replace_dlg.all_expanded, "replace_all_expanded", FALSE); /* dialog positions */ stash_group_add_integer(group, &find_dlg.position[0], "position_find_x", -1); stash_group_add_integer(group, &find_dlg.position[1], "position_find_y", -1); stash_group_add_integer(group, &replace_dlg.position[0], "position_replace_x", -1); stash_group_add_integer(group, &replace_dlg.position[1], "position_replace_y", -1); stash_group_add_integer(group, &fif_dlg.position[0], "position_fif_x", -1); stash_group_add_integer(group, &fif_dlg.position[1], "position_fif_y", -1); memset(&settings, '\0', sizeof(settings)); group = stash_group_new("search"); fif_prefs = group; configuration_add_pref_group(group, FALSE); stash_group_add_toggle_button(group, &settings.fif_regexp, "fif_regexp", FALSE, "check_regexp"); stash_group_add_toggle_button(group, &settings.fif_case_sensitive, "fif_case_sensitive", TRUE, "check_case"); stash_group_add_toggle_button(group, &settings.fif_match_whole_word, "fif_match_whole_word", FALSE, "check_wholeword"); stash_group_add_toggle_button(group, &settings.fif_invert_results, "fif_invert_results", FALSE, "check_invert"); stash_group_add_toggle_button(group, &settings.fif_recursive, "fif_recursive", FALSE, "check_recursive"); stash_group_add_entry(group, &settings.fif_extra_options, "fif_extra_options", "", "entry_extra"); stash_group_add_toggle_button(group, &settings.fif_use_extra_options, "fif_use_extra_options", FALSE, "check_extra"); stash_group_add_entry(group, &settings.fif_files, "fif_files", "", "entry_files"); stash_group_add_combo_box(group, &settings.fif_files_mode, "fif_files_mode", FILES_MODE_ALL, "combo_files_mode"); group = stash_group_new("search"); find_prefs = group; configuration_add_pref_group(group, FALSE); stash_group_add_toggle_button(group, &settings.find_regexp, "find_regexp", FALSE, "check_regexp"); stash_group_add_toggle_button(group, &settings.find_regexp_multiline, "find_regexp_multiline", FALSE, "check_multiline"); stash_group_add_toggle_button(group, &settings.find_case_sensitive, "find_case_sensitive", FALSE, "check_case"); stash_group_add_toggle_button(group, &settings.find_escape_sequences, "find_escape_sequences", FALSE, "check_escape"); stash_group_add_toggle_button(group, &settings.find_match_whole_word, "find_match_whole_word", FALSE, "check_word"); stash_group_add_toggle_button(group, &settings.find_match_word_start, "find_match_word_start", FALSE, "check_wordstart"); stash_group_add_toggle_button(group, &settings.find_close_dialog, "find_close_dialog", TRUE, "check_close"); group = stash_group_new("search"); replace_prefs = group; configuration_add_pref_group(group, FALSE); stash_group_add_toggle_button(group, &settings.replace_regexp, "replace_regexp", FALSE, "check_regexp"); stash_group_add_toggle_button(group, &settings.replace_regexp_multiline, "replace_regexp_multiline", FALSE, "check_multiline"); stash_group_add_toggle_button(group, &settings.replace_case_sensitive, "replace_case_sensitive", FALSE, "check_case"); stash_group_add_toggle_button(group, &settings.replace_escape_sequences, "replace_escape_sequences", FALSE, "check_escape"); stash_group_add_toggle_button(group, &settings.replace_match_whole_word, "replace_match_whole_word", FALSE, "check_word"); stash_group_add_toggle_button(group, &settings.replace_match_word_start, "replace_match_word_start", FALSE, "check_wordstart"); stash_group_add_toggle_button(group, &settings.replace_search_backwards, "replace_search_backwards", FALSE, "check_back"); stash_group_add_toggle_button(group, &settings.replace_close_dialog, "replace_close_dialog", TRUE, "check_close"); } void search_init(void) { search_data.text = NULL; search_data.original_text = NULL; init_prefs(); } #define FREE_WIDGET(wid) \ if (wid && GTK_IS_WIDGET(wid)) gtk_widget_destroy(wid); void search_finalize(void) { FREE_WIDGET(find_dlg.dialog); FREE_WIDGET(replace_dlg.dialog); FREE_WIDGET(fif_dlg.dialog); g_free(search_data.text); g_free(search_data.original_text); } static void on_widget_toggled_set_insensitive( GtkToggleButton *togglebutton, gpointer user_data) { gtk_widget_set_sensitive(GTK_WIDGET(user_data), !gtk_toggle_button_get_active(togglebutton)); } static GtkWidget *add_find_checkboxes(GtkDialog *dialog) { GtkWidget *checkbox1, *checkbox2, *check_regexp, *checkbox5, *checkbox7, *check_multiline, *hbox, *fbox, *mbox; check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions")); ui_hookup_widget(dialog, check_regexp, "check_regexp"); gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE); gtk_widget_set_tooltip_text(check_regexp, _("Use Perl-like regular expressions. " "For detailed information about using regular expressions, please refer to the manual.")); g_signal_connect(check_regexp, "toggled", G_CALLBACK(on_find_replace_checkbutton_toggled), dialog); checkbox7 = gtk_check_button_new_with_mnemonic(_("Use _escape sequences")); ui_hookup_widget(dialog, checkbox7, "check_escape"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox7), FALSE); gtk_widget_set_tooltip_text(checkbox7, _("Replace \\\\, \\t, \\n, \\r and \\uXXXX (Unicode characters) with the " "corresponding control characters")); check_multiline = gtk_check_button_new_with_mnemonic(_("Use multi-line matchin_g")); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_multiline), FALSE); gtk_widget_set_sensitive(check_multiline, FALSE); ui_hookup_widget(dialog, check_multiline, "check_multiline"); gtk_button_set_focus_on_click(GTK_BUTTON(check_multiline), FALSE); gtk_widget_set_tooltip_text(check_multiline, _("Perform regular expression " "matching on the whole buffer at once rather than line by line, allowing " "matches to span multiple lines. In this mode, newline characters are part " "of the input and can be captured as normal characters by the pattern.")); /* Search features */ fbox = gtk_vbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(fbox), check_regexp, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(fbox), check_multiline, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(fbox), checkbox7, FALSE, FALSE, 0); if (dialog != GTK_DIALOG(find_dlg.dialog)) { GtkWidget *check_back = gtk_check_button_new_with_mnemonic(_("Search _backwards")); ui_hookup_widget(dialog, check_back, "check_back"); gtk_button_set_focus_on_click(GTK_BUTTON(check_back), FALSE); gtk_container_add(GTK_CONTAINER(fbox), check_back); } checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive")); ui_hookup_widget(dialog, checkbox1, "check_case"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE); checkbox2 = gtk_check_button_new_with_mnemonic(_("Match only a _whole word")); ui_hookup_widget(dialog, checkbox2, "check_word"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE); checkbox5 = gtk_check_button_new_with_mnemonic(_("Match from s_tart of word")); ui_hookup_widget(dialog, checkbox5, "check_wordstart"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox5), FALSE); /* disable wordstart when wholeword is checked */ g_signal_connect(checkbox2, "toggled", G_CALLBACK(on_widget_toggled_set_insensitive), checkbox5); /* Matching options */ mbox = gtk_vbox_new(FALSE, 0); gtk_box_pack_start(GTK_BOX(mbox), checkbox1, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(mbox), checkbox2, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(mbox), checkbox5, FALSE, FALSE, 0); hbox = gtk_hbox_new(TRUE, 6); gtk_container_add(GTK_CONTAINER(hbox), fbox); gtk_container_add(GTK_CONTAINER(hbox), mbox); return hbox; } static void send_find_dialog_response(GtkButton *button, gpointer user_data) { gtk_dialog_response(GTK_DIALOG(find_dlg.dialog), GPOINTER_TO_INT(user_data)); } /* store text, clear search flags so we can use Search->Find Next/Previous */ static void setup_find_next(const gchar *text) { g_free(search_data.text); g_free(search_data.original_text); search_data.text = g_strdup(text); search_data.original_text = g_strdup(text); search_data.flags = 0; search_data.backwards = FALSE; search_data.search_bar = FALSE; } /* Search for next match of the current "selection". * Optionally for X11 based systems, this will try to use the system-wide * x-selection first. * If it doesn't find a suitable search string it will try to use * the current word instead, or just repeat the last search. * Search flags are always zero. */ void search_find_selection(GeanyDocument *doc, gboolean search_backwards) { gchar *s = NULL; g_return_if_fail(DOC_VALID(doc)); #ifdef G_OS_UNIX if (search_prefs.find_selection_type == GEANY_FIND_SEL_X) { GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY); s = gtk_clipboard_wait_for_text(clipboard); if (s && (strchr(s,'\n') || strchr(s, '\r'))) { g_free(s); s = NULL; }; } #endif if (!s && sci_has_selection(doc->editor->sci)) s = sci_get_selection_contents(doc->editor->sci); if (!s && search_prefs.find_selection_type != GEANY_FIND_SEL_AGAIN) { /* get the current word */ s = editor_get_default_selection(doc->editor, TRUE, NULL); } if (s) { setup_find_next(s); /* allow find next/prev */ if (document_find_text(doc, s, NULL, 0, search_backwards, NULL, FALSE, NULL) > -1) editor_display_current_line(doc->editor, 0.3F); g_free(s); } else if (search_prefs.find_selection_type == GEANY_FIND_SEL_AGAIN) { /* Repeat last search (in case selection was lost) */ search_find_again(search_backwards); } else { utils_beep(); } } static void on_expander_activated(GtkExpander *exp, gpointer data) { gboolean *setting = data; *setting = gtk_expander_get_expanded(exp); } static void create_find_dialog(void) { GtkWidget *label, *entry, *sbox, *vbox; GtkWidget *exp, *bbox, *button, *check_close; find_dlg.dialog = gtk_dialog_new_with_buttons(_("Find"), GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL); vbox = ui_dialog_vbox_new(GTK_DIALOG(find_dlg.dialog)); gtk_widget_set_name(find_dlg.dialog, "GeanyDialogSearch"); gtk_box_set_spacing(GTK_BOX(vbox), 9); button = ui_button_new_with_image(GTK_STOCK_GO_BACK, _("_Previous")); gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button, GEANY_RESPONSE_FIND_PREVIOUS); ui_hookup_widget(find_dlg.dialog, button, "btn_previous"); button = ui_button_new_with_image(GTK_STOCK_GO_FORWARD, _("_Next")); gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button, GEANY_RESPONSE_FIND); label = gtk_label_new_with_mnemonic(_("_Search for:")); gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); entry = gtk_combo_box_text_new_with_entry(); ui_entry_add_clear_icon(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry)))); gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry); gtk_entry_set_width_chars(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))), 50); find_dlg.entry = gtk_bin_get_child(GTK_BIN(entry)); g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate", G_CALLBACK(on_find_entry_activate), entry); ui_entry_add_activate_backward_signal(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry)))); g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate-backward", G_CALLBACK(on_find_entry_activate_backward), entry); g_signal_connect(find_dlg.dialog, "response", G_CALLBACK(on_find_dialog_response), entry); g_signal_connect(find_dlg.dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); sbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(sbox), entry, TRUE, TRUE, 0); gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0); gtk_container_add(GTK_CONTAINER(vbox), add_find_checkboxes(GTK_DIALOG(find_dlg.dialog))); /* Now add the multiple match options */ exp = gtk_expander_new_with_mnemonic(_("_Find All")); gtk_expander_set_expanded(GTK_EXPANDER(exp), find_dlg.all_expanded); g_signal_connect_after(exp, "activate", G_CALLBACK(on_expander_activated), &find_dlg.all_expanded); bbox = gtk_hbutton_box_new(); button = gtk_button_new_with_mnemonic(_("_Mark")); gtk_widget_set_tooltip_text(button, _("Mark all matches in the current document")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_MARK)); button = gtk_button_new_with_mnemonic(_("In Sessi_on")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_SESSION)); button = gtk_button_new_with_mnemonic(_("_In Document")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_FILE)); /* close window checkbox */ check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog")); ui_hookup_widget(find_dlg.dialog, check_close, "check_close"); gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE); gtk_widget_set_tooltip_text(check_close, _("Disable this option to keep the dialog open")); gtk_container_add(GTK_CONTAINER(bbox), check_close); gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE); ui_hbutton_box_copy_layout( GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(find_dlg.dialog))), GTK_BUTTON_BOX(bbox)); gtk_container_add(GTK_CONTAINER(exp), bbox); gtk_container_add(GTK_CONTAINER(vbox), exp); } static void set_dialog_position(GtkWidget *dialog, gint *position) { if (position[0] >= 0) gtk_window_move(GTK_WINDOW(dialog), position[0], position[1]); } void search_show_find_dialog(void) { GeanyDocument *doc = document_get_current(); gchar *sel = NULL; g_return_if_fail(doc != NULL); sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL); if (find_dlg.dialog == NULL) { create_find_dialog(); stash_group_display(find_prefs, find_dlg.dialog); if (sel) gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel); set_dialog_position(find_dlg.dialog, find_dlg.position); gtk_widget_show_all(find_dlg.dialog); } else { /* only set selection if the dialog is not already visible */ if (! gtk_widget_get_visible(find_dlg.dialog) && sel) gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel); gtk_widget_grab_focus(find_dlg.entry); set_dialog_position(find_dlg.dialog, find_dlg.position); gtk_widget_show(find_dlg.dialog); if (sel != NULL) /* when we have a selection, reset the entry widget's background colour */ ui_set_search_entry_background(find_dlg.entry, TRUE); /* bring the dialog back in the foreground in case it is already open but the focus is away */ gtk_window_present(GTK_WINDOW(find_dlg.dialog)); } g_free(sel); } static void send_replace_dialog_response(GtkButton *button, gpointer user_data) { gtk_dialog_response(GTK_DIALOG(replace_dlg.dialog), GPOINTER_TO_INT(user_data)); } static gboolean on_widget_key_pressed_set_focus(GtkWidget *widget, GdkEventKey *event, gpointer user_data) { if (event->keyval == GDK_Tab) { gtk_widget_grab_focus(GTK_WIDGET(user_data)); return TRUE; } return FALSE; } static void create_replace_dialog(void) { GtkWidget *label_find, *label_replace, *check_close, *button, *rbox, *fbox, *vbox, *exp, *bbox; GtkSizeGroup *label_size; replace_dlg.dialog = gtk_dialog_new_with_buttons(_("Replace"), GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL); vbox = ui_dialog_vbox_new(GTK_DIALOG(replace_dlg.dialog)); gtk_box_set_spacing(GTK_BOX(vbox), 9); gtk_widget_set_name(replace_dlg.dialog, "GeanyDialogSearch"); button = gtk_button_new_from_stock(GTK_STOCK_FIND); gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button, GEANY_RESPONSE_FIND); button = gtk_button_new_with_mnemonic(_("_Replace")); gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON)); gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button, GEANY_RESPONSE_REPLACE); button = gtk_button_new_with_mnemonic(_("Replace & Fi_nd")); gtk_button_set_image(GTK_BUTTON(button), gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON)); gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button, GEANY_RESPONSE_REPLACE_AND_FIND); label_find = gtk_label_new_with_mnemonic(_("_Search for:")); gtk_misc_set_alignment(GTK_MISC(label_find), 0, 0.5); label_replace = gtk_label_new_with_mnemonic(_("Replace wit_h:")); gtk_misc_set_alignment(GTK_MISC(label_replace), 0, 0.5); replace_dlg.find_combobox = gtk_combo_box_text_new_with_entry(); replace_dlg.find_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.find_combobox)); ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.find_entry)); gtk_label_set_mnemonic_widget(GTK_LABEL(label_find), replace_dlg.find_combobox); gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.find_entry), 50); ui_hookup_widget(replace_dlg.dialog, replace_dlg.find_combobox, "entry_find"); replace_dlg.replace_combobox = gtk_combo_box_text_new_with_entry(); replace_dlg.replace_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.replace_combobox)); ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.replace_entry)); gtk_label_set_mnemonic_widget(GTK_LABEL(label_replace), replace_dlg.replace_combobox); gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.replace_entry), 50); ui_hookup_widget(replace_dlg.dialog, replace_dlg.replace_combobox, "entry_replace"); /* tab from find to the replace entry */ g_signal_connect(replace_dlg.find_entry, "key-press-event", G_CALLBACK(on_widget_key_pressed_set_focus), replace_dlg.replace_entry); g_signal_connect(replace_dlg.find_entry, "activate", G_CALLBACK(on_replace_find_entry_activate), NULL); g_signal_connect(replace_dlg.replace_entry, "activate", G_CALLBACK(on_replace_entry_activate), NULL); g_signal_connect(replace_dlg.dialog, "response", G_CALLBACK(on_replace_dialog_response), NULL); g_signal_connect(replace_dlg.dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); fbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(fbox), label_find, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(fbox), replace_dlg.find_combobox, TRUE, TRUE, 0); rbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(rbox), label_replace, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(rbox), replace_dlg.replace_combobox, TRUE, TRUE, 0); label_size = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); gtk_size_group_add_widget(label_size, label_find); gtk_size_group_add_widget(label_size, label_replace); g_object_unref(G_OBJECT(label_size)); /* auto destroy the size group */ gtk_box_pack_start(GTK_BOX(vbox), fbox, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), rbox, TRUE, FALSE, 0); gtk_container_add(GTK_CONTAINER(vbox), add_find_checkboxes(GTK_DIALOG(replace_dlg.dialog))); /* Now add the multiple replace options */ exp = gtk_expander_new_with_mnemonic(_("Re_place All")); gtk_expander_set_expanded(GTK_EXPANDER(exp), replace_dlg.all_expanded); g_signal_connect_after(exp, "activate", G_CALLBACK(on_expander_activated), &replace_dlg.all_expanded); bbox = gtk_hbutton_box_new(); button = gtk_button_new_with_mnemonic(_("In Sessi_on")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SESSION)); button = gtk_button_new_with_mnemonic(_("_In Document")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_FILE)); button = gtk_button_new_with_mnemonic(_("In Se_lection")); gtk_widget_set_tooltip_text(button, _("Replace all matches found in the currently selected text")); gtk_container_add(GTK_CONTAINER(bbox), button); g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response), GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SEL)); /* close window checkbox */ check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog")); ui_hookup_widget(replace_dlg.dialog, check_close, "check_close"); gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE); gtk_widget_set_tooltip_text(check_close, _("Disable this option to keep the dialog open")); gtk_container_add(GTK_CONTAINER(bbox), check_close); gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE); ui_hbutton_box_copy_layout( GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(replace_dlg.dialog))), GTK_BUTTON_BOX(bbox)); gtk_container_add(GTK_CONTAINER(exp), bbox); gtk_container_add(GTK_CONTAINER(vbox), exp); } void search_show_replace_dialog(void) { GeanyDocument *doc = document_get_current(); gchar *sel = NULL; if (doc == NULL) return; sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL); if (replace_dlg.dialog == NULL) { create_replace_dialog(); stash_group_display(replace_prefs, replace_dlg.dialog); if (sel) gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel); set_dialog_position(replace_dlg.dialog, replace_dlg.position); gtk_widget_show_all(replace_dlg.dialog); } else { /* only set selection if the dialog is not already visible */ if (! gtk_widget_get_visible(replace_dlg.dialog) && sel) gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel); if (sel != NULL) /* when we have a selection, reset the entry widget's background colour */ ui_set_search_entry_background(replace_dlg.find_entry, TRUE); gtk_widget_grab_focus(replace_dlg.find_entry); set_dialog_position(replace_dlg.dialog, replace_dlg.position); gtk_widget_show(replace_dlg.dialog); /* bring the dialog back in the foreground in case it is already open but the focus is away */ gtk_window_present(GTK_WINDOW(replace_dlg.dialog)); } g_free(sel); } static void on_widget_toggled_set_sensitive(GtkToggleButton *togglebutton, gpointer user_data) { /* disable extra option entry when checkbutton not checked */ gtk_widget_set_sensitive(GTK_WIDGET(user_data), gtk_toggle_button_get_active(togglebutton)); } static void update_file_patterns(GtkWidget *mode_combo, GtkWidget *fcombo) { gint selection; GtkWidget *entry; entry = gtk_bin_get_child(GTK_BIN(fcombo)); selection = gtk_combo_box_get_active(GTK_COMBO_BOX(mode_combo)); if (selection == FILES_MODE_ALL) { gtk_entry_set_text(GTK_ENTRY(entry), ""); gtk_widget_set_sensitive(fcombo, FALSE); } else if (selection == FILES_MODE_CUSTOM) { gtk_widget_set_sensitive(fcombo, TRUE); } else if (selection == FILES_MODE_PROJECT) { if (app->project && !EMPTY(app->project->file_patterns)) { gchar *patterns; patterns = g_strjoinv(" ", app->project->file_patterns); gtk_entry_set_text(GTK_ENTRY(entry), patterns); g_free(patterns); } else { gtk_entry_set_text(GTK_ENTRY(entry), ""); } gtk_widget_set_sensitive(fcombo, FALSE); } } /* creates the combo to choose which files include in the search */ static GtkWidget *create_fif_file_mode_combo(void) { GtkWidget *combo; GtkCellRenderer *renderer; GtkListStore *store; GtkTreeIter iter; /* text/sensitive */ store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_BOOLEAN); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, _("all"), 1, TRUE, -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, _("project"), 1, app->project != NULL, -1); gtk_list_store_append(store, &iter); gtk_list_store_set(store, &iter, 0, _("custom"), 1, TRUE, -1); combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store)); g_object_unref(store); gtk_widget_set_tooltip_text(combo, _("All: search all files in the directory\n" "Project: use file patterns defined in the project settings\n" "Custom: specify file patterns manually")); renderer = gtk_cell_renderer_text_new(); gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE); gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", 0, "sensitive", 1, NULL); return combo; } /* updates the sensitivity of the project combo item */ static void update_fif_file_mode_combo(void) { GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(fif_dlg.files_mode_combo)); GtkTreeIter iter; /* "1" refers to the second list entry, project */ if (gtk_tree_model_get_iter_from_string(model, &iter, "1")) gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, app->project != NULL, -1); } static void create_fif_dialog(void) { GtkWidget *dir_combo, *combo, *fcombo, *e_combo, *entry; GtkWidget *label, *label1, *label2, *label3, *checkbox1, *checkbox2, *check_wholeword, *check_recursive, *check_extra, *entry_extra, *check_regexp, *combo_files_mode; GtkWidget *dbox, *sbox, *lbox, *rbox, *hbox, *vbox, *ebox; GtkSizeGroup *size_group; fif_dlg.dialog = gtk_dialog_new_with_buttons( _("Find in Files"), GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT, GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL); vbox = ui_dialog_vbox_new(GTK_DIALOG(fif_dlg.dialog)); gtk_box_set_spacing(GTK_BOX(vbox), 9); gtk_widget_set_name(fif_dlg.dialog, "GeanyDialogSearch"); gtk_dialog_add_button(GTK_DIALOG(fif_dlg.dialog), GTK_STOCK_FIND, GTK_RESPONSE_ACCEPT); gtk_dialog_set_default_response(GTK_DIALOG(fif_dlg.dialog), GTK_RESPONSE_ACCEPT); label = gtk_label_new_with_mnemonic(_("_Search for:")); gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5); combo = gtk_combo_box_text_new_with_entry(); entry = gtk_bin_get_child(GTK_BIN(combo)); ui_entry_add_clear_icon(GTK_ENTRY(entry)); gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry); gtk_entry_set_width_chars(GTK_ENTRY(entry), 50); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); fif_dlg.search_combo = combo; sbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(sbox), combo, TRUE, TRUE, 0); /* make labels same width */ size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); gtk_size_group_add_widget(size_group, label); label3 = gtk_label_new_with_mnemonic(_("Fi_les:")); gtk_misc_set_alignment(GTK_MISC(label3), 0, 0.5); combo_files_mode = create_fif_file_mode_combo(); gtk_label_set_mnemonic_widget(GTK_LABEL(label3), combo_files_mode); ui_hookup_widget(fif_dlg.dialog, combo_files_mode, "combo_files_mode"); fif_dlg.files_mode_combo = combo_files_mode; fcombo = gtk_combo_box_text_new_with_entry(); entry = gtk_bin_get_child(GTK_BIN(fcombo)); ui_entry_add_clear_icon(GTK_ENTRY(entry)); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); gtk_widget_set_tooltip_text(entry, _("File patterns, e.g. *.c *.h")); ui_hookup_widget(fif_dlg.dialog, entry, "entry_files"); fif_dlg.files_combo = fcombo; /* update the entry when selection is changed */ g_signal_connect(combo_files_mode, "changed", G_CALLBACK(update_file_patterns), fcombo); hbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), combo_files_mode, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), fcombo, TRUE, TRUE, 0); label1 = gtk_label_new_with_mnemonic(_("_Directory:")); gtk_misc_set_alignment(GTK_MISC(label1), 0, 0.5); dir_combo = gtk_combo_box_text_new_with_entry(); entry = gtk_bin_get_child(GTK_BIN(dir_combo)); ui_entry_add_clear_icon(GTK_ENTRY(entry)); gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE); gtk_label_set_mnemonic_widget(GTK_LABEL(label1), entry); gtk_entry_set_width_chars(GTK_ENTRY(entry), 50); fif_dlg.dir_combo = dir_combo; /* tab from files to the dir entry */ g_signal_connect(gtk_bin_get_child(GTK_BIN(fcombo)), "key-press-event", G_CALLBACK(on_widget_key_pressed_set_focus), entry); dbox = ui_path_box_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER, GTK_ENTRY(entry)); gtk_box_pack_start(GTK_BOX(dbox), label1, FALSE, FALSE, 0); label2 = gtk_label_new_with_mnemonic(_("E_ncoding:")); gtk_misc_set_alignment(GTK_MISC(label2), 0, 0.5); e_combo = ui_create_encodings_combo_box(FALSE, GEANY_ENCODING_UTF_8); gtk_label_set_mnemonic_widget(GTK_LABEL(label2), e_combo); fif_dlg.encoding_combo = e_combo; ebox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(ebox), label2, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(ebox), e_combo, TRUE, TRUE, 0); gtk_size_group_add_widget(size_group, label1); gtk_size_group_add_widget(size_group, label2); gtk_size_group_add_widget(size_group, label3); g_object_unref(G_OBJECT(size_group)); /* auto destroy the size group */ gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), dbox, TRUE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), ebox, TRUE, FALSE, 0); check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions")); ui_hookup_widget(fif_dlg.dialog, check_regexp, "check_regexp"); gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE); gtk_widget_set_tooltip_text(check_regexp, _("See grep's manual page for more information")); check_recursive = gtk_check_button_new_with_mnemonic(_("_Recurse in subfolders")); ui_hookup_widget(fif_dlg.dialog, check_recursive, "check_recursive"); gtk_button_set_focus_on_click(GTK_BUTTON(check_recursive), FALSE); checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive")); ui_hookup_widget(fif_dlg.dialog, checkbox1, "check_case"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox1), TRUE); check_wholeword = gtk_check_button_new_with_mnemonic(_("Match only a _whole word")); ui_hookup_widget(fif_dlg.dialog, check_wholeword, "check_wholeword"); gtk_button_set_focus_on_click(GTK_BUTTON(check_wholeword), FALSE); checkbox2 = gtk_check_button_new_with_mnemonic(_("_Invert search results")); ui_hookup_widget(fif_dlg.dialog, checkbox2, "check_invert"); gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE); gtk_widget_set_tooltip_text(checkbox2, _("Invert the sense of matching, to select non-matching lines")); lbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(lbox), check_regexp); gtk_container_add(GTK_CONTAINER(lbox), checkbox2); gtk_container_add(GTK_CONTAINER(lbox), check_recursive); rbox = gtk_vbox_new(FALSE, 0); gtk_container_add(GTK_CONTAINER(rbox), checkbox1); gtk_container_add(GTK_CONTAINER(rbox), check_wholeword); gtk_container_add(GTK_CONTAINER(rbox), gtk_label_new(NULL)); hbox = gtk_hbox_new(FALSE, 6); gtk_container_add(GTK_CONTAINER(hbox), lbox); gtk_container_add(GTK_CONTAINER(hbox), rbox); gtk_container_add(GTK_CONTAINER(vbox), hbox); check_extra = gtk_check_button_new_with_mnemonic(_("E_xtra options:")); ui_hookup_widget(fif_dlg.dialog, check_extra, "check_extra"); gtk_button_set_focus_on_click(GTK_BUTTON(check_extra), FALSE); entry_extra = gtk_entry_new(); ui_entry_add_clear_icon(GTK_ENTRY(entry_extra)); gtk_entry_set_activates_default(GTK_ENTRY(entry_extra), TRUE); gtk_widget_set_sensitive(entry_extra, FALSE); gtk_widget_set_tooltip_text(entry_extra, _("Other options to pass to Grep")); ui_hookup_widget(fif_dlg.dialog, entry_extra, "entry_extra"); /* enable entry_extra when check_extra is checked */ g_signal_connect(check_extra, "toggled", G_CALLBACK(on_widget_toggled_set_sensitive), entry_extra); hbox = gtk_hbox_new(FALSE, 6); gtk_box_pack_start(GTK_BOX(hbox), check_extra, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(hbox), entry_extra, TRUE, TRUE, 0); gtk_container_add(GTK_CONTAINER(vbox), hbox); g_signal_connect(fif_dlg.dialog, "response", G_CALLBACK(on_find_in_files_dialog_response), NULL); g_signal_connect(fif_dlg.dialog, "delete-event", G_CALLBACK(gtk_widget_hide_on_delete), NULL); } /** * Shows the Find in Files dialog. * * @param dir The directory to search in (UTF-8 encoding). May be @c NULL * to determine it the usual way by using the current document's path. * * @since 0.14, plugin API 53 */ GEANY_API_SYMBOL void search_show_find_in_files_dialog(const gchar *dir) { search_show_find_in_files_dialog_full(NULL, dir); } void search_show_find_in_files_dialog_full(const gchar *text, const gchar *dir) { GtkWidget *entry; /* for child GtkEntry of a GtkComboBoxEntry */ GeanyDocument *doc = document_get_current(); gchar *sel = NULL; gchar *cur_dir = NULL; GeanyEncodingIndex enc_idx = GEANY_ENCODING_UTF_8; if (fif_dlg.dialog == NULL) { create_fif_dialog(); gtk_widget_show_all(fif_dlg.dialog); if (doc && !text) sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL); } stash_group_display(fif_prefs, fif_dlg.dialog); if (!text) { /* only set selection if the dialog is not already visible, or has just been created */ if (doc && ! sel && ! gtk_widget_get_visible(fif_dlg.dialog)) sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL); text = sel; } entry = gtk_bin_get_child(GTK_BIN(fif_dlg.search_combo)); if (text) gtk_entry_set_text(GTK_ENTRY(entry), text); g_free(sel); /* add project's base path directory to the dir list, we do this here once * (in create_fif_dialog() it would fail if a project is opened after dialog creation) */ if (app->project != NULL && !EMPTY(app->project->base_path)) { ui_combo_box_prepend_text_once(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo), app->project->base_path); } entry = gtk_bin_get_child(GTK_BIN(fif_dlg.dir_combo)); if (!EMPTY(dir)) cur_dir = g_strdup(dir); /* custom directory argument passed */ else { if (search_prefs.use_current_file_dir) { static gchar *last_cur_dir = NULL; static GeanyDocument *last_doc = NULL; /* Only set the directory entry once for the current document */ cur_dir = utils_get_current_file_dir_utf8(); if (doc == last_doc && cur_dir && utils_str_equal(cur_dir, last_cur_dir)) { /* in case the user now wants the current directory, add it to history */ ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo), cur_dir, 0); SETPTR(cur_dir, NULL); } else SETPTR(last_cur_dir, g_strdup(cur_dir)); last_doc = doc; } if (!cur_dir && EMPTY(gtk_entry_get_text(GTK_ENTRY(entry)))) { /* use default_open_path if no directory could be determined * (e.g. when no files are open) */ if (!cur_dir) cur_dir = g_strdup(utils_get_default_dir_utf8()); if (!cur_dir) cur_dir = g_get_current_dir(); } } if (cur_dir) { gtk_entry_set_text(GTK_ENTRY(entry), cur_dir); g_free(cur_dir); } update_fif_file_mode_combo(); update_file_patterns(fif_dlg.files_mode_combo, fif_dlg.files_combo); /* set the encoding of the current file */ if (doc != NULL) enc_idx = encodings_get_idx_from_charset(doc->encoding); ui_encodings_combo_box_set_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo), enc_idx); /* put the focus to the directory entry if it is empty */ if (utils_str_equal(gtk_entry_get_text(GTK_ENTRY(entry)), "")) gtk_widget_grab_focus(fif_dlg.dir_combo); else gtk_widget_grab_focus(fif_dlg.search_combo); /* set dialog window position */ set_dialog_position(fif_dlg.dialog, fif_dlg.position); gtk_widget_show(fif_dlg.dialog); /* bring the dialog back in the foreground in case it is already open but the focus is away */ gtk_window_present(GTK_WINDOW(fif_dlg.dialog)); } static void on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data) { GtkWidget *dialog = GTK_WIDGET(user_data); GtkToggleButton *chk_regexp = GTK_TOGGLE_BUTTON( ui_lookup_widget(dialog, "check_regexp")); if (togglebutton == chk_regexp) { gboolean regex_set = gtk_toggle_button_get_active(chk_regexp); GtkWidget *check_word = ui_lookup_widget(dialog, "check_word"); GtkWidget *check_wordstart = ui_lookup_widget(dialog, "check_wordstart"); GtkWidget *check_escape = ui_lookup_widget(dialog, "check_escape"); GtkWidget *check_multiline = ui_lookup_widget(dialog, "check_multiline"); gboolean replace = (dialog != find_dlg.dialog); const char *back_button[2] = { "btn_previous" , "check_back" }; /* hide options that don't apply to regex searches */ gtk_widget_set_sensitive(check_escape, ! regex_set); gtk_widget_set_sensitive(ui_lookup_widget(dialog, back_button[replace]), ! regex_set); gtk_widget_set_sensitive(check_word, ! regex_set); gtk_widget_set_sensitive(check_wordstart, ! regex_set); gtk_widget_set_sensitive(check_multiline, regex_set); } } static GeanyMatchInfo *match_info_new(GeanyFindFlags flags, gint start, gint end) { GeanyMatchInfo *info = g_slice_alloc(sizeof *info); info->flags = flags; info->start = start; info->end = end; info->match_text = NULL; return info; } void geany_match_info_free(GeanyMatchInfo *info) { g_free(info->match_text); g_slice_free1(sizeof *info, info); } /* find all in the given range. * Returns a list of allocated GeanyMatchInfo, should be freed using: * * foreach_slist(node, matches) * geany_match_info_free(node->data); * g_slist_free(matches); */ static GSList *find_range(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf) { GSList *matches = NULL; GeanyMatchInfo *info; g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL, NULL); if (! *ttf->lpstrText) return NULL; while (search_find_text(sci, flags, ttf, &info) != -1) { if (ttf->chrgText.cpMax > ttf->chrg.cpMax) { /* found text is partially out of range */ geany_match_info_free(info); break; } matches = g_slist_prepend(matches, info); ttf->chrg.cpMin = ttf->chrgText.cpMax; /* avoid rematching with empty matches like "(?=[a-z])" or "^$". * note we cannot assume a match will always be empty or not and then break out, since * matches like "a?(?=b)" will sometimes be empty and sometimes not */ if (ttf->chrgText.cpMax == ttf->chrgText.cpMin) ttf->chrg.cpMin ++; } return g_slist_reverse(matches); } /* Clears markers if text is null/empty. * @return Number of matches marked. */ gint search_mark_all(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags) { gint count = 0; struct Sci_TextToFind ttf; GSList *match, *matches; g_return_val_if_fail(DOC_VALID(doc), 0); /* clear previous search indicators */ editor_indicator_clear(doc->editor, GEANY_INDICATOR_SEARCH); if (G_UNLIKELY(EMPTY(search_text))) return 0; ttf.chrg.cpMin = 0; ttf.chrg.cpMax = sci_get_length(doc->editor->sci); ttf.lpstrText = (gchar *)search_text; matches = find_range(doc->editor->sci, flags, &ttf); foreach_slist (match, matches) { GeanyMatchInfo *info = match->data; if (info->end != info->start) editor_indicator_set_on_range(doc->editor, GEANY_INDICATOR_SEARCH, info->start, info->end); count++; geany_match_info_free(info); } g_slist_free(matches); return count; } static void on_find_entry_activate(GtkEntry *entry, gpointer user_data) { on_find_dialog_response(NULL, GEANY_RESPONSE_FIND, user_data); } static void on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data) { /* can't search backwards with a regexp */ if (search_data.flags & GEANY_FIND_REGEXP) utils_beep(); else on_find_dialog_response(NULL, GEANY_RESPONSE_FIND_PREVIOUS, user_data); } static GeanyFindFlags int_search_flags(gint match_case, gint whole_word, gint regexp, gint multiline, gint word_start) { return (match_case ? GEANY_FIND_MATCHCASE : 0) | (regexp ? GEANY_FIND_REGEXP : 0) | (whole_word ? GEANY_FIND_WHOLEWORD : 0) | (multiline ? GEANY_FIND_MULTILINE : 0) | /* SCFIND_WORDSTART overrides SCFIND_WHOLEWORD, but we want the opposite */ (word_start && !whole_word ? GEANY_FIND_WORDSTART : 0); } static void on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data) { gtk_window_get_position(GTK_WINDOW(find_dlg.dialog), &find_dlg.position[0], &find_dlg.position[1]); stash_group_update(find_prefs, find_dlg.dialog); if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) gtk_widget_hide(find_dlg.dialog); else { GeanyDocument *doc = document_get_current(); gboolean check_close = settings.find_close_dialog; if (doc == NULL) return; search_data.backwards = FALSE; search_data.search_bar = FALSE; g_free(search_data.text); g_free(search_data.original_text); search_data.text = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(user_data))))); search_data.original_text = g_strdup(search_data.text); search_data.flags = int_search_flags(settings.find_case_sensitive, settings.find_match_whole_word, settings.find_regexp, settings.find_regexp_multiline, settings.find_match_word_start); if (EMPTY(search_data.text)) { fail: utils_beep(); gtk_widget_grab_focus(find_dlg.entry); return; } if (search_data.flags & GEANY_FIND_REGEXP) { GRegex *regex = compile_regex(search_data.text, search_data.flags); if (!regex) goto fail; else g_regex_unref(regex); } else if (settings.find_escape_sequences) { if (! utils_str_replace_escape(search_data.text, FALSE)) goto fail; } ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(user_data), search_data.original_text, 0); switch (response) { case GEANY_RESPONSE_FIND: case GEANY_RESPONSE_FIND_PREVIOUS: { gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags, (response == GEANY_RESPONSE_FIND_PREVIOUS), NULL, TRUE, GTK_WIDGET(find_dlg.dialog)); ui_set_search_entry_background(find_dlg.entry, (result > -1)); check_close = search_prefs.hide_find_dialog; break; } case GEANY_RESPONSE_FIND_IN_FILE: search_find_usage(search_data.text, search_data.original_text, search_data.flags, FALSE); break; case GEANY_RESPONSE_FIND_IN_SESSION: search_find_usage(search_data.text, search_data.original_text, search_data.flags, TRUE); break; case GEANY_RESPONSE_MARK: { gint count = search_mark_all(doc, search_data.text, search_data.flags); if (count == 0) ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), search_data.original_text); else ui_set_statusbar(FALSE, ngettext("Found %d match for \"%s\".", "Found %d matches for \"%s\".", count), count, search_data.original_text); } break; } if (check_close) gtk_widget_hide(find_dlg.dialog); } } static void on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data) { on_replace_dialog_response(NULL, GEANY_RESPONSE_FIND, NULL); } static void on_replace_entry_activate(GtkEntry *entry, gpointer user_data) { on_replace_dialog_response(NULL, search_prefs.replace_and_find_by_default ? GEANY_RESPONSE_REPLACE_AND_FIND : GEANY_RESPONSE_REPLACE, NULL); } static void replace_in_session(GeanyDocument *doc, GeanyFindFlags search_flags_re, gboolean search_replace_escape_re, const gchar *find, const gchar *replace, const gchar *original_find, const gchar *original_replace) { guint n, page_count, rep_count = 0, file_count = 0; /* replace in all documents following notebook tab order */ page_count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(main_widgets.notebook)); for (n = 0; n < page_count; n++) { GeanyDocument *tmp_doc = document_get_from_page(n); gint reps = 0; reps = document_replace_all(tmp_doc, find, replace, original_find, original_replace, search_flags_re); rep_count += reps; if (reps) file_count++; } if (file_count == 0) { utils_beep(); ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_find); return; } /* if only one file was changed, don't override that document's status message * so we don't have to translate 4 messages for ngettext */ if (file_count > 1) ui_set_statusbar(FALSE, _("Replaced %u matches in %u documents."), rep_count, file_count); /* show which docs had replacements: */ gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_STATUS); ui_save_buttons_toggle(doc->changed); /* update save all */ } static void on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data) { GeanyDocument *doc = document_get_current(); GeanyFindFlags search_flags_re; gboolean search_backwards_re, search_replace_escape_re; gchar *find, *replace, *original_find = NULL, *original_replace = NULL; gtk_window_get_position(GTK_WINDOW(replace_dlg.dialog), &replace_dlg.position[0], &replace_dlg.position[1]); stash_group_update(replace_prefs, replace_dlg.dialog); if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT) { gtk_widget_hide(replace_dlg.dialog); return; } search_backwards_re = settings.replace_search_backwards; search_replace_escape_re = settings.replace_escape_sequences; find = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.find_entry))); replace = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.replace_entry))); search_flags_re = int_search_flags(settings.replace_case_sensitive, settings.replace_match_whole_word, settings.replace_regexp, settings.replace_regexp_multiline, settings.replace_match_word_start); if ((response != GEANY_RESPONSE_FIND) && (search_flags_re & GEANY_FIND_MATCHCASE) && (strcmp(find, replace) == 0)) goto fail; original_find = g_strdup(find); original_replace = g_strdup(replace); if (search_flags_re & GEANY_FIND_REGEXP) { GRegex *regex = compile_regex(find, search_flags_re); if (regex) g_regex_unref(regex); /* find escapes will be handled by GRegex */ if (!regex || !utils_str_replace_escape(replace, TRUE)) goto fail; } else if (search_replace_escape_re) { if (! utils_str_replace_escape(find, FALSE) || ! utils_str_replace_escape(replace, FALSE)) goto fail; } ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.find_combobox), original_find, 0); ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.replace_combobox), original_replace, 0); switch (response) { case GEANY_RESPONSE_REPLACE_AND_FIND: { gint rep = document_replace_text(doc, find, original_find, replace, search_flags_re, search_backwards_re); if (rep != -1) document_find_text(doc, find, original_find, search_flags_re, search_backwards_re, NULL, TRUE, NULL); break; } case GEANY_RESPONSE_REPLACE: document_replace_text(doc, find, original_find, replace, search_flags_re, search_backwards_re); break; case GEANY_RESPONSE_FIND: { gint result = document_find_text(doc, find, original_find, search_flags_re, search_backwards_re, NULL, TRUE, GTK_WIDGET(dialog)); ui_set_search_entry_background(replace_dlg.find_entry, (result > -1)); break; } case GEANY_RESPONSE_REPLACE_IN_FILE: if (! document_replace_all(doc, find, replace, original_find, original_replace, search_flags_re)) utils_beep(); break; case GEANY_RESPONSE_REPLACE_IN_SESSION: replace_in_session(doc, search_flags_re, search_replace_escape_re, find, replace, original_find, original_replace); break; case GEANY_RESPONSE_REPLACE_IN_SEL: document_replace_sel(doc, find, replace, original_find, original_replace, search_flags_re); break; } switch (response) { case GEANY_RESPONSE_REPLACE_IN_SEL: case GEANY_RESPONSE_REPLACE_IN_FILE: case GEANY_RESPONSE_REPLACE_IN_SESSION: if (settings.replace_close_dialog) gtk_widget_hide(replace_dlg.dialog); } g_free(find); g_free(replace); g_free(original_find); g_free(original_replace); return; fail: utils_beep(); gtk_widget_grab_focus(replace_dlg.find_entry); g_free(find); g_free(replace); g_free(original_find); g_free(original_replace); } static GString *get_grep_options(void) { GString *gstr = g_string_new("-nHI"); /* line numbers, filenames, ignore binaries */ if (settings.fif_invert_results) g_string_append_c(gstr, 'v'); if (! settings.fif_case_sensitive) g_string_append_c(gstr, 'i'); if (settings.fif_match_whole_word) g_string_append_c(gstr, 'w'); if (settings.fif_recursive) g_string_append_c(gstr, 'r'); if (!settings.fif_regexp) g_string_append_c(gstr, 'F'); else g_string_append_c(gstr, 'E'); if (settings.fif_use_extra_options) { g_strstrip(settings.fif_extra_options); if (*settings.fif_extra_options != 0) { g_string_append_c(gstr, ' '); g_string_append(gstr, settings.fif_extra_options); } } g_strstrip(settings.fif_files); if (settings.fif_files_mode != FILES_MODE_ALL && *settings.fif_files) { GString *tmp; /* put --include= before each pattern */ tmp = g_string_new(settings.fif_files); do {} while (utils_string_replace_all(tmp, " ", " ")); g_string_prepend_c(tmp, ' '); utils_string_replace_all(tmp, " ", " --include="); g_string_append(gstr, tmp->str); g_string_free(tmp, TRUE); } return gstr; } static void on_find_in_files_dialog_response(GtkDialog *dialog, gint response, G_GNUC_UNUSED gpointer user_data) { gtk_window_get_position(GTK_WINDOW(fif_dlg.dialog), &fif_dlg.position[0], &fif_dlg.position[1]); stash_group_update(fif_prefs, fif_dlg.dialog); if (response == GTK_RESPONSE_ACCEPT) { GtkWidget *search_combo = fif_dlg.search_combo; const gchar *search_text = gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(search_combo)))); GtkWidget *dir_combo = fif_dlg.dir_combo; const gchar *utf8_dir = gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(dir_combo)))); GeanyEncodingIndex enc_idx = ui_encodings_combo_box_get_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo)); if (G_UNLIKELY(EMPTY(utf8_dir))) ui_set_statusbar(FALSE, _("Invalid directory for find in files.")); else if (!EMPTY(search_text)) { GString *opts = get_grep_options(); const gchar *enc = (enc_idx == GEANY_ENCODING_UTF_8) ? NULL : encodings_get_charset_from_index(enc_idx); if (search_find_in_files(search_text, utf8_dir, opts->str, enc)) { ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(search_combo), search_text, 0); ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.files_combo), NULL, 0); ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(dir_combo), utf8_dir, 0); gtk_widget_hide(fif_dlg.dialog); } g_string_free(opts, TRUE); } else ui_set_statusbar(FALSE, _("No text to find.")); } else gtk_widget_hide(fif_dlg.dialog); } static gboolean search_find_in_files(const gchar *utf8_search_text, const gchar *utf8_dir, const gchar *opts, const gchar *enc) { gchar **argv_prefix, **argv; gchar *command_grep; gchar *command_line, *dir; gchar *search_text = NULL; GError *error = NULL; gboolean ret = FALSE; gssize utf8_text_len; if (EMPTY(utf8_search_text) || ! utf8_dir) return TRUE; command_grep = g_find_program_in_path(tool_prefs.grep_cmd); if (command_grep == NULL) command_line = g_strdup_printf("%s %s --", tool_prefs.grep_cmd, opts); else { command_line = g_strdup_printf("\"%s\" %s --", command_grep, opts); g_free(command_grep); } /* convert the search text in the preferred encoding (if the text is not valid UTF-8. assume * it is already in the preferred encoding) */ utf8_text_len = strlen(utf8_search_text); if (enc != NULL && g_utf8_validate(utf8_search_text, utf8_text_len, NULL)) { search_text = g_convert(utf8_search_text, utf8_text_len, enc, "UTF-8", NULL, NULL, NULL); } if (search_text == NULL) search_text = g_strdup(utf8_search_text); argv_prefix = g_new(gchar*, 3); argv_prefix[0] = search_text; dir = utils_get_locale_from_utf8(utf8_dir); /* finally add the arguments(files to be searched) */ if (settings.fif_recursive) /* recursive option set */ { /* Use '.' so we get relative paths in the output */ argv_prefix[1] = g_strdup("."); argv_prefix[2] = NULL; argv = argv_prefix; } else { argv_prefix[1] = NULL; argv = search_get_argv((const gchar**)argv_prefix, dir); g_strfreev(argv_prefix); if (argv == NULL) /* no files */ { g_free(command_line); return FALSE; } } gtk_list_store_clear(msgwindow.store_msg); gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE); /* we can pass 'enc' without strdup'ing it here because it's a global const string and * always exits longer than the lifetime of this function */ if (spawn_with_callbacks(dir, command_line, argv, NULL, 0, NULL, NULL, search_read_io, (gpointer) enc, 0, search_read_io_stderr, (gpointer) enc, 0, search_finished, NULL, NULL, &error)) { gchar *utf8_str; ui_progress_bar_start(_("Searching...")); msgwin_set_messages_dir(dir); utf8_str = g_strdup_printf(_("%s %s -- %s (in directory: %s)"), tool_prefs.grep_cmd, opts, utf8_search_text, utf8_dir); msgwin_msg_add_string(COLOR_BLUE, -1, NULL, utf8_str); g_free(utf8_str); ret = TRUE; } else { ui_set_statusbar(TRUE, _("Cannot execute grep tool \"%s\": %s. " "Check the path setting in Preferences."), tool_prefs.grep_cmd, error->message); g_error_free(error); } utils_free_pointers(2, dir, command_line, NULL); g_strfreev(argv); return ret; } static gboolean pattern_list_match(GSList *patterns, const gchar *str) { GSList *item; foreach_slist(item, patterns) { if (g_pattern_match_string(item->data, str)) return TRUE; } return FALSE; } /* Creates an argument vector of strings, copying argv_prefix[] values for * the first arguments, then followed by filenames found in dir. * Returns NULL if no files were found, otherwise returned vector should be fully freed. */ static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir) { guint prefix_len, list_len, i, j; gchar **argv; GSList *list, *item, *patterns = NULL; GError *error = NULL; g_return_val_if_fail(dir != NULL, NULL); prefix_len = g_strv_length((gchar**)argv_prefix); list = utils_get_file_list(dir, &list_len, &error); if (error) { ui_set_statusbar(TRUE, _("Could not open directory (%s)"), error->message); g_error_free(error); return NULL; } if (list == NULL) return NULL; argv = g_new(gchar*, prefix_len + list_len + 1); for (i = 0, j = 0; i < prefix_len; i++) { if (g_str_has_prefix(argv_prefix[i], "--include=")) { const gchar *pat = &(argv_prefix[i][10]); /* the pattern part of the argument */ patterns = g_slist_prepend(patterns, g_pattern_spec_new(pat)); } else argv[j++] = g_strdup(argv_prefix[i]); } if (patterns) { GSList *pat; foreach_slist(item, list) { if (pattern_list_match(patterns, item->data)) argv[j++] = item->data; else g_free(item->data); } foreach_slist(pat, patterns) g_pattern_spec_free(pat->data); g_slist_free(patterns); } else { foreach_slist(item, list) argv[j++] = item->data; } argv[j] = NULL; g_slist_free(list); return argv; } static void read_fif_io(gchar *msg, GIOCondition condition, gchar *enc, gint msg_color) { if (condition & (G_IO_IN | G_IO_PRI)) { gchar *utf8_msg = NULL; g_strstrip(msg); /* enc is NULL when encoding is set to UTF-8, so we can skip any conversion */ if (enc != NULL) { if (! g_utf8_validate(msg, -1, NULL)) { utf8_msg = g_convert(msg, -1, "UTF-8", enc, NULL, NULL, NULL); } if (utf8_msg == NULL) utf8_msg = msg; } else utf8_msg = msg; msgwin_msg_add_string(msg_color, -1, NULL, utf8_msg); if (utf8_msg != msg) g_free(utf8_msg); } } static void search_read_io(GString *string, GIOCondition condition, gpointer data) { read_fif_io(string->str, condition, data, COLOR_BLACK); } static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data) { read_fif_io(string->str, condition, data, COLOR_DARK_RED); } static void search_finished(GPid child_pid, gint status, gpointer user_data) { const gchar *msg = _("Search failed."); gint exit_status; if (SPAWN_WIFEXITED(status)) { exit_status = SPAWN_WEXITSTATUS(status); } else if (SPAWN_WIFSIGNALED(status)) { exit_status = -1; g_warning("Find in Files: The command failed unexpectedly (signal received)."); } else { exit_status = 1; } switch (exit_status) { case 0: { gint count = gtk_tree_model_iter_n_children( GTK_TREE_MODEL(msgwindow.store_msg), NULL) - 1; gchar *text = ngettext( "Search completed with %d match.", "Search completed with %d matches.", count); msgwin_msg_add(COLOR_BLUE, -1, NULL, text, count); ui_set_statusbar(FALSE, text, count); break; } case 1: msg = _("No matches found."); /* fall through */ default: msgwin_msg_add_string(COLOR_BLUE, -1, NULL, msg); ui_set_statusbar(FALSE, "%s", msg); break; } utils_beep(); ui_progress_bar_stop(); } static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags) { GRegex *regex; GError *error = NULL; gint rflags = 0; if (sflags & GEANY_FIND_MULTILINE) rflags |= G_REGEX_MULTILINE; if (~sflags & GEANY_FIND_MATCHCASE) rflags |= G_REGEX_CASELESS; if (sflags & (GEANY_FIND_WHOLEWORD | GEANY_FIND_WORDSTART)) { geany_debug("%s: Unsupported regex flags found!", G_STRFUNC); } regex = g_regex_new(str, rflags, 0, &error); if (!regex) { ui_set_statusbar(FALSE, _("Bad regex: %s"), error->message); g_error_free(error); } return regex; } /* groups that don't exist are handled OK as len = end - start = (-1) - (-1) = 0 */ static gchar *get_regex_match_string(const gchar *text, const GeanyMatchInfo *match, guint nth) { const gint start = match->matches[nth].start; const gint end = match->matches[nth].end; return g_strndup(&text[start], end - start); } static gint find_regex(ScintillaObject *sci, guint pos, GRegex *regex, gboolean multiline, GeanyMatchInfo *match) { const gchar *text; GMatchInfo *minfo; guint document_length; gint ret = -1; gint offset = 0; document_length = (guint)sci_get_length(sci); if (document_length <= 0) return -1; /* skip empty documents */ g_return_val_if_fail(pos <= document_length, -1); if (multiline) { /* Warning: any SCI calls will invalidate 'text' after calling SCI_GETCHARACTERPOINTER */ text = (void*)SSM(sci, SCI_GETCHARACTERPOINTER, 0, 0); g_regex_match_full(regex, text, -1, pos, 0, &minfo, NULL); } else /* single-line mode, manually match against each line */ { gint line = sci_get_line_from_position(sci, pos); for (;;) { gint start = sci_get_position_from_line(sci, line); gint end = sci_get_line_end_position(sci, line); text = (void*)SSM(sci, SCI_GETRANGEPOINTER, start, end - start); if (g_regex_match_full(regex, text, end - start, pos - start, 0, &minfo, NULL)) { offset = start; break; } else /* not found, try next line */ { line ++; if (line >= sci_get_line_count(sci)) break; pos = sci_get_position_from_line(sci, line); /* don't free last info, it's freed below */ g_match_info_free(minfo); } } } /* Warning: minfo will become invalid when 'text' does! */ if (g_match_info_matches(minfo)) { guint i; /* copy whole match text and offsets before they become invalid */ SETPTR(match->match_text, g_match_info_fetch(minfo, 0)); foreach_range(i, G_N_ELEMENTS(match->matches)) { gint start = -1, end = -1; g_match_info_fetch_pos(minfo, (gint)i, &start, &end); match->matches[i].start = offset + start; match->matches[i].end = offset + end; } match->start = match->matches[0].start; match->end = match->matches[0].end; ret = match->start; } g_match_info_free(minfo); return ret; } static gint geany_find_flags_to_sci_flags(GeanyFindFlags flags) { g_warn_if_fail(! (flags & GEANY_FIND_REGEXP) || ! (flags & GEANY_FIND_MULTILINE)); return ((flags & GEANY_FIND_MATCHCASE) ? SCFIND_MATCHCASE : 0) | ((flags & GEANY_FIND_WHOLEWORD) ? SCFIND_WHOLEWORD : 0) | ((flags & GEANY_FIND_REGEXP) ? SCFIND_REGEXP | SCFIND_POSIX : 0) | ((flags & GEANY_FIND_WORDSTART) ? SCFIND_WORDSTART : 0); } gint search_find_prev(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_) { gint ret; g_return_val_if_fail(! (flags & GEANY_FIND_REGEXP), -1); ret = sci_search_prev(sci, geany_find_flags_to_sci_flags(flags), str); if (ret != -1 && match_) *match_ = match_info_new(flags, ret, ret + strlen(str)); return ret; } gint search_find_next(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_) { GeanyMatchInfo *match; GRegex *regex; gint ret = -1; gint pos; if (~flags & GEANY_FIND_REGEXP) { ret = sci_search_next(sci, geany_find_flags_to_sci_flags(flags), str); if (ret != -1 && match_) *match_ = match_info_new(flags, ret, ret + strlen(str)); return ret; } regex = compile_regex(str, flags); if (!regex) return -1; match = match_info_new(flags, 0, 0); pos = sci_get_current_position(sci); ret = find_regex(sci, pos, regex, flags & GEANY_FIND_MULTILINE, match); /* avoid re-matching the same position in case of empty matches */ if (ret == pos && match->matches[0].start == match->matches[0].end) ret = find_regex(sci, pos + 1, regex, flags & GEANY_FIND_MULTILINE, match); if (ret >= 0) sci_set_selection(sci, match->start, match->end); if (ret != -1 && match_) *match_ = match; else geany_match_info_free(match); g_regex_unref(regex); return ret; } gint search_replace_match(ScintillaObject *sci, const GeanyMatchInfo *match, const gchar *replace_text) { GString *str; gint ret = 0; gint i = 0; sci_set_target_start(sci, match->start); sci_set_target_end(sci, match->end); if (! (match->flags & GEANY_FIND_REGEXP)) return sci_replace_target(sci, replace_text, FALSE); str = g_string_new(replace_text); while (str->str[i]) { gchar *ptr = &str->str[i]; gchar *grp; gchar c; if (ptr[0] != '\\') { i++; continue; } c = ptr[1]; /* backslash or unnecessary escape */ if (c == '\\' || !isdigit(c)) { g_string_erase(str, i, 1); i++; continue; } /* digit escape */ g_string_erase(str, i, 2); /* fix match offsets by subtracting index of whole match start from the string */ grp = get_regex_match_string(match->match_text - match->matches[0].start, match, c - '0'); g_string_insert(str, i, grp); i += strlen(grp); g_free(grp); } ret = sci_replace_target(sci, str->str, FALSE); g_string_free(str, TRUE); return ret; } gint search_find_text(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf, GeanyMatchInfo **match_) { GeanyMatchInfo *match = NULL; GRegex *regex; gint ret; if (~flags & GEANY_FIND_REGEXP) { ret = sci_find_text(sci, geany_find_flags_to_sci_flags(flags), ttf); if (ret != -1 && match_) *match_ = match_info_new(flags, ttf->chrgText.cpMin, ttf->chrgText.cpMax); return ret; } regex = compile_regex(ttf->lpstrText, flags); if (!regex) return -1; match = match_info_new(flags, 0, 0); ret = find_regex(sci, ttf->chrg.cpMin, regex, flags & GEANY_FIND_MULTILINE, match); if (ret >= ttf->chrg.cpMax) ret = -1; else if (ret >= 0) { ttf->chrgText.cpMin = match->start; ttf->chrgText.cpMax = match->end; } if (ret != -1 && match_) *match_ = match; else geany_match_info_free(match); g_regex_unref(regex); return ret; } static gint find_document_usage(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags) { gchar *buffer, *short_file_name; struct Sci_TextToFind ttf; gint count = 0; gint prev_line = -1; GSList *match, *matches; g_return_val_if_fail(DOC_VALID(doc), 0); short_file_name = g_path_get_basename(DOC_FILENAME(doc)); ttf.chrg.cpMin = 0; ttf.chrg.cpMax = sci_get_length(doc->editor->sci); ttf.lpstrText = (gchar *)search_text; matches = find_range(doc->editor->sci, flags, &ttf); foreach_slist (match, matches) { GeanyMatchInfo *info = match->data; gint line = sci_get_line_from_position(doc->editor->sci, info->start); if (line != prev_line) { buffer = sci_get_line(doc->editor->sci, line); msgwin_msg_add(COLOR_BLACK, line + 1, doc, "%s:%d: %s", short_file_name, line + 1, g_strstrip(buffer)); g_free(buffer); prev_line = line; } count++; geany_match_info_free(info); } g_slist_free(matches); g_free(short_file_name); return count; } void search_find_usage(const gchar *search_text, const gchar *original_search_text, GeanyFindFlags flags, gboolean in_session) { GeanyDocument *doc; gint count = 0; doc = document_get_current(); g_return_if_fail(doc != NULL); if (G_UNLIKELY(EMPTY(search_text))) { utils_beep(); return; } gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE); gtk_list_store_clear(msgwindow.store_msg); if (! in_session) { /* use current document */ count = find_document_usage(doc, search_text, flags); } else { guint i; for (i = 0; i < documents_array->len; i++) { if (documents[i]->is_valid) { count += find_document_usage(documents[i], search_text, flags); } } } if (count == 0) /* no matches were found */ { ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_search_text); msgwin_msg_add(COLOR_BLUE, -1, NULL, _("No matches found for \"%s\"."), original_search_text); } else { ui_set_statusbar(FALSE, ngettext( "Found %d match for \"%s\".", "Found %d matches for \"%s\".", count), count, original_search_text); msgwin_msg_add(COLOR_BLUE, -1, NULL, ngettext( "Found %d match for \"%s\".", "Found %d matches for \"%s\".", count), count, original_search_text); } } /* ttf is updated to include the last match position (ttf->chrg.cpMin) and * the new search range end (ttf->chrg.cpMax). * Note: Normally you would call sci_start/end_undo_action() around this call. */ guint search_replace_range(ScintillaObject *sci, struct Sci_TextToFind *ttf, GeanyFindFlags flags, const gchar *replace_text) { gint count = 0; gint offset = 0; /* difference between search pos and replace pos */ GSList *match, *matches; g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL && replace_text != NULL, 0); if (! *ttf->lpstrText) return 0; matches = find_range(sci, flags, ttf); foreach_slist (match, matches) { GeanyMatchInfo *info = match->data; gint replace_len; info->start += offset; info->end += offset; replace_len = search_replace_match(sci, info, replace_text); offset += replace_len - (info->end - info->start); count ++; /* on last match, update the last match/new range end */ if (! match->next) { ttf->chrg.cpMin = info->start; ttf->chrg.cpMax += offset; } geany_match_info_free(info); } g_slist_free(matches); return count; } void search_find_again(gboolean change_direction) { GeanyDocument *doc = document_get_current(); g_return_if_fail(doc != NULL); if (search_data.text) { gboolean forward = ! search_data.backwards; gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags, change_direction ? forward : !forward, NULL, FALSE, NULL); if (result > -1) editor_display_current_line(doc->editor, 0.3F); if (search_data.search_bar) ui_set_search_entry_background( toolbar_get_widget_child_by_name("SearchEntry"), (result > -1)); } }