history.c (17860B)
1 /************************************************************************** 2 * history.c -- This file is part of GNU nano. * 3 * * 4 * Copyright (C) 2003-2011, 2013-2025 Free Software Foundation, Inc. * 5 * Copyright (C) 2016, 2017, 2019, 2025 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 #ifdef ENABLE_HISTORIES 25 26 #include <errno.h> 27 #include <string.h> 28 29 #ifndef SEARCH_HISTORY 30 #define SEARCH_HISTORY "search_history" 31 #endif 32 33 #ifndef POSITION_HISTORY 34 #define POSITION_HISTORY "filepos_history" 35 #endif 36 37 static bool history_changed = FALSE; 38 /* Whether any of the history lists has changed. */ 39 static char *poshistname = NULL; 40 /* The name of the positions-history file. */ 41 static time_t latest_timestamp = 942927132; 42 /* The last time the positions-history file was written. */ 43 static poshiststruct *position_history = NULL; 44 /* The list of filenames with their last cursor positions. */ 45 46 /* Initialize the lists of historical search and replace strings 47 * and the list of historical executed commands. */ 48 void history_init(void) 49 { 50 search_history = make_new_node(NULL); 51 search_history->data = copy_of(""); 52 searchtop = search_history; 53 searchbot = search_history; 54 55 replace_history = make_new_node(NULL); 56 replace_history->data = copy_of(""); 57 replacetop = replace_history; 58 replacebot = replace_history; 59 60 execute_history = make_new_node(NULL); 61 execute_history->data = copy_of(""); 62 executetop = execute_history; 63 executebot = execute_history; 64 } 65 66 /* Reset the pointer into the history list that contains item to the bottom. */ 67 void reset_history_pointer_for(const linestruct *item) 68 { 69 if (item == search_history) 70 search_history = searchbot; 71 else if (item == replace_history) 72 replace_history = replacebot; 73 else if (item == execute_history) 74 execute_history = executebot; 75 } 76 77 /* Return from the history list that starts at start and ends at end 78 * the first node that contains the first len characters of the given 79 * text, or NULL if there is no such node. */ 80 linestruct *find_in_history(const linestruct *start, const linestruct *end, 81 const char *text, size_t len) 82 { 83 const linestruct *item; 84 85 for (item = start; item != end->prev && item != NULL; item = item->prev) { 86 if (strncmp(item->data, text, len) == 0) 87 return (linestruct *)item; 88 } 89 90 return NULL; 91 } 92 93 /* Update a history list (the one in which item is the current position) 94 * with a fresh string text. That is: add text, or move it to the end. */ 95 void update_history(linestruct **item, const char *text, bool avoid_duplicates) 96 { 97 linestruct **htop = NULL, **hbot = NULL; 98 linestruct *thesame = NULL; 99 100 if (*item == search_history) { 101 htop = &searchtop; 102 hbot = &searchbot; 103 } else if (*item == replace_history) { 104 htop = &replacetop; 105 hbot = &replacebot; 106 } else if (*item == execute_history) { 107 htop = &executetop; 108 hbot = &executebot; 109 } 110 111 /* When requested, check if the string is already in the history. */ 112 if (avoid_duplicates) 113 thesame = find_in_history(*hbot, *htop, text, HIGHEST_POSITIVE); 114 115 /* If an identical string was found, delete that item. */ 116 if (thesame) { 117 linestruct *after = thesame->next; 118 119 /* If the string is at the head of the list, move the head. */ 120 if (thesame == *htop) 121 *htop = after; 122 123 unlink_node(thesame); 124 renumber_from(after); 125 } 126 127 /* If the history is full, delete the oldest item (the one at the 128 * head of the list), to make room for a new item at the end. */ 129 if ((*hbot)->lineno == MAX_SEARCH_HISTORY + 1) { 130 linestruct *oldest = *htop; 131 132 *htop = (*htop)->next; 133 unlink_node(oldest); 134 renumber_from(*htop); 135 } 136 137 /* Store the fresh string in the last item, then create a new item. */ 138 (*hbot)->data = mallocstrcpy((*hbot)->data, text); 139 splice_node(*hbot, make_new_node(*hbot)); 140 *hbot = (*hbot)->next; 141 (*hbot)->data = copy_of(""); 142 143 /* Indicate that the history needs to be saved on exit. */ 144 history_changed = TRUE; 145 146 /* Set the current position in the list to the bottom. */ 147 *item = *hbot; 148 } 149 150 #ifdef ENABLE_TABCOMP 151 /* Go backward through one of three history lists, starting at item *here, 152 * searching for a string that is a tab completion of the given string, 153 * looking at only its first len characters. When found, make *here point 154 * at the item and return its string; otherwise, just return the string. */ 155 char *get_history_completion(linestruct **here, char *string, size_t len) 156 { 157 linestruct *htop = NULL, *hbot = NULL; 158 linestruct *item; 159 160 if (*here == search_history) { 161 htop = searchtop; 162 hbot = searchbot; 163 } else if (*here == replace_history) { 164 htop = replacetop; 165 hbot = replacebot; 166 } else if (*here == execute_history) { 167 htop = executetop; 168 hbot = executebot; 169 } 170 171 /* First search from the current position to the top of the list 172 * for a match of len characters. Skip over an exact match. */ 173 item = find_in_history((*here)->prev, htop, string, len); 174 175 while (item != NULL && strcmp(item->data, string) == 0) 176 item = find_in_history(item->prev, htop, string, len); 177 178 if (item) { 179 *here = item; 180 return mallocstrcpy(string, item->data); 181 } 182 183 /* Now search from the bottom of the list to the original position. */ 184 item = find_in_history(hbot, *here, string, len); 185 186 while (item != NULL && strcmp(item->data, string) == 0) 187 item = find_in_history(item->prev, *here, string, len); 188 189 if (item) { 190 *here = item; 191 return mallocstrcpy(string, item->data); 192 } 193 194 /* When no useful match was found, simply return the given string. */ 195 return (char *)string; 196 } 197 #endif /* ENABLE_TABCOMP */ 198 199 /* Check whether we have or could make a directory for history files. */ 200 bool have_statedir(void) 201 { 202 const char *xdgdatadir; 203 struct stat dirinfo; 204 205 get_homedir(); 206 207 if (homedir != NULL) { 208 statedir = concatenate(homedir, "/.nano/"); 209 210 if (stat(statedir, &dirinfo) == 0 && S_ISDIR(dirinfo.st_mode)) { 211 poshistname = concatenate(statedir, POSITION_HISTORY); 212 return TRUE; 213 } 214 } 215 216 free(statedir); 217 xdgdatadir = getenv("XDG_DATA_HOME"); 218 219 if (homedir == NULL && xdgdatadir == NULL) 220 return FALSE; 221 222 if (xdgdatadir != NULL) 223 statedir = concatenate(xdgdatadir, "/nano/"); 224 else 225 statedir = concatenate(homedir, "/.local/share/nano/"); 226 227 if (stat(statedir, &dirinfo) == -1) { 228 if (xdgdatadir == NULL) { 229 char *statepath = concatenate(homedir, "/.local"); 230 mkdir(statepath, S_IRWXU | S_IRWXG | S_IRWXO); 231 free(statepath); 232 statepath = concatenate(homedir, "/.local/share"); 233 mkdir(statepath, S_IRWXU); 234 free(statepath); 235 } 236 if (mkdir(statedir, S_IRWXU) == -1) { 237 jot_error(N_("Unable to create directory %s: %s\n" 238 "It is required for saving/loading " 239 "search history or cursor positions.\n"), 240 statedir, strerror(errno)); 241 return FALSE; 242 } 243 } else if (!S_ISDIR(dirinfo.st_mode)) { 244 jot_error(N_("Path %s is not a directory and needs to be.\n" 245 "Nano will be unable to load or save " 246 "search history or cursor positions.\n"), 247 statedir); 248 return FALSE; 249 } 250 251 poshistname = concatenate(statedir, POSITION_HISTORY); 252 return TRUE; 253 } 254 255 /* Load the histories for Search, Replace With, and Execute Command. */ 256 void load_history(void) 257 { 258 char *histname = concatenate(statedir, SEARCH_HISTORY); 259 FILE *histfile = fopen(histname, "rb"); 260 261 /* If reading an existing file failed, don't save history when we quit. */ 262 if (histfile == NULL && errno != ENOENT) { 263 jot_error(N_("Error reading %s: %s"), histname, strerror(errno)); 264 UNSET(HISTORYLOG); 265 } 266 267 if (histfile == NULL) { 268 free(histname); 269 return; 270 } 271 272 linestruct **history = &search_history; 273 char *stanza = NULL; 274 size_t dummy = 0; 275 ssize_t read; 276 277 /* Load the three history lists (first search, then replace, then execute) 278 * from oldest entry to newest. Between two lists there is an empty line. */ 279 while ((read = getline(&stanza, &dummy, histfile)) > 0) { 280 stanza[--read] = '\0'; 281 if (read > 0) { 282 recode_NUL_to_LF(stanza, read); 283 update_history(history, stanza, IGNORE_DUPLICATES); 284 } else if (history == &search_history) 285 history = &replace_history; 286 else 287 history = &execute_history; 288 } 289 290 if (fclose(histfile) == EOF) 291 jot_error(N_("Error reading %s: %s"), histname, strerror(errno)); 292 293 free(histname); 294 free(stanza); 295 296 /* Reading in the lists has marked them as changed; undo this side effect. */ 297 history_changed = FALSE; 298 } 299 300 /* Write the lines of a history list, starting at head, from oldest to newest, 301 * to the given file. Return TRUE if writing succeeded, and FALSE otherwise. */ 302 bool write_list(const linestruct *head, FILE *histfile) 303 { 304 const linestruct *item; 305 306 for (item = head; item != NULL; item = item->next) { 307 /* Decode 0x0A bytes as embedded NULs. */ 308 size_t length = recode_LF_to_NUL(item->data); 309 310 if (fwrite(item->data, 1, length, histfile) < length) 311 return FALSE; 312 if (putc('\n', histfile) == EOF) 313 return FALSE; 314 } 315 316 return TRUE; 317 } 318 319 /* Save the histories for Search, Replace With, and Execute Command. */ 320 void save_history(void) 321 { 322 char *histname; 323 FILE *histfile; 324 325 /* If the histories are unchanged, don't bother saving them. */ 326 if (!history_changed) 327 return; 328 329 histname = concatenate(statedir, SEARCH_HISTORY); 330 histfile = fopen(histname, "wb"); 331 332 if (histfile == NULL) { 333 jot_error(N_("Error writing %s: %s"), histname, strerror(errno)); 334 free(histname); 335 return; 336 } 337 338 /* Don't allow others to read or write the history file. */ 339 if (chmod(histname, S_IRUSR | S_IWUSR) < 0) 340 jot_error(N_("Cannot limit permissions on %s: %s"), histname, strerror(errno)); 341 342 if (!write_list(searchtop, histfile) || !write_list(replacetop, histfile) || 343 !write_list(executetop, histfile)) 344 jot_error(N_("Error writing %s: %s"), histname, strerror(errno)); 345 346 if (fclose(histfile) == EOF) 347 jot_error(N_("Error writing %s: %s"), histname, strerror(errno)); 348 349 free(histname); 350 } 351 352 /* Return as a string... the line numbers of the lines with an anchor. */ 353 char *stringify_anchors(void) 354 { 355 char *string = copy_of(""); 356 #ifndef NANO_TINY 357 char number[24]; 358 359 for (linestruct *line = openfile->filetop; line != NULL; line = line->next) 360 if (line->has_anchor) { 361 sprintf(number, "%zi ", line->lineno); 362 string = nrealloc(string, strlen(string) + strlen(number) + 1); 363 strcat(string, number); 364 } 365 #endif 366 return string; 367 } 368 369 /* Set an anchor for each line number in the given string. */ 370 void restore_anchors(char *string) 371 { 372 #ifndef NANO_TINY 373 linestruct *line = openfile->filetop; 374 ssize_t number; 375 char *space; 376 377 while (*string) { 378 if ((space = strchr(string, ' ')) == NULL) 379 return; 380 *space = '\0'; 381 number = atoi(string); 382 string = space + 1; 383 384 while (line->lineno < number) 385 if ((line = line->next) == NULL) 386 return; 387 388 line->has_anchor = TRUE; 389 } 390 #endif 391 } 392 393 /* Load the recorded cursor positions for files that were edited. */ 394 void load_poshistory(void) 395 { 396 FILE *histfile = fopen(poshistname, "rb"); 397 398 /* If reading an existing file failed, don't save history when we quit. */ 399 if (histfile == NULL && errno != ENOENT) { 400 jot_error(N_("Error reading %s: %s"), poshistname, strerror(errno)); 401 UNSET(POSITIONLOG); 402 } 403 404 if (histfile == NULL) 405 return; 406 407 poshiststruct *lastitem = NULL; 408 poshiststruct *newitem; 409 char *stanza, *lineptr, *columnptr; 410 char *phrase = NULL; 411 struct stat fileinfo; 412 size_t dummy = 0; 413 int count = 0; 414 ssize_t length; 415 416 /* Read and parse each line, and store the extracted data. */ 417 while (count++ < 200 && (length = getline(&phrase, &dummy, histfile)) > 1) { 418 stanza = strchr(phrase, '/'); 419 length -= (stanza ? stanza - phrase : 0); 420 421 /* Decode NULs as embedded newlines. */ 422 recode_NUL_to_LF(stanza, length); 423 424 /* Find the spaces before column number and line number. */ 425 columnptr = revstrstr(stanza, " ", stanza + length - 3); 426 if (columnptr == NULL) 427 continue; 428 lineptr = revstrstr(stanza, " ", columnptr - 2); 429 if (lineptr == NULL) 430 continue; 431 432 /* Now separate the three elements of the line. */ 433 *(columnptr++) = '\0'; 434 *(lineptr++) = '\0'; 435 436 /* Create a new position record. */ 437 newitem = nmalloc(sizeof(poshiststruct)); 438 newitem->filename = copy_of(stanza); 439 newitem->linenumber = atoi(lineptr); 440 newitem->columnnumber = atoi(columnptr); 441 newitem->anchors = (phrase == stanza) ? NULL : measured_copy(phrase, stanza - phrase); 442 newitem->next = NULL; 443 444 /* Add the record to the list. */ 445 if (position_history == NULL) 446 position_history = newitem; 447 else 448 lastitem->next = newitem; 449 450 lastitem = newitem; 451 } 452 453 if (fclose(histfile) == EOF) 454 jot_error(N_("Error reading %s: %s"), poshistname, strerror(errno)); 455 456 free(phrase); 457 458 if (stat(poshistname, &fileinfo) == 0) 459 latest_timestamp = fileinfo.st_mtime; 460 } 461 462 /* Save the recorded cursor positions for files that were edited. */ 463 void save_poshistory(void) 464 { 465 FILE *histfile = fopen(poshistname, "wb"); 466 struct stat fileinfo; 467 poshiststruct *item; 468 int count = 0; 469 470 if (histfile == NULL) { 471 jot_error(N_("Error writing %s: %s"), poshistname, strerror(errno)); 472 return; 473 } 474 475 /* Don't allow others to read or write the history file. */ 476 if (chmod(poshistname, S_IRUSR | S_IWUSR) < 0) 477 jot_error(N_("Cannot limit permissions on %s: %s"), poshistname, strerror(errno)); 478 479 for (item = position_history; item != NULL && count++ < 200; item = item->next) { 480 char *path_and_place; 481 size_t length = (item->anchors == NULL) ? 0 : strlen(item->anchors); 482 483 /* First write the string of line numbers with anchors, if any. */ 484 if (length && fwrite(item->anchors, 1, length, histfile) < length) 485 jot_error(N_("Error writing %s: %s"), poshistname, strerror(errno)); 486 487 /* Assume 20 decimal positions each for line and column number, 488 * plus two spaces, plus the line feed, plus the null byte. */ 489 path_and_place = nmalloc(strlen(item->filename) + 44); 490 sprintf(path_and_place, "%s %zd %zd\n", 491 item->filename, item->linenumber, item->columnnumber); 492 493 /* Encode newlines in filenames as NULs. */ 494 length = recode_LF_to_NUL(path_and_place); 495 /* Restore the terminating newline. */ 496 path_and_place[length - 1] = '\n'; 497 498 if (fwrite(path_and_place, 1, length, histfile) < length) 499 jot_error(N_("Error writing %s: %s"), poshistname, strerror(errno)); 500 501 free(path_and_place); 502 } 503 504 if (fclose(histfile) == EOF) 505 jot_error(N_("Error writing %s: %s"), poshistname, strerror(errno)); 506 507 if (stat(poshistname, &fileinfo) == 0) 508 latest_timestamp = fileinfo.st_mtime; 509 } 510 511 /* Reload the position history file if it has been modified since last load. */ 512 void reload_positions_if_needed(void) 513 { 514 poshiststruct *item, *nextone; 515 struct stat fileinfo; 516 517 if (stat(poshistname, &fileinfo) != 0 || fileinfo.st_mtime == latest_timestamp) 518 return; 519 520 for (item = position_history; item != NULL; item = nextone) { 521 nextone = item->next; 522 free(item->filename); 523 free(item->anchors); 524 free(item); 525 } 526 527 position_history = NULL; 528 529 load_poshistory(); 530 } 531 532 /* Update the recorded last file positions with the current position in the 533 * current buffer. If no existing entry is found, add a new one at the end. */ 534 void update_poshistory(void) 535 { 536 char *fullpath = get_full_path(openfile->filename); 537 poshiststruct *previous = NULL; 538 poshiststruct *item; 539 540 if (fullpath == NULL || openfile->filename[0] == '\0') { 541 free(fullpath); 542 return; 543 } 544 545 reload_positions_if_needed(); 546 547 /* Look for a matching filename in the list. */ 548 for (item = position_history; item != NULL; item = item->next) { 549 if (!strcmp(item->filename, fullpath)) 550 break; 551 previous = item; 552 } 553 554 /* If no match was found, make a new node; otherwise, unlink the match. */ 555 if (item == NULL) { 556 item = nmalloc(sizeof(poshiststruct)); 557 item->filename = copy_of(fullpath); 558 item->anchors = NULL; 559 } else if (previous) 560 previous->next = item->next; 561 562 /* Place the found or new node at the beginning, if not already there. */ 563 if (item != position_history) { 564 item->next = position_history; 565 position_history = item; 566 } 567 568 /* Record the last cursor position and any anchors. */ 569 item->linenumber = openfile->current->lineno; 570 item->columnnumber = xplustabs() + 1; 571 free(item->anchors); 572 item->anchors = stringify_anchors(); 573 574 free(fullpath); 575 576 save_poshistory(); 577 } 578 579 /* Check whether the current filename matches an entry in the list of 580 * recorded positions. If yes, restore the relevant cursor position. */ 581 void restore_cursor_position_if_any(void) 582 { 583 char *fullpath = get_full_path(openfile->filename); 584 poshiststruct *item; 585 586 if (fullpath == NULL) 587 return; 588 589 reload_positions_if_needed(); 590 591 item = position_history; 592 while (item != NULL && strcmp(item->filename, fullpath) != 0) 593 item = item->next; 594 595 free(fullpath); 596 597 if (item && item->anchors) 598 restore_anchors(item->anchors); 599 if (item) 600 goto_line_and_column(item->linenumber, item->columnnumber, FALSE, FALSE); 601 } 602 #endif /* ENABLE_HISTORIES */