699c70ac91
tabs <-> space conversion functions did not create a ScopedEdition leading to uncommited modifications. Fixing this did fix the interactive error, but that error still existed in non interactive context so redo now considers there there is no redo child if there are uncommited modifiations (which is correct as this means we are currently creating a new leaf in the undo tree) Fixes #5124
799 lines
25 KiB
C++
799 lines
25 KiB
C++
#include "buffer.hh"
|
|
|
|
#include "assert.hh"
|
|
#include "buffer_manager.hh"
|
|
#include "buffer_utils.hh"
|
|
#include "client.hh"
|
|
#include "context.hh"
|
|
#include "diff.hh"
|
|
#include "file.hh"
|
|
#include "flags.hh"
|
|
#include "option_types.hh"
|
|
#include "ranges.hh"
|
|
#include "shared_string.hh"
|
|
#include "unit_tests.hh"
|
|
#include "utils.hh"
|
|
#include "window.hh"
|
|
|
|
#include <algorithm>
|
|
|
|
namespace Kakoune
|
|
{
|
|
|
|
Buffer::HistoryNode::HistoryNode(HistoryId parent)
|
|
: parent{parent}, committed{Clock::now()}
|
|
{}
|
|
|
|
Buffer::Buffer(String name, Flags flags, BufferLines lines,
|
|
ByteOrderMark bom, EolFormat eolformat,
|
|
FsStatus fs_status)
|
|
: 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{{HistoryId::Invalid}},
|
|
m_history_id{HistoryId::First},
|
|
m_last_save_history_id{HistoryId::First},
|
|
m_fs_status{fs_status}
|
|
{
|
|
#ifdef KAK_DEBUG
|
|
for (auto& line : lines)
|
|
kak_assert(not (line->length == 0) and
|
|
line->data()[line->length-1] == '\n');
|
|
#endif
|
|
static_cast<BufferLines&>(m_lines) = std::move(lines);
|
|
|
|
m_changes.push_back({ Change::Insert, {0,0}, line_count() });
|
|
|
|
options().get_local_option("eolformat").set(eolformat);
|
|
options().get_local_option("BOM").set(bom);
|
|
options().get_local_option("readonly").set((bool)(flags & Flags::ReadOnly));
|
|
|
|
// now we may begin to record undo data
|
|
if (not (flags & Flags::NoUndo))
|
|
m_flags &= ~Flags::NoUndo;
|
|
}
|
|
|
|
void Buffer::on_registered()
|
|
{
|
|
// Ignore debug buffer, as it can be created in many
|
|
// corner cases (including while destroying the BufferManager
|
|
// if a BufClose hooks triggers writing to it).
|
|
if (m_flags & Flags::Debug)
|
|
return;
|
|
|
|
options().register_watcher(*this);
|
|
|
|
if (m_flags & Buffer::Flags::NoHooks)
|
|
{
|
|
on_option_changed(options()["readonly"]);
|
|
return;
|
|
}
|
|
|
|
run_hook_in_own_context(Hook::BufCreate, m_name);
|
|
|
|
if (m_flags & Flags::File)
|
|
{
|
|
if (m_flags & Buffer::Flags::New)
|
|
run_hook_in_own_context(Hook::BufNewFile, m_name);
|
|
else
|
|
{
|
|
kak_assert(m_fs_status.timestamp != InvalidTime);
|
|
run_hook_in_own_context(Hook::BufOpenFile, m_name);
|
|
}
|
|
}
|
|
|
|
for (auto& option : options().flatten_options()
|
|
| transform(&std::unique_ptr<Option>::get)
|
|
| gather<Vector<Option*>>())
|
|
on_option_changed(*option);
|
|
}
|
|
|
|
void Buffer::on_unregistered()
|
|
{
|
|
if (m_flags & Flags::Debug)
|
|
return;
|
|
|
|
options().unregister_watcher(*this);
|
|
run_hook_in_own_context(Hook::BufClose, m_name);
|
|
}
|
|
|
|
Buffer::~Buffer()
|
|
{
|
|
m_values.clear();
|
|
}
|
|
|
|
bool Buffer::set_name(String name)
|
|
{
|
|
Buffer* other = BufferManager::instance().get_buffer_ifp(name);
|
|
if (other == nullptr or other == this)
|
|
{
|
|
if (m_flags & Flags::File)
|
|
{
|
|
m_name = real_path(name);
|
|
m_display_name = compact_path(m_name);
|
|
if (m_flags & Buffer::Flags::File and not file_exists(m_name))
|
|
{
|
|
m_flags |= Buffer::Flags::New;
|
|
m_last_save_history_id = HistoryId::Invalid;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_name = std::move(name);
|
|
m_display_name = m_name;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Buffer::throw_if_read_only() const
|
|
{
|
|
if (m_flags & Flags::ReadOnly)
|
|
throw runtime_error("buffer is read-only");
|
|
}
|
|
|
|
void Buffer::update_display_name()
|
|
{
|
|
if (m_flags & Flags::File)
|
|
m_display_name = compact_path(m_name);
|
|
}
|
|
|
|
BufferIterator Buffer::iterator_at(BufferCoord coord) const
|
|
{
|
|
kak_assert(is_valid(coord));
|
|
return {*this, coord};
|
|
}
|
|
|
|
BufferCoord Buffer::clamp(BufferCoord coord) const
|
|
{
|
|
if (coord > back_coord())
|
|
coord = back_coord();
|
|
kak_assert(coord.line >= 0 and coord.line < line_count());
|
|
ByteCount max_col = std::max(0_byte, m_lines[coord.line].length() - 1);
|
|
coord.column = Kakoune::clamp(coord.column, 0_byte, max_col);
|
|
return coord;
|
|
}
|
|
|
|
BufferCoord Buffer::offset_coord(BufferCoord coord, CharCount offset, ColumnCount) const
|
|
{
|
|
return utf8::advance(iterator_at(coord), offset < 0 ? begin() : end()-1, offset).coord();
|
|
}
|
|
|
|
BufferCoordAndTarget Buffer::offset_coord(BufferCoordAndTarget coord, LineCount offset, ColumnCount tabstop) const
|
|
{
|
|
const auto column = coord.target == -1 ? get_column(*this, tabstop, coord) : coord.target;
|
|
const bool avoid_eol = coord.target < max_column;
|
|
const auto line = Kakoune::clamp(coord.line + offset, 0_line, line_count()-1);
|
|
const auto max_column = get_column(*this, tabstop, {line, m_lines[line].length()-1});
|
|
const auto final_column = std::max(0_col, std::min(column, max_column - (avoid_eol ? 1 : 0)));
|
|
return {line, get_byte_to_column(*this, tabstop, {line, final_column}), column};
|
|
}
|
|
|
|
String Buffer::string(BufferCoord begin, BufferCoord end) const
|
|
{
|
|
String res;
|
|
const auto last_line = std::min(end.line, line_count()-1);
|
|
for (auto line = begin.line; line <= last_line; ++line)
|
|
{
|
|
ByteCount start = 0;
|
|
if (line == begin.line)
|
|
start = begin.column;
|
|
ByteCount count = -1;
|
|
if (line == end.line)
|
|
count = end.column - start;
|
|
res += m_lines[line].substr(start, count);
|
|
}
|
|
return res;
|
|
}
|
|
|
|
Buffer::Modification Buffer::Modification::inverse() const
|
|
{
|
|
return {type == Insert ? Erase : Insert, coord, content};
|
|
}
|
|
|
|
void Buffer::reload(BufferLines lines, ByteOrderMark bom, EolFormat eolformat, FsStatus fs_status)
|
|
{
|
|
const bool record_undo = not (m_flags & Flags::NoUndo);
|
|
|
|
commit_undo_group();
|
|
|
|
if (not record_undo)
|
|
{
|
|
// Erase history about to be invalidated history
|
|
m_history_id = HistoryId::First;
|
|
m_last_save_history_id = HistoryId::First;
|
|
m_history = {HistoryNode{HistoryId::Invalid}};
|
|
|
|
m_changes.push_back({ Change::Erase, {0,0}, line_count() });
|
|
static_cast<BufferLines&>(m_lines) = std::move(lines);
|
|
m_changes.push_back({ Change::Insert, {0,0}, line_count() });
|
|
}
|
|
else
|
|
{
|
|
Vector<Diff> diff;
|
|
for_each_diff(m_lines.begin(), m_lines.size(),
|
|
lines.begin(), lines.size(),
|
|
[&diff](DiffOp op, int len)
|
|
{ diff.push_back({op, len}); },
|
|
[](const StringDataPtr& lhs, const StringDataPtr& rhs)
|
|
{ return lhs->strview() == rhs->strview(); });
|
|
|
|
auto read_it = m_lines.begin();
|
|
auto write_it = m_lines.begin();
|
|
auto new_it = lines.begin();
|
|
for (auto [op, len] : diff)
|
|
{
|
|
kak_assert(read_it >= write_it);
|
|
if (op == DiffOp::Keep)
|
|
{
|
|
if (read_it != write_it)
|
|
std::move(read_it, read_it + len, write_it);
|
|
write_it += len;
|
|
read_it += len;
|
|
new_it += len;
|
|
}
|
|
else if (op == DiffOp::Add)
|
|
{
|
|
const LineCount cur_line = (int)(write_it - m_lines.begin());
|
|
for (LineCount line = 0; line < len; ++line)
|
|
m_current_undo_group.push_back({Modification::Insert, cur_line + line, *(new_it + (int)line)});
|
|
m_changes.push_back({Change::Insert, cur_line, cur_line + len});
|
|
|
|
if (read_it != write_it)
|
|
{
|
|
auto count = std::min(len, static_cast<int>(read_it - write_it));
|
|
write_it = std::copy(new_it, new_it + count, write_it);
|
|
new_it += count;
|
|
if (len == count)
|
|
continue;
|
|
len -= count;
|
|
}
|
|
|
|
auto read_pos = read_it - m_lines.begin();
|
|
write_it = m_lines.insert(write_it, new_it, new_it + len) + len;
|
|
read_it = m_lines.begin() + read_pos + len;
|
|
new_it += len;
|
|
}
|
|
else if (op == DiffOp::Remove)
|
|
{
|
|
const LineCount cur_line = (int)(write_it - m_lines.begin());
|
|
for (LineCount line = len-1; line >= 0; --line)
|
|
m_current_undo_group.push_back({
|
|
Modification::Erase, cur_line + line,
|
|
*(read_it + (size_t)line)});
|
|
|
|
read_it += len;
|
|
m_changes.push_back({ Change::Erase, cur_line, cur_line + len });
|
|
}
|
|
}
|
|
m_lines.erase(write_it, m_lines.end());
|
|
}
|
|
|
|
commit_undo_group();
|
|
|
|
options().get_local_option("eolformat").set(eolformat);
|
|
options().get_local_option("BOM").set(bom);
|
|
|
|
|
|
m_last_save_history_id = m_history_id;
|
|
m_fs_status = fs_status;
|
|
}
|
|
|
|
void Buffer::commit_undo_group()
|
|
{
|
|
if (m_flags & Flags::NoUndo)
|
|
return;
|
|
|
|
if (m_current_undo_group.empty())
|
|
return;
|
|
|
|
const HistoryId id = next_history_id();
|
|
m_history.push_back({m_history_id});
|
|
m_history.back().undo_group = std::move(m_current_undo_group);
|
|
m_current_undo_group.clear();
|
|
current_history_node().redo_child = id;
|
|
m_history_id = id;
|
|
}
|
|
|
|
bool Buffer::undo(size_t count)
|
|
{
|
|
throw_if_read_only();
|
|
|
|
commit_undo_group();
|
|
|
|
if (current_history_node().parent == HistoryId::Invalid)
|
|
return false;
|
|
|
|
while (count-- != 0 and current_history_node().parent != HistoryId::Invalid)
|
|
{
|
|
for (const Modification& modification : current_history_node().undo_group | reverse())
|
|
apply_modification(modification.inverse());
|
|
|
|
m_history_id = current_history_node().parent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool Buffer::redo(size_t count)
|
|
{
|
|
throw_if_read_only();
|
|
|
|
if (current_history_node().redo_child == HistoryId::Invalid or
|
|
not m_current_undo_group.empty())
|
|
return false;
|
|
|
|
while (count-- != 0 and current_history_node().redo_child != HistoryId::Invalid)
|
|
{
|
|
m_history_id = current_history_node().redo_child;
|
|
|
|
for (const Modification& modification : current_history_node().undo_group)
|
|
apply_modification(modification);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool Buffer::move_to(HistoryId id)
|
|
{
|
|
if (id >= next_history_id())
|
|
return false;
|
|
|
|
throw_if_read_only();
|
|
|
|
commit_undo_group();
|
|
|
|
auto find_lowest_common_parent = [this](HistoryId a, HistoryId b) {
|
|
auto depth_of = [this](HistoryId id) {
|
|
size_t depth = 0;
|
|
for (; history_node(id).parent != HistoryId::Invalid; id = history_node(id).parent)
|
|
++depth;
|
|
return depth;
|
|
};
|
|
auto depthA = depth_of(a), depthB = depth_of(b);
|
|
|
|
for (; depthA > depthB; --depthA)
|
|
a = history_node(a).parent;
|
|
for (; depthB > depthA; --depthB)
|
|
b = history_node(b).parent;
|
|
|
|
while (a != b)
|
|
{
|
|
a = history_node(a).parent;
|
|
b = history_node(b).parent;
|
|
}
|
|
|
|
kak_assert(a == b and a != HistoryId::Invalid);
|
|
return a;
|
|
};
|
|
|
|
auto parent = find_lowest_common_parent(m_history_id, id);
|
|
|
|
// undo up to common parent
|
|
for (auto id = m_history_id; id != parent; id = history_node(id).parent)
|
|
{
|
|
for (const Modification& modification : history_node(id).undo_group | reverse())
|
|
apply_modification(modification.inverse());
|
|
}
|
|
|
|
static void (*apply_from_parent)(Buffer&, HistoryId, HistoryId) =
|
|
[](Buffer& buffer, HistoryId parent, HistoryId id) {
|
|
if (id == parent)
|
|
return;
|
|
|
|
auto& node = buffer.history_node(id);
|
|
apply_from_parent(buffer, parent, node.parent);
|
|
|
|
buffer.history_node(node.parent).redo_child = id;
|
|
|
|
for (const Modification& modification : node.undo_group)
|
|
buffer.apply_modification(modification);
|
|
};
|
|
|
|
apply_from_parent(*this, parent, id);
|
|
m_history_id = id;
|
|
return true;
|
|
}
|
|
|
|
void Buffer::check_invariant() const
|
|
{
|
|
#ifdef KAK_DEBUG
|
|
kak_assert(not m_lines.empty());
|
|
for (auto& line : m_lines)
|
|
{
|
|
kak_assert(line->strview().length() > 0);
|
|
kak_assert(line->strview().back() == '\n');
|
|
}
|
|
#endif
|
|
}
|
|
|
|
BufferRange Buffer::do_insert(BufferCoord pos, StringView content)
|
|
{
|
|
kak_assert(is_valid(pos));
|
|
|
|
if (content.empty())
|
|
return {pos, pos};
|
|
|
|
const bool at_end = is_end(pos);
|
|
const bool append_lines = at_end and (m_lines.empty() or byte_at(back_coord()) == '\n');
|
|
|
|
const StringView prefix = append_lines ?
|
|
StringView{} : m_lines[pos.line].substr(0, pos.column);
|
|
const StringView suffix = at_end ?
|
|
StringView{} : m_lines[pos.line].substr(pos.column);
|
|
|
|
LineList new_lines;
|
|
ByteCount start = 0;
|
|
for (ByteCount i = 0; i < content.length(); ++i)
|
|
{
|
|
if (content[i] == '\n')
|
|
{
|
|
StringView line = content.substr(start, i + 1 - start);
|
|
new_lines.push_back(start == 0 ? StringData::create(prefix, line) : StringData::create(line));
|
|
start = i + 1;
|
|
}
|
|
}
|
|
if (start == 0)
|
|
new_lines.push_back(StringData::create(prefix, content, suffix));
|
|
else if (start != content.length() or not suffix.empty())
|
|
new_lines.push_back(StringData::create(content.substr(start), suffix));
|
|
|
|
auto line_it = m_lines.begin() + (int)pos.line;
|
|
auto new_lines_it = new_lines.begin();
|
|
if (not append_lines) // replace first line with new first line
|
|
*line_it++ = std::move(*new_lines_it++);
|
|
|
|
m_lines.insert(line_it,
|
|
std::make_move_iterator(new_lines_it),
|
|
std::make_move_iterator(new_lines.end()));
|
|
|
|
const LineCount last_line = pos.line + new_lines.size() - 1;
|
|
const auto end = at_end ? line_count()
|
|
: BufferCoord{ last_line, m_lines[last_line].length() - suffix.length() };
|
|
|
|
m_changes.push_back({ Change::Insert, pos, end });
|
|
return {pos, end};
|
|
}
|
|
|
|
BufferCoord Buffer::do_erase(BufferCoord begin, BufferCoord end)
|
|
{
|
|
if (begin == end)
|
|
return begin;
|
|
|
|
kak_assert(is_valid(begin));
|
|
kak_assert(is_valid(end));
|
|
StringView prefix = m_lines[begin.line].substr(0, begin.column);
|
|
StringView suffix = end.line == line_count() ? StringView{} : m_lines[end.line].substr(end.column);
|
|
|
|
auto new_line = (not prefix.empty() or not suffix.empty()) ? StringData::create(prefix, suffix) : StringDataPtr{};
|
|
m_lines.erase(m_lines.begin() + (int)begin.line, m_lines.begin() + (int)end.line);
|
|
|
|
m_changes.push_back({ Change::Erase, begin, end });
|
|
if (new_line)
|
|
m_lines.get_storage(begin.line) = std::move(new_line);
|
|
|
|
return begin;
|
|
}
|
|
|
|
void Buffer::apply_modification(const Modification& modification)
|
|
{
|
|
StringView content = modification.content->strview();
|
|
BufferCoord coord = modification.coord;
|
|
|
|
kak_assert(is_valid(coord));
|
|
switch (modification.type)
|
|
{
|
|
case Modification::Insert:
|
|
do_insert(coord, content);
|
|
break;
|
|
case Modification::Erase:
|
|
{
|
|
auto end = advance(coord, content.length());
|
|
kak_assert(string(coord, end) == content);
|
|
do_erase(coord, end);
|
|
break;
|
|
}
|
|
default:
|
|
kak_assert(false);
|
|
}
|
|
}
|
|
|
|
BufferRange Buffer::insert(BufferCoord pos, StringView content)
|
|
{
|
|
throw_if_read_only();
|
|
|
|
kak_assert(is_valid(pos));
|
|
if (content.empty())
|
|
return {pos, pos};
|
|
|
|
StringDataPtr real_content;
|
|
if (is_end(pos) and content.back() != '\n')
|
|
real_content = intern(content + "\n");
|
|
else
|
|
real_content = intern(content);
|
|
|
|
if (not (m_flags & Flags::NoUndo))
|
|
m_current_undo_group.push_back({Modification::Insert, pos, real_content});
|
|
return do_insert(pos, real_content->strview());
|
|
}
|
|
|
|
BufferCoord Buffer::erase(BufferCoord begin, BufferCoord end)
|
|
{
|
|
throw_if_read_only();
|
|
|
|
kak_assert(is_valid(begin) and is_valid(end));
|
|
// do not erase last \n except if we erase from the start of a line, and normalize
|
|
// end coord
|
|
if (is_end(end))
|
|
end = (begin.column != 0 or begin == BufferCoord{0,0}) ? prev(end) : end_coord();
|
|
|
|
if (begin >= end) // use >= to handle case where begin is {line_count}
|
|
return begin;
|
|
|
|
if (not (m_flags & Flags::NoUndo))
|
|
m_current_undo_group.push_back({Modification::Erase, begin,
|
|
intern(string(begin, end))});
|
|
return do_erase(begin, end);
|
|
}
|
|
|
|
BufferRange Buffer::replace(BufferCoord begin, BufferCoord end, StringView content)
|
|
{
|
|
throw_if_read_only();
|
|
|
|
if (std::equal(iterator_at(begin), iterator_at(end), content.begin(), content.end()))
|
|
return {begin, end};
|
|
|
|
if (is_end(end) and not content.empty() and content.back() == '\n')
|
|
{
|
|
auto pos = insert(erase(begin, back_coord()),
|
|
content.substr(0, content.length() - 1)).begin;
|
|
return {pos, end_coord()};
|
|
}
|
|
return insert(erase(begin, end), content);
|
|
}
|
|
|
|
bool Buffer::is_modified() const
|
|
{
|
|
return m_flags & Flags::File and
|
|
(m_history_id != m_last_save_history_id or
|
|
not m_current_undo_group.empty());
|
|
}
|
|
|
|
void Buffer::notify_saved(FsStatus status)
|
|
{
|
|
if (not m_current_undo_group.empty())
|
|
commit_undo_group();
|
|
|
|
m_flags &= ~Flags::New;
|
|
m_last_save_history_id = m_history_id;
|
|
m_fs_status = status;
|
|
}
|
|
|
|
BufferCoord Buffer::advance(BufferCoord coord, ByteCount count) const
|
|
{
|
|
if (count > 0)
|
|
{
|
|
auto line = coord.line;
|
|
count += coord.column;
|
|
while (count >= m_lines[line].length())
|
|
{
|
|
count -= m_lines[line++].length();
|
|
if (line == line_count())
|
|
return end_coord();
|
|
}
|
|
return { line, count };
|
|
}
|
|
else if (count < 0)
|
|
{
|
|
auto line = coord.line;
|
|
count += coord.column;
|
|
while (count < 0)
|
|
{
|
|
if (--line < 0)
|
|
return {0, 0};
|
|
count += m_lines[line].length();
|
|
}
|
|
return { line, count };
|
|
}
|
|
return coord;
|
|
}
|
|
|
|
BufferCoord Buffer::char_next(BufferCoord coord) const
|
|
{
|
|
if (coord.column < m_lines[coord.line].length() - 1)
|
|
{
|
|
auto line = m_lines[coord.line];
|
|
auto column = coord.column + utf8::codepoint_size(line[coord.column]);
|
|
if (column >= line.length()) // Handle invalid utf-8
|
|
return { coord.line + 1, 0 };
|
|
return { coord.line, column };
|
|
}
|
|
return { coord.line + 1, 0 };
|
|
}
|
|
|
|
BufferCoord Buffer::char_prev(BufferCoord coord) const
|
|
{
|
|
kak_assert(is_valid(coord));
|
|
if (coord.column == 0)
|
|
return { coord.line - 1, m_lines[coord.line - 1].length() - 1 };
|
|
|
|
auto line = m_lines[coord.line];
|
|
auto column = (int)(utf8::character_start(line.begin() + coord.column - 1, line.begin()) - line.begin());
|
|
return { coord.line, column };
|
|
}
|
|
|
|
void Buffer::set_fs_status(FsStatus status)
|
|
{
|
|
kak_assert(m_flags & Flags::File);
|
|
m_fs_status = std::move(status);
|
|
}
|
|
|
|
const FsStatus& Buffer::fs_status() const
|
|
{
|
|
kak_assert(m_flags & Flags::File);
|
|
return m_fs_status;
|
|
}
|
|
|
|
void Buffer::on_option_changed(const Option& option)
|
|
{
|
|
if (option.name() == "readonly")
|
|
{
|
|
if (option.get<bool>())
|
|
m_flags |= Flags::ReadOnly;
|
|
else
|
|
m_flags &= ~Flags::ReadOnly;
|
|
}
|
|
run_hook_in_own_context(Hook::BufSetOption,
|
|
format("{}={}", option.name(), option.get_desc_string()));
|
|
}
|
|
|
|
void Buffer::run_hook_in_own_context(Hook hook, StringView param, String client_name)
|
|
{
|
|
if (m_flags & Buffer::Flags::NoHooks)
|
|
return;
|
|
|
|
InputHandler hook_handler{{ *this, Selection{} },
|
|
Context::Flags::Draft,
|
|
std::move(client_name)};
|
|
hooks().run_hook(hook, param, hook_handler.context());
|
|
}
|
|
|
|
Optional<BufferCoord> Buffer::last_modification_coord() const
|
|
{
|
|
if (m_history_id == HistoryId::First)
|
|
return {};
|
|
return current_history_node().undo_group.back().coord;
|
|
}
|
|
|
|
String Buffer::debug_description() const
|
|
{
|
|
size_t content_size = 0;
|
|
for (auto& line : m_lines)
|
|
content_size += (int)line->strview().length();
|
|
|
|
const size_t additional_size = accumulate(m_history, 0, [](size_t s, auto&& history) {
|
|
return sizeof(history) + history.undo_group.size() * sizeof(Modification) + s;
|
|
}) + m_changes.size() * sizeof(Change);
|
|
|
|
return format("{}\nFlags: {}{}{}{}{}{}{}{}\nUsed mem: content={} additional={}\n",
|
|
display_name(),
|
|
(m_flags & Flags::File) ? "File (" + name() + ") " : "",
|
|
(m_flags & Flags::New) ? "New " : "",
|
|
(m_flags & Flags::Fifo) ? "Fifo " : "",
|
|
(m_flags & Flags::NoUndo) ? "NoUndo " : "",
|
|
(m_flags & Flags::NoHooks) ? "NoHooks " : "",
|
|
(m_flags & Flags::Debug) ? "Debug " : "",
|
|
(m_flags & Flags::ReadOnly) ? "ReadOnly " : "",
|
|
is_modified() ? "Modified " : "",
|
|
content_size, additional_size);
|
|
}
|
|
|
|
UnitTest test_buffer{[]()
|
|
{
|
|
auto make_lines = [](auto&&... lines) { return BufferLines{StringData::create(lines)...}; };
|
|
|
|
Buffer empty_buffer("empty", Buffer::Flags::None, make_lines("\n"));
|
|
|
|
Buffer buffer("test", Buffer::Flags::None, make_lines("allo ?\n", "mais que fais la police\n", " hein ?\n", " youpi\n"));
|
|
kak_assert(buffer.line_count() == 4);
|
|
|
|
BufferIterator pos = buffer.begin();
|
|
kak_assert(*pos == 'a');
|
|
pos += 6;
|
|
kak_assert(pos.coord() == BufferCoord{0, 6});
|
|
++pos;
|
|
kak_assert(pos.coord() == BufferCoord{1, 0});
|
|
--pos;
|
|
kak_assert(pos.coord() == BufferCoord{0, 6});
|
|
pos += 1;
|
|
kak_assert(pos.coord() == BufferCoord{1, 0});
|
|
buffer.insert(pos.coord(), "tchou kanaky\n");
|
|
kak_assert(buffer.line_count() == 5);
|
|
BufferIterator pos2 = buffer.end();
|
|
pos2 -= 9;
|
|
kak_assert(*pos2 == '?');
|
|
|
|
String str = buffer.string({ 4, 1 }, buffer.next({ 4, 5 }));
|
|
kak_assert(str == "youpi");
|
|
|
|
// check insert at end behaviour: auto add end of line if necessary
|
|
pos = buffer.end()-1;
|
|
buffer.insert(pos.coord(), "tchou");
|
|
kak_assert(buffer.string(pos.coord(), buffer.end_coord()) == "tchou\n"_sv);
|
|
|
|
pos = buffer.end()-1;
|
|
buffer.insert(buffer.end_coord(), "kanaky\n");
|
|
kak_assert(buffer.string((pos+1).coord(), buffer.end_coord()) == "kanaky\n"_sv);
|
|
|
|
buffer.commit_undo_group();
|
|
buffer.erase((pos+1).coord(), buffer.end_coord());
|
|
buffer.insert(buffer.end_coord(), "mutch\n");
|
|
buffer.commit_undo_group();
|
|
buffer.undo();
|
|
kak_assert(buffer.string(buffer.advance(buffer.end_coord(), -7), buffer.end_coord()) == "kanaky\n"_sv);
|
|
buffer.redo();
|
|
kak_assert(buffer.string(buffer.advance(buffer.end_coord(), -6), buffer.end_coord()) == "mutch\n"_sv);
|
|
}};
|
|
|
|
UnitTest test_undo{[]()
|
|
{
|
|
auto make_lines = [](auto&&... lines) { return BufferLines{StringData::create(lines)...}; };
|
|
|
|
Buffer buffer("test", Buffer::Flags::None, make_lines("allo ?\n", "mais que fais la police\n", " hein ?\n", " youpi\n"));
|
|
auto pos = buffer.end_coord();
|
|
buffer.insert(pos, "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.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() == 3);
|
|
kak_assert(buffer[0_line] == "allo ?\n");
|
|
kak_assert(buffer[1_line] == "mais que fais la police\n");
|
|
kak_assert(buffer[2_line] == "foo\n");
|
|
|
|
buffer.move_to((Buffer::HistoryId)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((Buffer::HistoryId)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");
|
|
|
|
buffer.move_to(Buffer::HistoryId::First);
|
|
kak_assert((int)buffer.line_count() == 4);
|
|
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(not buffer.undo());
|
|
|
|
buffer.move_to((Buffer::HistoryId)5);
|
|
kak_assert(not buffer.redo());
|
|
|
|
buffer.move_to((Buffer::HistoryId)6);
|
|
kak_assert(not buffer.redo());
|
|
}};
|
|
|
|
}
|