nano

nano with my custom patches
git clone git://bsandro.tech/nano
Log | Files | Refs | README | LICENSE

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 */