1
1
mirror of https://github.com/mawww/kakoune.git synced 2024-12-29 22:52:06 +03:00
kakoune/src/buffer.cc

730 lines
22 KiB
C++
Raw Normal View History

2011-09-02 20:51:20 +04:00
#include "buffer.hh"
2011-09-09 23:24:18 +04:00
#include "assert.hh"
2013-04-09 22:05:40 +04:00
#include "buffer_manager.hh"
#include "client.hh"
#include "containers.hh"
#include "context.hh"
#include "diff.hh"
#include "file.hh"
#include "shared_string.hh"
#include "unit_tests.hh"
2013-04-09 22:05:40 +04:00
#include "utils.hh"
#include "window.hh"
#include <algorithm>
2011-09-02 20:51:20 +04:00
namespace Kakoune
{
struct ParsedLines
{
BufferLines lines;
ByteOrderMark bom = ByteOrderMark::None;
EolFormat eolformat = EolFormat::Lf;
};
static ParsedLines parse_lines(StringView data)
{
ParsedLines res;
const char* pos = data.begin();
if (data.substr(0, 3_byte) == "\xEF\xBB\xBF")
{
res.bom = ByteOrderMark::Utf8;
pos = data.begin() + 3;
}
bool has_crlf = false, has_lf = false;
for (auto it = pos; it != data.end(); ++it)
{
if (*it == '\n')
((it != pos and *(it-1) == '\r') ? has_crlf : has_lf) = true;
}
const bool crlf = has_crlf and not has_lf;
res.eolformat = crlf ? EolFormat::Crlf : EolFormat::Lf;
while (pos < data.end())
{
const char* eol = std::find(pos, data.end(), '\n');
res.lines.emplace_back(StringData::create({pos, eol - (crlf and eol != data.end() ? 1 : 0)}, '\n'));
pos = eol + 1;
}
return res;
}
static void apply_options(OptionManager& options, const ParsedLines& parsed_lines)
{
options.get_local_option("eolformat").set(parsed_lines.eolformat);
options.get_local_option("BOM").set(parsed_lines.bom);
}
Buffer::Buffer(String name, Flags flags, StringView data,
timespec fs_timestamp)
: Scope(GlobalScope::instance()),
2015-09-01 15:59:40 +03:00
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),
2012-11-21 16:37:36 +04:00
m_history(), m_history_cursor(m_history.begin()),
m_last_save_undo_index(0),
m_fs_timestamp(fs_timestamp)
2011-09-02 20:51:20 +04:00
{
options().register_watcher(*this);
ParsedLines parsed_lines = parse_lines(data);
if (parsed_lines.lines.empty())
parsed_lines.lines.emplace_back(StringData::create("\n"));
2012-11-27 16:39:35 +04:00
2015-02-17 01:27:40 +03:00
#ifdef KAK_DEBUG
for (auto& line : parsed_lines.lines)
2015-02-17 01:27:40 +03:00
kak_assert(not (line->length == 0) and
line->data()[line->length-1] == '\n');
#endif
static_cast<BufferLines&>(m_lines) = std::move(parsed_lines.lines);
2015-01-30 01:44:07 +03:00
m_changes.push_back({ Change::Insert, true, {0,0}, line_count() });
apply_options(options(), parsed_lines);
if (flags & Flags::File)
{
if (flags & Flags::New)
run_hook_in_own_context("BufNew", m_name);
else
{
kak_assert(m_fs_timestamp != InvalidTime);
run_hook_in_own_context("BufOpen", m_name);
}
}
run_hook_in_own_context("BufCreate", m_name);
// now we may begin to record undo data
if (not (flags & Flags::NoUndo))
m_flags &= ~Flags::NoUndo;
2013-11-13 00:36:42 +04:00
for (auto& option : options().flatten_options())
2013-11-13 00:36:42 +04:00
on_option_changed(*option);
2011-09-02 20:51:20 +04:00
}
2011-10-24 18:23:13 +04:00
Buffer::~Buffer()
{
run_hook_in_own_context("BufClose", m_name);
options().unregister_watcher(*this);
m_values.clear();
2011-10-24 18:23:13 +04:00
}
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)
2015-09-01 15:59:40 +03:00
{
m_name = real_path(name);
2015-09-01 15:59:40 +03:00
m_display_name = compact_path(m_name);
}
else
2015-09-01 15:59:40 +03:00
{
m_name = std::move(name);
2015-09-01 15:59:40 +03:00
m_display_name = m_name;
}
return true;
}
return false;
}
void Buffer::update_display_name()
{
if (m_flags & Flags::File)
m_display_name = compact_path(m_name);
}
BufferIterator Buffer::iterator_at(ByteCoord coord) const
2011-09-02 20:51:20 +04:00
{
return is_end(coord) ? end() : BufferIterator(*this, clamp(coord));
2011-09-02 20:51:20 +04:00
}
ByteCoord Buffer::clamp(ByteCoord coord) const
2011-09-02 20:51:20 +04:00
{
coord.line = Kakoune::clamp(coord.line, 0_line, line_count() - 1);
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;
2011-09-02 20:51:20 +04:00
}
ByteCoord Buffer::offset_coord(ByteCoord coord, CharCount offset)
2013-12-15 18:14:52 +04:00
{
StringView line = m_lines[coord.line];
2013-12-15 18:14:52 +04:00
auto character = std::max(0_char, std::min(line.char_count_to(coord.column) + offset,
line.char_length() - 1));
return {coord.line, line.byte_count_to(character)};
}
ByteCoordAndTarget Buffer::offset_coord(ByteCoordAndTarget coord, LineCount offset)
2013-12-15 18:14:52 +04:00
{
auto character = coord.target == -1 ? m_lines[coord.line].char_count_to(coord.column) : coord.target;
2013-12-15 18:14:52 +04:00
auto line = Kakoune::clamp(coord.line + offset, 0_line, line_count()-1);
StringView content = m_lines[line];
2013-12-15 18:14:52 +04:00
character = std::max(0_char, std::min(character, content.char_length() - 2));
return {line, content.byte_count_to(character), character};
2013-12-15 18:14:52 +04:00
}
String Buffer::string(ByteCoord begin, ByteCoord end) const
2011-09-02 20:51:20 +04:00
{
String res;
2013-06-28 01:49:34 +04:00
for (auto line = begin.line; line <= end.line and line < line_count(); ++line)
{
2013-06-28 01:49:34 +04:00
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;
2011-09-02 20:51:20 +04:00
}
// A Modification holds a single atomic modification to Buffer
struct Buffer::Modification
{
enum Type { Insert, Erase };
2014-05-10 19:25:07 +04:00
Type type;
ByteCoord coord;
StringDataPtr content;
Modification(Type type, ByteCoord coord, StringDataPtr content)
: type(type), coord(coord), content(std::move(content)) {}
Modification inverse() const
{
2014-05-10 19:25:07 +04:00
return {type == Insert ? Erase : Insert, coord, content};
}
};
void Buffer::reload(StringView data, timespec fs_timestamp)
{
ParsedLines parsed_lines = parse_lines(data);
if (parsed_lines.lines.empty())
parsed_lines.lines.emplace_back(StringData::create("\n"));
2015-05-13 22:54:44 +03:00
const bool record_undo = not (m_flags & Flags::NoUndo);
commit_undo_group();
if (not record_undo)
{
m_changes.push_back({ Change::Erase, true, {0,0}, line_count() });
static_cast<BufferLines&>(m_lines) = std::move(parsed_lines.lines);
m_changes.push_back({ Change::Insert, true, {0,0}, line_count() });
}
else
{
auto diff = find_diff(m_lines.begin(), m_lines.size(),
parsed_lines.lines.begin(), (int)parsed_lines.lines.size(),
[](const StringDataPtr& lhs, const StringDataPtr& rhs)
2015-05-14 21:13:52 +03:00
{ return lhs->hash == rhs->hash and lhs->strview() == rhs->strview(); });
auto it = m_lines.begin();
for (auto& d : diff)
2015-05-13 22:54:44 +03:00
{
if (d.mode == Diff::Keep)
it += d.len;
else if (d.mode == Diff::Add)
2015-05-13 22:54:44 +03:00
{
const LineCount cur_line = (int)(it - m_lines.begin());
for (LineCount line = 0; line < d.len; ++line)
m_current_undo_group.emplace_back(
Modification::Insert, cur_line + line,
parsed_lines.lines[(int)(d.posB + line)]);
2015-05-13 22:54:44 +03:00
m_changes.push_back({ Change::Insert, it == m_lines.end(), cur_line, cur_line + d.len });
m_lines.insert(it, &parsed_lines.lines[d.posB], &parsed_lines.lines[d.posB + d.len]);
it = m_lines.begin() + (int)(cur_line + d.len);
2015-05-13 22:54:44 +03:00
}
else if (d.mode == Diff::Remove)
2015-05-13 22:54:44 +03:00
{
const LineCount cur_line = (int)(it - m_lines.begin());
for (LineCount line = d.len-1; line >= 0; --line)
m_current_undo_group.emplace_back(
Modification::Erase, cur_line + line,
m_lines.get_storage(cur_line + line));
2015-05-13 22:54:44 +03:00
it = m_lines.erase(it, it + d.len);
m_changes.push_back({ Change::Erase, it == m_lines.end(), cur_line, cur_line + d.len });
}
}
}
commit_undo_group();
apply_options(options(), parsed_lines);
m_last_save_undo_index = m_history_cursor - m_history.begin();
m_fs_timestamp = fs_timestamp;
}
void Buffer::commit_undo_group()
{
if (m_flags & Flags::NoUndo)
return;
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));
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;
}
bool Buffer::undo()
{
commit_undo_group();
if (m_history_cursor == m_history.begin())
return false;
--m_history_cursor;
for (const Modification& modification : *m_history_cursor | reverse())
apply_modification(modification.inverse());
2012-06-05 17:33:02 +04:00
return true;
}
bool Buffer::redo()
{
if (m_history_cursor == m_history.end())
return false;
kak_assert(m_current_undo_group.empty());
for (const Modification& modification : *m_history_cursor)
apply_modification(modification);
++m_history_cursor;
2012-06-05 17:33:02 +04:00
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
}
ByteCoord Buffer::do_insert(ByteCoord pos, StringView content)
{
kak_assert(is_valid(pos));
if (content.empty())
return pos;
2016-03-16 02:06:49 +03:00
const bool at_end = is_end(pos);
const bool append_lines = at_end and (m_lines.empty() or byte_at(back_coord()) == '\n');
2016-03-16 02:06:49 +03:00
if (at_end)
pos = append_lines ? line_count() : end_coord();
const StringView prefix = append_lines ?
2016-03-16 02:28:10 +03:00
StringView{} : m_lines[pos.line].substr(0, pos.column);
const StringView suffix = at_end ?
StringView{} : m_lines[pos.line].substr(pos.column);
2016-03-16 02:28:10 +03:00
LineList new_lines;
ByteCount start = 0;
for (ByteCount i = 0; i < content.length(); ++i)
{
if (content[i] == '\n')
{
2016-03-16 02:28:10 +03:00
StringView line = content.substr(start, i + 1 - start);
new_lines.push_back(StringData::create(start == 0 ? prefix + line : line));
start = i + 1;
}
}
2016-03-16 02:28:10 +03:00
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)
2016-03-16 02:28:10 +03:00
*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;
2016-03-16 16:22:11 +03:00
const ByteCoord end = ByteCoord{ last_line, m_lines[last_line].length() - suffix.length() };
2016-03-16 16:22:11 +03:00
m_changes.push_back({ Change::Insert, at_end, pos, end });
return pos;
}
ByteCoord Buffer::do_erase(ByteCoord begin, ByteCoord end)
{
kak_assert(is_valid(begin));
kak_assert(is_valid(end));
2014-06-26 22:01:39 +04:00
StringView prefix = m_lines[begin.line].substr(0, begin.column);
StringView suffix = m_lines[end.line].substr(end.column);
String new_line = prefix + suffix;
ByteCoord next;
if (new_line.length() != 0)
{
m_lines.erase(m_lines.begin() + (int)begin.line, m_lines.begin() + (int)end.line);
2015-03-01 15:03:08 +03:00
m_lines.get_storage(begin.line) = StringData::create(new_line);
next = begin;
}
else
{
m_lines.erase(m_lines.begin() + (int)begin.line, m_lines.begin() + (int)end.line + 1);
next = is_end(begin) ? end_coord() : ByteCoord{begin.line, 0};
}
2015-01-30 01:44:07 +03:00
m_changes.push_back({ Change::Erase, is_end(begin), begin, end });
return next;
}
void Buffer::apply_modification(const Modification& modification)
{
StringView content = modification.content->strview();
ByteCoord 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);
}
}
ByteCoord Buffer::insert(ByteCoord pos, StringView content)
{
kak_assert(is_valid(pos));
if (content.empty())
return pos;
StringDataPtr real_content;
if (is_end(pos) and content.back() != '\n')
real_content = intern(content + "\n");
else
real_content = intern(content);
// for undo and redo purpose it is better to use one past last line rather
// than one past last char coord.
auto coord = is_end(pos) ? line_count() : pos;
if (not (m_flags & Flags::NoUndo))
m_current_undo_group.emplace_back(Modification::Insert, coord, real_content);
return do_insert(pos, real_content->strview());
}
ByteCoord Buffer::erase(ByteCoord begin, ByteCoord end)
{
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 == ByteCoord{0,0}) ? prev(end) : end_coord();
if (begin >= end) // use >= to handle case where begin is {line_count}
return begin;
2012-03-13 01:31:27 +04:00
if (not (m_flags & Flags::NoUndo))
m_current_undo_group.emplace_back(Modification::Erase, begin,
intern(string(begin, end)));
return do_erase(begin, end);
}
ByteCoord Buffer::replace(ByteCoord begin, ByteCoord end, StringView content)
{
if (not (m_flags & Flags::NoUndo))
m_current_undo_group.emplace_back(Modification::Erase, begin,
intern(string(begin, end)));
auto pos = do_erase(begin, end);
StringDataPtr real_content;
if (is_end(pos) and content.back() != '\n')
real_content = intern(content + "\n");
else
real_content = intern(content);
bool last_char_is_eol = not (m_lines.empty() or
m_lines.back().empty() or
m_lines.back().back() != '\n');
auto coord = is_end(pos) and last_char_is_eol ? line_count() : pos;
if (not (m_flags & Flags::NoUndo))
m_current_undo_group.emplace_back(Modification::Insert, coord, real_content);
return do_insert(pos, real_content->strview());
}
2011-10-05 18:21:24 +04:00
bool Buffer::is_modified() const
{
2011-11-03 17:44:02 +04:00
size_t history_cursor_index = m_history_cursor - m_history.begin();
return m_last_save_undo_index != history_cursor_index
2011-10-05 18:21:24 +04:00
or not m_current_undo_group.empty();
}
void Buffer::notify_saved()
{
2012-12-19 21:56:20 +04:00
if (not m_current_undo_group.empty())
commit_undo_group();
2012-12-19 21:56:20 +04:00
m_flags &= ~Flags::New;
2011-11-03 17:44:02 +04:00
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_fs_timestamp = get_fs_timestamp(m_name);
2011-10-05 18:21:24 +04:00
}
ByteCoord Buffer::advance(ByteCoord 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)
{
count += m_lines[--line].length();
if (line < 0)
return {0, 0};
}
return { line, count };
}
return coord;
}
ByteCoord Buffer::char_next(ByteCoord coord) const
{
if (coord.column < m_lines[coord.line].length() - 1)
{
auto line = m_lines[coord.line];
2014-10-05 13:20:50 +04:00
coord.column += utf8::codepoint_size(line[coord.column]);
// Handle invalid utf-8
if (coord.column >= line.length())
{
++coord.line;
coord.column = 0;
}
}
else if (coord.line == m_lines.size() - 1)
coord.column = m_lines.back().length();
else
{
++coord.line;
coord.column = 0;
}
return coord;
}
ByteCoord Buffer::char_prev(ByteCoord coord) const
{
kak_assert(is_valid(coord));
if (is_end(coord))
return coord = {(int)m_lines.size()-1, m_lines.back().length() - 1};
else if (coord.column == 0)
{
if (coord.line > 0)
2013-06-04 16:11:55 +04:00
coord.column = m_lines[--coord.line].length() - 1;
}
else
{
auto line = m_lines[coord.line];
coord.column = (int)(utf8::character_start(line.begin() + (int)coord.column - 1, line.begin()) - line.begin());
}
return coord;
}
timespec Buffer::fs_timestamp() const
{
kak_assert(m_flags & Flags::File);
return m_fs_timestamp;
}
void Buffer::set_fs_timestamp(timespec ts)
{
kak_assert(m_flags & Flags::File);
m_fs_timestamp = ts;
}
2013-11-13 00:36:42 +04:00
void Buffer::on_option_changed(const Option& option)
{
run_hook_in_own_context("BufSetOption",
format("{}={}", option.name(), option.get_as_string()));
}
void Buffer::run_hook_in_own_context(StringView hook_name, StringView param)
{
InputHandler hook_handler({ *this, Selection{} }, Context::Flags::Transient);
hooks().run_hook(hook_name, param, hook_handler.context());
2013-11-13 00:36:42 +04:00
}
ByteCoord Buffer::last_modification_coord() const
{
if (m_history.empty())
return {};
return m_history.back().back().coord;
}
String Buffer::debug_description() const
{
size_t content_size = 0;
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);
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 " : "",
content_size, additional_size);
}
UnitTest test_parse_line{[]
{
{
auto lines = parse_lines("foo\nbar\nbaz\n");
kak_assert(lines.eolformat == EolFormat::Lf);
kak_assert(lines.bom == ByteOrderMark::None);
kak_assert(lines.lines.size() == 3);
kak_assert(lines.lines[0]->strview() == "foo\n");
kak_assert(lines.lines[1]->strview() == "bar\n");
kak_assert(lines.lines[2]->strview() == "baz\n");
}
{
auto lines = parse_lines("\xEF\xBB\xBF" "foo\nbar\r\nbaz");
kak_assert(lines.eolformat == EolFormat::Lf);
kak_assert(lines.bom == ByteOrderMark::Utf8);
kak_assert(lines.lines.size() == 3);
kak_assert(lines.lines[0]->strview() == "foo\n");
kak_assert(lines.lines[1]->strview() == "bar\r\n");
kak_assert(lines.lines[2]->strview() == "baz\n");
}
{
auto lines = parse_lines("foo\r\nbar\r\nbaz\r\n");
kak_assert(lines.eolformat == EolFormat::Crlf);
kak_assert(lines.bom == ByteOrderMark::None);
kak_assert(lines.lines.size() == 3);
kak_assert(lines.lines[0]->strview() == "foo\n");
kak_assert(lines.lines[1]->strview() == "bar\n");
kak_assert(lines.lines[2]->strview() == "baz\n");
}
}};
UnitTest test_buffer{[]()
{
Buffer empty_buffer("empty", Buffer::Flags::None, {});
Buffer buffer("test", Buffer::Flags::None, "allo ?\nmais 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() == ByteCoord{0, 6});
++pos;
kak_assert(pos.coord() == ByteCoord{1, 0});
--pos;
kak_assert(pos.coord() == ByteCoord{0, 6});
pos += 1;
kak_assert(pos.coord() == ByteCoord{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()) == StringView{"tchou\n"});
pos = buffer.end()-1;
buffer.insert(buffer.end_coord(), "kanaky\n");
kak_assert(buffer.string((pos+1).coord(), buffer.end_coord()) == StringView{"kanaky\n"});
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()) == StringView{"kanaky\n"});
buffer.redo();
kak_assert(buffer.string(buffer.advance(buffer.end_coord(), -6), buffer.end_coord()) == StringView{"mutch\n"});
}};
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");
buffer.undo();
buffer.redo();
buffer.undo();
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");
}};
2011-09-02 20:51:20 +04:00
}