kakoune/src/buffer.cc
Maxime Coste 3d5a0c672e Templatize StringData::create
This improves performance by letting the compiler optimize most use
cases where string count and length are known are compile time.
2024-02-28 19:02:29 +11:00

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);
// 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)
return false;
kak_assert(m_current_undo_group.empty());
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());
}};
}