/* * Copyright (C) 2012-2013 Robin Haberkorn * * This program 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. * * This program 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include #include #include #include #include #include #include "sciteco.h" #include "interface.h" #include "undo.h" #include "parser.h" #include "expressions.h" #include "ring.h" #ifdef HAVE_WINDOWS_H /* here it shouldn't cause conflicts with other headers */ #include /* still need to clean up */ #ifdef interface #undef interface #endif #endif namespace States { StateEditFile editfile; StateSaveFile savefile; } #ifdef G_OS_WIN32 typedef DWORD FileAttributes; /* INVALID_FILE_ATTRIBUTES already defined */ static inline FileAttributes get_file_attributes(const gchar *filename) { return GetFileAttributes((LPCTSTR)filename); } static inline void set_file_attributes(const gchar *filename, FileAttributes attrs) { SetFileAttributes((LPCTSTR)filename, attrs); } #else typedef int FileAttributes; #define INVALID_FILE_ATTRIBUTES (-1) static inline FileAttributes get_file_attributes(const gchar *filename) { struct stat buf; return g_stat(filename, &buf) ? INVALID_FILE_ATTRIBUTES : buf.st_mode; } static inline void set_file_attributes(const gchar *filename, FileAttributes attrs) { g_chmod(filename, attrs); } #endif /* !G_OS_WIN32 */ void Buffer::UndoTokenClose::run(void) { ring.close(buffer); /* NOTE: the buffer is NOT deleted on Token destruction */ delete buffer; } /* * The following simple implementation of file reading is actually the * most efficient and useful in the common case of editing small files, * since * a) it works with minimal number of syscalls and * b) small files cause little temporary memory overhead. * Reading large files however could be very inefficient since the file * must first be read into memory and then copied in-memory. Also it could * result in thrashing. * Alternatively we could iteratively read into a smaller buffer trading * in speed against (temporary) memory consumption. * The best way to do it could be memory mapping the file as we could * let Scintilla copy from the file's virtual memory directly. * Unfortunately since every page of the mapped file is * only touched once by Scintilla TLB caching is useless and the TLB is * effectively thrashed with entries of the mapped file. * This results in the doubling of page faults and weighs out the other * advantages of memory mapping (has been benchmarked). * * So in the future, the following approach could be implemented: * 1.) On Unix/Posix, mmap() one page at a time, hopefully preventing * TLB thrashing. * 2.) On other platforms read into and copy from a statically sized buffer * (perhaps page-sized) */ bool Buffer::load(const gchar *filename) { gchar *contents; gsize size; if (!g_file_get_contents(filename, &contents, &size, NULL)) return false; edit(); interface.ssm(SCI_BEGINUNDOACTION); interface.ssm(SCI_CLEARALL); interface.ssm(SCI_APPENDTEXT, size, (sptr_t)contents); interface.ssm(SCI_ENDUNDOACTION); g_free(contents); /* NOTE: currently buffer cannot be dirty */ #if 0 interface.undo_info_update(this); undo.push_var(dirty); dirty = false; #endif set_filename(filename); return true; } void Ring::UndoTokenEdit::run(void) { /* * assumes that buffer still has correct prev/next * pointers */ if (buffer->next()) TAILQ_INSERT_BEFORE(buffer->next(), buffer, buffers); else TAILQ_INSERT_TAIL(&ring->head, buffer, buffers); ring->current = buffer; buffer->edit(); buffer = NULL; } Buffer * Ring::find(const gchar *filename) { gchar *resolved = get_absolute_path(filename); Buffer *cur; TAILQ_FOREACH(cur, &head, buffers) if (!g_strcmp0(cur->filename, resolved)) break; g_free(resolved); return cur; } Buffer * Ring::find(tecoInt id) { Buffer *cur; TAILQ_FOREACH(cur, &head, buffers) if (!--id) break; return cur; } void Ring::dirtify(void) { if (!current || current->dirty) return; interface.undo_info_update(current); undo.push_var(current->dirty); current->dirty = true; interface.info_update(current); } bool Ring::is_any_dirty(void) { Buffer *cur; TAILQ_FOREACH(cur, &head, buffers) if (cur->dirty) return true; return false; } bool Ring::edit(tecoInt id) { Buffer *buffer = find(id); if (!buffer) return false; current_doc_update(); QRegisters::current = NULL; current = buffer; buffer->edit(); QRegisters::hook(QRegisters::HOOK_EDIT); return true; } void Ring::edit(const gchar *filename) { Buffer *buffer = find(filename); current_doc_update(); QRegisters::current = NULL; if (buffer) { current = buffer; buffer->edit(); QRegisters::hook(QRegisters::HOOK_EDIT); } else { buffer = new Buffer(); TAILQ_INSERT_TAIL(&head, buffer, buffers); current = buffer; undo_close(); if (filename && g_file_test(filename, G_FILE_TEST_IS_REGULAR)) { buffer->load(filename); interface.msg(Interface::MSG_INFO, "Added file \"%s\" to ring", filename); } else { buffer->edit(); buffer->set_filename(filename); if (filename) interface.msg(Interface::MSG_INFO, "Added new file \"%s\" to ring", filename); else interface.msg(Interface::MSG_INFO, "Added new unnamed file to ring."); } QRegisters::hook(QRegisters::HOOK_ADD); } } #if 0 /* * TODO: on UNIX it may be better to open() the current file, unlink() it * and keep the file descriptor in the UndoToken. * When the operation is undone, the file descriptor's contents are written to * the file (which should be efficient enough because it is written to the same * filesystem). This way we could avoid messing around with save point files. */ #else class UndoTokenRestoreSavePoint : public UndoToken { gchar *savepoint; Buffer *buffer; public: #ifdef G_OS_WIN32 FileAttributes orig_attrs; #endif UndoTokenRestoreSavePoint(gchar *_savepoint, Buffer *_buffer) : savepoint(_savepoint), buffer(_buffer) {} ~UndoTokenRestoreSavePoint() { if (savepoint) g_unlink(savepoint); g_free(savepoint); buffer->savepoint_id--; } void run(void) { if (!g_rename(savepoint, buffer->filename)) { g_free(savepoint); savepoint = NULL; #ifdef G_OS_WIN32 if (orig_attrs != INVALID_FILE_ATTRIBUTES) set_file_attributes(buffer->filename, orig_attrs); #endif } else { interface.msg(Interface::MSG_WARNING, "Unable to restore save point file \"%s\"", savepoint); } } }; static inline FileAttributes make_savepoint(Buffer *buffer) { gchar *dirname, *basename, *savepoint; gchar savepoint_basename[FILENAME_MAX]; FileAttributes attributes = get_file_attributes(buffer->filename); basename = g_path_get_basename(buffer->filename); g_snprintf(savepoint_basename, sizeof(savepoint_basename), ".teco-%s-%d", basename, buffer->savepoint_id); g_free(basename); dirname = g_path_get_dirname(buffer->filename); savepoint = g_build_filename(dirname, savepoint_basename, NIL); g_free(dirname); if (!g_rename(buffer->filename, savepoint)) { UndoTokenRestoreSavePoint *token; buffer->savepoint_id++; token = new UndoTokenRestoreSavePoint(savepoint, buffer); #ifdef G_OS_WIN32 token->orig_attrs = attributes; if (attributes != INVALID_FILE_ATTRIBUTES) set_file_attributes(savepoint, attributes | FILE_ATTRIBUTE_HIDDEN); #endif undo.push(token); } else { interface.msg(Interface::MSG_WARNING, "Unable to create save point file \"%s\"", savepoint); g_free(savepoint); } return attributes; } #endif /* !G_OS_UNIX */ bool Ring::save(const gchar *filename) { const void *buffer; sptr_t gap; size_t size; FILE *file; #ifdef G_OS_UNIX struct stat file_stat; file_stat.st_uid = -1; file_stat.st_gid = -1; #endif FileAttributes attributes = INVALID_FILE_ATTRIBUTES; if (!current) return false; if (!filename) filename = current->filename; if (!filename) return false; if (undo.enabled) { if (current->filename && g_file_test(current->filename, G_FILE_TEST_IS_REGULAR)) { #ifdef G_OS_UNIX g_stat(current->filename, &file_stat); #endif attributes = make_savepoint(current); } else { undo.push(new UndoTokenRemoveFile(filename)); } } /* leaves mode intact if file exists */ file = g_fopen(filename, "w"); if (!file) return false; /* write part of buffer before gap */ gap = interface.ssm(SCI_GETGAPPOSITION); if (gap > 0) { buffer = (const void *)interface.ssm(SCI_GETRANGEPOINTER, 0, gap); if (!fwrite(buffer, (size_t)gap, 1, file)) { fclose(file); return false; } } /* write part of buffer after gap */ size = interface.ssm(SCI_GETLENGTH) - gap; if (size > 0) { buffer = (const void *)interface.ssm(SCI_GETRANGEPOINTER, gap, size); if (!fwrite(buffer, size, 1, file)) { fclose(file); return false; } } /* if file existed but has been renamed, restore attributes */ if (attributes != INVALID_FILE_ATTRIBUTES) set_file_attributes(filename, attributes); #ifdef G_OS_UNIX /* * only a good try to inherit owner since process user must have * CHOWN capability traditionally reserved to root only */ fchown(fileno(file), file_stat.st_uid, file_stat.st_gid); #endif fclose(file); interface.undo_info_update(current); undo.push_var(current->dirty); current->dirty = false; /* * FIXME: necessary also if the filename was not specified but the file * is (was) new, in order to canonicalize the filename. * May be circumvented by cananonicalizing without requiring the file * name to exist (like readlink -f) */ //if (filename) { undo.push_str(current->filename); current->set_filename(filename); //} return true; } void Ring::close(Buffer *buffer) { TAILQ_REMOVE(&head, buffer, buffers); if (buffer->filename) interface.msg(Interface::MSG_INFO, "Removed file \"%s\" from the ring", buffer->filename); else interface.msg(Interface::MSG_INFO, "Removed unnamed file from the ring."); } void Ring::close(void) { Buffer *buffer = current; buffer->update(); close(buffer); current = buffer->next() ? : buffer->prev(); /* transfer responsibility to UndoToken object */ undo.push(new UndoTokenEdit(this, buffer)); if (current) { current->edit(); QRegisters::hook(QRegisters::HOOK_EDIT); } else { edit((const gchar *)NULL); } } Ring::~Ring() { Buffer *buffer, *next; TAILQ_FOREACH_SAFE(buffer, &head, buffers, next) delete buffer; } /* * Auxiliary functions */ #ifdef G_OS_UNIX gchar * get_absolute_path(const gchar *path) { gchar buf[PATH_MAX]; gchar *resolved; if (!path) return NULL; if (!realpath(path, buf)) { if (g_path_is_absolute(path)) { resolved = g_strdup(path); } else { gchar *cwd = g_get_current_dir(); resolved = g_build_filename(cwd, path, NIL); g_free(cwd); } } else { resolved = g_strdup(buf); } return resolved; } #elif defined(G_OS_WIN32) gchar * get_absolute_path(const gchar *path) { TCHAR buf[MAX_PATH]; gchar *resolved = NULL; if (path && GetFullPathName(path, sizeof(buf), buf, NULL)) resolved = g_strdup(buf); return resolved; } #else /* * FIXME: I doubt that works on any platform... */ gchar * get_absolute_path(const gchar *path) { return path ? g_file_read_link(path, NULL) : NULL; } #endif /* !G_OS_UNIX && !G_OS_WIN32 */ /* * Command states */ void StateEditFile::do_edit(const gchar *filename) throw (Error) { if (ring.current) ring.undo_edit(); else /* QRegisters::current != NULL */ QRegisters::undo_edit(); ring.edit(filename); } void StateEditFile::do_edit(tecoInt id) throw (Error) { if (ring.current) ring.undo_edit(); else /* QRegisters::current != NULL */ QRegisters::undo_edit(); if (!ring.edit(id)) throw Error("Invalid buffer id %" TECO_INTEGER_FORMAT, id); } /*$ * [n]EB[file]$ -- Open or edit file * nEB$ * * Opens or edits the file with name . * If is not in the buffer ring it is opened, * added to the ring and set as the currently edited * buffer. * If it already exists in the ring, it is merely * made the current file. * may be omitted in which case the default * unnamed buffer is created/edited. * If an argument is specified as 0, EB will additionally * display the buffer ring contents in the window's popup * area. * Naturally this only has any effect in interactive * mode. * * may also be a glob-pattern, in which case * all files matching the pattern are opened/edited. * * File names of buffers in the ring are normalized * by making them absolute. * Any comparison on file names is performed using * guessed or actual absolute file paths, so that * one file may be referred to in many different ways * (paths). * * does not have to exist on disk. * In this case, an empty buffer is created and its * name is guessed from . * When the newly created buffer is first saved, * the file is created on disk and the buffer's name * will be updated to the absolute path of the file * on disk. * * File names may also be tab-completed and string building * characters are enabled by default. * * If is greater than zero, the string argument * must be empty. * Instead selects a buffer from the ring to edit. * A value of 1 denotes the first buffer, 2 the second, * ecetera. */ void StateEditFile::initial(void) throw (Error) { tecoInt id = expressions.pop_num_calc(1, -1); allowFilename = true; if (id == 0) { for (Buffer *cur = ring.first(); cur; cur = cur->next()) interface.popup_add(Interface::POPUP_FILE, cur->filename ? : "(Unnamed)", cur == ring.current); interface.popup_show(); } else if (id > 0) { allowFilename = false; do_edit(id); } } State * StateEditFile::done(const gchar *str) throw (Error) { BEGIN_EXEC(&States::start); if (!allowFilename) { if (*str) throw Error("If a buffer is selected by id, the " "string argument must be empty"); return &States::start; } if (is_glob_pattern(str)) { gchar *dirname; GDir *dir; dirname = g_path_get_dirname(str); dir = g_dir_open(dirname, 0, NULL); if (dir) { const gchar *basename; GPatternSpec *pattern; basename = g_path_get_basename(str); pattern = g_pattern_spec_new(basename); g_free((gchar *)basename); while ((basename = g_dir_read_name(dir))) { if (g_pattern_match_string(pattern, basename)) { gchar *filename; filename = g_build_filename(dirname, basename, NIL); do_edit(filename); g_free(filename); } } g_pattern_spec_free(pattern); g_dir_close(dir); } g_free(dirname); } else { do_edit(*str ? str : NULL); } return &States::start; } /*$ * EW$ -- Save or rename current buffer * EWfile$ * * Saves the current buffer to disk. * If the buffer was dirty, it will be clean afterwards. * If the string argument is not empty, * the buffer is saved with the specified file name * and is renamed in the ring. * * In interactive mode, EW is executed immediately and * may be rubbed out. * In order to support that, \*(ST creates so called * save point files. * It does not merely overwrite existing files when saving * but moves them to save point files instead. * Save point files are called \(lq.teco--\(rq * where is the name of the saved file and is * a number that is increased with every save operation. * Save point files are always created in the same directory * as the original file to ensure that no copying of the file * on disk is necessary but only a rename of the file. * When rubbing out the EW command, \*(ST restores the latest * save point file by moving (renaming) it back to its * original path - also not requiring any on-disk copying. * \*(ST is impossible to crash, but just in case it still * does it may leave behind these save point files which * must be manually deleted by the user. * Otherwise save point files are deleted on command line * termination. * * File names may also be tab-completed and string building * characters are enabled by default. */ State * StateSaveFile::done(const gchar *str) throw (Error) { BEGIN_EXEC(&States::start); if (!ring.save(*str ? str : NULL)) throw Error("Unable to save file"); return &States::start; }