/************************************************************************** * history.c -- This file is part of GNU nano. * * * * Copyright (C) 2003-2011, 2013-2017 Free Software Foundation, Inc. * * Copyright (C) 2016 Benno Schulenberg * * * * GNU nano is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published * * by the Free Software Foundation, either version 3 of the License, * * or (at your option) any later version. * * * * GNU nano is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty * * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * See the GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see http://www.gnu.org/licenses/. * * * **************************************************************************/ #include "proto.h" #include #include #ifndef DISABLE_HISTORIES static bool history_changed = FALSE; /* Have any of the history lists changed? */ /* Indicate whether any of the history lists have changed. */ bool history_has_changed(void) { return history_changed; } /* Initialize the search and replace history lists. */ void history_init(void) { search_history = make_new_node(NULL); search_history->data = mallocstrcpy(NULL, ""); searchage = search_history; searchbot = search_history; replace_history = make_new_node(NULL); replace_history->data = mallocstrcpy(NULL, ""); replaceage = replace_history; replacebot = replace_history; execute_history = make_new_node(NULL); execute_history->data = mallocstrcpy(NULL, ""); executetop = execute_history; executebot = execute_history; } /* Set the current position in the history list h to the bottom. */ void history_reset(const filestruct *h) { if (h == search_history) search_history = searchbot; else if (h == replace_history) replace_history = replacebot; else if (h == execute_history) execute_history = executebot; } /* Return the first node containing the first len characters of the * string s in the history list, starting at h_start and ending at * h_end, or NULL if there isn't one. */ filestruct *find_history(const filestruct *h_start, const filestruct *h_end, const char *s, size_t len) { const filestruct *p; for (p = h_start; p != h_end->prev && p != NULL; p = p->prev) { if (strncmp(s, p->data, len) == 0) return (filestruct *)p; } return NULL; } /* Update a history list (the one in which h is the current position) * with a fresh string s. That is: add s, or move it to the end. */ void update_history(filestruct **h, const char *s) { filestruct **hage = NULL, **hbot = NULL, *thesame; assert(h != NULL && s != NULL); if (*h == search_history) { hage = &searchage; hbot = &searchbot; } else if (*h == replace_history) { hage = &replaceage; hbot = &replacebot; } else if (*h == execute_history) { hage = &executetop; hbot = &executebot; } assert(hage != NULL && hbot != NULL); /* See if the string is already in the history. */ thesame = find_history(*hbot, *hage, s, HIGHEST_POSITIVE); /* If an identical string was found, delete that item. */ if (thesame != NULL) { filestruct *after = thesame->next; /* If the string is at the head of the list, move the head. */ if (thesame == *hage) *hage = after; unlink_node(thesame); renumber(after); } /* If the history is full, delete the oldest item (the one at the * head of the list), to make room for a new item at the end. */ if ((*hbot)->lineno == MAX_SEARCH_HISTORY + 1) { filestruct *oldest = *hage; *hage = (*hage)->next; unlink_node(oldest); renumber(*hage); } /* Store the fresh string in the last item, then create a new item. */ (*hbot)->data = mallocstrcpy((*hbot)->data, s); splice_node(*hbot, make_new_node(*hbot)); *hbot = (*hbot)->next; (*hbot)->data = mallocstrcpy(NULL, ""); /* Indicate that the history needs to be saved on exit. */ history_changed = TRUE; /* Set the current position in the list to the bottom. */ *h = *hbot; } /* Move h to the string in the history list just before it, and return * that string. If there isn't one, don't move h and return NULL. */ char *get_history_older(filestruct **h) { assert(h != NULL); if ((*h)->prev == NULL) return NULL; *h = (*h)->prev; return (*h)->data; } /* Move h to the string in the history list just after it, and return * that string. If there isn't one, don't move h and return NULL. */ char *get_history_newer(filestruct **h) { assert(h != NULL); if ((*h)->next == NULL) return NULL; *h = (*h)->next; return (*h)->data; } /* More placeholders. */ void get_history_newer_void(void) { ; } void get_history_older_void(void) { ; } #ifdef ENABLE_TABCOMP /* Move h to the next string that's a tab completion of the string s, * looking at only the first len characters of s, and return that * string. If there isn't one, or if len is 0, don't move h and return * s. */ char *get_history_completion(filestruct **h, char *s, size_t len) { assert(s != NULL); if (len > 0) { filestruct *hage = NULL, *hbot = NULL, *p; assert(h != NULL); if (*h == search_history) { hage = searchage; hbot = searchbot; } else if (*h == replace_history) { hage = replaceage; hbot = replacebot; } else if (*h == execute_history) { hage = executetop; hbot = executebot; } assert(hage != NULL && hbot != NULL); /* Search the history list from the current position to the top * for a match of len characters. Skip over an exact match. */ p = find_history((*h)->prev, hage, s, len); while (p != NULL && strcmp(p->data, s) == 0) p = find_history(p->prev, hage, s, len); if (p != NULL) { *h = p; return mallocstrcpy(s, (*h)->data); } /* Search the history list from the bottom to the current position * for a match of len characters. Skip over an exact match. */ p = find_history(hbot, *h, s, len); while (p != NULL && strcmp(p->data, s) == 0) p = find_history(p->prev, *h, s, len); if (p != NULL) { *h = p; return mallocstrcpy(s, (*h)->data); } } /* If we're here, we didn't find a match, we didn't find an inexact * match, or len is 0. Return s. */ return (char *)s; } #endif /* ENSABLE_TABCOMP */ /* Return the constructed dirfile path, or NULL if we can't find the home * directory. The string is dynamically allocated, and should be freed. */ char *construct_filename(const char *str) { char *newstr = NULL; if (homedir != NULL) { size_t homelen = strlen(homedir); newstr = charalloc(homelen + strlen(str) + 1); strcpy(newstr, homedir); strcpy(newstr + homelen, str); } return newstr; } char *histfilename(void) { return construct_filename("/.nano/search_history"); } /* Construct the legacy history filename. */ /* (To be removed in 2018.) */ char *legacyhistfilename(void) { return construct_filename("/.nano_history"); } char *poshistfilename(void) { return construct_filename("/.nano/filepos_history"); } void history_error(const char *msg, ...) { va_list ap; va_start(ap, msg); vfprintf(stderr, _(msg), ap); va_end(ap); fprintf(stderr, _("\nPress Enter to continue\n")); while (getchar() != '\n') ; } /* Now that we have more than one history file, let's just rely on a * .nano dir for this stuff. Return 1 if the dir exists or was * successfully created, and return 0 otherwise. */ int check_dotnano(void) { int ret = 1; struct stat dirstat; char *nanodir = construct_filename("/.nano"); if (stat(nanodir, &dirstat) == -1) { if (mkdir(nanodir, S_IRWXU | S_IRWXG | S_IRWXO) == -1) { history_error(N_("Unable to create directory %s: %s\n" "It is required for saving/loading " "search history or cursor positions.\n"), nanodir, strerror(errno)); ret = 0; } } else if (!S_ISDIR(dirstat.st_mode)) { history_error(N_("Path %s is not a directory and needs to be.\n" "Nano will be unable to load or save " "search history or cursor positions.\n"), nanodir); ret = 0; } free(nanodir); return ret; } /* Load the search and replace histories from ~/.nano/search_history. */ void load_history(void) { char *searchhist = histfilename(); char *legacyhist = legacyhistfilename(); struct stat hstat; FILE *hist; /* If no home directory was found, we can't do anything. */ if (searchhist == NULL || legacyhist == NULL) return; /* If there is an old history file, migrate it. */ /* (To be removed in 2018.) */ if (stat(legacyhist, &hstat) != -1 && stat(searchhist, &hstat) == -1) { if (rename(legacyhist, searchhist) == -1) history_error(N_("Detected a legacy nano history file (%s) which I tried to move\n" "to the preferred location (%s) but encountered an error: %s"), legacyhist, searchhist, strerror(errno)); else history_error(N_("Detected a legacy nano history file (%s) which I moved\n" "to the preferred location (%s)\n(see the nano FAQ about this change)"), legacyhist, searchhist); } hist = fopen(searchhist, "rb"); if (hist == NULL) { if (errno != ENOENT) { /* When reading failed, don't save history when we quit. */ UNSET(HISTORYLOG); history_error(N_("Error reading %s: %s"), searchhist, strerror(errno)); } } else { /* Load the two history lists -- first the search history, then * the replace history -- from the oldest entry to the newest. * The two lists are separated by an empty line. */ filestruct **history = &search_history; char *line = NULL; size_t buf_len = 0; ssize_t read; while ((read = getline(&line, &buf_len, hist)) > 0) { line[--read] = '\0'; if (read > 0) { /* Encode any embedded NUL as 0x0A. */ unsunder(line, read); update_history(history, line); } else if (history == &search_history) history = &replace_history; else history = &execute_history; } fclose(hist); free(line); } free(searchhist); free(legacyhist); } /* Write the lines of a history list, starting with the line at head, to * the open file at hist. Return TRUE if the write succeeded, and FALSE * otherwise. */ bool writehist(FILE *hist, const filestruct *head) { const filestruct *item; /* Write a history list, from the oldest item to the newest. */ for (item = head; item != NULL; item = item->next) { size_t length = strlen(item->data); /* Decode 0x0A bytes as embedded NULs. */ sunder(item->data); if (fwrite(item->data, sizeof(char), length, hist) < length) return FALSE; if (putc('\n', hist) == EOF) return FALSE; } return TRUE; } /* Save the search and replace histories to ~/.nano/search_history. */ void save_history(void) { char *searchhist; FILE *hist; /* If the histories are unchanged or empty, don't bother saving them. */ if (!history_has_changed() || (searchbot->lineno == 1 && replacebot->lineno == 1 && executebot->lineno == 1)) return; searchhist = histfilename(); if (searchhist == NULL) return; hist = fopen(searchhist, "wb"); if (hist == NULL) fprintf(stderr, _("Error writing %s: %s\n"), searchhist, strerror(errno)); else { /* Don't allow others to read or write the history file. */ chmod(searchhist, S_IRUSR | S_IWUSR); if (!writehist(hist, searchage) || !writehist(hist, replaceage) || !writehist(hist, executetop)) fprintf(stderr, _("Error writing %s: %s\n"), searchhist, strerror(errno)); fclose(hist); } free(searchhist); } /* Load the recorded file positions from ~/.nano/filepos_history. */ void load_poshistory(void) { char *poshist = poshistfilename(); FILE *hist; /* If the home directory is missing, do_rcfile() will have reported it. */ if (poshist == NULL) return; hist = fopen(poshist, "rb"); if (hist == NULL) { if (errno != ENOENT) { /* When reading failed, don't save history when we quit. */ UNSET(POS_HISTORY); history_error(N_("Error reading %s: %s"), poshist, strerror(errno)); } } else { char *line = NULL, *lineptr, *xptr; size_t buf_len = 0; ssize_t read, count = 0; poshiststruct *record_ptr = NULL, *newrecord; /* Read and parse each line, and store the extracted data. */ while ((read = getline(&line, &buf_len, hist)) > 5) { /* Decode nulls as embedded newlines. */ unsunder(line, read); /* Find where the x index and line number are in the line. */ xptr = revstrstr(line, " ", line + read - 3); if (xptr == NULL) continue; lineptr = revstrstr(line, " ", xptr - 2); if (lineptr == NULL) continue; /* Now separate the three elements of the line. */ *(xptr++) = '\0'; *(lineptr++) = '\0'; /* Create a new position record. */ newrecord = (poshiststruct *)nmalloc(sizeof(poshiststruct)); newrecord->filename = mallocstrcpy(NULL, line); newrecord->lineno = atoi(lineptr); newrecord->xno = atoi(xptr); newrecord->next = NULL; /* Add the record to the list. */ if (position_history == NULL) position_history = newrecord; else record_ptr->next = newrecord; record_ptr = newrecord; /* Impose a limit, so the file will not grow indefinitely. */ if (++count > 200) { poshiststruct *drop_record = position_history; position_history = position_history->next; free(drop_record->filename); free(drop_record); } } fclose(hist); free(line); } free(poshist); } /* Save the recorded last file positions to ~/.nano/filepos_history. */ void save_poshistory(void) { char *poshist = poshistfilename(); poshiststruct *posptr; FILE *hist; if (poshist == NULL) return; hist = fopen(poshist, "wb"); if (hist == NULL) fprintf(stderr, _("Error writing %s: %s\n"), poshist, strerror(errno)); else { /* Don't allow others to read or write the history file. */ chmod(poshist, S_IRUSR | S_IWUSR); for (posptr = position_history; posptr != NULL; posptr = posptr->next) { char *path_and_place; size_t length; /* Assume 20 decimal positions each for line and column number, * plus two spaces, plus the line feed, plus the null byte. */ path_and_place = charalloc(strlen(posptr->filename) + 44); sprintf(path_and_place, "%s %ld %ld\n", posptr->filename, (long)posptr->lineno, (long)posptr->xno); length = strlen(path_and_place); /* Encode newlines in filenames as nulls. */ sunder(path_and_place); /* Restore the terminating newline. */ path_and_place[length - 1] = '\n'; if (fwrite(path_and_place, sizeof(char), length, hist) < length) fprintf(stderr, _("Error writing %s: %s\n"), poshist, strerror(errno)); free(path_and_place); } fclose(hist); } free(poshist); } /* Update the recorded last file positions, given a filename, a line * and a column. If no entry is found, add a new one at the end. */ void update_poshistory(char *filename, ssize_t lineno, ssize_t xpos) { poshiststruct *posptr, *theone, *posprev = NULL; char *fullpath = get_full_path(filename); if (fullpath == NULL || fullpath[strlen(fullpath) - 1] == '/' || inhelp) { free(fullpath); return; } /* Look for a matching filename in the list. */ for (posptr = position_history; posptr != NULL; posptr = posptr->next) { if (!strcmp(posptr->filename, fullpath)) break; posprev = posptr; } /* Don't record files that have the default cursor position. */ if (lineno == 1 && xpos == 1) { if (posptr != NULL) { if (posprev == NULL) position_history = posptr->next; else posprev->next = posptr->next; free(posptr->filename); free(posptr); } free(fullpath); return; } theone = posptr; /* If we didn't find it, make a new node; otherwise, if we're * not at the end, move the matching one to the end. */ if (theone == NULL) { theone = (poshiststruct *)nmalloc(sizeof(poshiststruct)); theone->filename = mallocstrcpy(NULL, fullpath); if (position_history == NULL) position_history = theone; else posprev->next = theone; } else if (posptr->next != NULL) { if (posprev == NULL) position_history = posptr->next; else posprev->next = posptr->next; while (posptr->next != NULL) posptr = posptr->next; posptr->next = theone; } /* Store the last cursor position. */ theone->lineno = lineno; theone->xno = xpos; theone->next = NULL; free(fullpath); } /* Check whether the given file matches an existing entry in the recorded * last file positions. If not, return FALSE. If yes, return TRUE and * set line and column to the retrieved values. */ bool has_old_position(const char *file, ssize_t *line, ssize_t *column) { poshiststruct *posptr = position_history; char *fullpath = get_full_path(file); if (fullpath == NULL) return FALSE; while (posptr != NULL && strcmp(posptr->filename, fullpath) != 0) posptr = posptr->next; free(fullpath); if (posptr == NULL) return FALSE; *line = posptr->lineno; *column = posptr->xno; return TRUE; } #endif /* !DISABLE_HISTORIES */