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