/* * Copyright (C) 2012-2015 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 "sciteco.h" #include "interface.h" #include "ioview.h" #include "undo.h" #include "parser.h" #include "expressions.h" #include "qregisters.h" #include "glob.h" #include "error.h" #include "ring.h" namespace SciTECO { namespace States { StateEditFile editfile; StateSaveFile savefile; } void Buffer::UndoTokenClose::run(void) { ring.close(buffer); /* NOTE: the buffer is NOT deleted on Token destruction */ delete buffer; } void Buffer::save(const gchar *filename) { if (!filename && !Buffer::filename) throw Error("File name expected"); IOView::save(filename ? : Buffer::filename); /* * Undirtify * NOTE: info update is performed by set_filename() */ interface.undo_info_update(this); undo.push_var(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) * NOTE: undo_info_update is already called above */ undo.push_str(Buffer::filename); set_filename(filename ? : Buffer::filename); } 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; } tecoInt Ring::get_id(Buffer *buffer) { tecoInt ret = 0; Buffer *cur; TAILQ_FOREACH(cur, &head, buffers) { ret++; if (cur == buffer) break; } return ret; } 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 (QRegisters::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; QRegisters::current = NULL; current = buffer; buffer->edit(); QRegisters::hook(QRegisters::HOOK_EDIT); return true; } void Ring::edit(const gchar *filename) { Buffer *buffer = find(filename); 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->edit(); buffer->load(filename); interface.msg(InterfaceCurrent::MSG_INFO, "Added file \"%s\" to ring", filename); } else { buffer->edit(); buffer->set_filename(filename); if (filename) interface.msg(InterfaceCurrent::MSG_INFO, "Added new file \"%s\" to ring", filename); else interface.msg(InterfaceCurrent::MSG_INFO, "Added new unnamed file to ring."); } QRegisters::hook(QRegisters::HOOK_ADD); } } void Ring::close(Buffer *buffer) { TAILQ_REMOVE(&head, buffer, buffers); if (buffer->filename) interface.msg(InterfaceCurrent::MSG_INFO, "Removed file \"%s\" from the ring", buffer->filename); else interface.msg(InterfaceCurrent::MSG_INFO, "Removed unnamed file from the ring."); } void Ring::close(void) { Buffer *buffer = current; QRegisters::hook(QRegisters::HOOK_CLOSE); 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); } } void Ring::set_scintilla_undo(bool state) { Buffer *cur; TAILQ_FOREACH(cur, &head, buffers) cur->set_scintilla_undo(state); } Ring::~Ring() { Buffer *buffer, *next; TAILQ_FOREACH_SAFE(buffer, &head, buffers, next) delete buffer; } /* * Command states */ void StateEditFile::do_edit(const gchar *filename) { current_doc_undo_edit(); ring.edit(filename); } void StateEditFile::do_edit(tecoInt id) { current_doc_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. * Globbing is performed exactly the same as the * EN command does. * * 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) { tecoInt id = expressions.pop_num_calc(0, -1); allowFilename = true; if (id == 0) { for (Buffer *cur = ring.first(); cur; cur = cur->next()) interface.popup_add(InterfaceCurrent::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) { 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)) { Globber globber(str); gchar *filename; while ((filename = globber.next())) do_edit(filename); } else { do_edit(*str ? str : NULL); } return &States::start; } /*$ * EW$ -- Save current buffer or Q-Register * 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. * * The EW command also works if the current document * is a Q-Register, i.e. a Q-Register is edited. * In this case, the string contents of the current * Q-Register are saved to . * Q-Registers have no notion of associated file names, * so must be always specified. * * 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-\fIn\fP-\fIfilename\fP~\(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 \(em 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) { BEGIN_EXEC(&States::start); if (QRegisters::current) QRegisters::current->save(str); else ring.current->save(*str ? str : NULL); return &States::start; } void current_doc_undo_edit(void) { if (!QRegisters::current) ring.undo_edit(); else undo.push_var(QRegisters::current)->undo_edit(); } } /* namespace SciTECO */