1 /*
2  *      search.c - this file is part of Geany, a fast and lightweight IDE
3  *
4  *      Copyright 2006 The Geany contributors
5  *
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.
10  *
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.
15  *
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.
19  */
20 
21 /*
22  * Find, Replace, Find in Files dialog related functions.
23  * Note that the basic text find functions are in document.c.
24  */
25 
26 #ifdef HAVE_CONFIG_H
27 # include "config.h"
28 #endif
29 
30 #include "search.h"
31 
32 #include "app.h"
33 #include "document.h"
34 #include "encodings.h"
35 #include "encodingsprivate.h"
36 #include "keyfile.h"
37 #include "msgwindow.h"
38 #include "prefs.h"
39 #include "sciwrappers.h"
40 #include "spawn.h"
41 #include "stash.h"
42 #include "support.h"
43 #include "toolbar.h"
44 #include "ui_utils.h"
45 #include "utils.h"
46 
47 #include <unistd.h>
48 #include <string.h>
49 #include <ctype.h>
50 
51 #include <gtk/gtk.h>
52 #include <gdk/gdkkeysyms.h>
53 
54 enum
55 {
56 	GEANY_RESPONSE_FIND = 1,
57 	GEANY_RESPONSE_FIND_PREVIOUS,
58 	GEANY_RESPONSE_FIND_IN_FILE,
59 	GEANY_RESPONSE_FIND_IN_SESSION,
60 	GEANY_RESPONSE_MARK,
61 	GEANY_RESPONSE_REPLACE,
62 	GEANY_RESPONSE_REPLACE_AND_FIND,
63 	GEANY_RESPONSE_REPLACE_IN_SESSION,
64 	GEANY_RESPONSE_REPLACE_IN_FILE,
65 	GEANY_RESPONSE_REPLACE_IN_SEL
66 };
67 
68 
69 enum
70 {
71 	FILES_MODE_ALL,
72 	FILES_MODE_PROJECT,
73 	FILES_MODE_CUSTOM
74 };
75 
76 
77 GeanySearchData search_data;
78 GeanySearchPrefs search_prefs;
79 
80 
81 static struct
82 {
83 	gboolean fif_regexp;
84 	gboolean fif_case_sensitive;
85 	gboolean fif_match_whole_word;
86 	gboolean fif_invert_results;
87 	gboolean fif_recursive;
88 	gboolean fif_use_extra_options;
89 	gchar *fif_extra_options;
90 	gint fif_files_mode;
91 	gchar *fif_files;
92 	gboolean find_regexp;
93 	gboolean find_regexp_multiline;
94 	gboolean find_escape_sequences;
95 	gboolean find_case_sensitive;
96 	gboolean find_match_whole_word;
97 	gboolean find_match_word_start;
98 	gboolean find_close_dialog;
99 	gboolean replace_regexp;
100 	gboolean replace_regexp_multiline;
101 	gboolean replace_escape_sequences;
102 	gboolean replace_case_sensitive;
103 	gboolean replace_match_whole_word;
104 	gboolean replace_match_word_start;
105 	gboolean replace_search_backwards;
106 	gboolean replace_close_dialog;
107 }
108 settings;
109 
110 static StashGroup *fif_prefs = NULL;
111 static StashGroup *find_prefs = NULL;
112 static StashGroup *replace_prefs = NULL;
113 
114 
115 static struct
116 {
117 	GtkWidget	*dialog;
118 	GtkWidget	*entry;
119 	gboolean	all_expanded;
120 	gint		position[2]; /* x, y */
121 }
122 find_dlg = {NULL, NULL, FALSE, {0, 0}};
123 
124 static struct
125 {
126 	GtkWidget	*dialog;
127 	GtkWidget	*find_combobox;
128 	GtkWidget	*find_entry;
129 	GtkWidget	*replace_combobox;
130 	GtkWidget	*replace_entry;
131 	gboolean	all_expanded;
132 	gint		position[2]; /* x, y */
133 }
134 replace_dlg = {NULL, NULL, NULL, NULL, NULL, FALSE, {0, 0}};
135 
136 static struct
137 {
138 	GtkWidget	*dialog;
139 	GtkWidget	*dir_combo;
140 	GtkWidget	*files_combo;
141 	GtkWidget	*search_combo;
142 	GtkWidget	*encoding_combo;
143 	GtkWidget	*files_mode_combo;
144 	gint		position[2]; /* x, y */
145 }
146 fif_dlg = {NULL, NULL, NULL, NULL, NULL, NULL, {0, 0}};
147 
148 
149 static void search_read_io(GString *string, GIOCondition condition, gpointer data);
150 static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data);
151 
152 static void search_finished(GPid child_pid, gint status, gpointer user_data);
153 
154 static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir);
155 
156 static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags);
157 
158 
159 static void
160 on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data);
161 
162 static void
163 on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
164 
165 static void
166 on_find_entry_activate(GtkEntry *entry, gpointer user_data);
167 
168 static void
169 on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data);
170 
171 static void
172 on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
173 
174 static void
175 on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data);
176 
177 static void
178 on_replace_entry_activate(GtkEntry *entry, gpointer user_data);
179 
180 static void
181 on_find_in_files_dialog_response(GtkDialog *dialog, gint response, gpointer user_data);
182 
183 static gboolean
184 search_find_in_files(const gchar *utf8_search_text, const gchar *dir, const gchar *opts,
185 	const gchar *enc);
186 
187 
init_prefs(void)188 static void init_prefs(void)
189 {
190 	StashGroup *group;
191 
192 	group = stash_group_new("search");
193 	configuration_add_pref_group(group, TRUE);
194 	stash_group_add_toggle_button(group, &search_prefs.always_wrap,
195 		"pref_search_hide_find_dialog", FALSE, "check_always_wrap_search");
196 	stash_group_add_toggle_button(group, &search_prefs.hide_find_dialog,
197 		"pref_search_always_wrap", FALSE, "check_hide_find_dialog");
198 	stash_group_add_toggle_button(group, &search_prefs.use_current_file_dir,
199 		"pref_search_current_file_dir", TRUE, "check_fif_current_dir");
200 	stash_group_add_boolean(group, &find_dlg.all_expanded, "find_all_expanded", FALSE);
201 	stash_group_add_boolean(group, &replace_dlg.all_expanded, "replace_all_expanded", FALSE);
202 	/* dialog positions */
203 	stash_group_add_integer(group, &find_dlg.position[0], "position_find_x", -1);
204 	stash_group_add_integer(group, &find_dlg.position[1], "position_find_y", -1);
205 	stash_group_add_integer(group, &replace_dlg.position[0], "position_replace_x", -1);
206 	stash_group_add_integer(group, &replace_dlg.position[1], "position_replace_y", -1);
207 	stash_group_add_integer(group, &fif_dlg.position[0], "position_fif_x", -1);
208 	stash_group_add_integer(group, &fif_dlg.position[1], "position_fif_y", -1);
209 
210 	memset(&settings, '\0', sizeof(settings));
211 
212 	group = stash_group_new("search");
213 	fif_prefs = group;
214 	configuration_add_pref_group(group, FALSE);
215 	stash_group_add_toggle_button(group, &settings.fif_regexp,
216 		"fif_regexp", FALSE, "check_regexp");
217 	stash_group_add_toggle_button(group, &settings.fif_case_sensitive,
218 		"fif_case_sensitive", TRUE, "check_case");
219 	stash_group_add_toggle_button(group, &settings.fif_match_whole_word,
220 		"fif_match_whole_word", FALSE, "check_wholeword");
221 	stash_group_add_toggle_button(group, &settings.fif_invert_results,
222 		"fif_invert_results", FALSE, "check_invert");
223 	stash_group_add_toggle_button(group, &settings.fif_recursive,
224 		"fif_recursive", FALSE, "check_recursive");
225 	stash_group_add_entry(group, &settings.fif_extra_options,
226 		"fif_extra_options", "", "entry_extra");
227 	stash_group_add_toggle_button(group, &settings.fif_use_extra_options,
228 		"fif_use_extra_options", FALSE, "check_extra");
229 	stash_group_add_entry(group, &settings.fif_files,
230 		"fif_files", "", "entry_files");
231 	stash_group_add_combo_box(group, &settings.fif_files_mode,
232 		"fif_files_mode", FILES_MODE_ALL, "combo_files_mode");
233 
234 	group = stash_group_new("search");
235 	find_prefs = group;
236 	configuration_add_pref_group(group, FALSE);
237 	stash_group_add_toggle_button(group, &settings.find_regexp,
238 		"find_regexp", FALSE, "check_regexp");
239 	stash_group_add_toggle_button(group, &settings.find_regexp_multiline,
240 		"find_regexp_multiline", FALSE, "check_multiline");
241 	stash_group_add_toggle_button(group, &settings.find_case_sensitive,
242 		"find_case_sensitive", FALSE, "check_case");
243 	stash_group_add_toggle_button(group, &settings.find_escape_sequences,
244 		"find_escape_sequences", FALSE, "check_escape");
245 	stash_group_add_toggle_button(group, &settings.find_match_whole_word,
246 		"find_match_whole_word", FALSE, "check_word");
247 	stash_group_add_toggle_button(group, &settings.find_match_word_start,
248 		"find_match_word_start", FALSE, "check_wordstart");
249 	stash_group_add_toggle_button(group, &settings.find_close_dialog,
250 		"find_close_dialog", TRUE, "check_close");
251 
252 	group = stash_group_new("search");
253 	replace_prefs = group;
254 	configuration_add_pref_group(group, FALSE);
255 	stash_group_add_toggle_button(group, &settings.replace_regexp,
256 		"replace_regexp", FALSE, "check_regexp");
257 	stash_group_add_toggle_button(group, &settings.replace_regexp_multiline,
258 		"replace_regexp_multiline", FALSE, "check_multiline");
259 	stash_group_add_toggle_button(group, &settings.replace_case_sensitive,
260 		"replace_case_sensitive", FALSE, "check_case");
261 	stash_group_add_toggle_button(group, &settings.replace_escape_sequences,
262 		"replace_escape_sequences", FALSE, "check_escape");
263 	stash_group_add_toggle_button(group, &settings.replace_match_whole_word,
264 		"replace_match_whole_word", FALSE, "check_word");
265 	stash_group_add_toggle_button(group, &settings.replace_match_word_start,
266 		"replace_match_word_start", FALSE, "check_wordstart");
267 	stash_group_add_toggle_button(group, &settings.replace_search_backwards,
268 		"replace_search_backwards", FALSE, "check_back");
269 	stash_group_add_toggle_button(group, &settings.replace_close_dialog,
270 		"replace_close_dialog", TRUE, "check_close");
271 }
272 
273 
search_init(void)274 void search_init(void)
275 {
276 	search_data.text = NULL;
277 	search_data.original_text = NULL;
278 	init_prefs();
279 }
280 
281 
282 #define FREE_WIDGET(wid) \
283 	if (wid && GTK_IS_WIDGET(wid)) gtk_widget_destroy(wid);
284 
search_finalize(void)285 void search_finalize(void)
286 {
287 	FREE_WIDGET(find_dlg.dialog);
288 	FREE_WIDGET(replace_dlg.dialog);
289 	FREE_WIDGET(fif_dlg.dialog);
290 	g_free(search_data.text);
291 	g_free(search_data.original_text);
292 }
293 
294 
on_widget_toggled_set_insensitive(GtkToggleButton * togglebutton,gpointer user_data)295 static void on_widget_toggled_set_insensitive(
296 	GtkToggleButton *togglebutton, gpointer user_data)
297 {
298 	gtk_widget_set_sensitive(GTK_WIDGET(user_data),
299 		!gtk_toggle_button_get_active(togglebutton));
300 }
301 
302 
add_find_checkboxes(GtkDialog * dialog)303 static GtkWidget *add_find_checkboxes(GtkDialog *dialog)
304 {
305 	GtkWidget *checkbox1, *checkbox2, *check_regexp, *checkbox5,
306 			  *checkbox7, *check_multiline, *hbox, *fbox, *mbox;
307 
308 	check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions"));
309 	ui_hookup_widget(dialog, check_regexp, "check_regexp");
310 	gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE);
311 	gtk_widget_set_tooltip_text(check_regexp, _("Use Perl-like regular expressions. "
312 		"For detailed information about using regular expressions, please refer to the manual."));
313 	g_signal_connect(check_regexp, "toggled",
314 		G_CALLBACK(on_find_replace_checkbutton_toggled), dialog);
315 
316 	checkbox7 = gtk_check_button_new_with_mnemonic(_("Use _escape sequences"));
317 	ui_hookup_widget(dialog, checkbox7, "check_escape");
318 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox7), FALSE);
319 	gtk_widget_set_tooltip_text(checkbox7,
320 		_("Replace \\\\, \\t, \\n, \\r and \\uXXXX (Unicode characters) with the "
321 		  "corresponding control characters"));
322 
323 	check_multiline = gtk_check_button_new_with_mnemonic(_("Use multi-line matchin_g"));
324 	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(check_multiline), FALSE);
325 	gtk_widget_set_sensitive(check_multiline, FALSE);
326 	ui_hookup_widget(dialog, check_multiline, "check_multiline");
327 	gtk_button_set_focus_on_click(GTK_BUTTON(check_multiline), FALSE);
328 	gtk_widget_set_tooltip_text(check_multiline, _("Perform regular expression "
329 		"matching on the whole buffer at once rather than line by line, allowing "
330 		"matches to span multiple lines. In this mode, newline characters are part "
331 		"of the input and can be captured as normal characters by the pattern."));
332 
333 	/* Search features */
334 	fbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
335 	gtk_box_pack_start(GTK_BOX(fbox), check_regexp, FALSE, FALSE, 0);
336 	gtk_box_pack_start(GTK_BOX(fbox), check_multiline, FALSE, FALSE, 0);
337 	gtk_box_pack_start(GTK_BOX(fbox), checkbox7, FALSE, FALSE, 0);
338 
339 	if (dialog != GTK_DIALOG(find_dlg.dialog))
340 	{
341 		GtkWidget *check_back = gtk_check_button_new_with_mnemonic(_("Search _backwards"));
342 		ui_hookup_widget(dialog, check_back, "check_back");
343 		gtk_button_set_focus_on_click(GTK_BUTTON(check_back), FALSE);
344 		gtk_container_add(GTK_CONTAINER(fbox), check_back);
345 	}
346 
347 	checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive"));
348 	ui_hookup_widget(dialog, checkbox1, "check_case");
349 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE);
350 
351 	checkbox2 = gtk_check_button_new_with_mnemonic(_("Match only a _whole word"));
352 	ui_hookup_widget(dialog, checkbox2, "check_word");
353 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE);
354 
355 	checkbox5 = gtk_check_button_new_with_mnemonic(_("Match from s_tart of word"));
356 	ui_hookup_widget(dialog, checkbox5, "check_wordstart");
357 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox5), FALSE);
358 
359 	/* disable wordstart when wholeword is checked */
360 	g_signal_connect(checkbox2, "toggled",
361 		G_CALLBACK(on_widget_toggled_set_insensitive), checkbox5);
362 
363 	/* Matching options */
364 	mbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
365 	gtk_box_pack_start(GTK_BOX(mbox), checkbox1, FALSE, FALSE, 0);
366 	gtk_box_pack_start(GTK_BOX(mbox), checkbox2, FALSE, FALSE, 0);
367 	gtk_box_pack_start(GTK_BOX(mbox), checkbox5, FALSE, FALSE, 0);
368 
369 	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
370 	gtk_box_set_homogeneous(GTK_BOX(hbox), TRUE);
371 	gtk_container_add(GTK_CONTAINER(hbox), fbox);
372 	gtk_container_add(GTK_CONTAINER(hbox), mbox);
373 	return hbox;
374 }
375 
376 
send_find_dialog_response(GtkButton * button,gpointer user_data)377 static void send_find_dialog_response(GtkButton *button, gpointer user_data)
378 {
379 	gtk_dialog_response(GTK_DIALOG(find_dlg.dialog), GPOINTER_TO_INT(user_data));
380 }
381 
382 
383 /* store text, clear search flags so we can use Search->Find Next/Previous */
setup_find_next(const gchar * text)384 static void setup_find_next(const gchar *text)
385 {
386 	g_free(search_data.text);
387 	g_free(search_data.original_text);
388 	search_data.text = g_strdup(text);
389 	search_data.original_text = g_strdup(text);
390 	search_data.flags = 0;
391 	search_data.backwards = FALSE;
392 	search_data.search_bar = FALSE;
393 }
394 
395 
396 /* Search for next match of the current "selection".
397  * Optionally for X11 based systems, this will try to use the system-wide
398  * x-selection first.
399  * If it doesn't find a suitable search string it will try to use
400  * the current word instead, or just repeat the last search.
401  * Search flags are always zero.
402  */
search_find_selection(GeanyDocument * doc,gboolean search_backwards)403 void search_find_selection(GeanyDocument *doc, gboolean search_backwards)
404 {
405 	gchar *s = NULL;
406 
407 	g_return_if_fail(DOC_VALID(doc));
408 
409 #ifdef G_OS_UNIX
410 	if (search_prefs.find_selection_type == GEANY_FIND_SEL_X)
411 	{
412 		GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_PRIMARY);
413 
414 		s = gtk_clipboard_wait_for_text(clipboard);
415 		if (s && (strchr(s,'\n') || strchr(s, '\r')))
416 		{
417 			g_free(s);
418 			s = NULL;
419 		};
420 	}
421 #endif
422 
423 	if (!s && sci_has_selection(doc->editor->sci))
424 		s = sci_get_selection_contents(doc->editor->sci);
425 
426 	if (!s && search_prefs.find_selection_type != GEANY_FIND_SEL_AGAIN)
427 	{
428 		/* get the current word */
429 		s = editor_get_default_selection(doc->editor, TRUE, NULL);
430 	}
431 
432 	if (s)
433 	{
434 		setup_find_next(s);	/* allow find next/prev */
435 
436 		if (document_find_text(doc, s, NULL, 0, search_backwards, NULL, FALSE, NULL) > -1)
437 			editor_display_current_line(doc->editor, 0.3F);
438 		g_free(s);
439 	}
440 	else if (search_prefs.find_selection_type == GEANY_FIND_SEL_AGAIN)
441 	{
442 		/* Repeat last search (in case selection was lost) */
443 		search_find_again(search_backwards);
444 	}
445 	else
446 	{
447 		utils_beep();
448 	}
449 }
450 
451 
on_expander_activated(GtkExpander * exp,gpointer data)452 static void on_expander_activated(GtkExpander *exp, gpointer data)
453 {
454 	gboolean *setting = data;
455 
456 	*setting = gtk_expander_get_expanded(exp);
457 }
458 
459 
create_find_dialog(void)460 static void create_find_dialog(void)
461 {
462 	GtkWidget *label, *entry, *sbox, *vbox;
463 	GtkWidget *exp, *bbox, *button, *check_close;
464 
465 	find_dlg.dialog = gtk_dialog_new_with_buttons(_("Find"),
466 		GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
467 		GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL);
468 	vbox = ui_dialog_vbox_new(GTK_DIALOG(find_dlg.dialog));
469 	gtk_widget_set_name(find_dlg.dialog, "GeanyDialogSearch");
470 	gtk_box_set_spacing(GTK_BOX(vbox), 9);
471 
472 	button = ui_button_new_with_image(GTK_STOCK_GO_BACK, _("_Previous"));
473 	gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button,
474 		GEANY_RESPONSE_FIND_PREVIOUS);
475 	ui_hookup_widget(find_dlg.dialog, button, "btn_previous");
476 
477 	button = ui_button_new_with_image(GTK_STOCK_GO_FORWARD, _("_Next"));
478 	gtk_dialog_add_action_widget(GTK_DIALOG(find_dlg.dialog), button,
479 		GEANY_RESPONSE_FIND);
480 
481 	label = gtk_label_new_with_mnemonic(_("_Search for:"));
482 	gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
483 
484 	entry = gtk_combo_box_text_new_with_entry();
485 	ui_entry_add_clear_icon(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))));
486 	gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry);
487 	gtk_entry_set_width_chars(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))), 50);
488 	find_dlg.entry = gtk_bin_get_child(GTK_BIN(entry));
489 
490 	g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate",
491 			G_CALLBACK(on_find_entry_activate), entry);
492 	ui_entry_add_activate_backward_signal(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(entry))));
493 	g_signal_connect(gtk_bin_get_child(GTK_BIN(entry)), "activate-backward",
494 			G_CALLBACK(on_find_entry_activate_backward), entry);
495 	g_signal_connect(find_dlg.dialog, "response",
496 			G_CALLBACK(on_find_dialog_response), entry);
497 	g_signal_connect(find_dlg.dialog, "delete-event",
498 			G_CALLBACK(gtk_widget_hide_on_delete), NULL);
499 
500 	sbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
501 	gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0);
502 	gtk_box_pack_start(GTK_BOX(sbox), entry, TRUE, TRUE, 0);
503 	gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0);
504 
505 	gtk_container_add(GTK_CONTAINER(vbox),
506 		add_find_checkboxes(GTK_DIALOG(find_dlg.dialog)));
507 
508 	/* Now add the multiple match options */
509 	exp = gtk_expander_new_with_mnemonic(_("_Find All"));
510 	gtk_expander_set_expanded(GTK_EXPANDER(exp), find_dlg.all_expanded);
511 	g_signal_connect_after(exp, "activate",
512 		G_CALLBACK(on_expander_activated), &find_dlg.all_expanded);
513 
514 	bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
515 
516 	button = gtk_button_new_with_mnemonic(_("_Mark"));
517 	gtk_widget_set_tooltip_text(button,
518 			_("Mark all matches in the current document"));
519 	gtk_container_add(GTK_CONTAINER(bbox), button);
520 	g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
521 		GINT_TO_POINTER(GEANY_RESPONSE_MARK));
522 
523 	button = gtk_button_new_with_mnemonic(_("In Sessi_on"));
524 	gtk_container_add(GTK_CONTAINER(bbox), button);
525 	g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
526 		GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_SESSION));
527 
528 	button = gtk_button_new_with_mnemonic(_("_In Document"));
529 	gtk_container_add(GTK_CONTAINER(bbox), button);
530 	g_signal_connect(button, "clicked", G_CALLBACK(send_find_dialog_response),
531 		GINT_TO_POINTER(GEANY_RESPONSE_FIND_IN_FILE));
532 
533 	/* close window checkbox */
534 	check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog"));
535 	ui_hookup_widget(find_dlg.dialog, check_close, "check_close");
536 	gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE);
537 	gtk_widget_set_tooltip_text(check_close,
538 			_("Disable this option to keep the dialog open"));
539 	gtk_container_add(GTK_CONTAINER(bbox), check_close);
540 	gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE);
541 
542 	ui_hbutton_box_copy_layout(
543 		GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(find_dlg.dialog))),
544 		GTK_BUTTON_BOX(bbox));
545 	gtk_container_add(GTK_CONTAINER(exp), bbox);
546 	gtk_container_add(GTK_CONTAINER(vbox), exp);
547 }
548 
549 
set_dialog_position(GtkWidget * dialog,gint * position)550 static void set_dialog_position(GtkWidget *dialog, gint *position)
551 {
552 	if (position[0] >= 0)
553 		gtk_window_move(GTK_WINDOW(dialog), position[0], position[1]);
554 }
555 
556 
search_show_find_dialog(void)557 void search_show_find_dialog(void)
558 {
559 	GeanyDocument *doc = document_get_current();
560 	gchar *sel = NULL;
561 
562 	g_return_if_fail(doc != NULL);
563 
564 	sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
565 
566 	if (find_dlg.dialog == NULL)
567 	{
568 		create_find_dialog();
569 		stash_group_display(find_prefs, find_dlg.dialog);
570 		if (sel)
571 			gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel);
572 
573 		set_dialog_position(find_dlg.dialog, find_dlg.position);
574 		gtk_widget_show_all(find_dlg.dialog);
575 	}
576 	else
577 	{
578 		if (sel != NULL)
579 		{
580 			/* update the search text from current selection */
581 			gtk_entry_set_text(GTK_ENTRY(find_dlg.entry), sel);
582 			/* reset the entry widget's background colour */
583 			ui_set_search_entry_background(find_dlg.entry, TRUE);
584 		}
585 		gtk_widget_grab_focus(find_dlg.entry);
586 		set_dialog_position(find_dlg.dialog, find_dlg.position);
587 		gtk_widget_show(find_dlg.dialog);
588 		/* bring the dialog back in the foreground in case it is already open but the focus is away */
589 		gtk_window_present(GTK_WINDOW(find_dlg.dialog));
590 	}
591 
592 	g_free(sel);
593 }
594 
595 
send_replace_dialog_response(GtkButton * button,gpointer user_data)596 static void send_replace_dialog_response(GtkButton *button, gpointer user_data)
597 {
598 	gtk_dialog_response(GTK_DIALOG(replace_dlg.dialog), GPOINTER_TO_INT(user_data));
599 }
600 
601 
602 static gboolean
on_widget_key_pressed_set_focus(GtkWidget * widget,GdkEventKey * event,gpointer user_data)603 on_widget_key_pressed_set_focus(GtkWidget *widget, GdkEventKey *event, gpointer user_data)
604 {
605 	if (event->keyval == GDK_KEY_Tab)
606 	{
607 		gtk_widget_grab_focus(GTK_WIDGET(user_data));
608 		return TRUE;
609 	}
610 	return FALSE;
611 }
612 
613 
create_replace_dialog(void)614 static void create_replace_dialog(void)
615 {
616 	GtkWidget *label_find, *label_replace,
617 		*check_close, *button, *rbox, *fbox, *vbox, *exp, *bbox;
618 	GtkSizeGroup *label_size;
619 
620 	replace_dlg.dialog = gtk_dialog_new_with_buttons(_("Replace"),
621 		GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
622 		GTK_STOCK_CLOSE, GTK_RESPONSE_CANCEL, NULL);
623 	vbox = ui_dialog_vbox_new(GTK_DIALOG(replace_dlg.dialog));
624 	gtk_box_set_spacing(GTK_BOX(vbox), 9);
625 	gtk_widget_set_name(replace_dlg.dialog, "GeanyDialogSearch");
626 
627 	button = gtk_button_new_from_stock(GTK_STOCK_FIND);
628 	gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
629 		GEANY_RESPONSE_FIND);
630 	button = gtk_button_new_with_mnemonic(_("_Replace"));
631 	gtk_button_set_image(GTK_BUTTON(button),
632 		gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON));
633 	gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
634 		GEANY_RESPONSE_REPLACE);
635 	button = gtk_button_new_with_mnemonic(_("Replace & Fi_nd"));
636 	gtk_button_set_image(GTK_BUTTON(button),
637 		gtk_image_new_from_stock(GTK_STOCK_FIND_AND_REPLACE, GTK_ICON_SIZE_BUTTON));
638 	gtk_dialog_add_action_widget(GTK_DIALOG(replace_dlg.dialog), button,
639 		GEANY_RESPONSE_REPLACE_AND_FIND);
640 
641 	label_find = gtk_label_new_with_mnemonic(_("_Search for:"));
642 	gtk_misc_set_alignment(GTK_MISC(label_find), 0, 0.5);
643 
644 	label_replace = gtk_label_new_with_mnemonic(_("Replace wit_h:"));
645 	gtk_misc_set_alignment(GTK_MISC(label_replace), 0, 0.5);
646 
647 	replace_dlg.find_combobox = gtk_combo_box_text_new_with_entry();
648 	replace_dlg.find_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.find_combobox));
649 	ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.find_entry));
650 	gtk_label_set_mnemonic_widget(GTK_LABEL(label_find), replace_dlg.find_combobox);
651 	gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.find_entry), 50);
652 	ui_hookup_widget(replace_dlg.dialog, replace_dlg.find_combobox, "entry_find");
653 
654 	replace_dlg.replace_combobox = gtk_combo_box_text_new_with_entry();
655 	replace_dlg.replace_entry = gtk_bin_get_child(GTK_BIN(replace_dlg.replace_combobox));
656 	ui_entry_add_clear_icon(GTK_ENTRY(replace_dlg.replace_entry));
657 	gtk_label_set_mnemonic_widget(GTK_LABEL(label_replace), replace_dlg.replace_combobox);
658 	gtk_entry_set_width_chars(GTK_ENTRY(replace_dlg.replace_entry), 50);
659 	ui_hookup_widget(replace_dlg.dialog, replace_dlg.replace_combobox, "entry_replace");
660 
661 	/* tab from find to the replace entry */
662 	g_signal_connect(replace_dlg.find_entry,
663 			"key-press-event", G_CALLBACK(on_widget_key_pressed_set_focus),
664 			replace_dlg.replace_entry);
665 	g_signal_connect(replace_dlg.find_entry, "activate",
666 			G_CALLBACK(on_replace_find_entry_activate), NULL);
667 	g_signal_connect(replace_dlg.replace_entry, "activate",
668 			G_CALLBACK(on_replace_entry_activate), NULL);
669 	g_signal_connect(replace_dlg.dialog, "response",
670 			G_CALLBACK(on_replace_dialog_response), NULL);
671 	g_signal_connect(replace_dlg.dialog, "delete-event",
672 			G_CALLBACK(gtk_widget_hide_on_delete), NULL);
673 
674 	fbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
675 	gtk_box_pack_start(GTK_BOX(fbox), label_find, FALSE, FALSE, 0);
676 	gtk_box_pack_start(GTK_BOX(fbox), replace_dlg.find_combobox, TRUE, TRUE, 0);
677 
678 	rbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
679 	gtk_box_pack_start(GTK_BOX(rbox), label_replace, FALSE, FALSE, 0);
680 	gtk_box_pack_start(GTK_BOX(rbox), replace_dlg.replace_combobox, TRUE, TRUE, 0);
681 
682 	label_size = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
683 	gtk_size_group_add_widget(label_size, label_find);
684 	gtk_size_group_add_widget(label_size, label_replace);
685 	g_object_unref(G_OBJECT(label_size));	/* auto destroy the size group */
686 
687 	gtk_box_pack_start(GTK_BOX(vbox), fbox, TRUE, FALSE, 0);
688 	gtk_box_pack_start(GTK_BOX(vbox), rbox, TRUE, FALSE, 0);
689 	gtk_container_add(GTK_CONTAINER(vbox),
690 		add_find_checkboxes(GTK_DIALOG(replace_dlg.dialog)));
691 
692 	/* Now add the multiple replace options */
693 	exp = gtk_expander_new_with_mnemonic(_("Re_place All"));
694 	gtk_expander_set_expanded(GTK_EXPANDER(exp), replace_dlg.all_expanded);
695 	g_signal_connect_after(exp, "activate",
696 		G_CALLBACK(on_expander_activated), &replace_dlg.all_expanded);
697 
698 	bbox = gtk_button_box_new(GTK_ORIENTATION_HORIZONTAL);
699 
700 	button = gtk_button_new_with_mnemonic(_("In Sessi_on"));
701 	gtk_container_add(GTK_CONTAINER(bbox), button);
702 	g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
703 		GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SESSION));
704 
705 	button = gtk_button_new_with_mnemonic(_("_In Document"));
706 	gtk_container_add(GTK_CONTAINER(bbox), button);
707 	g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
708 		GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_FILE));
709 
710 	button = gtk_button_new_with_mnemonic(_("In Se_lection"));
711 	gtk_widget_set_tooltip_text(button,
712 		_("Replace all matches found in the currently selected text"));
713 	gtk_container_add(GTK_CONTAINER(bbox), button);
714 	g_signal_connect(button, "clicked", G_CALLBACK(send_replace_dialog_response),
715 		GINT_TO_POINTER(GEANY_RESPONSE_REPLACE_IN_SEL));
716 
717 	/* close window checkbox */
718 	check_close = gtk_check_button_new_with_mnemonic(_("Close _dialog"));
719 	ui_hookup_widget(replace_dlg.dialog, check_close, "check_close");
720 	gtk_button_set_focus_on_click(GTK_BUTTON(check_close), FALSE);
721 	gtk_widget_set_tooltip_text(check_close,
722 			_("Disable this option to keep the dialog open"));
723 	gtk_container_add(GTK_CONTAINER(bbox), check_close);
724 	gtk_button_box_set_child_secondary(GTK_BUTTON_BOX(bbox), check_close, TRUE);
725 
726 	ui_hbutton_box_copy_layout(
727 		GTK_BUTTON_BOX(gtk_dialog_get_action_area(GTK_DIALOG(replace_dlg.dialog))),
728 		GTK_BUTTON_BOX(bbox));
729 	gtk_container_add(GTK_CONTAINER(exp), bbox);
730 	gtk_container_add(GTK_CONTAINER(vbox), exp);
731 }
732 
733 
search_show_replace_dialog(void)734 void search_show_replace_dialog(void)
735 {
736 	GeanyDocument *doc = document_get_current();
737 	gchar *sel = NULL;
738 
739 	if (doc == NULL)
740 		return;
741 
742 	sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
743 
744 	if (replace_dlg.dialog == NULL)
745 	{
746 		create_replace_dialog();
747 		stash_group_display(replace_prefs, replace_dlg.dialog);
748 		if (sel)
749 			gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel);
750 
751 		set_dialog_position(replace_dlg.dialog, replace_dlg.position);
752 		gtk_widget_show_all(replace_dlg.dialog);
753 	}
754 	else
755 	{
756 		if (sel != NULL)
757 		{
758 			/* update the search text from current selection */
759 			gtk_entry_set_text(GTK_ENTRY(replace_dlg.find_entry), sel);
760 			/* reset the entry widget's background colour */
761 			ui_set_search_entry_background(replace_dlg.find_entry, TRUE);
762 		}
763 		gtk_widget_grab_focus(replace_dlg.find_entry);
764 		set_dialog_position(replace_dlg.dialog, replace_dlg.position);
765 		gtk_widget_show(replace_dlg.dialog);
766 		/* bring the dialog back in the foreground in case it is already open but the focus is away */
767 		gtk_window_present(GTK_WINDOW(replace_dlg.dialog));
768 	}
769 
770 	g_free(sel);
771 }
772 
773 
on_widget_toggled_set_sensitive(GtkToggleButton * togglebutton,gpointer user_data)774 static void on_widget_toggled_set_sensitive(GtkToggleButton *togglebutton, gpointer user_data)
775 {
776 	/* disable extra option entry when checkbutton not checked */
777 	gtk_widget_set_sensitive(GTK_WIDGET(user_data),
778 		gtk_toggle_button_get_active(togglebutton));
779 }
780 
781 
update_file_patterns(GtkWidget * mode_combo,GtkWidget * fcombo)782 static void update_file_patterns(GtkWidget *mode_combo, GtkWidget *fcombo)
783 {
784 	gint selection;
785 	GtkWidget *entry;
786 
787 	entry = gtk_bin_get_child(GTK_BIN(fcombo));
788 
789 	selection = gtk_combo_box_get_active(GTK_COMBO_BOX(mode_combo));
790 
791 	if (selection == FILES_MODE_ALL)
792 	{
793 		gtk_entry_set_text(GTK_ENTRY(entry), "");
794 		gtk_widget_set_sensitive(fcombo, FALSE);
795 	}
796 	else if (selection == FILES_MODE_CUSTOM)
797 	{
798 		gtk_widget_set_sensitive(fcombo, TRUE);
799 	}
800 	else if (selection == FILES_MODE_PROJECT)
801 	{
802 		if (app->project && !EMPTY(app->project->file_patterns))
803 		{
804 			gchar *patterns;
805 
806 			patterns = g_strjoinv(" ", app->project->file_patterns);
807 			gtk_entry_set_text(GTK_ENTRY(entry), patterns);
808 			g_free(patterns);
809 		}
810 		else
811 		{
812 			gtk_entry_set_text(GTK_ENTRY(entry), "");
813 		}
814 
815 		gtk_widget_set_sensitive(fcombo, FALSE);
816 	}
817 }
818 
819 
820 /* creates the combo to choose which files include in the search */
create_fif_file_mode_combo(void)821 static GtkWidget *create_fif_file_mode_combo(void)
822 {
823 	GtkWidget *combo;
824 	GtkCellRenderer *renderer;
825 	GtkListStore *store;
826 	GtkTreeIter iter;
827 
828 	/* text/sensitive */
829 	store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_BOOLEAN);
830 	gtk_list_store_append(store, &iter);
831 	gtk_list_store_set(store, &iter, 0, _("all"), 1, TRUE, -1);
832 	gtk_list_store_append(store, &iter);
833 	gtk_list_store_set(store, &iter, 0, _("project"), 1, app->project != NULL, -1);
834 	gtk_list_store_append(store, &iter);
835 	gtk_list_store_set(store, &iter, 0, _("custom"), 1, TRUE, -1);
836 
837 	combo = gtk_combo_box_new_with_model(GTK_TREE_MODEL(store));
838 	g_object_unref(store);
839 	gtk_widget_set_tooltip_text(combo, _("All: search all files in the directory\n"
840 										"Project: use file patterns defined in the project settings\n"
841 										"Custom: specify file patterns manually"));
842 
843 	renderer = gtk_cell_renderer_text_new();
844 	gtk_cell_layout_pack_start(GTK_CELL_LAYOUT(combo), renderer, TRUE);
845 	gtk_cell_layout_set_attributes(GTK_CELL_LAYOUT(combo), renderer, "text", 0, "sensitive", 1, NULL);
846 
847 	return combo;
848 }
849 
850 
851 /* updates the sensitivity of the project combo item */
update_fif_file_mode_combo(void)852 static void update_fif_file_mode_combo(void)
853 {
854 	GtkTreeModel *model = gtk_combo_box_get_model(GTK_COMBO_BOX(fif_dlg.files_mode_combo));
855 	GtkTreeIter iter;
856 
857 	/* "1" refers to the second list entry, project  */
858 	if (gtk_tree_model_get_iter_from_string(model, &iter, "1"))
859 		gtk_list_store_set(GTK_LIST_STORE(model), &iter, 1, app->project != NULL, -1);
860 }
861 
862 
create_fif_dialog(void)863 static void create_fif_dialog(void)
864 {
865 	GtkWidget *dir_combo, *combo, *fcombo, *e_combo, *entry;
866 	GtkWidget *label, *label1, *label2, *label3, *checkbox1, *checkbox2, *check_wholeword,
867 		*check_recursive, *check_extra, *entry_extra, *check_regexp, *combo_files_mode;
868 	GtkWidget *dbox, *sbox, *lbox, *rbox, *hbox, *vbox, *ebox;
869 	GtkSizeGroup *size_group;
870 
871 	fif_dlg.dialog = gtk_dialog_new_with_buttons(
872 		_("Find in Files"), GTK_WINDOW(main_widgets.window), GTK_DIALOG_DESTROY_WITH_PARENT,
873 		GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL);
874 	vbox = ui_dialog_vbox_new(GTK_DIALOG(fif_dlg.dialog));
875 	gtk_box_set_spacing(GTK_BOX(vbox), 9);
876 	gtk_widget_set_name(fif_dlg.dialog, "GeanyDialogSearch");
877 
878 	gtk_dialog_add_button(GTK_DIALOG(fif_dlg.dialog), GTK_STOCK_FIND, GTK_RESPONSE_ACCEPT);
879 	gtk_dialog_set_default_response(GTK_DIALOG(fif_dlg.dialog),
880 		GTK_RESPONSE_ACCEPT);
881 
882 	label = gtk_label_new_with_mnemonic(_("_Search for:"));
883 	gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
884 
885 	combo = gtk_combo_box_text_new_with_entry();
886 	entry = gtk_bin_get_child(GTK_BIN(combo));
887 	ui_entry_add_clear_icon(GTK_ENTRY(entry));
888 	gtk_label_set_mnemonic_widget(GTK_LABEL(label), entry);
889 	gtk_entry_set_width_chars(GTK_ENTRY(entry), 50);
890 	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
891 	fif_dlg.search_combo = combo;
892 
893 	sbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
894 	gtk_box_pack_start(GTK_BOX(sbox), label, FALSE, FALSE, 0);
895 	gtk_box_pack_start(GTK_BOX(sbox), combo, TRUE, TRUE, 0);
896 
897 	/* make labels same width */
898 	size_group = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
899 	gtk_size_group_add_widget(size_group, label);
900 
901 	label3 = gtk_label_new_with_mnemonic(_("File _patterns:"));
902 	gtk_misc_set_alignment(GTK_MISC(label3), 0, 0.5);
903 
904 	combo_files_mode = create_fif_file_mode_combo();
905 	gtk_label_set_mnemonic_widget(GTK_LABEL(label3), combo_files_mode);
906 	ui_hookup_widget(fif_dlg.dialog, combo_files_mode, "combo_files_mode");
907 	fif_dlg.files_mode_combo = combo_files_mode;
908 
909 	fcombo = gtk_combo_box_text_new_with_entry();
910 	entry = gtk_bin_get_child(GTK_BIN(fcombo));
911 	ui_entry_add_clear_icon(GTK_ENTRY(entry));
912 	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
913 	gtk_widget_set_tooltip_text(entry,
914 		_("Space separated list of file patterns (e.g. *.c *.h)"));
915 	ui_hookup_widget(fif_dlg.dialog, entry, "entry_files");
916 	fif_dlg.files_combo = fcombo;
917 
918 	/* update the entry when selection is changed */
919 	g_signal_connect(combo_files_mode, "changed", G_CALLBACK(update_file_patterns), fcombo);
920 
921 	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
922 	gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 0);
923 	gtk_box_pack_start(GTK_BOX(hbox), combo_files_mode, FALSE, FALSE, 0);
924 	gtk_box_pack_start(GTK_BOX(hbox), fcombo, TRUE, TRUE, 0);
925 
926 	label1 = gtk_label_new_with_mnemonic(_("_Directory:"));
927 	gtk_misc_set_alignment(GTK_MISC(label1), 0, 0.5);
928 
929 	dir_combo = gtk_combo_box_text_new_with_entry();
930 	entry = gtk_bin_get_child(GTK_BIN(dir_combo));
931 	ui_entry_add_clear_icon(GTK_ENTRY(entry));
932 	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
933 	gtk_label_set_mnemonic_widget(GTK_LABEL(label1), entry);
934 	gtk_entry_set_width_chars(GTK_ENTRY(entry), 50);
935 	fif_dlg.dir_combo = dir_combo;
936 
937 	/* tab from files to the dir entry */
938 	g_signal_connect(gtk_bin_get_child(GTK_BIN(fcombo)), "key-press-event",
939 		G_CALLBACK(on_widget_key_pressed_set_focus), entry);
940 
941 	dbox = ui_path_box_new(NULL, GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER,
942 		GTK_ENTRY(entry));
943 	gtk_box_pack_start(GTK_BOX(dbox), label1, FALSE, FALSE, 0);
944 
945 	label2 = gtk_label_new_with_mnemonic(_("E_ncoding:"));
946 	gtk_misc_set_alignment(GTK_MISC(label2), 0, 0.5);
947 
948 	e_combo = ui_create_encodings_combo_box(FALSE, GEANY_ENCODING_UTF_8);
949 	gtk_label_set_mnemonic_widget(GTK_LABEL(label2), e_combo);
950 	fif_dlg.encoding_combo = e_combo;
951 
952 	ebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
953 	gtk_box_pack_start(GTK_BOX(ebox), label2, FALSE, FALSE, 0);
954 	gtk_box_pack_start(GTK_BOX(ebox), e_combo, TRUE, TRUE, 0);
955 
956 	gtk_size_group_add_widget(size_group, label1);
957 	gtk_size_group_add_widget(size_group, label2);
958 	gtk_size_group_add_widget(size_group, label3);
959 	g_object_unref(G_OBJECT(size_group));	/* auto destroy the size group */
960 
961 	gtk_box_pack_start(GTK_BOX(vbox), sbox, TRUE, FALSE, 0);
962 	gtk_box_pack_start(GTK_BOX(vbox), hbox, TRUE, FALSE, 0);
963 	gtk_box_pack_start(GTK_BOX(vbox), dbox, TRUE, FALSE, 0);
964 	gtk_box_pack_start(GTK_BOX(vbox), ebox, TRUE, FALSE, 0);
965 
966 	check_regexp = gtk_check_button_new_with_mnemonic(_("_Use regular expressions"));
967 	ui_hookup_widget(fif_dlg.dialog, check_regexp, "check_regexp");
968 	gtk_button_set_focus_on_click(GTK_BUTTON(check_regexp), FALSE);
969 	gtk_widget_set_tooltip_text(check_regexp, _("See grep's manual page for more information"));
970 
971 	check_recursive = gtk_check_button_new_with_mnemonic(_("_Recurse in subfolders"));
972 	ui_hookup_widget(fif_dlg.dialog, check_recursive, "check_recursive");
973 	gtk_button_set_focus_on_click(GTK_BUTTON(check_recursive), FALSE);
974 
975 	checkbox1 = gtk_check_button_new_with_mnemonic(_("C_ase sensitive"));
976 	ui_hookup_widget(fif_dlg.dialog, checkbox1, "check_case");
977 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox1), FALSE);
978 	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(checkbox1), TRUE);
979 
980 	check_wholeword = gtk_check_button_new_with_mnemonic(_("Match only a _whole word"));
981 	ui_hookup_widget(fif_dlg.dialog, check_wholeword, "check_wholeword");
982 	gtk_button_set_focus_on_click(GTK_BUTTON(check_wholeword), FALSE);
983 
984 	checkbox2 = gtk_check_button_new_with_mnemonic(_("_Invert search results"));
985 	ui_hookup_widget(fif_dlg.dialog, checkbox2, "check_invert");
986 	gtk_button_set_focus_on_click(GTK_BUTTON(checkbox2), FALSE);
987 	gtk_widget_set_tooltip_text(checkbox2,
988 			_("Invert the sense of matching, to select non-matching lines"));
989 
990 	lbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
991 	gtk_container_add(GTK_CONTAINER(lbox), check_regexp);
992 	gtk_container_add(GTK_CONTAINER(lbox), checkbox2);
993 	gtk_container_add(GTK_CONTAINER(lbox), check_recursive);
994 
995 	rbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
996 	gtk_container_add(GTK_CONTAINER(rbox), checkbox1);
997 	gtk_container_add(GTK_CONTAINER(rbox), check_wholeword);
998 	gtk_container_add(GTK_CONTAINER(rbox), gtk_label_new(NULL));
999 
1000 	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1001 	gtk_container_add(GTK_CONTAINER(hbox), lbox);
1002 	gtk_container_add(GTK_CONTAINER(hbox), rbox);
1003 	gtk_container_add(GTK_CONTAINER(vbox), hbox);
1004 
1005 	check_extra = gtk_check_button_new_with_mnemonic(_("E_xtra options:"));
1006 	ui_hookup_widget(fif_dlg.dialog, check_extra, "check_extra");
1007 	gtk_button_set_focus_on_click(GTK_BUTTON(check_extra), FALSE);
1008 
1009 	entry_extra = gtk_entry_new();
1010 	ui_entry_add_clear_icon(GTK_ENTRY(entry_extra));
1011 	gtk_entry_set_activates_default(GTK_ENTRY(entry_extra), TRUE);
1012 	gtk_widget_set_sensitive(entry_extra, FALSE);
1013 	gtk_widget_set_tooltip_text(entry_extra, _("Other options to pass to Grep"));
1014 	ui_hookup_widget(fif_dlg.dialog, entry_extra, "entry_extra");
1015 
1016 	/* enable entry_extra when check_extra is checked */
1017 	g_signal_connect(check_extra, "toggled",
1018 		G_CALLBACK(on_widget_toggled_set_sensitive), entry_extra);
1019 
1020 	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
1021 	gtk_box_pack_start(GTK_BOX(hbox), check_extra, FALSE, FALSE, 0);
1022 	gtk_box_pack_start(GTK_BOX(hbox), entry_extra, TRUE, TRUE, 0);
1023 	gtk_container_add(GTK_CONTAINER(vbox), hbox);
1024 
1025 	g_signal_connect(fif_dlg.dialog, "response",
1026 			G_CALLBACK(on_find_in_files_dialog_response), NULL);
1027 	g_signal_connect(fif_dlg.dialog, "delete-event",
1028 			G_CALLBACK(gtk_widget_hide_on_delete), NULL);
1029 }
1030 
1031 
1032 /**
1033  * Shows the Find in Files dialog.
1034  *
1035  * @param dir The directory to search in (UTF-8 encoding). May be @c NULL
1036  * to determine it the usual way by using the current document's path.
1037  *
1038  * @since 0.14, plugin API 53
1039  */
1040 GEANY_API_SYMBOL
search_show_find_in_files_dialog(const gchar * dir)1041 void search_show_find_in_files_dialog(const gchar *dir)
1042 {
1043 	search_show_find_in_files_dialog_full(NULL, dir);
1044 }
1045 
1046 
search_show_find_in_files_dialog_full(const gchar * text,const gchar * dir)1047 void search_show_find_in_files_dialog_full(const gchar *text, const gchar *dir)
1048 {
1049 	GtkWidget *entry; /* for child GtkEntry of a GtkComboBoxEntry */
1050 	GeanyDocument *doc = document_get_current();
1051 	gchar *sel = NULL;
1052 	gchar *cur_dir = NULL;
1053 	GeanyEncodingIndex enc_idx = GEANY_ENCODING_UTF_8;
1054 
1055 	if (fif_dlg.dialog == NULL)
1056 	{
1057 		create_fif_dialog();
1058 		gtk_widget_show_all(fif_dlg.dialog);
1059 		if (doc && !text)
1060 			sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
1061 	}
1062 	stash_group_display(fif_prefs, fif_dlg.dialog);
1063 
1064 	if (!text)
1065 	{
1066 		if (doc && ! sel)
1067 			sel = editor_get_default_selection(doc->editor, search_prefs.use_current_word, NULL);
1068 		text = sel;
1069 	}
1070 	entry = gtk_bin_get_child(GTK_BIN(fif_dlg.search_combo));
1071 	if (text)
1072 		gtk_entry_set_text(GTK_ENTRY(entry), text);
1073 	g_free(sel);
1074 
1075 	/* add project's base path directory to the dir list, we do this here once
1076 	 * (in create_fif_dialog() it would fail if a project is opened after dialog creation) */
1077 	if (app->project != NULL && !EMPTY(app->project->base_path))
1078 	{
1079 		ui_combo_box_prepend_text_once(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo),
1080 			app->project->base_path);
1081 	}
1082 
1083 	entry = gtk_bin_get_child(GTK_BIN(fif_dlg.dir_combo));
1084 	if (!EMPTY(dir))
1085 		cur_dir = g_strdup(dir);	/* custom directory argument passed */
1086 	else
1087 	{
1088 		if (search_prefs.use_current_file_dir)
1089 		{
1090 			static gchar *last_cur_dir = NULL;
1091 			static GeanyDocument *last_doc = NULL;
1092 
1093 			/* Only set the directory entry once for the current document */
1094 			cur_dir = utils_get_current_file_dir_utf8();
1095 			if (doc == last_doc && cur_dir && utils_str_equal(cur_dir, last_cur_dir))
1096 			{
1097 				/* in case the user now wants the current directory, add it to history */
1098 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.dir_combo), cur_dir, 0);
1099 				SETPTR(cur_dir, NULL);
1100 			}
1101 			else
1102 				SETPTR(last_cur_dir, g_strdup(cur_dir));
1103 
1104 			last_doc = doc;
1105 		}
1106 		if (!cur_dir && EMPTY(gtk_entry_get_text(GTK_ENTRY(entry))))
1107 		{
1108 			/* use default_open_path if no directory could be determined
1109 			 * (e.g. when no files are open) */
1110 			if (!cur_dir)
1111 				cur_dir = g_strdup(utils_get_default_dir_utf8());
1112 			if (!cur_dir)
1113 				cur_dir = g_get_current_dir();
1114 		}
1115 	}
1116 	if (cur_dir)
1117 	{
1118 		gtk_entry_set_text(GTK_ENTRY(entry), cur_dir);
1119 		g_free(cur_dir);
1120 	}
1121 
1122 	update_fif_file_mode_combo();
1123 	update_file_patterns(fif_dlg.files_mode_combo, fif_dlg.files_combo);
1124 
1125 	/* set the encoding of the current file */
1126 	if (doc != NULL)
1127 		enc_idx = encodings_get_idx_from_charset(doc->encoding);
1128 	ui_encodings_combo_box_set_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo), enc_idx);
1129 
1130 	/* put the focus to the directory entry if it is empty */
1131 	if (utils_str_equal(gtk_entry_get_text(GTK_ENTRY(entry)), ""))
1132 		gtk_widget_grab_focus(fif_dlg.dir_combo);
1133 	else
1134 		gtk_widget_grab_focus(fif_dlg.search_combo);
1135 
1136 	/* set dialog window position */
1137 	set_dialog_position(fif_dlg.dialog, fif_dlg.position);
1138 
1139 	gtk_widget_show(fif_dlg.dialog);
1140 	/* bring the dialog back in the foreground in case it is already open but the focus is away */
1141 	gtk_window_present(GTK_WINDOW(fif_dlg.dialog));
1142 }
1143 
1144 
1145 static void
on_find_replace_checkbutton_toggled(GtkToggleButton * togglebutton,gpointer user_data)1146 on_find_replace_checkbutton_toggled(GtkToggleButton *togglebutton, gpointer user_data)
1147 {
1148 	GtkWidget *dialog = GTK_WIDGET(user_data);
1149 	GtkToggleButton *chk_regexp = GTK_TOGGLE_BUTTON(
1150 		ui_lookup_widget(dialog, "check_regexp"));
1151 
1152 	if (togglebutton == chk_regexp)
1153 	{
1154 		gboolean regex_set = gtk_toggle_button_get_active(chk_regexp);
1155 		GtkWidget *check_word = ui_lookup_widget(dialog, "check_word");
1156 		GtkWidget *check_wordstart = ui_lookup_widget(dialog, "check_wordstart");
1157 		GtkWidget *check_escape = ui_lookup_widget(dialog, "check_escape");
1158 		GtkWidget *check_multiline = ui_lookup_widget(dialog, "check_multiline");
1159 		gboolean replace = (dialog != find_dlg.dialog);
1160 		const char *back_button[2] = { "btn_previous" , "check_back" };
1161 
1162 		/* hide options that don't apply to regex searches */
1163 		gtk_widget_set_sensitive(check_escape, ! regex_set);
1164 		gtk_widget_set_sensitive(ui_lookup_widget(dialog, back_button[replace]), ! regex_set);
1165 		gtk_widget_set_sensitive(check_word, ! regex_set);
1166 		gtk_widget_set_sensitive(check_wordstart, ! regex_set);
1167 		gtk_widget_set_sensitive(check_multiline, regex_set);
1168 	}
1169 }
1170 
1171 
match_info_new(GeanyFindFlags flags,gint start,gint end)1172 static GeanyMatchInfo *match_info_new(GeanyFindFlags flags, gint start, gint end)
1173 {
1174 	GeanyMatchInfo *info = g_slice_alloc(sizeof *info);
1175 
1176 	info->flags = flags;
1177 	info->start = start;
1178 	info->end = end;
1179 	info->match_text = NULL;
1180 
1181 	return info;
1182 }
1183 
geany_match_info_free(GeanyMatchInfo * info)1184 void geany_match_info_free(GeanyMatchInfo *info)
1185 {
1186 	g_free(info->match_text);
1187 	g_slice_free1(sizeof *info, info);
1188 }
1189 
1190 
1191 /* find all in the given range.
1192  * Returns a list of allocated GeanyMatchInfo, should be freed using:
1193  *
1194  * 	foreach_slist(node, matches)
1195  * 		geany_match_info_free(node->data);
1196  * 	g_slist_free(matches); */
find_range(ScintillaObject * sci,GeanyFindFlags flags,struct Sci_TextToFind * ttf)1197 static GSList *find_range(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf)
1198 {
1199 	GSList *matches = NULL;
1200 	GeanyMatchInfo *info;
1201 
1202 	g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL, NULL);
1203 	if (! *ttf->lpstrText)
1204 		return NULL;
1205 
1206 	while (search_find_text(sci, flags, ttf, &info) != -1)
1207 	{
1208 		if (ttf->chrgText.cpMax > ttf->chrg.cpMax)
1209 		{
1210 			/* found text is partially out of range */
1211 			geany_match_info_free(info);
1212 			break;
1213 		}
1214 
1215 		matches = g_slist_prepend(matches, info);
1216 		ttf->chrg.cpMin = ttf->chrgText.cpMax;
1217 
1218 		/* avoid rematching with empty matches like "(?=[a-z])" or "^$".
1219 		 * note we cannot assume a match will always be empty or not and then break out, since
1220 		 * matches like "a?(?=b)" will sometimes be empty and sometimes not */
1221 		if (ttf->chrgText.cpMax == ttf->chrgText.cpMin)
1222 			ttf->chrg.cpMin ++;
1223 	}
1224 
1225 	return g_slist_reverse(matches);
1226 }
1227 
1228 
1229 /* Clears markers if text is null/empty.
1230  * @return Number of matches marked. */
search_mark_all(GeanyDocument * doc,const gchar * search_text,GeanyFindFlags flags)1231 gint search_mark_all(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags)
1232 {
1233 	gint count = 0;
1234 	struct Sci_TextToFind ttf;
1235 	GSList *match, *matches;
1236 
1237 	g_return_val_if_fail(DOC_VALID(doc), 0);
1238 
1239 	/* clear previous search indicators */
1240 	editor_indicator_clear(doc->editor, GEANY_INDICATOR_SEARCH);
1241 
1242 	if (G_UNLIKELY(EMPTY(search_text)))
1243 		return 0;
1244 
1245 	ttf.chrg.cpMin = 0;
1246 	ttf.chrg.cpMax = sci_get_length(doc->editor->sci);
1247 	ttf.lpstrText = (gchar *)search_text;
1248 
1249 	matches = find_range(doc->editor->sci, flags, &ttf);
1250 	foreach_slist (match, matches)
1251 	{
1252 		GeanyMatchInfo *info = match->data;
1253 
1254 		if (info->end != info->start)
1255 			editor_indicator_set_on_range(doc->editor, GEANY_INDICATOR_SEARCH, info->start, info->end);
1256 		count++;
1257 
1258 		geany_match_info_free(info);
1259 	}
1260 	g_slist_free(matches);
1261 
1262 	return count;
1263 }
1264 
1265 
1266 static void
on_find_entry_activate(GtkEntry * entry,gpointer user_data)1267 on_find_entry_activate(GtkEntry *entry, gpointer user_data)
1268 {
1269 	on_find_dialog_response(NULL, GEANY_RESPONSE_FIND, user_data);
1270 }
1271 
1272 
1273 static void
on_find_entry_activate_backward(GtkEntry * entry,gpointer user_data)1274 on_find_entry_activate_backward(GtkEntry *entry, gpointer user_data)
1275 {
1276 	/* can't search backwards with a regexp */
1277 	if (search_data.flags & GEANY_FIND_REGEXP)
1278 		utils_beep();
1279 	else
1280 		on_find_dialog_response(NULL, GEANY_RESPONSE_FIND_PREVIOUS, user_data);
1281 }
1282 
1283 
int_search_flags(gint match_case,gint whole_word,gint regexp,gint multiline,gint word_start)1284 static GeanyFindFlags int_search_flags(gint match_case, gint whole_word, gint regexp, gint multiline, gint word_start)
1285 {
1286 	return (match_case ? GEANY_FIND_MATCHCASE : 0) |
1287 		(regexp ? GEANY_FIND_REGEXP : 0) |
1288 		(whole_word ? GEANY_FIND_WHOLEWORD : 0) |
1289 		(multiline ? GEANY_FIND_MULTILINE : 0) |
1290 		/* SCFIND_WORDSTART overrides SCFIND_WHOLEWORD, but we want the opposite */
1291 		(word_start && !whole_word ? GEANY_FIND_WORDSTART : 0);
1292 }
1293 
1294 
1295 static void
on_find_dialog_response(GtkDialog * dialog,gint response,gpointer user_data)1296 on_find_dialog_response(GtkDialog *dialog, gint response, gpointer user_data)
1297 {
1298 	gtk_window_get_position(GTK_WINDOW(find_dlg.dialog),
1299 		&find_dlg.position[0], &find_dlg.position[1]);
1300 
1301 	stash_group_update(find_prefs, find_dlg.dialog);
1302 
1303 	if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT)
1304 		gtk_widget_hide(find_dlg.dialog);
1305 	else
1306 	{
1307 		GeanyDocument *doc = document_get_current();
1308 		gboolean check_close = settings.find_close_dialog;
1309 
1310 		if (doc == NULL)
1311 			return;
1312 
1313 		search_data.backwards = FALSE;
1314 		search_data.search_bar = FALSE;
1315 
1316 		g_free(search_data.text);
1317 		g_free(search_data.original_text);
1318 		search_data.text = g_strdup(gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(user_data)))));
1319 		search_data.original_text = g_strdup(search_data.text);
1320 		search_data.flags = int_search_flags(settings.find_case_sensitive,
1321 			settings.find_match_whole_word, settings.find_regexp, settings.find_regexp_multiline,
1322 			settings.find_match_word_start);
1323 
1324 		if (EMPTY(search_data.text))
1325 		{
1326 			fail:
1327 			utils_beep();
1328 			gtk_widget_grab_focus(find_dlg.entry);
1329 			return;
1330 		}
1331 		if (search_data.flags & GEANY_FIND_REGEXP)
1332 		{
1333 			GRegex *regex = compile_regex(search_data.text, search_data.flags);
1334 			if (!regex)
1335 				goto fail;
1336 			else
1337 				g_regex_unref(regex);
1338 		}
1339 		else if (settings.find_escape_sequences)
1340 		{
1341 			if (! utils_str_replace_escape(search_data.text, FALSE))
1342 				goto fail;
1343 		}
1344 		ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(user_data), search_data.original_text, 0);
1345 
1346 		switch (response)
1347 		{
1348 			case GEANY_RESPONSE_FIND:
1349 			case GEANY_RESPONSE_FIND_PREVIOUS:
1350 			{
1351 				gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags,
1352 					(response == GEANY_RESPONSE_FIND_PREVIOUS), NULL, TRUE, GTK_WIDGET(find_dlg.dialog));
1353 				ui_set_search_entry_background(find_dlg.entry, (result > -1));
1354 				check_close = search_prefs.hide_find_dialog;
1355 				break;
1356 			}
1357 			case GEANY_RESPONSE_FIND_IN_FILE:
1358 				search_find_usage(search_data.text, search_data.original_text, search_data.flags, FALSE);
1359 				break;
1360 
1361 			case GEANY_RESPONSE_FIND_IN_SESSION:
1362 				search_find_usage(search_data.text, search_data.original_text, search_data.flags, TRUE);
1363 				break;
1364 
1365 			case GEANY_RESPONSE_MARK:
1366 			{
1367 				gint count = search_mark_all(doc, search_data.text, search_data.flags);
1368 
1369 				if (count == 0)
1370 					ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), search_data.original_text);
1371 				else
1372 					ui_set_statusbar(FALSE,
1373 						ngettext("Found %d match for \"%s\".",
1374 								 "Found %d matches for \"%s\".", count),
1375 						count, search_data.original_text);
1376 			}
1377 			break;
1378 		}
1379 		if (check_close)
1380 			gtk_widget_hide(find_dlg.dialog);
1381 	}
1382 }
1383 
1384 
1385 static void
on_replace_find_entry_activate(GtkEntry * entry,gpointer user_data)1386 on_replace_find_entry_activate(GtkEntry *entry, gpointer user_data)
1387 {
1388 	on_replace_dialog_response(NULL, GEANY_RESPONSE_FIND, NULL);
1389 }
1390 
1391 
1392 static void
on_replace_entry_activate(GtkEntry * entry,gpointer user_data)1393 on_replace_entry_activate(GtkEntry *entry, gpointer user_data)
1394 {
1395 	on_replace_dialog_response(NULL,
1396 		search_prefs.replace_and_find_by_default ? GEANY_RESPONSE_REPLACE_AND_FIND : GEANY_RESPONSE_REPLACE,
1397 		NULL);
1398 }
1399 
1400 
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)1401 static void replace_in_session(GeanyDocument *doc,
1402 		GeanyFindFlags search_flags_re, gboolean search_replace_escape_re,
1403 		const gchar *find, const gchar *replace,
1404 		const gchar *original_find, const gchar *original_replace)
1405 {
1406 	guint n, page_count, rep_count = 0, file_count = 0;
1407 
1408 	/* replace in all documents following notebook tab order */
1409 	page_count = gtk_notebook_get_n_pages(GTK_NOTEBOOK(main_widgets.notebook));
1410 	for (n = 0; n < page_count; n++)
1411 	{
1412 		GeanyDocument *tmp_doc = document_get_from_page(n);
1413 		gint reps = 0;
1414 
1415 		reps = document_replace_all(tmp_doc, find, replace, original_find, original_replace, search_flags_re);
1416 		rep_count += reps;
1417 		if (reps)
1418 			file_count++;
1419 	}
1420 	if (file_count == 0)
1421 	{
1422 		utils_beep();
1423 		ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_find);
1424 		return;
1425 	}
1426 	/* if only one file was changed, don't override that document's status message
1427 	 * so we don't have to translate 4 messages for ngettext */
1428 	if (file_count > 1)
1429 		ui_set_statusbar(FALSE, _("Replaced %u matches in %u documents."),
1430 			rep_count, file_count);
1431 
1432 	/* show which docs had replacements: */
1433 	gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_STATUS);
1434 
1435 	ui_save_buttons_toggle(doc->changed);	/* update save all */
1436 }
1437 
1438 
1439 static void
on_replace_dialog_response(GtkDialog * dialog,gint response,gpointer user_data)1440 on_replace_dialog_response(GtkDialog *dialog, gint response, gpointer user_data)
1441 {
1442 	GeanyDocument *doc = document_get_current();
1443 	GeanyFindFlags search_flags_re;
1444 	gboolean search_backwards_re, search_replace_escape_re;
1445 	gchar *find, *replace, *original_find = NULL, *original_replace = NULL;
1446 
1447 	gtk_window_get_position(GTK_WINDOW(replace_dlg.dialog),
1448 		&replace_dlg.position[0], &replace_dlg.position[1]);
1449 
1450 	stash_group_update(replace_prefs, replace_dlg.dialog);
1451 
1452 	if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT)
1453 	{
1454 		gtk_widget_hide(replace_dlg.dialog);
1455 		return;
1456 	}
1457 
1458 	search_backwards_re = settings.replace_search_backwards;
1459 	search_replace_escape_re = settings.replace_escape_sequences;
1460 	find = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.find_entry)));
1461 	replace = g_strdup(gtk_entry_get_text(GTK_ENTRY(replace_dlg.replace_entry)));
1462 
1463 	search_flags_re = int_search_flags(settings.replace_case_sensitive,
1464 		settings.replace_match_whole_word, settings.replace_regexp,
1465 		settings.replace_regexp_multiline, settings.replace_match_word_start);
1466 
1467 	if ((response != GEANY_RESPONSE_FIND) && (search_flags_re & GEANY_FIND_MATCHCASE)
1468 		&& (strcmp(find, replace) == 0))
1469 		goto fail;
1470 
1471 	original_find = g_strdup(find);
1472 	original_replace = g_strdup(replace);
1473 
1474 	if (search_flags_re & GEANY_FIND_REGEXP)
1475 	{
1476 		GRegex *regex = compile_regex(find, search_flags_re);
1477 		if (regex)
1478 			g_regex_unref(regex);
1479 		/* find escapes will be handled by GRegex */
1480 		if (!regex || !utils_str_replace_escape(replace, TRUE))
1481 			goto fail;
1482 	}
1483 	else if (search_replace_escape_re)
1484 	{
1485 		if (! utils_str_replace_escape(find, FALSE) ||
1486 			! utils_str_replace_escape(replace, FALSE))
1487 			goto fail;
1488 	}
1489 
1490 	ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.find_combobox), original_find, 0);
1491 	ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(replace_dlg.replace_combobox), original_replace, 0);
1492 
1493 	switch (response)
1494 	{
1495 		case GEANY_RESPONSE_REPLACE_AND_FIND:
1496 		{
1497 			gint rep = document_replace_text(doc, find, original_find, replace, search_flags_re,
1498 				search_backwards_re);
1499 			if (rep != -1)
1500 				document_find_text(doc, find, original_find, search_flags_re, search_backwards_re,
1501 					NULL, TRUE, NULL);
1502 			break;
1503 		}
1504 		case GEANY_RESPONSE_REPLACE:
1505 			document_replace_text(doc, find, original_find, replace, search_flags_re,
1506 				search_backwards_re);
1507 			break;
1508 
1509 		case GEANY_RESPONSE_FIND:
1510 		{
1511 			gint result = document_find_text(doc, find, original_find, search_flags_re,
1512 								search_backwards_re, NULL, TRUE, GTK_WIDGET(dialog));
1513 			ui_set_search_entry_background(replace_dlg.find_entry, (result > -1));
1514 			break;
1515 		}
1516 		case GEANY_RESPONSE_REPLACE_IN_FILE:
1517 			if (! document_replace_all(doc, find, replace, original_find, original_replace, search_flags_re))
1518 				utils_beep();
1519 			break;
1520 
1521 		case GEANY_RESPONSE_REPLACE_IN_SESSION:
1522 			replace_in_session(doc, search_flags_re, search_replace_escape_re, find, replace, original_find, original_replace);
1523 			break;
1524 
1525 		case GEANY_RESPONSE_REPLACE_IN_SEL:
1526 			document_replace_sel(doc, find, replace, original_find, original_replace, search_flags_re);
1527 			break;
1528 	}
1529 	switch (response)
1530 	{
1531 		case GEANY_RESPONSE_REPLACE_IN_SEL:
1532 		case GEANY_RESPONSE_REPLACE_IN_FILE:
1533 		case GEANY_RESPONSE_REPLACE_IN_SESSION:
1534 			if (settings.replace_close_dialog)
1535 				gtk_widget_hide(replace_dlg.dialog);
1536 	}
1537 	g_free(find);
1538 	g_free(replace);
1539 	g_free(original_find);
1540 	g_free(original_replace);
1541 	return;
1542 
1543 fail:
1544 	utils_beep();
1545 	gtk_widget_grab_focus(replace_dlg.find_entry);
1546 	g_free(find);
1547 	g_free(replace);
1548 	g_free(original_find);
1549 	g_free(original_replace);
1550 }
1551 
1552 
get_grep_options(void)1553 static GString *get_grep_options(void)
1554 {
1555 	GString *gstr = g_string_new("-nHI");	/* line numbers, filenames, ignore binaries */
1556 
1557 	if (settings.fif_invert_results)
1558 		g_string_append_c(gstr, 'v');
1559 	if (! settings.fif_case_sensitive)
1560 		g_string_append_c(gstr, 'i');
1561 	if (settings.fif_match_whole_word)
1562 		g_string_append_c(gstr, 'w');
1563 	if (settings.fif_recursive)
1564 		g_string_append_c(gstr, 'r');
1565 
1566 	if (!settings.fif_regexp)
1567 		g_string_append_c(gstr, 'F');
1568 	else
1569 		g_string_append_c(gstr, 'E');
1570 
1571 	if (settings.fif_use_extra_options)
1572 	{
1573 		g_strstrip(settings.fif_extra_options);
1574 
1575 		if (*settings.fif_extra_options != 0)
1576 		{
1577 			g_string_append_c(gstr, ' ');
1578 			g_string_append(gstr, settings.fif_extra_options);
1579 		}
1580 	}
1581 	g_strstrip(settings.fif_files);
1582 	if (settings.fif_files_mode != FILES_MODE_ALL && *settings.fif_files)
1583 	{
1584 		GString *tmp;
1585 
1586 		/* put --include= before each pattern */
1587 		tmp = g_string_new(settings.fif_files);
1588 		do {} while (utils_string_replace_all(tmp, "  ", " "));
1589 		g_string_prepend_c(tmp, ' ');
1590 		utils_string_replace_all(tmp, " ", " --include=");
1591 		g_string_append(gstr, tmp->str);
1592 		g_string_free(tmp, TRUE);
1593 	}
1594 	return gstr;
1595 }
1596 
1597 
1598 static void
on_find_in_files_dialog_response(GtkDialog * dialog,gint response,G_GNUC_UNUSED gpointer user_data)1599 on_find_in_files_dialog_response(GtkDialog *dialog, gint response,
1600 		G_GNUC_UNUSED gpointer user_data)
1601 {
1602 	gtk_window_get_position(GTK_WINDOW(fif_dlg.dialog), &fif_dlg.position[0], &fif_dlg.position[1]);
1603 
1604 	stash_group_update(fif_prefs, fif_dlg.dialog);
1605 
1606 	if (response == GTK_RESPONSE_ACCEPT)
1607 	{
1608 		GtkWidget *search_combo = fif_dlg.search_combo;
1609 		const gchar *search_text =
1610 			gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(search_combo))));
1611 		GtkWidget *dir_combo = fif_dlg.dir_combo;
1612 		const gchar *utf8_dir =
1613 			gtk_entry_get_text(GTK_ENTRY(gtk_bin_get_child(GTK_BIN(dir_combo))));
1614 		GeanyEncodingIndex enc_idx =
1615 			ui_encodings_combo_box_get_active_encoding(GTK_COMBO_BOX(fif_dlg.encoding_combo));
1616 
1617 		if (G_UNLIKELY(EMPTY(utf8_dir)))
1618 			ui_set_statusbar(FALSE, _("Invalid directory for find in files."));
1619 		else if (!EMPTY(search_text))
1620 		{
1621 			GString *opts = get_grep_options();
1622 			const gchar *enc = (enc_idx == GEANY_ENCODING_UTF_8) ? NULL :
1623 				encodings_get_charset_from_index(enc_idx);
1624 
1625 			if (search_find_in_files(search_text, utf8_dir, opts->str, enc))
1626 			{
1627 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(search_combo), search_text, 0);
1628 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(fif_dlg.files_combo), NULL, 0);
1629 				ui_combo_box_add_to_history(GTK_COMBO_BOX_TEXT(dir_combo), utf8_dir, 0);
1630 				gtk_widget_hide(fif_dlg.dialog);
1631 			}
1632 			g_string_free(opts, TRUE);
1633 		}
1634 		else
1635 			ui_set_statusbar(FALSE, _("No text to find."));
1636 	}
1637 	else
1638 		gtk_widget_hide(fif_dlg.dialog);
1639 }
1640 
1641 
1642 static gboolean
search_find_in_files(const gchar * utf8_search_text,const gchar * utf8_dir,const gchar * opts,const gchar * enc)1643 search_find_in_files(const gchar *utf8_search_text, const gchar *utf8_dir, const gchar *opts,
1644 	const gchar *enc)
1645 {
1646 	gchar **argv_prefix, **argv;
1647 	gchar *command_grep;
1648 	gchar *command_line, *dir;
1649 	gchar *search_text = NULL;
1650 	GError *error = NULL;
1651 	gboolean ret = FALSE;
1652 	gssize utf8_text_len;
1653 
1654 	if (EMPTY(utf8_search_text) || ! utf8_dir) return TRUE;
1655 
1656 	command_grep = g_find_program_in_path(tool_prefs.grep_cmd);
1657 	if (command_grep == NULL)
1658 		command_line = g_strdup_printf("%s %s --", tool_prefs.grep_cmd, opts);
1659 	else
1660 	{
1661 		command_line = g_strdup_printf("\"%s\" %s --", command_grep, opts);
1662 		g_free(command_grep);
1663 	}
1664 
1665 	/* convert the search text in the preferred encoding (if the text is not valid UTF-8. assume
1666 	 * it is already in the preferred encoding) */
1667 	utf8_text_len = strlen(utf8_search_text);
1668 	if (enc != NULL && g_utf8_validate(utf8_search_text, utf8_text_len, NULL))
1669 	{
1670 		search_text = g_convert(utf8_search_text, utf8_text_len, enc, "UTF-8", NULL, NULL, NULL);
1671 	}
1672 	if (search_text == NULL)
1673 		search_text = g_strdup(utf8_search_text);
1674 
1675 	argv_prefix = g_new(gchar*, 3);
1676 	argv_prefix[0] = search_text;
1677 	dir = utils_get_locale_from_utf8(utf8_dir);
1678 
1679 	/* finally add the arguments(files to be searched) */
1680 	if (settings.fif_recursive)	/* recursive option set */
1681 	{
1682 		/* Use '.' so we get relative paths in the output */
1683 		argv_prefix[1] = g_strdup(".");
1684 		argv_prefix[2] = NULL;
1685 		argv = argv_prefix;
1686 	}
1687 	else
1688 	{
1689 		argv_prefix[1] = NULL;
1690 		argv = search_get_argv((const gchar**)argv_prefix, dir);
1691 		g_strfreev(argv_prefix);
1692 
1693 		if (argv == NULL)	/* no files */
1694 		{
1695 			g_free(command_line);
1696 			return FALSE;
1697 		}
1698 	}
1699 
1700 	gtk_list_store_clear(msgwindow.store_msg);
1701 	gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE);
1702 
1703 	/* we can pass 'enc' without strdup'ing it here because it's a global const string and
1704 	 * always exits longer than the lifetime of this function */
1705 	if (spawn_with_callbacks(dir, command_line, argv, NULL, 0, NULL, NULL, search_read_io,
1706 		(gpointer) enc, 0, search_read_io_stderr, (gpointer) enc, 0, search_finished, NULL,
1707 		NULL, &error))
1708  	{
1709 		gchar *utf8_str;
1710 
1711  		ui_progress_bar_start(_("Searching..."));
1712  		msgwin_set_messages_dir(dir);
1713 		utf8_str = g_strdup_printf(_("%s %s -- %s (in directory: %s)"),
1714 			tool_prefs.grep_cmd, opts, utf8_search_text, utf8_dir);
1715  		msgwin_msg_add_string(COLOR_BLUE, -1, NULL, utf8_str);
1716 		g_free(utf8_str);
1717  		ret = TRUE;
1718 	}
1719 	else
1720 	{
1721 		ui_set_statusbar(TRUE, _("Cannot execute grep tool \"%s\": %s. "
1722 			"Check the path setting in Preferences."), tool_prefs.grep_cmd, error->message);
1723 		g_error_free(error);
1724 	}
1725 
1726 	utils_free_pointers(2, dir, command_line, NULL);
1727 	g_strfreev(argv);
1728 	return ret;
1729 }
1730 
1731 
pattern_list_match(GSList * patterns,const gchar * str)1732 static gboolean pattern_list_match(GSList *patterns, const gchar *str)
1733 {
1734 	GSList *item;
1735 
1736 	foreach_slist(item, patterns)
1737 	{
1738 		if (g_pattern_match_string(item->data, str))
1739 			return TRUE;
1740 	}
1741 	return FALSE;
1742 }
1743 
1744 
1745 /* Creates an argument vector of strings, copying argv_prefix[] values for
1746  * the first arguments, then followed by filenames found in dir.
1747  * Returns NULL if no files were found, otherwise returned vector should be fully freed. */
search_get_argv(const gchar ** argv_prefix,const gchar * dir)1748 static gchar **search_get_argv(const gchar **argv_prefix, const gchar *dir)
1749 {
1750 	guint prefix_len, list_len, i, j;
1751 	gchar **argv;
1752 	GSList *list, *item, *patterns = NULL;
1753 	GError *error = NULL;
1754 
1755 	g_return_val_if_fail(dir != NULL, NULL);
1756 
1757 	prefix_len = g_strv_length((gchar**)argv_prefix);
1758 	list = utils_get_file_list(dir, &list_len, &error);
1759 	if (error)
1760 	{
1761 		ui_set_statusbar(TRUE, _("Could not open directory (%s)"), error->message);
1762 		g_error_free(error);
1763 		return NULL;
1764 	}
1765 	if (list == NULL)
1766 		return NULL;
1767 
1768 	argv = g_new(gchar*, prefix_len + list_len + 1);
1769 
1770 	for (i = 0, j = 0; i < prefix_len; i++)
1771 	{
1772 		if (g_str_has_prefix(argv_prefix[i], "--include="))
1773 		{
1774 			const gchar *pat = &(argv_prefix[i][10]); /* the pattern part of the argument */
1775 
1776 			patterns = g_slist_prepend(patterns, g_pattern_spec_new(pat));
1777 		}
1778 		else
1779 			argv[j++] = g_strdup(argv_prefix[i]);
1780 	}
1781 
1782 	if (patterns)
1783 	{
1784 		GSList *pat;
1785 
1786 		foreach_slist(item, list)
1787 		{
1788 			if (pattern_list_match(patterns, item->data))
1789 				argv[j++] = item->data;
1790 			else
1791 				g_free(item->data);
1792 		}
1793 		foreach_slist(pat, patterns)
1794 			g_pattern_spec_free(pat->data);
1795 		g_slist_free(patterns);
1796 	}
1797 	else
1798 	{
1799 		foreach_slist(item, list)
1800 			argv[j++] = item->data;
1801 	}
1802 
1803 	argv[j] = NULL;
1804 	g_slist_free(list);
1805 	return argv;
1806 }
1807 
1808 
read_fif_io(gchar * msg,GIOCondition condition,gchar * enc,gint msg_color)1809 static void read_fif_io(gchar *msg, GIOCondition condition, gchar *enc, gint msg_color)
1810 {
1811 	if (condition & (G_IO_IN | G_IO_PRI))
1812 	{
1813 		gchar *utf8_msg = NULL;
1814 
1815 		g_strstrip(msg);
1816 		/* enc is NULL when encoding is set to UTF-8, so we can skip any conversion */
1817 		if (enc != NULL)
1818 		{
1819 			if (! g_utf8_validate(msg, -1, NULL))
1820 			{
1821 				utf8_msg = g_convert(msg, -1, "UTF-8", enc, NULL, NULL, NULL);
1822 			}
1823 			if (utf8_msg == NULL)
1824 				utf8_msg = msg;
1825 		}
1826 		else
1827 			utf8_msg = msg;
1828 
1829 		msgwin_msg_add_string(msg_color, -1, NULL, utf8_msg);
1830 
1831 		if (utf8_msg != msg)
1832 			g_free(utf8_msg);
1833 	}
1834 }
1835 
1836 
search_read_io(GString * string,GIOCondition condition,gpointer data)1837 static void search_read_io(GString *string, GIOCondition condition, gpointer data)
1838 {
1839 	read_fif_io(string->str, condition, data, COLOR_BLACK);
1840 }
1841 
1842 
search_read_io_stderr(GString * string,GIOCondition condition,gpointer data)1843 static void search_read_io_stderr(GString *string, GIOCondition condition, gpointer data)
1844 {
1845 	read_fif_io(string->str, condition, data, COLOR_DARK_RED);
1846 }
1847 
1848 
search_finished(GPid child_pid,gint status,gpointer user_data)1849 static void search_finished(GPid child_pid, gint status, gpointer user_data)
1850 {
1851 	const gchar *msg = _("Search failed.");
1852 	gint exit_status;
1853 
1854 	if (SPAWN_WIFEXITED(status))
1855 	{
1856 		exit_status = SPAWN_WEXITSTATUS(status);
1857 	}
1858 	else if (SPAWN_WIFSIGNALED(status))
1859 	{
1860 		exit_status = -1;
1861 		g_warning("Find in Files: The command failed unexpectedly (signal received).");
1862 	}
1863 	else
1864 	{
1865 		exit_status = 1;
1866 	}
1867 
1868 	switch (exit_status)
1869 	{
1870 		case 0:
1871 		{
1872 			gint count = gtk_tree_model_iter_n_children(
1873 				GTK_TREE_MODEL(msgwindow.store_msg), NULL) - 1;
1874 			gchar *text = ngettext(
1875 						"Search completed with %d match.",
1876 						"Search completed with %d matches.", count);
1877 
1878 			msgwin_msg_add(COLOR_BLUE, -1, NULL, text, count);
1879 			ui_set_statusbar(FALSE, text, count);
1880 			break;
1881 		}
1882 		case 1:
1883 			msg = _("No matches found.");
1884 			/* fall through */
1885 		default:
1886 			msgwin_msg_add_string(COLOR_BLUE, -1, NULL, msg);
1887 			ui_set_statusbar(FALSE, "%s", msg);
1888 			break;
1889 	}
1890 	utils_beep();
1891 	ui_progress_bar_stop();
1892 }
1893 
1894 
compile_regex(const gchar * str,GeanyFindFlags sflags)1895 static GRegex *compile_regex(const gchar *str, GeanyFindFlags sflags)
1896 {
1897 	GRegex *regex;
1898 	GError *error = NULL;
1899 	gint rflags = 0;
1900 
1901 	if (sflags & GEANY_FIND_MULTILINE)
1902 		rflags |= G_REGEX_MULTILINE;
1903 	if (~sflags & GEANY_FIND_MATCHCASE)
1904 		rflags |= G_REGEX_CASELESS;
1905 	if (sflags & (GEANY_FIND_WHOLEWORD | GEANY_FIND_WORDSTART))
1906 	{
1907 		geany_debug("%s: Unsupported regex flags found!", G_STRFUNC);
1908 	}
1909 
1910 	regex = g_regex_new(str, rflags, 0, &error);
1911 	if (!regex)
1912 	{
1913 		ui_set_statusbar(FALSE, _("Bad regex: %s"), error->message);
1914 		g_error_free(error);
1915 	}
1916 	return regex;
1917 }
1918 
1919 
1920 /* groups that don't exist are handled OK as len = end - start = (-1) - (-1) = 0 */
get_regex_match_string(const gchar * text,const GeanyMatchInfo * match,guint nth)1921 static gchar *get_regex_match_string(const gchar *text, const GeanyMatchInfo *match, guint nth)
1922 {
1923 	const gint start = match->matches[nth].start;
1924 	const gint end = match->matches[nth].end;
1925 	return g_strndup(&text[start], end - start);
1926 }
1927 
1928 
find_regex(ScintillaObject * sci,guint pos,GRegex * regex,gboolean multiline,GeanyMatchInfo * match)1929 static gint find_regex(ScintillaObject *sci, guint pos, GRegex *regex, gboolean multiline, GeanyMatchInfo *match)
1930 {
1931 	const gchar *text;
1932 	GMatchInfo *minfo;
1933 	guint document_length;
1934 	gint ret = -1;
1935 	gint offset = 0;
1936 
1937 	document_length = (guint)sci_get_length(sci);
1938 	if (document_length <= 0)
1939 		return -1; /* skip empty documents */
1940 
1941 	g_return_val_if_fail(pos <= document_length, -1);
1942 
1943 	if (multiline)
1944 	{
1945 		/* Warning: any SCI calls will invalidate 'text' after calling SCI_GETCHARACTERPOINTER */
1946 		text = (void*)SSM(sci, SCI_GETCHARACTERPOINTER, 0, 0);
1947 		g_regex_match_full(regex, text, -1, pos, 0, &minfo, NULL);
1948 	}
1949 	else /* single-line mode, manually match against each line */
1950 	{
1951 		gint line = sci_get_line_from_position(sci, pos);
1952 
1953 		for (;;)
1954 		{
1955 			gint start = sci_get_position_from_line(sci, line);
1956 			gint end = sci_get_line_end_position(sci, line);
1957 
1958 			text = (void*)SSM(sci, SCI_GETRANGEPOINTER, start, end - start);
1959 			if (g_regex_match_full(regex, text, end - start, pos - start, 0, &minfo, NULL))
1960 			{
1961 				offset = start;
1962 				break;
1963 			}
1964 			else /* not found, try next line */
1965 			{
1966 				line ++;
1967 				if (line >= sci_get_line_count(sci))
1968 					break;
1969 				pos = sci_get_position_from_line(sci, line);
1970 				/* don't free last info, it's freed below */
1971 				g_match_info_free(minfo);
1972 			}
1973 		}
1974 	}
1975 
1976 	/* Warning: minfo will become invalid when 'text' does! */
1977 	if (g_match_info_matches(minfo))
1978 	{
1979 		guint i;
1980 
1981 		/* copy whole match text and offsets before they become invalid */
1982 		SETPTR(match->match_text, g_match_info_fetch(minfo, 0));
1983 
1984 		foreach_range(i, G_N_ELEMENTS(match->matches))
1985 		{
1986 			gint start = -1, end = -1;
1987 
1988 			g_match_info_fetch_pos(minfo, (gint)i, &start, &end);
1989 			match->matches[i].start = offset + start;
1990 			match->matches[i].end = offset + end;
1991 		}
1992 		match->start = match->matches[0].start;
1993 		match->end = match->matches[0].end;
1994 		ret = match->start;
1995 	}
1996 	g_match_info_free(minfo);
1997 	return ret;
1998 }
1999 
2000 
geany_find_flags_to_sci_flags(GeanyFindFlags flags)2001 static gint geany_find_flags_to_sci_flags(GeanyFindFlags flags)
2002 {
2003 	g_warn_if_fail(! (flags & GEANY_FIND_REGEXP) || ! (flags & GEANY_FIND_MULTILINE));
2004 
2005 	return ((flags & GEANY_FIND_MATCHCASE) ? SCFIND_MATCHCASE : 0) |
2006 		((flags & GEANY_FIND_WHOLEWORD) ? SCFIND_WHOLEWORD : 0) |
2007 		((flags & GEANY_FIND_REGEXP) ? SCFIND_REGEXP | SCFIND_POSIX : 0) |
2008 		((flags & GEANY_FIND_WORDSTART) ? SCFIND_WORDSTART : 0);
2009 }
2010 
2011 
search_find_prev(ScintillaObject * sci,const gchar * str,GeanyFindFlags flags,GeanyMatchInfo ** match_)2012 gint search_find_prev(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_)
2013 {
2014 	gint ret;
2015 
2016 	g_return_val_if_fail(! (flags & GEANY_FIND_REGEXP), -1);
2017 
2018 	ret = sci_search_prev(sci, geany_find_flags_to_sci_flags(flags), str);
2019 	if (ret != -1 && match_)
2020 		*match_ = match_info_new(flags, ret, ret + strlen(str));
2021 	return ret;
2022 }
2023 
2024 
search_find_next(ScintillaObject * sci,const gchar * str,GeanyFindFlags flags,GeanyMatchInfo ** match_)2025 gint search_find_next(ScintillaObject *sci, const gchar *str, GeanyFindFlags flags, GeanyMatchInfo **match_)
2026 {
2027 	GeanyMatchInfo *match;
2028 	GRegex *regex;
2029 	gint ret = -1;
2030 	gint pos;
2031 
2032 	if (~flags & GEANY_FIND_REGEXP)
2033 	{
2034 		ret = sci_search_next(sci, geany_find_flags_to_sci_flags(flags), str);
2035 		if (ret != -1 && match_)
2036 			*match_ = match_info_new(flags, ret, ret + strlen(str));
2037 		return ret;
2038 	}
2039 
2040 	regex = compile_regex(str, flags);
2041 	if (!regex)
2042 		return -1;
2043 
2044 	match = match_info_new(flags, 0, 0);
2045 
2046 	pos = sci_get_current_position(sci);
2047 	ret = find_regex(sci, pos, regex, flags & GEANY_FIND_MULTILINE, match);
2048 	/* avoid re-matching the same position in case of empty matches */
2049 	if (ret == pos && match->matches[0].start == match->matches[0].end)
2050 		ret = find_regex(sci, pos + 1, regex, flags & GEANY_FIND_MULTILINE, match);
2051 	if (ret >= 0)
2052 		sci_set_selection(sci, match->start, match->end);
2053 
2054 	if (ret != -1 && match_)
2055 		*match_ = match;
2056 	else
2057 		geany_match_info_free(match);
2058 
2059 	g_regex_unref(regex);
2060 	return ret;
2061 }
2062 
2063 
search_replace_match(ScintillaObject * sci,const GeanyMatchInfo * match,const gchar * replace_text)2064 gint search_replace_match(ScintillaObject *sci, const GeanyMatchInfo *match, const gchar *replace_text)
2065 {
2066 	GString *str;
2067 	gint ret = 0;
2068 	gint i = 0;
2069 
2070 	sci_set_target_start(sci, match->start);
2071 	sci_set_target_end(sci, match->end);
2072 
2073 	if (! (match->flags & GEANY_FIND_REGEXP))
2074 		return sci_replace_target(sci, replace_text, FALSE);
2075 
2076 	str = g_string_new(replace_text);
2077 	while (str->str[i])
2078 	{
2079 		gchar *ptr = &str->str[i];
2080 		gchar *grp;
2081 		gchar c;
2082 
2083 		if (ptr[0] != '\\')
2084 		{
2085 			i++;
2086 			continue;
2087 		}
2088 		c = ptr[1];
2089 		/* backslash or unnecessary escape */
2090 		if (c == '\\' || !isdigit(c))
2091 		{
2092 			g_string_erase(str, i, 1);
2093 			i++;
2094 			continue;
2095 		}
2096 		/* digit escape */
2097 		g_string_erase(str, i, 2);
2098 		/* fix match offsets by subtracting index of whole match start from the string */
2099 		grp = get_regex_match_string(match->match_text - match->matches[0].start, match, c - '0');
2100 		g_string_insert(str, i, grp);
2101 		i += strlen(grp);
2102 		g_free(grp);
2103 	}
2104 	ret = sci_replace_target(sci, str->str, FALSE);
2105 	g_string_free(str, TRUE);
2106 	return ret;
2107 }
2108 
2109 
search_find_text(ScintillaObject * sci,GeanyFindFlags flags,struct Sci_TextToFind * ttf,GeanyMatchInfo ** match_)2110 gint search_find_text(ScintillaObject *sci, GeanyFindFlags flags, struct Sci_TextToFind *ttf, GeanyMatchInfo **match_)
2111 {
2112 	GeanyMatchInfo *match = NULL;
2113 	GRegex *regex;
2114 	gint ret;
2115 
2116 	if (~flags & GEANY_FIND_REGEXP)
2117 	{
2118 		ret = sci_find_text(sci, geany_find_flags_to_sci_flags(flags), ttf);
2119 		if (ret != -1 && match_)
2120 			*match_ = match_info_new(flags, ttf->chrgText.cpMin, ttf->chrgText.cpMax);
2121 		return ret;
2122 	}
2123 
2124 	regex = compile_regex(ttf->lpstrText, flags);
2125 	if (!regex)
2126 		return -1;
2127 
2128 	match = match_info_new(flags, 0, 0);
2129 
2130 	ret = find_regex(sci, ttf->chrg.cpMin, regex, flags & GEANY_FIND_MULTILINE, match);
2131 	if (ret >= ttf->chrg.cpMax)
2132 		ret = -1;
2133 	else if (ret >= 0)
2134 	{
2135 		ttf->chrgText.cpMin = match->start;
2136 		ttf->chrgText.cpMax = match->end;
2137 	}
2138 
2139 	if (ret != -1 && match_)
2140 		*match_ = match;
2141 	else
2142 		geany_match_info_free(match);
2143 
2144 	g_regex_unref(regex);
2145 	return ret;
2146 }
2147 
2148 
find_document_usage(GeanyDocument * doc,const gchar * search_text,GeanyFindFlags flags)2149 static gint find_document_usage(GeanyDocument *doc, const gchar *search_text, GeanyFindFlags flags)
2150 {
2151 	gchar *buffer, *short_file_name;
2152 	struct Sci_TextToFind ttf;
2153 	gint count = 0;
2154 	gint prev_line = -1;
2155 	GSList *match, *matches;
2156 
2157 	g_return_val_if_fail(DOC_VALID(doc), 0);
2158 
2159 	short_file_name = g_path_get_basename(DOC_FILENAME(doc));
2160 
2161 	ttf.chrg.cpMin = 0;
2162 	ttf.chrg.cpMax = sci_get_length(doc->editor->sci);
2163 	ttf.lpstrText = (gchar *)search_text;
2164 
2165 	matches = find_range(doc->editor->sci, flags, &ttf);
2166 	foreach_slist (match, matches)
2167 	{
2168 		GeanyMatchInfo *info = match->data;
2169 		gint line = sci_get_line_from_position(doc->editor->sci, info->start);
2170 
2171 		if (line != prev_line)
2172 		{
2173 			buffer = sci_get_line(doc->editor->sci, line);
2174 			msgwin_msg_add(COLOR_BLACK, line + 1, doc,
2175 				"%s:%d: %s", short_file_name, line + 1, g_strstrip(buffer));
2176 			g_free(buffer);
2177 			prev_line = line;
2178 		}
2179 		count++;
2180 
2181 		geany_match_info_free(info);
2182 	}
2183 	g_slist_free(matches);
2184 	g_free(short_file_name);
2185 	return count;
2186 }
2187 
2188 
search_find_usage(const gchar * search_text,const gchar * original_search_text,GeanyFindFlags flags,gboolean in_session)2189 void search_find_usage(const gchar *search_text, const gchar *original_search_text,
2190 		GeanyFindFlags flags, gboolean in_session)
2191 {
2192 	GeanyDocument *doc;
2193 	gint count = 0;
2194 
2195 	doc = document_get_current();
2196 	g_return_if_fail(doc != NULL);
2197 
2198 	if (G_UNLIKELY(EMPTY(search_text)))
2199 	{
2200 		utils_beep();
2201 		return;
2202 	}
2203 
2204 	gtk_notebook_set_current_page(GTK_NOTEBOOK(msgwindow.notebook), MSG_MESSAGE);
2205 	gtk_list_store_clear(msgwindow.store_msg);
2206 
2207 	if (! in_session)
2208 	{	/* use current document */
2209 		count = find_document_usage(doc, search_text, flags);
2210 	}
2211 	else
2212 	{
2213 		guint i;
2214 		for (i = 0; i < documents_array->len; i++)
2215 		{
2216 			if (documents[i]->is_valid)
2217 			{
2218 				count += find_document_usage(documents[i], search_text, flags);
2219 			}
2220 		}
2221 	}
2222 
2223 	if (count == 0) /* no matches were found */
2224 	{
2225 		ui_set_statusbar(FALSE, _("No matches found for \"%s\"."), original_search_text);
2226 		msgwin_msg_add(COLOR_BLUE, -1, NULL, _("No matches found for \"%s\"."), original_search_text);
2227 	}
2228 	else
2229 	{
2230 		ui_set_statusbar(FALSE, ngettext(
2231 			"Found %d match for \"%s\".", "Found %d matches for \"%s\".", count),
2232 			count, original_search_text);
2233 		msgwin_msg_add(COLOR_BLUE, -1, NULL, ngettext(
2234 			"Found %d match for \"%s\".", "Found %d matches for \"%s\".", count),
2235 			count, original_search_text);
2236 	}
2237 }
2238 
2239 
2240 /* ttf is updated to include the last match position (ttf->chrg.cpMin) and
2241  * the new search range end (ttf->chrg.cpMax).
2242  * Note: Normally you would call sci_start/end_undo_action() around this call. */
search_replace_range(ScintillaObject * sci,struct Sci_TextToFind * ttf,GeanyFindFlags flags,const gchar * replace_text)2243 guint search_replace_range(ScintillaObject *sci, struct Sci_TextToFind *ttf,
2244 		GeanyFindFlags flags, const gchar *replace_text)
2245 {
2246 	gint count = 0;
2247 	gint offset = 0; /* difference between search pos and replace pos */
2248 	GSList *match, *matches;
2249 
2250 	g_return_val_if_fail(sci != NULL && ttf->lpstrText != NULL && replace_text != NULL, 0);
2251 	if (! *ttf->lpstrText)
2252 		return 0;
2253 
2254 	matches = find_range(sci, flags, ttf);
2255 	foreach_slist (match, matches)
2256 	{
2257 		GeanyMatchInfo *info = match->data;
2258 		gint replace_len;
2259 
2260 		info->start += offset;
2261 		info->end += offset;
2262 
2263 		replace_len = search_replace_match(sci, info, replace_text);
2264 		offset += replace_len - (info->end - info->start);
2265 		count ++;
2266 
2267 		/* on last match, update the last match/new range end */
2268 		if (! match->next)
2269 		{
2270 			ttf->chrg.cpMin = info->start;
2271 			ttf->chrg.cpMax += offset;
2272 		}
2273 
2274 		geany_match_info_free(info);
2275 	}
2276 	g_slist_free(matches);
2277 
2278 	return count;
2279 }
2280 
2281 
search_find_again(gboolean change_direction)2282 void search_find_again(gboolean change_direction)
2283 {
2284 	GeanyDocument *doc = document_get_current();
2285 
2286 	g_return_if_fail(doc != NULL);
2287 
2288 	if (search_data.text)
2289 	{
2290 		gboolean forward = ! search_data.backwards;
2291 		gint result = document_find_text(doc, search_data.text, search_data.original_text, search_data.flags,
2292 			change_direction ? forward : !forward, NULL, FALSE, NULL);
2293 
2294 		if (result > -1)
2295 			editor_display_current_line(doc->editor, 0.3F);
2296 
2297 		if (search_data.search_bar)
2298 			ui_set_search_entry_background(
2299 				toolbar_get_widget_child_by_name("SearchEntry"), (result > -1));
2300 	}
2301 }
2302