diff --git a/src/buffer.cc b/src/buffer.cc index 51be811f..ec970676 100644 --- a/src/buffer.cc +++ b/src/buffer.cc @@ -59,15 +59,19 @@ static void apply_options(OptionManager& options, const ParsedLines& parsed_line options.get_local_option("BOM").set(parsed_lines.bom); } +Buffer::HistoryNode::HistoryNode(size_t id, HistoryNode* parent) + : id{id}, parent(parent), timepoint{Clock::now()} +{} + Buffer::Buffer(String name, Flags flags, StringView data, timespec fs_timestamp) - : Scope(GlobalScope::instance()), - m_name((flags & Flags::File) ? real_path(parse_filename(name)) : std::move(name)), - m_display_name((flags & Flags::File) ? compact_path(m_name) : m_name), - m_flags(flags | Flags::NoUndo), - m_history(), m_history_cursor(m_history.begin()), - m_last_save_undo_index(0), - m_fs_timestamp(fs_timestamp) + : Scope{GlobalScope::instance()}, + m_name{(flags & Flags::File) ? real_path(parse_filename(name)) : std::move(name)}, + m_display_name{(flags & Flags::File) ? compact_path(m_name) : m_name}, + m_flags{flags | Flags::NoUndo}, + m_history{m_next_history_id++, nullptr}, m_history_cursor{&m_history}, + m_last_save_history_cursor{&m_history}, + m_fs_timestamp{fs_timestamp} { ParsedLines parsed_lines = parse_lines(data); @@ -285,7 +289,7 @@ void Buffer::reload(StringView data, timespec fs_timestamp) apply_options(options(), parsed_lines); - m_last_save_undo_index = m_history_cursor - m_history.begin(); + m_last_save_history_cursor = m_history_cursor; m_fs_timestamp = fs_timestamp; } @@ -297,44 +301,135 @@ void Buffer::commit_undo_group() if (m_current_undo_group.empty()) return; - m_history.erase(m_history_cursor, m_history.end()); - - m_history.push_back(std::move(m_current_undo_group)); + auto* node = new HistoryNode{m_next_history_id++, m_history_cursor.get()}; + node->undo_group = std::move(m_current_undo_group); m_current_undo_group.clear(); - m_history_cursor = m_history.end(); - if (m_history.size() < m_last_save_undo_index) - m_last_save_undo_index = -1; + m_history_cursor->childs.emplace_back(node); + m_history_cursor->redo_child = node; + m_history_cursor = node; } -bool Buffer::undo() +bool Buffer::undo(size_t count) noexcept { commit_undo_group(); - if (m_history_cursor == m_history.begin()) + if (not m_history_cursor->parent) return false; - --m_history_cursor; + while (count-- and m_history_cursor->parent) + { + for (const Modification& modification : m_history_cursor->undo_group | reverse()) + apply_modification(modification.inverse()); + + m_history_cursor = m_history_cursor->parent; + } - for (const Modification& modification : *m_history_cursor | reverse()) - apply_modification(modification.inverse()); return true; } -bool Buffer::redo() +bool Buffer::redo(size_t count) noexcept { - if (m_history_cursor == m_history.end()) + if (not m_history_cursor->redo_child) return false; kak_assert(m_current_undo_group.empty()); - for (const Modification& modification : *m_history_cursor) - apply_modification(modification); + while (count-- and m_history_cursor->redo_child) + { + m_history_cursor = m_history_cursor->redo_child.get(); - ++m_history_cursor; + for (const Modification& modification : m_history_cursor->undo_group) + apply_modification(modification); + } return true; } +void Buffer::move_to(HistoryNode* history_node) noexcept +{ + commit_undo_group(); + + auto find_lowest_common_parent = [](HistoryNode* a, HistoryNode* b) { + auto depth_of = [](HistoryNode* node) { + size_t depth = 0; + for (; node->parent; node = node->parent.get()) + ++depth; + return depth; + }; + auto depthA = depth_of(a), depthB = depth_of(b); + + for (; depthA > depthB; --depthA) + a = a->parent.get(); + for (; depthB > depthA; --depthB) + b = b->parent.get(); + + while (a != b) + { + a = a->parent.get(); + b = b->parent.get(); + } + + kak_assert(a == b and a != nullptr); + return a; + }; + + auto parent = find_lowest_common_parent(m_history_cursor.get(), history_node); + + // undo up to common parent + for (auto it = m_history_cursor.get(); it != parent; it = it->parent.get()) + { + for (const Modification& modification : it->undo_group | reverse()) + apply_modification(modification.inverse()); + } + + static void (*apply_from_parent)(Buffer&, HistoryNode*, HistoryNode*) = + [](Buffer& buffer, HistoryNode* parent, HistoryNode* node) { + if (node == parent) + return; + + apply_from_parent(buffer, parent, node->parent.get()); + + node->parent->redo_child = node; + + for (const Modification& modification : node->undo_group) + buffer.apply_modification(modification); + }; + + apply_from_parent(*this, parent, history_node); + m_history_cursor = history_node; +} + +template +Buffer::HistoryNode* Buffer::find_history_node(HistoryNode* node, const Func& func) +{ + if (func(node)) + return node; + + for (auto&& child : node->childs) + { + if (auto res = find_history_node(child.get(), func)) + return res; + } + + return nullptr; +} + +bool Buffer::move_to(size_t history_id) noexcept +{ + HistoryNode* target_node = find_history_node(&m_history, [history_id](HistoryNode* node) { return node->id == history_id; }); + + if (not target_node) + return false; + + move_to(target_node); + return true; +} + +size_t Buffer::current_history_id() const noexcept +{ + return m_history_cursor->id; +} + void Buffer::check_invariant() const { #ifdef KAK_DEBUG @@ -505,8 +600,7 @@ ByteCoord Buffer::replace(ByteCoord begin, ByteCoord end, StringView content) bool Buffer::is_modified() const { - size_t history_cursor_index = m_history_cursor - m_history.begin(); - return m_last_save_undo_index != history_cursor_index + return m_history_cursor != m_last_save_history_cursor or not m_current_undo_group.empty(); } @@ -516,9 +610,7 @@ void Buffer::notify_saved() commit_undo_group(); m_flags &= ~Flags::New; - size_t history_cursor_index = m_history_cursor - m_history.begin(); - if (m_last_save_undo_index != history_cursor_index) - m_last_save_undo_index = history_cursor_index; + m_last_save_history_cursor = m_history_cursor; m_fs_timestamp = get_fs_timestamp(m_name); } @@ -625,9 +717,9 @@ void Buffer::run_hook_in_own_context(StringView hook_name, StringView param) ByteCoord Buffer::last_modification_coord() const { - if (m_history.empty()) + if (m_history_cursor.get() == &m_history) return {}; - return m_history.back().back().coord; + return m_history_cursor->undo_group.back().coord; } String Buffer::debug_description() const @@ -636,10 +728,14 @@ String Buffer::debug_description() const for (auto& line : m_lines) content_size += (int)line->strview().length(); - size_t additional_size = 0; - for (auto& undo_group : m_history) - additional_size += undo_group.size() * sizeof(Modification); - additional_size += m_changes.size() * sizeof(Change); + static size_t (*count_mem)(const HistoryNode&) = [](const HistoryNode& node) { + size_t size = node.undo_group.size() * sizeof(Modification); + for (auto& child : node.childs) + size += count_mem(*child); + return size; + }; + const size_t additional_size = count_mem(m_history) + + m_changes.size() * sizeof(Change); return format("{}\nFlags: {}{}{}{}\nUsed mem: content={} additional={}\n", display_name(), @@ -731,21 +827,43 @@ UnitTest test_buffer{[]() UnitTest test_undo{[]() { Buffer buffer("test", Buffer::Flags::None, "allo ?\nmais que fais la police\n hein ?\n youpi\n"); - auto pos = buffer.insert(buffer.end_coord(), "kanaky\n"); - buffer.erase(pos, buffer.end_coord()); - buffer.insert(2_line, "tchou\n"); - buffer.insert(2_line, "mutch\n"); - buffer.erase({2, 1}, {2, 5}); - buffer.replace(2_line, buffer.end_coord(), "youpi"); + auto pos = buffer.insert(buffer.end_coord(), "kanaky\n"); // change 1 + buffer.commit_undo_group(); + buffer.erase(pos, buffer.end_coord()); // change 2 + buffer.commit_undo_group(); + buffer.insert(2_line, "tchou\n"); // change 3 + buffer.commit_undo_group(); buffer.undo(); - buffer.redo(); + buffer.insert(2_line, "mutch\n"); // change 4 + buffer.commit_undo_group(); + buffer.erase({2, 1}, {2, 5}); // change 5 + buffer.commit_undo_group(); + buffer.undo(2); + buffer.redo(2); buffer.undo(); + buffer.replace(2_line, buffer.end_coord(), "foo"); // change 6 + buffer.commit_undo_group(); - kak_assert((int)buffer.line_count() == 4); + kak_assert((int)buffer.line_count() == 3); kak_assert(buffer[0_line] == "allo ?\n"); kak_assert(buffer[1_line] == "mais que fais la police\n"); - kak_assert(buffer[2_line] == " hein ?\n"); - kak_assert(buffer[3_line] == " youpi\n"); + kak_assert(buffer[2_line] == "foo\n"); + + buffer.move_to(3); + kak_assert((int)buffer.line_count() == 5); + kak_assert(buffer[0_line] == "allo ?\n"); + kak_assert(buffer[1_line] == "mais que fais la police\n"); + kak_assert(buffer[2_line] == "tchou\n"); + kak_assert(buffer[3_line] == " hein ?\n"); + kak_assert(buffer[4_line] == " youpi\n"); + + buffer.move_to(4); + kak_assert((int)buffer.line_count() == 5); + kak_assert(buffer[0_line] == "allo ?\n"); + kak_assert(buffer[1_line] == "mais que fais la police\n"); + kak_assert(buffer[2_line] == "mutch\n"); + kak_assert(buffer[3_line] == " hein ?\n"); + kak_assert(buffer[4_line] == " youpi\n"); }}; } diff --git a/src/buffer.hh b/src/buffer.hh index e84c14e0..845d04b1 100644 --- a/src/buffer.hh +++ b/src/buffer.hh @@ -1,6 +1,7 @@ #ifndef buffer_hh_INCLUDED #define buffer_hh_INCLUDED +#include "clock.hh" #include "coord.hh" #include "flags.hh" #include "safe_ptr.hh" @@ -131,8 +132,10 @@ public: void set_fs_timestamp(timespec ts); void commit_undo_group(); - bool undo(); - bool redo(); + bool undo(size_t count = 1) noexcept; + bool redo(size_t count = 1) noexcept; + bool move_to(size_t history_id) noexcept; + size_t current_history_id() const noexcept; String string(ByteCoord begin, ByteCoord end) const; @@ -241,13 +244,28 @@ private: Flags m_flags; using UndoGroup = Vector; - using History = Vector; - History m_history; - History::iterator m_history_cursor; - UndoGroup m_current_undo_group; + struct HistoryNode : SafeCountable, UseMemoryDomain + { + HistoryNode(size_t id, HistoryNode* parent); - size_t m_last_save_undo_index; + size_t id; + SafePtr parent; + UndoGroup undo_group; + Vector, MemoryDomain::BufferMeta> childs; + SafePtr redo_child; + TimePoint timepoint; + }; + + size_t m_next_history_id = 0; + HistoryNode m_history; + SafePtr m_history_cursor; + SafePtr m_last_save_history_cursor; + UndoGroup m_current_undo_group; + + void move_to(HistoryNode* history_node) noexcept; + + template HistoryNode* find_history_node(HistoryNode* node, const Func& func); Vector m_changes; diff --git a/src/clock.hh b/src/clock.hh new file mode 100644 index 00000000..b4138de0 --- /dev/null +++ b/src/clock.hh @@ -0,0 +1,14 @@ +#ifndef clock_hh_INCLUDED +#define clock_hh_INCLUDED + +#include + +namespace Kakoune +{ + +using Clock = std::chrono::steady_clock; +using TimePoint = Clock::time_point; + +} + +#endif // clock_hh_INCLUDED diff --git a/src/event_manager.hh b/src/event_manager.hh index 81bfc508..3c1567c9 100644 --- a/src/event_manager.hh +++ b/src/event_manager.hh @@ -1,11 +1,11 @@ #ifndef event_manager_hh_INCLUDED #define event_manager_hh_INCLUDED +#include "clock.hh" #include "utils.hh" #include "flags.hh" #include "vector.hh" -#include #include #include @@ -41,9 +41,6 @@ private: Callback m_callback; }; -using Clock = std::chrono::steady_clock; -using TimePoint = Clock::time_point; - class Timer { public: diff --git a/src/hook_manager.cc b/src/hook_manager.cc index 30f90ae3..be940482 100644 --- a/src/hook_manager.cc +++ b/src/hook_manager.cc @@ -1,5 +1,6 @@ #include "hook_manager.hh" +#include "clock.hh" #include "containers.hh" #include "context.hh" #include "buffer_utils.hh" @@ -7,8 +8,6 @@ #include "face_registry.hh" #include "regex.hh" -#include - namespace Kakoune { @@ -61,9 +60,6 @@ void HookManager::run_hook(StringView hook_name, m_running_hooks.emplace_back(hook_name, param); auto pop_running_hook = on_scope_end([this]{ m_running_hooks.pop_back(); }); - using Clock = std::chrono::steady_clock; - using TimePoint = Clock::time_point; - const DebugFlags debug_flags = context.options()["debug"].get(); const bool profile = debug_flags & DebugFlags::Profile; auto start_time = profile ? Clock::now() : TimePoint{}; diff --git a/src/normal.cc b/src/normal.cc index 940e11d8..50f7ca61 100644 --- a/src/normal.cc +++ b/src/normal.cc @@ -1414,40 +1414,59 @@ void restore_selections(Context& context, NormalParams params) context.print_status({format("Restored selections from register '{}'", reg), get_face("Information")}); } -void undo(Context& context, NormalParams) +void undo(Context& context, NormalParams params) { Buffer& buffer = context.buffer(); size_t timestamp = buffer.timestamp(); - bool res = buffer.undo(); - if (res) + if (buffer.undo(std::max(1, params.count))) { auto ranges = compute_modified_ranges(buffer, timestamp); if (not ranges.empty()) context.selections_write_only() = std::move(ranges); context.selections().avoid_eol(); } - else if (not res) + else context.print_status({ "nothing left to undo", get_face("Information") }); } -void redo(Context& context, NormalParams) +void redo(Context& context, NormalParams params) { - using namespace std::placeholders; Buffer& buffer = context.buffer(); size_t timestamp = buffer.timestamp(); - bool res = buffer.redo(); - if (res) + if (buffer.redo(std::max(1, params.count))) { auto ranges = compute_modified_ranges(buffer, timestamp); if (not ranges.empty()) context.selections_write_only() = std::move(ranges); context.selections().avoid_eol(); } - - else if (not res) + else context.print_status({ "nothing left to redo", get_face("Information") }); } +template +void move_in_history(Context& context, NormalParams params) +{ + Buffer& buffer = context.buffer(); + size_t timestamp = buffer.timestamp(); + const int count = std::max(1, params.count); + const int history_id = (int)buffer.current_history_id() + + (direction == Direction::Forward ? count : -count); + if (buffer.move_to((size_t)history_id)) + { + auto ranges = compute_modified_ranges(buffer, timestamp); + if (not ranges.empty()) + context.selections_write_only() = std::move(ranges); + context.selections().avoid_eol(); + + context.print_status({ format("moved to change #{}", history_id), + get_face("Information") }); + } + else + context.print_status({ format("no such change: #{}", history_id), + get_face("Information") }); +} + void exec_user_mappings(Context& context, NormalParams params) { on_next_key_with_autoinfo(context, KeymapMode::None, @@ -1682,6 +1701,8 @@ static NormalCmdDesc cmds[] = { 'u', "undo", undo }, { 'U', "redo", redo }, + { alt('u'), "move backward in history", move_in_history }, + { alt('U'), "move forward in history", move_in_history }, { alt('i'), "select inner object", select_object }, { alt('a'), "select whole object", select_object }, diff --git a/src/shell_manager.cc b/src/shell_manager.cc index e0345e36..01ff1db2 100644 --- a/src/shell_manager.cc +++ b/src/shell_manager.cc @@ -1,12 +1,11 @@ #include "shell_manager.hh" +#include "clock.hh" #include "context.hh" #include "buffer_utils.hh" #include "event_manager.hh" #include "file.hh" -#include - #include #include #include @@ -115,18 +114,16 @@ std::pair ShellManager::eval( StringView cmdline, const Context& context, StringView input, Flags flags, const ShellContext& shell_context) { - using namespace std::chrono; - const DebugFlags debug_flags = context.options()["debug"].get(); const bool profile = debug_flags & DebugFlags::Profile; if (debug_flags & DebugFlags::Shell) write_to_debug_buffer(format("shell:\n{}\n----\n", cmdline)); - auto start_time = profile ? steady_clock::now() : steady_clock::time_point{}; + auto start_time = profile ? Clock::now() : Clock::time_point{}; auto kak_env = generate_env(cmdline, context, shell_context); - auto spawn_time = profile ? steady_clock::now() : steady_clock::time_point{}; + auto spawn_time = profile ? Clock::now() : Clock::time_point{}; Pipe child_stdin{not input.empty()}, child_stdout, child_stderr; pid_t pid = spawn_shell(cmdline, shell_context.params, kak_env, @@ -153,7 +150,7 @@ std::pair ShellManager::eval( write(child_stdin.write_fd(), input); child_stdin.close_write_fd(); - auto wait_time = profile ? steady_clock::now() : steady_clock::time_point{}; + auto wait_time = profile ? Clock::now() : Clock::time_point{}; struct PipeReader : FDWatcher { @@ -203,7 +200,8 @@ std::pair ShellManager::eval( if (profile) { - auto end_time = steady_clock::now(); + using namespace std::chrono; + auto end_time = Clock::now(); auto full = duration_cast(end_time - start_time); auto spawn = duration_cast(wait_time - spawn_time); auto wait = duration_cast(end_time - wait_time); diff --git a/src/window.cc b/src/window.cc index de3f4ea4..efffe1e1 100644 --- a/src/window.cc +++ b/src/window.cc @@ -1,6 +1,7 @@ #include "window.hh" #include "assert.hh" +#include "clock.hh" #include "context.hh" #include "highlighter.hh" #include "hook_manager.hh" @@ -9,7 +10,6 @@ #include "buffer_utils.hh" #include -#include #include namespace Kakoune @@ -109,12 +109,10 @@ bool Window::needs_redraw(const Context& context) const const DisplayBuffer& Window::update_display_buffer(const Context& context) { - using namespace std::chrono; - const bool profile = context.options()["debug"].get() & DebugFlags::Profile; - auto start_time = profile ? steady_clock::now() : steady_clock::time_point{}; + auto start_time = profile ? Clock::now() : Clock::time_point{}; DisplayBuffer::LineList& lines = m_display_buffer.lines(); lines.clear(); @@ -147,7 +145,8 @@ const DisplayBuffer& Window::update_display_buffer(const Context& context) if (profile and not (buffer().flags() & Buffer::Flags::Debug)) { - auto duration = duration_cast(steady_clock::now() - start_time); + using namespace std::chrono; + auto duration = duration_cast(Clock::now() - start_time); write_to_debug_buffer(format("window display update for '{}' took {} ms", buffer().display_name(), (size_t)duration.count())); }