1 /**************************************************************************
2  *   search.c  --  This file is part of GNU nano.                         *
3  *                                                                        *
4  *   Copyright (C) 1999-2011, 2013-2021 Free Software Foundation, Inc.    *
5  *   Copyright (C) 2015-2020 Benno Schulenberg                            *
6  *                                                                        *
7  *   GNU nano is free software: you can redistribute it and/or modify     *
8  *   it under the terms of the GNU General Public License as published    *
9  *   by the Free Software Foundation, either version 3 of the License,    *
10  *   or (at your option) any later version.                               *
11  *                                                                        *
12  *   GNU nano is distributed in the hope that it will be useful,          *
13  *   but WITHOUT ANY WARRANTY; without even the implied warranty          *
14  *   of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.              *
15  *   See the GNU General Public License for more details.                 *
16  *                                                                        *
17  *   You should have received a copy of the GNU General Public License    *
18  *   along with this program.  If not, see http://www.gnu.org/licenses/.  *
19  *                                                                        *
20  **************************************************************************/
21 
22 #include "prototypes.h"
23 
24 #include <string.h>
25 #include <time.h>
26 
27 static bool came_full_circle = FALSE;
28 		/* Have we reached the starting line again while searching? */
29 static bool have_compiled_regexp = FALSE;
30 		/* Whether we have compiled a regular expression for the search. */
31 
32 /* Compile the given regular expression and store it in search_regexp.
33  * Return TRUE if the expression is valid, and FALSE otherwise. */
regexp_init(const char * regexp)34 bool regexp_init(const char *regexp)
35 {
36 	int value = regcomp(&search_regexp, regexp,
37 				NANO_REG_EXTENDED | (ISSET(CASE_SENSITIVE) ? 0 : REG_ICASE));
38 
39 	/* If regex compilation failed, show the error message. */
40 	if (value != 0) {
41 		size_t len = regerror(value, &search_regexp, NULL, 0);
42 		char *str = nmalloc(len);
43 
44 		regerror(value, &search_regexp, str, len);
45 		statusline(AHEM, _("Bad regex \"%s\": %s"), regexp, str);
46 		free(str);
47 
48 		return FALSE;
49 	}
50 
51 	have_compiled_regexp = TRUE;
52 
53 	return TRUE;
54 }
55 
56 /* Free a compiled regular expression, if one was compiled; and schedule a
57  * full screen refresh when the mark is on, in case the cursor has moved. */
tidy_up_after_search(void)58 void tidy_up_after_search(void)
59 {
60 	if (have_compiled_regexp) {
61 		regfree(&search_regexp);
62 		have_compiled_regexp = FALSE;
63 	}
64 #ifndef NANO_TINY
65 	if (openfile->mark)
66 		refresh_needed = TRUE;
67 #endif
68 }
69 
70 /* Prepare the prompt and ask the user what to search for.  Keep looping
71  * as long as the user presses a toggle, and only take action and exit
72  * when <Enter> is pressed or a non-toggle shortcut was executed. */
search_init(bool replacing,bool retain_answer)73 void search_init(bool replacing, bool retain_answer)
74 {
75 	char *thedefault;
76 		/* What will be searched for when the user types just <Enter>. */
77 
78 	/* If something was searched for earlier, include it in the prompt. */
79 	if (*last_search != '\0') {
80 		char *disp = display_string(last_search, 0, COLS / 3, FALSE, FALSE);
81 
82 		thedefault = nmalloc(strlen(disp) + 7);
83 		/* We use (COLS / 3) here because we need to see more on the line. */
84 		sprintf(thedefault, " [%s%s]", disp,
85 					(breadth(last_search) > COLS / 3) ? "..." : "");
86 		free(disp);
87 	} else
88 		thedefault = copy_of("");
89 
90 	while (TRUE) {
91 		functionptrtype func;
92 		/* Ask the user what to search for (or replace). */
93 		int response = do_prompt(
94 					inhelp ? MFINDINHELP : (replacing ? MREPLACE : MWHEREIS),
95 					retain_answer ? answer : "", &search_history, edit_refresh,
96 					/* TRANSLATORS: This is the main search prompt. */
97 					"%s%s%s%s%s%s", _("Search"),
98 					/* TRANSLATORS: The next four modify the search prompt. */
99 					ISSET(CASE_SENSITIVE) ? _(" [Case Sensitive]") : "",
100 					ISSET(USE_REGEXP) ? _(" [Regexp]") : "",
101 					ISSET(BACKWARDS_SEARCH) ? _(" [Backwards]") : "",
102 					replacing ?
103 #ifndef NANO_TINY
104 					openfile->mark ? _(" (to replace) in selection") :
105 #endif
106 					_(" (to replace)") : "", thedefault);
107 
108 		/* If the search was cancelled, or we have a blank answer and
109 		 * nothing was searched for yet during this session, get out. */
110 		if (response == -1 || (response == -2 && *last_search == '\0')) {
111 			statusbar(_("Cancelled"));
112 			break;
113 		}
114 
115 		/* If Enter was pressed, prepare to do a replace or a search. */
116 		if (response == 0 || response == -2) {
117 			/* If an actual answer was typed, remember it. */
118 			if (*answer != '\0') {
119 				last_search = mallocstrcpy(last_search, answer);
120 #ifdef ENABLE_HISTORIES
121 				update_history(&search_history, answer);
122 #endif
123 			}
124 
125 			if (ISSET(USE_REGEXP) && !regexp_init(last_search))
126 				break;
127 
128 			if (replacing)
129 				ask_for_and_do_replacements();
130 			else
131 				go_looking();
132 
133 			break;
134 		}
135 
136 		retain_answer = TRUE;
137 
138 		func = func_from_key(&response);
139 
140 		/* If we're here, one of the five toggles was pressed, or
141 		 * a shortcut was executed. */
142 		if (func == case_sens_void)
143 			TOGGLE(CASE_SENSITIVE);
144 		else if (func == backwards_void)
145 			TOGGLE(BACKWARDS_SEARCH);
146 		else if (func == regexp_void)
147 			TOGGLE(USE_REGEXP);
148 		else if (func == flip_replace) {
149 			if (ISSET(VIEW_MODE)) {
150 				print_view_warning();
151 				napms(600);
152 			} else
153 				replacing = !replacing;
154 		} else if (func == flip_goto) {
155 			do_gotolinecolumn(openfile->current->lineno,
156 								openfile->placewewant + 1, TRUE, TRUE);
157 			break;
158 		} else
159 			break;
160 	}
161 
162 	tidy_up_after_search();
163 	free(thedefault);
164 }
165 
166 /* Look for needle, starting at (current, current_x).  begin is the line
167  * where we first started searching, at column begin_x.  Return 1 when we
168  * found something, 0 when nothing, and -2 on cancel.  When match_len is
169  * not NULL, set it to the length of the found string, if any. */
findnextstr(const char * needle,bool whole_word_only,int modus,size_t * match_len,bool skipone,const linestruct * begin,size_t begin_x)170 int findnextstr(const char *needle, bool whole_word_only, int modus,
171 		size_t *match_len, bool skipone, const linestruct *begin, size_t begin_x)
172 {
173 	size_t found_len = strlen(needle);
174 		/* The length of a match -- will be recomputed for a regex. */
175 	int feedback = 0;
176 		/* When bigger than zero, show and wipe the "Searching..." message. */
177 	linestruct *line = openfile->current;
178 		/* The line that we will search through now. */
179 	const char *from = line->data + openfile->current_x;
180 		/* The point in the line from where we start searching. */
181 	const char *found = NULL;
182 		/* A pointer to the location of the match, if any. */
183 	size_t found_x;
184 		/* The x coordinate of a found occurrence. */
185 	time_t lastkbcheck = time(NULL);
186 		/* The time we last looked at the keyboard. */
187 
188 	/* Set non-blocking input so that we can just peek for a Cancel. */
189 	nodelay(edit, TRUE);
190 
191 	if (begin == NULL)
192 		came_full_circle = FALSE;
193 
194 	while (TRUE) {
195 		/* When starting a new search, skip the first character, then
196 		 * (in either case) search for the needle in the current line. */
197 		if (skipone) {
198 			skipone = FALSE;
199 			if (ISSET(BACKWARDS_SEARCH) && from != line->data) {
200 				from = line->data + step_left(line->data, from - line->data);
201 				found = strstrwrapper(line->data, needle, from);
202 			} else if (!ISSET(BACKWARDS_SEARCH) && *from != '\0') {
203 				from += char_length(from);
204 				found = strstrwrapper(line->data, needle, from);
205 			}
206 		} else
207 			found = strstrwrapper(line->data, needle, from);
208 
209 		if (found != NULL) {
210 			/* When doing a regex search, compute the length of the match. */
211 			if (ISSET(USE_REGEXP))
212 				found_len = regmatches[0].rm_eo - regmatches[0].rm_so;
213 #ifdef ENABLE_SPELLER
214 			/* When we're spell checking, a match should be a separate word;
215 			 * if it's not, continue looking in the rest of the line. */
216 			if (whole_word_only && !is_separate_word(found - line->data,
217 												found_len, line->data)) {
218 				from = found + char_length(found);
219 				continue;
220 			}
221 #endif
222 			/* The match is valid. */
223 			break;
224 		}
225 
226 #ifndef NANO_TINY
227 		if (the_window_resized) {
228 			regenerate_screen();
229 			nodelay(edit, TRUE);
230 			statusbar(_("Searching..."));
231 			feedback = 1;
232 		}
233 #endif
234 		/* If we're back at the beginning, then there is no needle. */
235 		if (came_full_circle) {
236 			nodelay(edit, FALSE);
237 			return 0;
238 		}
239 
240 		/* Move to the previous or next line in the file. */
241 		line = (ISSET(BACKWARDS_SEARCH)) ? line->prev : line->next;
242 
243 		/* If we've reached the start or end of the buffer, wrap around;
244 		 * but stop when spell-checking or replacing in a region. */
245 		if (line == NULL) {
246 			if (whole_word_only || modus == INREGION) {
247 				nodelay(edit, FALSE);
248 				return 0;
249 			}
250 
251 			line = (ISSET(BACKWARDS_SEARCH)) ? openfile->filebot : openfile->filetop;
252 
253 			if (modus == JUSTFIND) {
254 				statusline(REMARK, _("Search Wrapped"));
255 				/* Delay the "Searching..." message for at least two seconds. */
256 				feedback = -2;
257 			}
258 		}
259 
260 		/* If we've reached the original starting line, take note. */
261 		if (line == begin)
262 			came_full_circle = TRUE;
263 
264 		/* Set the starting x to the start or end of the line. */
265 		from = line->data;
266 		if (ISSET(BACKWARDS_SEARCH))
267 			from += strlen(line->data);
268 
269 		/* Glance at the keyboard once every second, to check for a Cancel. */
270 		if (time(NULL) - lastkbcheck > 0) {
271 			int input = wgetch(edit);
272 
273 			lastkbcheck = time(NULL);
274 
275 			/* Consume any queued-up keystrokes, until a Cancel or nothing. */
276 			while (input != ERR) {
277 				if (input == ESC_CODE) {
278 					napms(20);
279 					input = wgetch(edit);
280 					meta_key = TRUE;
281 				} else
282 					meta_key = FALSE;
283 
284 				if (func_from_key(&input) == do_cancel) {
285 #ifndef NANO_TINY
286 					if (the_window_resized)
287 						regenerate_screen();
288 #endif
289 					statusbar(_("Cancelled"));
290 					/* Clear out the key buffer (in case a macro is running). */
291 					while (input != ERR)
292 						input = parse_kbinput(NULL);
293 					nodelay(edit, FALSE);
294 					return -2;
295 				}
296 
297 				input = wgetch(edit);
298 			}
299 
300 			if (++feedback > 0)
301 				/* TRANSLATORS: This is shown when searching takes
302 				 * more than half a second. */
303 				statusbar(_("Searching..."));
304 		}
305 	}
306 
307 	found_x = found - line->data;
308 
309 	nodelay(edit, FALSE);
310 
311 	/* Ensure that the found occurrence is not beyond the starting x. */
312 	if (came_full_circle && ((!ISSET(BACKWARDS_SEARCH) && (found_x > begin_x ||
313 						(modus == REPLACING && found_x == begin_x))) ||
314 						(ISSET(BACKWARDS_SEARCH) && found_x < begin_x)))
315 		return 0;
316 
317 	/* Set the current position to point at what we found. */
318 	openfile->current = line;
319 	openfile->current_x = found_x;
320 
321 	/* When requested, pass back the length of the match. */
322 	if (match_len != NULL)
323 		*match_len = found_len;
324 
325 #ifndef NANO_TINY
326 	if (modus == JUSTFIND && (!openfile->mark || openfile->softmark)) {
327 		spotlighted = TRUE;
328 		light_from_col = xplustabs();
329 		light_to_col = wideness(line->data, found_x + found_len);
330 		if (!ISSET(SHOW_CURSOR))
331 			hide_cursor = TRUE;
332 		edit_refresh();
333 	}
334 #endif
335 
336 	if (feedback > 0)
337 		wipe_statusbar();
338 
339 	return 1;
340 }
341 
342 /* Ask for a string and then search forward for it. */
do_search_forward(void)343 void do_search_forward(void)
344 {
345 	UNSET(BACKWARDS_SEARCH);
346 	search_init(FALSE, FALSE);
347 }
348 
349 /* Ask for a string and then search backwards for it. */
do_search_backward(void)350 void do_search_backward(void)
351 {
352 	SET(BACKWARDS_SEARCH);
353 	search_init(FALSE, FALSE);
354 }
355 
356 /* Search for the last string without prompting. */
do_research(void)357 void do_research(void)
358 {
359 #ifdef ENABLE_HISTORIES
360 	/* If nothing was searched for yet during this run of nano, but
361 	 * there is a search history, take the most recent item. */
362 	if (*last_search == '\0' && searchbot->prev != NULL)
363 		last_search = mallocstrcpy(last_search, searchbot->prev->data);
364 #endif
365 
366 	if (*last_search == '\0') {
367 		statusline(AHEM, _("No current search pattern"));
368 		return;
369 	}
370 
371 	if (ISSET(USE_REGEXP) && !regexp_init(last_search))
372 		return;
373 
374 	/* Use the search-menu key bindings, to allow cancelling. */
375 	currmenu = MWHEREIS;
376 
377 	if (LINES > 1)
378 		wipe_statusbar();
379 
380 	go_looking();
381 
382 	tidy_up_after_search();
383 }
384 
385 /* Search in the backward direction for the next occurrence. */
do_findprevious(void)386 void do_findprevious(void)
387 {
388 	SET(BACKWARDS_SEARCH);
389 	do_research();
390 }
391 
392 /* Search in the forward direction for the next occurrence. */
do_findnext(void)393 void do_findnext(void)
394 {
395 	UNSET(BACKWARDS_SEARCH);
396 	do_research();
397 }
398 
399 /* Report on the status bar that the given string was not found. */
not_found_msg(const char * str)400 void not_found_msg(const char *str)
401 {
402 	char *disp = display_string(str, 0, (COLS / 2) + 1, FALSE, FALSE);
403 	size_t numchars = actual_x(disp, wideness(disp, COLS / 2));
404 
405 	statusline(AHEM, _("\"%.*s%s\" not found"), numchars, disp,
406 						(disp[numchars] == '\0') ? "" : "...");
407 	free(disp);
408 }
409 
410 /* Search for the global string 'last_search'.  Inform the user when
411  * the string occurs only once. */
go_looking(void)412 void go_looking(void)
413 {
414 	linestruct *was_current = openfile->current;
415 	size_t was_current_x = openfile->current_x;
416 
417 //#define TIMEIT  12
418 #ifdef TIMEIT
419 #include <time.h>
420 	clock_t start = clock();
421 #endif
422 
423 	came_full_circle = FALSE;
424 
425 	didfind = findnextstr(last_search, FALSE, JUSTFIND, NULL, TRUE,
426 								openfile->current, openfile->current_x);
427 
428 	/* If we found something, and we're back at the exact same spot
429 	 * where we started searching, then this is the only occurrence. */
430 	if (didfind == 1 && openfile->current == was_current &&
431 						openfile->current_x == was_current_x)
432 		statusline(REMARK, _("This is the only occurrence"));
433 	else if (didfind == 0)
434 		not_found_msg(last_search);
435 
436 #ifdef TIMEIT
437 	statusline(INFO, "Took: %.2f", (double)(clock() - start) / CLOCKS_PER_SEC);
438 #endif
439 
440 	edit_redraw(was_current, CENTERING);
441 }
442 
443 /* Calculate the size of the replacement text, taking possible
444  * subexpressions \1 to \9 into account.  Return the replacement
445  * text in the passed string only when create is TRUE. */
replace_regexp(char * string,bool create)446 int replace_regexp(char *string, bool create)
447 {
448 	const char *c = answer;
449 	size_t replacement_size = 0;
450 
451 	/* Iterate through the replacement text to handle subexpression
452 	 * replacement using \1, \2, \3, etc. */
453 	while (*c != '\0') {
454 		int num = (*(c + 1) - '0');
455 
456 		if (*c != '\\' || num < 1 || num > 9 || num > search_regexp.re_nsub) {
457 			if (create)
458 				*string++ = *c;
459 			c++;
460 			replacement_size++;
461 		} else {
462 			size_t i = regmatches[num].rm_eo - regmatches[num].rm_so;
463 
464 			/* Skip over the replacement expression. */
465 			c += 2;
466 
467 			/* But add the length of the subexpression to new_size. */
468 			replacement_size += i;
469 
470 			/* And if create is TRUE, append the result of the
471 			 * subexpression match to the new line. */
472 			if (create) {
473 				strncpy(string, openfile->current->data + regmatches[num].rm_so, i);
474 				string += i;
475 			}
476 		}
477 	}
478 
479 	if (create)
480 		*string = '\0';
481 
482 	return replacement_size;
483 }
484 
485 /* Return a copy of the current line with one needle replaced. */
replace_line(const char * needle)486 char *replace_line(const char *needle)
487 {
488 	size_t new_size = strlen(openfile->current->data) + 1;
489 	size_t match_len;
490 	char *copy;
491 
492 	/* First adjust the size of the new line for the change. */
493 	if (ISSET(USE_REGEXP)) {
494 		match_len = regmatches[0].rm_eo - regmatches[0].rm_so;
495 		new_size += replace_regexp(NULL, FALSE) - match_len;
496 	} else {
497 		match_len = strlen(needle);
498 		new_size += strlen(answer) - match_len;
499 	}
500 
501 	copy = nmalloc(new_size);
502 
503 	/* Copy the head of the original line. */
504 	strncpy(copy, openfile->current->data, openfile->current_x);
505 
506 	/* Add the replacement text. */
507 	if (ISSET(USE_REGEXP))
508 		replace_regexp(copy + openfile->current_x, TRUE);
509 	else
510 		strcpy(copy + openfile->current_x, answer);
511 
512 	/* Copy the tail of the original line. */
513 	strcat(copy, openfile->current->data + openfile->current_x + match_len);
514 
515 	return copy;
516 }
517 
518 /* Step through each occurrence of the search string and prompt the user
519  * before replacing it.  We seek for needle, and replace it with answer.
520  * The parameters real_current and real_current_x are needed in order to
521  * allow the cursor position to be updated when a word before the cursor
522  * is replaced by a shorter word.  Return -1 if needle isn't found, -2 if
523  * the seeking is aborted, else the number of replacements performed. */
do_replace_loop(const char * needle,bool whole_word_only,const linestruct * real_current,size_t * real_current_x)524 ssize_t do_replace_loop(const char *needle, bool whole_word_only,
525 		const linestruct *real_current, size_t *real_current_x)
526 {
527 	ssize_t numreplaced = -1;
528 	size_t match_len;
529 	bool replaceall = FALSE;
530 	bool skipone = ISSET(BACKWARDS_SEARCH);
531 	int modus = REPLACING;
532 #ifndef NANO_TINY
533 	linestruct *was_mark = openfile->mark;
534 	linestruct *top, *bot;
535 	size_t top_x, bot_x;
536 	bool right_side_up = (openfile->mark && mark_is_before_cursor());
537 
538 	/* If the mark is on, frame the region, and turn the mark off. */
539 	if (openfile->mark) {
540 		get_region(&top, &top_x, &bot, &bot_x);
541 		openfile->mark = NULL;
542 		modus = INREGION;
543 
544 		/* Start either at the top or the bottom of the marked region. */
545 		if (!ISSET(BACKWARDS_SEARCH)) {
546 			openfile->current = top;
547 			openfile->current_x = top_x;
548 		} else {
549 			openfile->current = bot;
550 			openfile->current_x = bot_x;
551 		}
552 	}
553 #endif
554 
555 	came_full_circle = FALSE;
556 
557 	while (TRUE) {
558 		int choice = 0;
559 		int result = findnextstr(needle, whole_word_only, modus,
560 						&match_len, skipone, real_current, *real_current_x);
561 
562 		/* If nothing more was found, or the user aborted, stop looping. */
563 		if (result < 1) {
564 			if (result < 0)
565 				numreplaced = -2;  /* It's a Cancel instead of Not found. */
566 			break;
567 		}
568 
569 #ifndef NANO_TINY
570 		/* An occurrence outside of the marked region means we're done. */
571 		if (was_mark && (openfile->current->lineno > bot->lineno ||
572 								openfile->current->lineno < top->lineno ||
573 								(openfile->current == bot &&
574 								openfile->current_x + match_len > bot_x) ||
575 								(openfile->current == top &&
576 								openfile->current_x < top_x)))
577 			break;
578 #endif
579 
580 		/* Indicate that we found the search string. */
581 		if (numreplaced == -1)
582 			numreplaced = 0;
583 
584 		if (!replaceall) {
585 			spotlighted = TRUE;
586 			light_from_col = xplustabs();
587 			light_to_col = wideness(openfile->current->data,
588 										openfile->current_x + match_len);
589 
590 			/* Refresh the edit window, scrolling it if necessary. */
591 			edit_refresh();
592 
593 			/* TRANSLATORS: This is a prompt. */
594 			choice = do_yesno_prompt(TRUE, _("Replace this instance?"));
595 
596 			spotlighted = FALSE;
597 
598 			if (choice == -1)  /* The replacing was cancelled. */
599 				break;
600 			else if (choice == 2)
601 				replaceall = TRUE;
602 
603 			/* When "No" or moving backwards, the search routine should
604 			 * first move one character further before continuing. */
605 			skipone = (choice == 0 || ISSET(BACKWARDS_SEARCH));
606 		}
607 
608 		if (choice == 1 || replaceall) {  /* Yes, replace it. */
609 			size_t length_change;
610 			char *altered;
611 
612 			altered = replace_line(needle);
613 
614 			length_change = strlen(altered) - strlen(openfile->current->data);
615 
616 #ifndef NANO_TINY
617 			add_undo(REPLACE, NULL);
618 
619 			/* If the mark was on and it was located after the cursor,
620 			 * then adjust its x position for any text length changes. */
621 			if (was_mark && !right_side_up) {
622 				if (openfile->current == was_mark &&
623 						openfile->mark_x > openfile->current_x) {
624 					if (openfile->mark_x < openfile->current_x + match_len)
625 						openfile->mark_x = openfile->current_x;
626 					else
627 						openfile->mark_x += length_change;
628 					bot_x = openfile->mark_x;
629 				}
630 			}
631 
632 			/* If the mark was not on or it was before the cursor, then
633 			 * adjust the cursor's x position for any text length changes. */
634 			if (!was_mark || right_side_up)
635 #endif
636 			{
637 				if (openfile->current == real_current &&
638 						openfile->current_x < *real_current_x) {
639 					if (*real_current_x < openfile->current_x + match_len)
640 						*real_current_x = openfile->current_x + match_len;
641 					*real_current_x += length_change;
642 #ifndef NANO_TINY
643 					bot_x = *real_current_x;
644 #endif
645 				}
646 			}
647 
648 			/* Don't find the same zero-length or BOL match again. */
649 			if (match_len == 0 || (*needle == '^' && ISSET(USE_REGEXP)))
650 				skipone = TRUE;
651 
652 			/* When moving forward, put the cursor just after the replacement
653 			 * text, so that searching will continue there. */
654 			if (!ISSET(BACKWARDS_SEARCH))
655 				openfile->current_x += match_len + length_change;
656 
657 			/* Update the file size, and put the changed line into place. */
658 			openfile->totsize += mbstrlen(altered) - mbstrlen(openfile->current->data);
659 			free(openfile->current->data);
660 			openfile->current->data = altered;
661 
662 			set_modified();
663 			as_an_at = TRUE;
664 			numreplaced++;
665 		}
666 	}
667 
668 	if (numreplaced == -1)
669 		not_found_msg(needle);
670 
671 #ifndef NANO_TINY
672 	openfile->mark = was_mark;
673 #endif
674 
675 	/* If "automatic newline" is enabled, and text has been added to the
676 	 * magic line, make a new magic line. */
677 	if (!ISSET(NO_NEWLINES) && openfile->filebot->data[0] != '\0')
678 		new_magicline();
679 
680 	return numreplaced;
681 }
682 
683 /* Replace a string. */
do_replace(void)684 void do_replace(void)
685 {
686 	if (ISSET(VIEW_MODE))
687 		print_view_warning();
688 	else {
689 		UNSET(BACKWARDS_SEARCH);
690 		search_init(TRUE, FALSE);
691 	}
692 }
693 
694 /* Ask the user what to replace the search string with, and do the replacements. */
ask_for_and_do_replacements(void)695 void ask_for_and_do_replacements(void)
696 {
697 	linestruct *was_edittop = openfile->edittop;
698 	size_t was_firstcolumn = openfile->firstcolumn;
699 	linestruct *beginline = openfile->current;
700 	size_t begin_x = openfile->current_x;
701 	ssize_t numreplaced;
702 	int response = do_prompt(MREPLACEWITH, "", &replace_history,
703 							/* TRANSLATORS: This is a prompt. */
704 							edit_refresh, _("Replace with"));
705 
706 #ifdef ENABLE_HISTORIES
707 	/* When not "", add the replace string to the replace history list. */
708 	if (response == 0)
709 		update_history(&replace_history, answer);
710 #endif
711 
712 	/* When cancelled, or when a function was run, get out. */
713 	if (response == -1) {
714 		statusbar(_("Cancelled"));
715 		return;
716 	} else if (response > 0)
717 		return;
718 
719 	numreplaced = do_replace_loop(last_search, FALSE, beginline, &begin_x);
720 
721 	/* Restore where we were. */
722 	openfile->edittop = was_edittop;
723 	openfile->firstcolumn = was_firstcolumn;
724 	openfile->current = beginline;
725 	openfile->current_x = begin_x;
726 
727 	edit_refresh();
728 
729 	if (numreplaced >= 0)
730 		statusline(REMARK, P_("Replaced %zd occurrence",
731 					"Replaced %zd occurrences", numreplaced), numreplaced);
732 }
733 
734 /* Go to the specified line and x position. */
goto_line_posx(ssize_t line,size_t pos_x)735 void goto_line_posx(ssize_t line, size_t pos_x)
736 {
737 	for (openfile->current = openfile->filetop; line > 1 &&
738 				openfile->current != openfile->filebot; line--)
739 		openfile->current = openfile->current->next;
740 
741 	openfile->current_x = pos_x;
742 	openfile->placewewant = xplustabs();
743 
744 	refresh_needed = TRUE;
745 }
746 
747 /* Go to the specified line and column, or ask for them if interactive
748  * is TRUE.  In the latter case also update the screen afterwards.
749  * Note that both the line and column number should be one-based. */
do_gotolinecolumn(ssize_t line,ssize_t column,bool retain_answer,bool interactive)750 void do_gotolinecolumn(ssize_t line, ssize_t column, bool retain_answer,
751 		bool interactive)
752 {
753 	if (interactive) {
754 		/* Ask for the line and column. */
755 		int response = do_prompt(MGOTOLINE, retain_answer ? answer : "", NULL,
756 						/* TRANSLATORS: This is a prompt. */
757 						edit_refresh, _("Enter line number, column number"));
758 
759 		/* If the user cancelled or gave a blank answer, get out. */
760 		if (response < 0) {
761 			statusbar(_("Cancelled"));
762 			return;
763 		}
764 
765 		if (func_from_key(&response) == flip_goto) {
766 			UNSET(BACKWARDS_SEARCH);
767 			/* Switch to searching but retain what the user typed so far. */
768 			search_init(FALSE, TRUE);
769 			return;
770 		}
771 
772 		/* If a function was executed, we're done here. */
773 		if (response > 0)
774 			return;
775 
776 		/* Try to extract one or two numbers from the user's response. */
777 		if (!parse_line_column(answer, &line, &column)) {
778 			statusline(AHEM, _("Invalid line or column number"));
779 			return;
780 		}
781 	} else {
782 		if (line == 0)
783 			line = openfile->current->lineno;
784 
785 		if (column == 0)
786 			column = openfile->placewewant + 1;
787 	}
788 
789 	/* Take a negative line number to mean: from the end of the file. */
790 	if (line < 0)
791 		line = openfile->filebot->lineno + line + 1;
792 	if (line < 1)
793 		line = 1;
794 
795 	/* Iterate to the requested line. */
796 	for (openfile->current = openfile->filetop; line > 1 &&
797 				openfile->current != openfile->filebot; line--)
798 		openfile->current = openfile->current->next;
799 
800 	/* Take a negative column number to mean: from the end of the line. */
801 	if (column < 0)
802 		column = breadth(openfile->current->data) + column + 2;
803 	if (column < 1)
804 		column = 1;
805 
806 	/* Set the x position that corresponds to the requested column. */
807 	openfile->current_x = actual_x(openfile->current->data, column - 1);
808 	openfile->placewewant = column - 1;
809 
810 #ifndef NANO_TINY
811 	if (ISSET(SOFTWRAP) && openfile->placewewant / editwincols >
812 						breadth(openfile->current->data) / editwincols)
813 		openfile->placewewant = breadth(openfile->current->data);
814 #endif
815 
816 	/* When the position was manually given, center the target line. */
817 	if (interactive) {
818 		adjust_viewport(CENTERING);
819 		refresh_needed = TRUE;
820 	} else {
821 		int rows_from_tail;
822 
823 #ifndef NANO_TINY
824 		if (ISSET(SOFTWRAP)) {
825 			linestruct *currentline = openfile->current;
826 			size_t leftedge = leftedge_for(xplustabs(), openfile->current);
827 
828 			rows_from_tail = (editwinrows / 2) - go_forward_chunks(
829 								editwinrows / 2, &currentline, &leftedge);
830 		} else
831 #endif
832 			rows_from_tail = openfile->filebot->lineno -
833 								openfile->current->lineno;
834 
835 		/* If the target line is close to the tail of the file, put the last
836 		 * line or chunk on the bottom line of the screen; otherwise, just
837 		 * center the target line. */
838 		if (rows_from_tail < editwinrows / 2 && !ISSET(JUMPY_SCROLLING)) {
839 			openfile->current_y = editwinrows - 1 - rows_from_tail;
840 			adjust_viewport(STATIONARY);
841 		} else
842 			adjust_viewport(CENTERING);
843 	}
844 }
845 
846 /* Go to the specified line and column, asking for them beforehand. */
do_gotolinecolumn_void(void)847 void do_gotolinecolumn_void(void)
848 {
849 	do_gotolinecolumn(openfile->current->lineno,
850 						openfile->placewewant + 1, FALSE, TRUE);
851 }
852 
853 #ifndef NANO_TINY
854 /* Search, starting from the current position, for any of the two characters
855  * in bracket_pair.  If reverse is TRUE, search backwards, otherwise forwards.
856  * Return TRUE when one of the brackets was found, and FALSE otherwise. */
find_a_bracket(bool reverse,const char * bracket_pair)857 bool find_a_bracket(bool reverse, const char *bracket_pair)
858 {
859 	linestruct *line = openfile->current;
860 	const char *pointer, *found;
861 
862 	if (reverse) {
863 		/* First step away from the current bracket. */
864 		if (openfile->current_x == 0) {
865 			line = line->prev;
866 			if (line == NULL)
867 				return FALSE;
868 			pointer = line->data + strlen(line->data);
869 		} else
870 			pointer = line->data + step_left(line->data, openfile->current_x);
871 
872 		/* Now seek for any of the two brackets we are interested in. */
873 		while (!(found = mbrevstrpbrk(line->data, bracket_pair, pointer))) {
874 			line = line->prev;
875 			if (line == NULL)
876 				return FALSE;
877 			pointer = line->data + strlen(line->data);
878 		}
879 	} else {
880 		pointer = line->data + step_right(line->data, openfile->current_x);
881 
882 		while (!(found = mbstrpbrk(pointer, bracket_pair))) {
883 			line = line->next;
884 			if (line == NULL)
885 				return FALSE;
886 			pointer = line->data;
887 		}
888 	}
889 
890 	/* Set the current position to the found bracket. */
891 	openfile->current = line;
892 	openfile->current_x = found - line->data;
893 
894 	return TRUE;
895 }
896 
897 /* Search for a match to the bracket at the current cursor position, if
898  * there is one. */
do_find_bracket(void)899 void do_find_bracket(void)
900 {
901 	linestruct *was_current = openfile->current;
902 	size_t was_current_x = openfile->current_x;
903 		/* The current cursor position, in case we don't find a complement. */
904 	const char *ch;
905 		/* The location in matchbrackets of the bracket under the cursor. */
906 	int ch_len;
907 		/* The length of ch in bytes. */
908 	const char *wanted_ch;
909 		/* The location in matchbrackets of the complementing bracket. */
910 	int wanted_ch_len;
911 		/* The length of wanted_ch in bytes. */
912 	char bracket_pair[MAXCHARLEN * 2 + 1];
913 		/* The pair of characters in ch and wanted_ch. */
914 	size_t halfway = 0;
915 		/* The index in matchbrackets where the closing brackets start. */
916 	size_t charcount = mbstrlen(matchbrackets) / 2;
917 		/* Half the number of characters in matchbrackets. */
918 	size_t balance = 1;
919 		/* The initial bracket count. */
920 	bool reverse;
921 		/* The direction we search. */
922 
923 	ch = mbstrchr(matchbrackets, openfile->current->data + openfile->current_x);
924 
925 	if (ch == NULL) {
926 		statusline(AHEM, _("Not a bracket"));
927 		return;
928 	}
929 
930 	/* Find the halfway point in matchbrackets, where the closing ones start. */
931 	for (size_t i = 0; i < charcount; i++)
932 		halfway += char_length(matchbrackets + halfway);
933 
934 	/* When on a closing bracket, we have to search backwards for a matching
935 	 * opening bracket; otherwise, forward for a matching closing bracket. */
936 	reverse = (ch >= (matchbrackets + halfway));
937 
938 	/* Step half the number of total characters either backwards or forwards
939 	 * through matchbrackets to find the wanted complementary bracket. */
940 	wanted_ch = ch;
941 	while (charcount-- > 0) {
942 		if (reverse)
943 			wanted_ch = matchbrackets + step_left(matchbrackets,
944 													wanted_ch - matchbrackets);
945 		else
946 			wanted_ch += char_length(wanted_ch);
947 	}
948 
949 	ch_len = char_length(ch);
950 	wanted_ch_len = char_length(wanted_ch);
951 
952 	/* Copy the two complementary brackets into a single string. */
953 	strncpy(bracket_pair, ch, ch_len);
954 	strncpy(bracket_pair + ch_len, wanted_ch, wanted_ch_len);
955 	bracket_pair[ch_len + wanted_ch_len] = '\0';
956 
957 	while (find_a_bracket(reverse, bracket_pair)) {
958 		/* Increment/decrement balance for an identical/other bracket. */
959 		balance += (strncmp(openfile->current->data + openfile->current_x,
960 							ch, ch_len) == 0) ? 1 : -1;
961 
962 		/* When balance reached zero, we've found the complementary bracket. */
963 		if (balance == 0) {
964 			edit_redraw(was_current, FLOWING);
965 			return;
966 		}
967 	}
968 
969 	statusline(AHEM, _("No matching bracket"));
970 
971 	/* Restore the cursor position. */
972 	openfile->current = was_current;
973 	openfile->current_x = was_current_x;
974 }
975 
976 /* Place an anchor at the current line when none exists, otherwise remove it. */
put_or_lift_anchor(void)977 void put_or_lift_anchor(void)
978 {
979 	openfile->current->has_anchor = !openfile->current->has_anchor;
980 
981 	update_line(openfile->current, openfile->current_x);
982 
983 	if (openfile->current->has_anchor)
984 		statusline(REMARK, _("Placed anchor"));
985 	else
986 		statusline(REMARK, _("Removed anchor"));
987 }
988 
989 /* Make the given line the current line, or report the anchoredness. */
go_to_and_confirm(linestruct * line)990 void go_to_and_confirm(linestruct *line)
991 {
992 	linestruct *was_current = openfile->current;
993 
994 	if (line != openfile->current) {
995 		openfile->current = line;
996 		openfile->current_x = 0;
997 		edit_redraw(was_current, CENTERING);
998 		statusbar(_("Jumped to anchor"));
999 	} else if (openfile->current->has_anchor)
1000 		statusline(REMARK, _("This is the only anchor"));
1001 	else
1002 		statusline(AHEM, _("There are no anchors"));
1003 }
1004 
1005 /* Jump to the first anchor before the current line; wrap around at the top. */
to_prev_anchor(void)1006 void to_prev_anchor(void)
1007 {
1008 	linestruct *line = openfile->current;
1009 
1010 	do { line = (line->prev) ? line->prev : openfile->filebot;
1011 	} while (!line->has_anchor && line != openfile->current);
1012 
1013 	go_to_and_confirm(line);
1014 }
1015 
1016 /* Jump to the first anchor after the current line; wrap around at the bottom. */
to_next_anchor(void)1017 void to_next_anchor(void)
1018 {
1019 	linestruct *line = openfile->current;
1020 
1021 	do { line = (line->next) ? line->next : openfile->filetop;
1022 	} while (!line->has_anchor && line != openfile->current);
1023 
1024 	go_to_and_confirm(line);
1025 }
1026 #endif /* !NANO_TINY */
1027