From a996beb4aa454589f7888d49357d527da8d5dc79 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Mon, 31 Aug 2026 08:14:12 +0100 Subject: [PATCH] Add all basic ed functions - except global and history ones --- include/bed.h | 5 +- include/internal/buffer/clip.h | 6 +- include/internal/buffer/decl.h | 64 ++- include/internal/buffer/generic.h | 6 +- include/internal/io/io.h | 1 + include/internal/vase/vase.h | 2 + include/pch.h | 1 + src/bed/run.cc | 9 +- src/internal/buffer/generic.cc | 159 ++----- src/internal/buffer/special/clip.cc | 85 ++-- src/internal/functions/functions.cc | 617 ++++++++++++++++++------- src/internal/io/io.cc | 62 +++ src/internal/parser/address_promise.cc | 14 +- src/internal/parser/operation.cc | 2 + src/internal/vase/vase.cc | 61 ++- 15 files changed, 732 insertions(+), 362 deletions(-) diff --git a/include/bed.h b/include/bed.h index 7bd3001..f3af9d2 100644 --- a/include/bed.h +++ b/include/bed.h @@ -23,14 +23,15 @@ struct BEd { internal::vase::AppendStorage append{"/tmp"}; bool help_mode = false; - std::string last_help = ""; bool prompt_mode = true; std::function prompt = nullptr; bool suppress_mode = false; bool temporary_current = false; + std::string last_help = ""; std::string last_regex = ""; std::string last_symbol = ""; std::string last_replacement = ""; + std::string last_shell = ""; std::unordered_map buffers; @@ -44,7 +45,7 @@ struct BEd { internal::buffer::Buffer &buffer(const std::string &); internal::buffer::Line ¤t(); internal::buffer::Range &prev(); - void mark(char, internal::buffer::Line); + void mark(uint8_t, internal::buffer::Line); void handle(std::string_view cmd, bool eof); void run(); diff --git a/include/internal/buffer/clip.h b/include/internal/buffer/clip.h index 48216ed..3f4a1a6 100644 --- a/include/internal/buffer/clip.h +++ b/include/internal/buffer/clip.h @@ -13,10 +13,9 @@ struct ClipBuffer : Buffer { bool waste() override; uint64_t lines() override; uint64_t bytes() override; - void load(BEd &ctx) override; void load(BEd &ctx, vase::Shard *text) override; - void load(BEd &ctx, const char *cmd) override; - void load(BEd &ctx, std::filesystem::path path) override; + void set_filename(std::filesystem::path path) override; + std::filesystem::path filename() override; vase::Shard *copy(uint64_t start_line, uint64_t end_line) override; void substitute( BEd &ctx, uint64_t start_line, uint64_t end_line, @@ -27,6 +26,7 @@ struct ClipBuffer : Buffer { void append(BEd &ctx, vase::Shard *text, uint64_t line) override; void print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; void number_print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; + void list_print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; uint64_t next_closing(uint64_t start) override; uint64_t prev_closing(uint64_t start) override; uint64_t find_next(std::string_view pattern, uint64_t start) override; diff --git a/include/internal/buffer/decl.h b/include/internal/buffer/decl.h index 643a786..f49572f 100644 --- a/include/internal/buffer/decl.h +++ b/include/internal/buffer/decl.h @@ -32,10 +32,9 @@ struct Buffer { virtual bool waste() = 0; virtual uint64_t lines() = 0; virtual uint64_t bytes() = 0; + virtual void set_filename(std::filesystem::path path) = 0; + virtual std::filesystem::path filename() = 0; virtual void load(BEd &ctx, vase::Shard *text) = 0; - virtual void load(BEd &ctx, const char *cmd) = 0; - virtual void load(BEd &ctx, std::filesystem::path path) = 0; - virtual void load(BEd &ctx) = 0; virtual vase::Shard *copy(uint64_t start_line, uint64_t end_line) = 0; virtual void substitute( BEd &ctx, uint64_t start_line, uint64_t end_line, @@ -46,6 +45,7 @@ struct Buffer { virtual void append(BEd &ctx, vase::Shard *text, uint64_t line) = 0; virtual void print(BEd &ctx, uint64_t start_line, uint64_t end_line) = 0; virtual void number_print(BEd &ctx, uint64_t start_line, uint64_t end_line) = 0; + virtual void list_print(BEd &ctx, uint64_t start_line, uint64_t end_line) = 0; virtual uint64_t find_next(std::string_view pattern, uint64_t start) = 0; virtual uint64_t find_prev(std::string_view pattern, uint64_t start) = 0; virtual uint64_t next_closing(uint64_t start) = 0; @@ -85,4 +85,62 @@ using Address = std::variant< std::string, buffer::Range, buffer::Line>; + +inline static std::string list_string(std::string_view s) { + uint32_t width = 80; + winsize ws{}; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col != 0) + width = ws.ws_col; + std::string out; + out.reserve(s.size()); + const uint32_t max_width = width > 1 ? width - 1 : 1; + uint32_t column = 0; + auto append = [&](std::string_view text) { + if (column + text.size() > max_width) { + out += "\\\n"; + column = 0; + } + out += text; + column += text.size(); + }; + for (unsigned char c : s) { + switch (c) { + case '\\': + append("\\\\"); + break; + case '$': + append("\\$"); + break; + case '\a': + append("\\a"); + break; + case '\b': + append("\\b"); + break; + case '\f': + append("\\f"); + break; + case '\r': + append("\\r"); + break; + case '\t': + append("\\t"); + break; + case '\v': + append("\\v"); + break; + default: + if (!std::isprint(c)) { + char buf[5]; + std::snprintf(buf, sizeof(buf), "\\%03o", c); + append(buf); + } else { + append(std::string_view((const char *)&c, 1)); + } + break; + } + } + out += '$'; + return out; +} } // namespace bed::internal::buffer diff --git a/include/internal/buffer/generic.h b/include/internal/buffer/generic.h index 9c2be19..a4a3758 100644 --- a/include/internal/buffer/generic.h +++ b/include/internal/buffer/generic.h @@ -17,10 +17,9 @@ struct GenericBuffer : Buffer { bool waste() override; uint64_t lines() override; uint64_t bytes() override; - void load(BEd &ctx) override; void load(BEd &ctx, vase::Shard *text) override; - void load(BEd &ctx, const char *cmd) override; - void load(BEd &ctx, std::filesystem::path path) override; + void set_filename(std::filesystem::path path) override; + std::filesystem::path filename() override; vase::Shard *copy(uint64_t start_line, uint64_t end_line) override; void substitute( BEd &ctx, uint64_t start_line, uint64_t end_line, @@ -31,6 +30,7 @@ struct GenericBuffer : Buffer { void append(BEd &ctx, vase::Shard *text, uint64_t line) override; void print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; void number_print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; + void list_print(BEd &ctx, uint64_t start_line, uint64_t end_line) override; uint64_t next_closing(uint64_t start) override; uint64_t prev_closing(uint64_t start) override; uint64_t find_next(std::string_view pattern, uint64_t start) override; diff --git a/include/internal/io/io.h b/include/internal/io/io.h index d92df91..f6bc445 100644 --- a/include/internal/io/io.h +++ b/include/internal/io/io.h @@ -74,6 +74,7 @@ struct IO { KeyEvent read_key(); void write(const char *, uint64_t); void write(std::string_view); + void run_pty(const std::string &); std::deque input_queue; diff --git a/include/internal/vase/vase.h b/include/internal/vase/vase.h index 490c7fd..a8d190d 100644 --- a/include/internal/vase/vase.h +++ b/include/internal/vase/vase.h @@ -50,6 +50,8 @@ Shard *insert(AppendStorage *ap, Shard *root, Shard *text, uint64_t line); Shard *erase(Shard *root, uint64_t start, uint64_t end); Shard *join(Shard *root, uint64_t start, uint64_t end); Shard *copy(Shard *root, uint64_t start, uint64_t end); +void write_file(std::filesystem::path path, Shard *text); +void write_command(const char *cmd, Shard *text); uint64_t find_next(Shard *root, std::string_view pattern, uint64_t start); uint64_t find_prev(Shard *root, std::string_view pattern, uint64_t start); diff --git a/include/pch.h b/include/pch.h index 1db16a9..f48f1b0 100644 --- a/include/pch.h +++ b/include/pch.h @@ -35,6 +35,7 @@ extern "C" { #include #include #include +#include #include #include #include diff --git a/src/bed/run.cc b/src/bed/run.cc index bfc1858..9386cc7 100644 --- a/src/bed/run.cc +++ b/src/bed/run.cc @@ -17,6 +17,8 @@ BEd::BEd(std::vector args, internal::io::IO &io) prompt_ = args[i]; } else if (args[i] == "-s") { suppress = true; + } else if (args[i] == "-v" || args[i] == "--verbose") { + help_mode = true; } else { if (file.size()) throw fatal_error("Invalid arguments given.", 1); @@ -29,6 +31,7 @@ BEd::BEd(std::vector args, internal::io::IO &io) prompt_mode = false; suppress_mode = suppress; buffers["clip"] = new internal::buffer::ClipBuffer("clip"); + current() = {"default", 0}; try { if (file != "") handle(":default:E " + file, false); @@ -105,12 +108,16 @@ void BEd::handle(std::string_view cmd, bool eof) { } break; } if (std::holds_alternative(c.argument)) { + if (c.argument_addresses.empty()) + throw ed_error("Function needs address argument."); auto a = internal::parser::AddressPromise::get_line(*this, c.argument_addresses); if (a.has_value()) c.argument = *a; else c.argument = current(); } else if (std::holds_alternative(c.argument)) { + if (c.argument_addresses.empty()) + throw ed_error("Function needs address argument."); auto a = internal::parser::AddressPromise::get_range(*this, c.argument_addresses); if (a.has_value()) c.argument = *a; @@ -173,7 +180,7 @@ internal::buffer::Range &BEd::prev() { return prev_1; } -void BEd::mark(char m, internal::buffer::Line line) { +void BEd::mark(uint8_t m, internal::buffer::Line line) { marks.get(m) = line; } } // namespace bed diff --git a/src/internal/buffer/generic.cc b/src/internal/buffer/generic.cc index 8ef85a3..535dc56 100644 --- a/src/internal/buffer/generic.cc +++ b/src/internal/buffer/generic.cc @@ -24,81 +24,31 @@ uint64_t GenericBuffer::bytes() { } void GenericBuffer::load(BEd &ctx, vase::Shard *text) { - try { - if (lines()) - ctx.marks.erase(name, 1, lines()); - vase::Shard::release(root); - root = text; - state = buffer::GenericBuffer::Unmodified; - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - parser.emplace(root, lines(), syntax::ruby::lang_ruby()); - } catch (...) { - vase::Shard::release(text); - throw; + if (lines()) + ctx.marks.erase(name, 1, lines()); + vase::Shard::release(root); + vase::Shard::retain(text); + root = text; + state = buffer::GenericBuffer::Unmodified; + if (!text) { + ctx.prev().buffername = name; + ctx.prev().start = 0; + ctx.prev().end = 0; + } else { + ctx.prev().buffername = name; + ctx.prev().start = 1; + ctx.prev().end = text->lines + 1; } + parser.emplace(root, lines(), syntax::ruby::lang_ruby()); } -void GenericBuffer::load(BEd &ctx) { - if (save_path.empty()) - throw ed_error("File cannot be implied."); - load(ctx, save_path); +void GenericBuffer::set_filename(std::filesystem::path path) { + save_path = path; } -void GenericBuffer::load(BEd &ctx, const char *cmd) { - auto text = vase::Shard::from_command(cmd, true); - try { - if (lines()) - ctx.marks.erase(name, 1, lines()); - vase::Shard::release(root); - root = text; - state = buffer::GenericBuffer::Unmodified; - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - parser.emplace(root, lines(), syntax::ruby::lang_ruby()); - } catch (...) { - vase::Shard::release(text); - throw; - } -} - -void GenericBuffer::load(BEd &ctx, std::filesystem::path path) { - auto text = vase::Shard::from_file(path, true); - try { - if (lines()) - ctx.marks.erase(name, 1, lines()); - vase::Shard::release(root); - root = text; - state = buffer::GenericBuffer::Unmodified; - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - parser.emplace(root, lines(), syntax::ruby::lang_ruby()); - } catch (...) { - vase::Shard::release(text); - throw; - } -} +std::filesystem::path GenericBuffer::filename() { + return save_path; +}; void GenericBuffer::append(BEd &ctx, vase::Shard *text, uint64_t line) { ctx.prev().buffername = name; @@ -114,8 +64,8 @@ void GenericBuffer::append(BEd &ctx, vase::Shard *text, uint64_t line) { void GenericBuffer::remove(BEd &ctx, uint64_t start_line, uint64_t end_line) { root = vase::erase(root, start_line, end_line); ctx.prev().buffername = name; - ctx.prev().start = lines() ? 1 : 0; - ctx.prev().end = lines(); + ctx.prev().start = std::min(start_line, lines()); + ctx.prev().end = std::min(start_line, lines()); ctx.marks.erase(name, start_line, end_line - start_line + 1); if (parser) parser->erase(root, start_line, end_line - start_line + 1); @@ -327,61 +277,12 @@ void GenericBuffer::number_print(BEd &ctx, uint64_t start_line, uint64_t end_lin } } -/*std::string GenericBuffer::list_string(std::string_view s) { - uint32_t width = 80; - winsize ws{}; - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col != 0) - width = ws.ws_col; - std::string out; - out.reserve(s.size()); - const uint32_t max_width = width > 1 ? width - 1 : 1; - uint32_t column = 0; - auto append = [&](std::string_view text) { - if (column + text.size() > max_width) { - out += "\\\n"; - column = 0; - } - out += text; - column += text.size(); - }; - for (unsigned char c : s) { - switch (c) { - case '\\': - append("\\\\"); - break; - case '$': - append("\\$"); - break; - case '\a': - append("\\a"); - break; - case '\b': - append("\\b"); - break; - case '\f': - append("\\f"); - break; - case '\r': - append("\\r"); - break; - case '\t': - append("\\t"); - break; - case '\v': - append("\\v"); - break; - default: - if (!std::isprint(c)) { - char buf[5]; - std::snprintf(buf, sizeof(buf), "\\%03o", c); - append(buf); - } else { - append(std::string_view((const char *)&c, 1)); - } - break; - } - } - out += '$'; - return out; -}*/ +void GenericBuffer::list_print(BEd &ctx, uint64_t start_line, uint64_t end_line) { + ctx.prev().buffername = name; + ctx.prev().start = start_line; + ctx.prev().end = end_line; + vase::Iterator it(root, start_line - 1, Direction::Forward); + while (it.next() && start_line++ <= end_line) + std::cout << list_string(it.line) << std::endl; +} } // namespace bed::internal::buffer diff --git a/src/internal/buffer/special/clip.cc b/src/internal/buffer/special/clip.cc index 385020f..a713eb5 100644 --- a/src/internal/buffer/special/clip.cc +++ b/src/internal/buffer/special/clip.cc @@ -33,67 +33,23 @@ void ClipBuffer::clip_write(vase::Shard *text) { } void ClipBuffer::load(BEd &ctx, vase::Shard *text) { - try { - clip_write(text); - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - vase::Shard::release(text); - } catch (...) { - vase::Shard::release(text); - throw; + clip_write(text); + if (!text) { + ctx.prev().buffername = name; + ctx.prev().start = 0; + ctx.prev().end = 0; + } else { + ctx.prev().buffername = name; + ctx.prev().start = 1; + ctx.prev().end = text->lines + 1; } } -void ClipBuffer::load(BEd &) { - throw ed_error("File cannot be implied."); -} +void ClipBuffer::set_filename(std::filesystem::path) {} -void ClipBuffer::load(BEd &ctx, const char *cmd) { - auto text = vase::Shard::from_command(cmd, true); - try { - clip_write(text); - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - vase::Shard::release(text); - } catch (...) { - vase::Shard::release(text); - throw; - } -} - -void ClipBuffer::load(BEd &ctx, std::filesystem::path path) { - auto text = vase::Shard::from_file(path, true); - try { - clip_write(text); - if (!text) { - ctx.prev().buffername = name; - ctx.prev().start = 0; - ctx.prev().end = 0; - } else { - ctx.prev().buffername = name; - ctx.prev().start = 1; - ctx.prev().end = text->lines + 1; - } - vase::Shard::release(text); - } catch (...) { - vase::Shard::release(text); - throw; - } -} +std::filesystem::path ClipBuffer::filename() { + return ""; +}; void ClipBuffer::append(BEd &ctx, vase::Shard *text, uint64_t line) { ctx.prev().buffername = name; @@ -111,8 +67,8 @@ void ClipBuffer::remove(BEd &ctx, uint64_t start_line, uint64_t end_line) { s = vase::erase(s, start_line, end_line); clip_write(s); ctx.prev().buffername = name; - ctx.prev().start = (s ? s->length + 1 : 0) ? 1 : 0; - ctx.prev().end = s ? s->length + 1 : 0; + ctx.prev().start = std::min(start_line, s ? s->lines + 1 : 0); + ctx.prev().end = std::min(start_line, s ? s->lines + 1 : 0); vase::Shard::release(s); ctx.marks.erase(name, start_line, end_line - start_line + 1); } @@ -215,4 +171,15 @@ void ClipBuffer::number_print(BEd &ctx, uint64_t start_line, uint64_t end_line) std::cout << std::setw(width) << start_line++ << "\t" << it.line << std::endl; vase::Shard::release(s); } + +void ClipBuffer::list_print(BEd &ctx, uint64_t start_line, uint64_t end_line) { + ctx.prev().buffername = name; + ctx.prev().start = start_line; + ctx.prev().end = end_line; + auto s = vase::Shard::from_command("xclip -selection clipboard -o", true); + vase::Iterator it(s, start_line - 1, Direction::Forward); + while (it.next() && start_line++ <= end_line) + std::cout << list_string(it.line) << std::endl; + vase::Shard::release(s); +} } // namespace bed::internal::buffer diff --git a/src/internal/functions/functions.cc b/src/internal/functions/functions.cc index d967c98..20a47fa 100644 --- a/src/internal/functions/functions.cc +++ b/src/internal/functions/functions.cc @@ -3,6 +3,32 @@ #include "internal/functions/suffixes.h" namespace bed::internal::functions { +static bool escape_command(BEd &ctx, std::string &cmd, std::string_view filename) { + bool modified = false; + if (cmd == "!") { + cmd = ctx.last_shell; + modified = true; + } + ctx.last_shell = cmd; + for (size_t i = 0; i < cmd.size();) { + if (cmd[i] == '\\') { + if (i + 1 >= cmd.size()) + break; + cmd.erase(i++, 1); + continue; + } + if (cmd[i] == '%') { + cmd.erase(i, 1); + cmd.insert(i, filename); + i += filename.size(); + modified = true; + continue; + } + i++; + } + return modified; +} + void Suffix::register_suffixes(BEd &ctx) { ctx.suffixes['p' - 'a'] = Suffix{ .desc = "Prints current line.", @@ -18,23 +44,16 @@ void Suffix::register_suffixes(BEd &ctx) { ctx.buffer(addr.buffername).number_print(ctx, addr.number, addr.number); } }; + ctx.suffixes['l' - 'a'] = Suffix{ + .desc = "Prints current line unambiguously.", + .handle = [](BEd &ctx) { + auto &addr = ctx.current(); + ctx.buffer(addr.buffername).list_print(ctx, addr.number, addr.number); + } + }; } void Function::register_posix(BEd &ctx) { - ctx.no_op = Function{ - .address_kind = Function::AddressKind::Line, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::None, - .desc = "Prints a line and jumps to it.", - .default_address = ".+1", - .accept_zero = false, - .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &, std::vector *) { - auto addr = std::get(addr_); - ctx.buffer(addr.buffername).print(ctx, addr.number, addr.number); - ctx.current() = addr; - } - }; ctx.eof_op = Function{ .address_kind = Function::AddressKind::None, .argument_kind = Function::ArgumentKind::None, @@ -71,6 +90,25 @@ void Function::register_posix(BEd &ctx) { } } ); + ctx.functions.insert( + "c", + Function{ + .address_kind = Function::AddressKind::Range, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::Text, + .desc = "Change set of lines.", + .default_address = ".,.", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *text, const Argument &, std::vector *) { + auto addr = std::get(addr_); + ctx.buffer(addr.buffername).remove(ctx, addr.start, addr.end); + ctx.buffer(addr.buffername).append(ctx, text, addr.start - 1); + ctx.current() = {ctx.prev().buffername, ctx.prev().end}; + vase::Shard::release(text); + } + } + ); ctx.functions.insert( "d", Function{ @@ -89,19 +127,162 @@ void Function::register_posix(BEd &ctx) { } ); ctx.functions.insert( - "c", + "e", Function{ - .address_kind = Function::AddressKind::Range, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::Text, - .desc = "Change set of lines.", - .default_address = ".,.", + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::File, + .input_mode = Function::InputMode::None, + .desc = "Try load a file into the current buffer.", + .default_address = "", .accept_zero = false, .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { + auto &addr = std::get(addr_); + auto &buf = ctx.buffer(addr); + if (buf.state == buffer::Buffer::Modified) { + buf.state = buffer::Buffer::Warned; + throw ed_error("Buffer modified."); + } + vase::Shard *s = nullptr; + if (std::holds_alternative(arg)) { + auto path = std::get(arg); + s = vase::Shard::from_file(path, true); + buf.set_filename(path); + } else if (std::holds_alternative(arg)) { + auto cmd = std::get(arg).cmd; + escape_command(ctx, cmd, buf.filename().string()); + s = vase::Shard::from_command(cmd.c_str(), true); + } else { + auto path = buf.filename(); + if (path.empty()) + throw ed_error("Need filename."); + s = vase::Shard::from_file(path, true); + }; + try { + buf.load(ctx, s); + vase::Shard::release(s); + } catch (...) { + vase::Shard::release(s); + throw; + } + std::cout << buf.bytes() << std::endl; + ctx.current() = {addr, buf.lines()}; + } + } + ); + ctx.functions.insert( + "E", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::File, + .input_mode = Function::InputMode::None, + .desc = "Load a file into the current buffer.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { + auto &addr = std::get(addr_); + auto &buf = ctx.buffer(addr); + vase::Shard *s = nullptr; + if (std::holds_alternative(arg)) { + auto path = std::get(arg); + s = vase::Shard::from_file(path, true); + buf.set_filename(path); + } else if (std::holds_alternative(arg)) { + auto cmd = std::get(arg).cmd; + escape_command(ctx, cmd, buf.filename().string()); + s = vase::Shard::from_command(cmd.c_str(), true); + } else { + auto path = buf.filename(); + if (path.empty()) + throw ed_error("Need filename."); + s = vase::Shard::from_file(path, true); + }; + try { + buf.load(ctx, s); + vase::Shard::release(s); + } catch (...) { + vase::Shard::release(s); + throw; + } + std::cout << buf.bytes() << std::endl; + ctx.current() = {addr, buf.lines()}; + } + } + ); + ctx.functions.insert( + "f", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::File, + .input_mode = Function::InputMode::None, + .desc = "Set a save path.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { + auto &addr = std::get(addr_); + auto &buf_ = ctx.buffer(addr); + if (buf_.kind != buffer::Buffer::Kind::Generic) + return; + auto &buf = *(buffer::GenericBuffer *)&buf_; + if (std::holds_alternative(arg)) + buf.set_filename(std::get(arg)); + else if (std::holds_alternative(arg)) + throw ed_error("Can't save shell command as save path."); + if (buf.filename().empty()) + throw ed_error("Filename needed."); + std::cout << buf.filename() << std::endl; + } + } + ); + ctx.functions.insert( + "h", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Print last help message", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { + std::cout << ctx.last_help << std::endl; + } + } + ); + ctx.functions.insert( + "H", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Toggle help mode.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { + ctx.help_mode = !ctx.help_mode; + if (ctx.help_mode) + std::cout << ctx.last_help << std::endl; + } + } + ); + ctx.functions.insert( + "i", + Function{ + .address_kind = Function::AddressKind::Line, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::Text, + .desc = "Insert text before a line.", + .default_address = ".", + .accept_zero = true, + .pre_text_mode = nullptr, .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *text, const Argument &, std::vector *) { - auto addr = std::get(addr_); - ctx.buffer(addr.buffername).remove(ctx, addr.start, addr.end); - ctx.buffer(addr.buffername).append(ctx, text, addr.start - 1); + auto addr = std::get(addr_); + if (addr.number) + addr.number--; + ctx.buffer(addr.buffername).append(ctx, text, addr.number); ctx.current() = {ctx.prev().buffername, ctx.prev().end}; vase::Shard::release(text); } @@ -125,58 +306,77 @@ void Function::register_posix(BEd &ctx) { } ); ctx.functions.insert( - "q", + "k", Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::None, + .address_kind = Function::AddressKind::Line, + .argument_kind = Function::ArgumentKind::Mark, .input_mode = Function::InputMode::None, - .desc = "Try quitting.", - .default_address = "", + .desc = "Mark a line.", + .default_address = ".", .accept_zero = false, .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { - for (auto &[name, buffer] : ctx.buffers) { - if (buffer->state == buffer::Buffer::Modified) { - buffer->state = buffer::Buffer::Warned; - throw ed_error("Buffer " + name + " modified."); - } - } - throw fatal_error("Quitting", 0); + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { + auto &addr = std::get(addr_); + ctx.mark(std::get(arg), addr); } } ); ctx.functions.insert( - "Q", - Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::None, - .desc = "Force quit.", - .default_address = "", - .accept_zero = false, - .pre_text_mode = nullptr, - .handle = [](BEd &, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { - throw fatal_error("Force Quitting", 0); - } - } - ); - ctx.functions.insert( - "p", + "l", Function{ .address_kind = Function::AddressKind::Range, .argument_kind = Function::ArgumentKind::None, .input_mode = Function::InputMode::None, - .desc = "Print range", + .desc = "List (print unambiguous) range", .default_address = ".,.", .accept_zero = false, .pre_text_mode = nullptr, .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &, std::vector *) { auto addr = std::get(addr_); - ctx.buffer(addr.buffername).print(ctx, addr.start, addr.end); + ctx.buffer(addr.buffername).list_print(ctx, addr.start, addr.end); ctx.current() = {addr.buffername, addr.end}; }, } ); + ctx.functions.insert( + "m", + Function{ + .address_kind = Function::AddressKind::Range, + .argument_kind = Function::ArgumentKind::Line, + .input_mode = Function::InputMode::None, + .desc = "Move a range of lines.", + .default_address = ".,.", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg_, std::vector *) { + auto addr = std::get(addr_); + auto arg = std::get(arg_); + if (arg.buffername == addr.buffername + && addr.start <= arg.number + && addr.end < arg.number) + throw ed_error("Can't move lines within themselves."); + auto text = ctx.buffer(addr.buffername).copy(addr.start, addr.end); + ctx.mark(252, arg); + ctx.buffer(addr.buffername).remove(ctx, addr.start, addr.end); + arg = ctx.marks.get(252); + try { + if (arg.number == UINT64_MAX) + throw ed_error("Unexpected error when moving lines."); + ctx.buffer(arg.buffername).append(ctx, text, arg.number); + vase::Shard::release(text); + } catch (...) { + if (!addr.start) { + vase::Shard::release(text); + throw; + } + ctx.buffer(addr.buffername).append(ctx, text, --addr.start); + vase::Shard::release(text); + throw; + } + ctx.current() = {arg.buffername, arg.number + addr.end - addr.start + 1}; + }, + } + ); ctx.functions.insert( "n", Function{ @@ -195,38 +395,118 @@ void Function::register_posix(BEd &ctx) { } ); ctx.functions.insert( - "=", + "p", Function{ .address_kind = Function::AddressKind::Range, .argument_kind = Function::ArgumentKind::None, .input_mode = Function::InputMode::None, - .desc = "Print line numbers", - .default_address = "$", - .accept_zero = true, + .desc = "Print range", + .default_address = ".,.", + .accept_zero = false, .pre_text_mode = nullptr, .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &, std::vector *) { - auto &addr = std::get(addr_); - if (addr.start == addr.end) - std::cout << ':' << addr.buffername << ':' << addr.start << "\n"; - else - std::cout << ':' << addr.buffername << ':' << addr.start << "," << addr.end << "\n"; + auto addr = std::get(addr_); + ctx.buffer(addr.buffername).print(ctx, addr.start, addr.end); ctx.current() = {addr.buffername, addr.end}; }, } ); ctx.functions.insert( - "k", + "P", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Toggle prompt.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { + ctx.prompt_mode = !ctx.prompt_mode; + if (ctx.prompt_mode && !ctx.prompt) { + ctx.prompt = [](BEd &) { return "*"; }; + } + } + } + ); + ctx.functions.insert( + "q", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Try quitting.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { + std::string modified_buffers; + for (auto &[name, buffer] : ctx.buffers) { + if (buffer->state == buffer::Buffer::Modified) { + buffer->state = buffer::Buffer::Warned; + modified_buffers.append(name + ", "); + } + } + if (!modified_buffers.size()) + throw fatal_error("Quitting", 0); + modified_buffers.erase(modified_buffers.size() - 2); + throw ed_error("Buffer(s) " + modified_buffers + " modified."); + } + } + ); + ctx.functions.insert( + "Q", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Force quit.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { + throw fatal_error("Force Quitting", 0); + } + } + ); + ctx.functions.insert( + "r", Function{ .address_kind = Function::AddressKind::Line, - .argument_kind = Function::ArgumentKind::Mark, + .argument_kind = Function::ArgumentKind::File, .input_mode = Function::InputMode::None, - .desc = "Mark a line.", - .default_address = ".", + .desc = "Read from file into buffer.", + .default_address = "$", .accept_zero = false, .pre_text_mode = nullptr, .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { auto &addr = std::get(addr_); - ctx.mark(std::get(arg), addr); + auto &buf = ctx.buffer(addr.buffername); + vase::Shard *s = nullptr; + if (std::holds_alternative(arg)) { + auto path = std::get(arg); + s = vase::Shard::from_file(path, true); + if (buf.filename().empty()) + buf.set_filename(path); + } else if (std::holds_alternative(arg)) { + auto cmd = std::get(arg).cmd; + escape_command(ctx, cmd, buf.filename().string()); + s = vase::Shard::from_command(cmd.c_str(), true); + } else { + auto path = buf.filename(); + if (path.empty()) + throw ed_error("Need filename."); + s = vase::Shard::from_file(path, true); + }; + try { + buf.append(ctx, s, addr.number); + std::cout << (s ? s->length + 1 : 0) << std::endl; + ctx.current() = {addr.buffername, addr.number + (s ? s->lines + 1 : 0)}; + vase::Shard::release(s); + } catch (...) { + vase::Shard::release(s); + throw; + } } } ); @@ -264,109 +544,136 @@ void Function::register_posix(BEd &ctx) { } ); ctx.functions.insert( - "e", + "t", Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::File, + .address_kind = Function::AddressKind::Range, + .argument_kind = Function::ArgumentKind::Line, .input_mode = Function::InputMode::None, - .desc = "Try load a file into the current buffer.", - .default_address = "", + .desc = "Copy a range of lines.", + .default_address = ".,.", .accept_zero = false, .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { - auto &addr = std::get(addr_); - auto &buf = ctx.buffer(addr); - if (buf.state == buffer::Buffer::Modified) { - buf.state = buffer::Buffer::Warned; - throw ed_error("Buffer modified."); + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg_, std::vector *) { + auto addr = std::get(addr_); + auto arg = std::get(arg_); + auto text = ctx.buffer(addr.buffername).copy(addr.start, addr.end); + try { + ctx.buffer(arg.buffername).append(ctx, text, arg.number); + vase::Shard::release(text); + } catch (...) { + if (!addr.start) { + vase::Shard::release(text); + throw; + } + ctx.buffer(addr.buffername).append(ctx, text, --addr.start); + vase::Shard::release(text); + throw; } - if (std::holds_alternative(arg)) - buf.load(ctx, std::get(arg)); - else if (std::holds_alternative(arg)) - buf.load(ctx, std::get(arg).cmd.c_str()); - else if (std::holds_alternative(arg)) - buf.load(ctx); - std::cout << buf.bytes() << std::endl; - ctx.current() = {addr, buf.lines()}; - } + ctx.current() = {arg.buffername, arg.number + addr.end - addr.start + 1}; + }, } ); ctx.functions.insert( - "E", + "w", Function{ - .address_kind = Function::AddressKind::None, + .address_kind = Function::AddressKind::Range, .argument_kind = Function::ArgumentKind::File, .input_mode = Function::InputMode::None, - .desc = "Load a file into the current buffer.", - .default_address = "", - .accept_zero = false, + .desc = "Write buffer to disk.", + .default_address = "0,$", + .accept_zero = true, .pre_text_mode = nullptr, .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg, std::vector *) { - auto &addr = std::get(addr_); - auto &buf_ = ctx.buffer(addr); - if (buf_.kind != buffer::Buffer::Kind::Generic) - return; - auto &buf = *(buffer::GenericBuffer *)&buf_; - if (std::holds_alternative(arg)) - buf.load(ctx, std::get(arg)); - else if (std::holds_alternative(arg)) - buf.load(ctx, std::get(arg).cmd.c_str()); - else if (std::holds_alternative(arg)) - buf.load(ctx); - std::cout << buf.bytes() << std::endl; - ctx.current() = {addr, buf.lines()}; - } - } - ); - ctx.functions.insert( - "H", - Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::None, - .desc = "Toggle help mode.", - .default_address = "", - .accept_zero = false, - .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { - ctx.help_mode = !ctx.help_mode; - if (ctx.help_mode) - std::cout << ctx.last_help << std::endl; - } - } - ); - ctx.functions.insert( - "h", - Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::None, - .desc = "Print last help message", - .default_address = "", - .accept_zero = false, - .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { - std::cout << ctx.last_help << std::endl; - } - } - ); - ctx.functions.insert( - "P", - Function{ - .address_kind = Function::AddressKind::None, - .argument_kind = Function::ArgumentKind::None, - .input_mode = Function::InputMode::None, - .desc = "Toggle prompt.", - .default_address = "", - .accept_zero = false, - .pre_text_mode = nullptr, - .handle = [](BEd &ctx, const buffer::Address &, vase::Shard *, const Argument &, std::vector *) { - ctx.prompt_mode = !ctx.prompt_mode; - if (ctx.prompt_mode && !ctx.prompt) { - ctx.prompt = [](BEd &) { return "*"; }; + auto addr = std::get(addr_); + auto &buf = ctx.buffer(addr.buffername); + vase::Shard *text; + if (!addr.start && !addr.end) { + text = nullptr; + } else { + if (!addr.start && addr.end) + addr.start = 1; + text = buf.copy(addr.start, addr.end); + } + try { + if (std::holds_alternative(arg)) { + auto path = std::get(arg); + vase::write_file(path, text); + buf.set_filename(path); + } else if (std::holds_alternative(arg)) { + auto cmd = std::get(arg).cmd; + escape_command(ctx, cmd, buf.filename().string()); + vase::write_command(cmd.c_str(), text); + } else { + auto path = buf.filename(); + if (path.empty()) + throw ed_error("Need filename."); + vase::write_file(path, text); + }; + ctx.io.write(std::format("{}\n", text ? text->length + 1 : 0)); + vase::Shard::release(text); + } catch (...) { + vase::Shard::release(text); + throw; } } } ); + ctx.functions.insert( + "=", + Function{ + .address_kind = Function::AddressKind::Range, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Print line numbers", + .default_address = "$", + .accept_zero = true, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &, std::vector *) { + auto &addr = std::get(addr_); + if (addr.start == addr.end) + std::cout << ':' << addr.buffername << ':' << addr.start << "\n"; + else + std::cout << ':' << addr.buffername << ':' << addr.start << "," << addr.end << "\n"; + ctx.current() = {addr.buffername, addr.end}; + }, + } + ); + ctx.functions.insert( + "!", + Function{ + .address_kind = Function::AddressKind::None, + .argument_kind = Function::ArgumentKind::Shell, + .input_mode = Function::InputMode::None, + .desc = "Run a shell command.", + .default_address = "", + .accept_zero = false, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &arg_, std::vector *) { + auto &addr = std::get(addr_); + auto filename = ctx.buffer(addr).filename(); + auto &arg = std::get(arg_); + auto cmd = arg.cmd; + if (escape_command(ctx, cmd, filename.string())) + ctx.io.write(cmd + "\n"); + ctx.io.run_pty(cmd); + ctx.io.write("!\n"); + }, + } + ); + ctx.no_op = Function{ + .address_kind = Function::AddressKind::Line, + .argument_kind = Function::ArgumentKind::None, + .input_mode = Function::InputMode::None, + .desc = "Prints a line and jumps to it.", + .default_address = ".+1", + .accept_zero = true, + .pre_text_mode = nullptr, + .handle = [](BEd &ctx, const buffer::Address &addr_, vase::Shard *, const Argument &, std::vector *) { + auto addr = std::get(addr_); + if (addr.number != 0) + ctx.buffer(addr.buffername).print(ctx, addr.number, addr.number); + ctx.current() = addr; + } + }; } } // namespace bed::internal::functions diff --git a/src/internal/io/io.cc b/src/internal/io/io.cc index 72b9d4f..e39e3ea 100644 --- a/src/internal/io/io.cc +++ b/src/internal/io/io.cc @@ -105,4 +105,66 @@ void IO::write(const char *buf, uint64_t n) { void IO::write(std::string_view s) { write_all(STDOUT_FILENO, s.data(), s.size()); } + +void IO::run_pty(const std::string &cmd) { + int master_fd = -1; + struct winsize ws{}; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) + throw fatal_error("Can't get terminal size.", 1); + pid_t pid = forkpty( + &master_fd, + nullptr, + &orig_termios, + &ws + ); + if (pid == -1) + throw fatal_error("Can't create PTY.", 1); + if (pid == 0) { + execl("/bin/sh", "sh", "-c", cmd.c_str(), (char *)nullptr); + _exit(127); + } + struct pollfd fds[2]; + while (true) { + fds[0].fd = STDIN_FILENO; + fds[0].events = POLLIN; + fds[1].fd = master_fd; + fds[1].events = POLLIN; + int rc = poll(fds, 2, -1); + if (rc == -1) { + if (errno == EINTR) + continue; + break; + } + if (resized.exchange(false)) { + struct winsize new_ws{}; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &new_ws) == 0) + ioctl(master_fd, TIOCSWINSZ, &new_ws); + } + if (fds[0].revents & POLLIN) { + char buf[4096]; + ssize_t n = read(STDIN_FILENO, buf, sizeof(buf)); + if (n > 0) + write_all(master_fd, buf, n); + else if (n == 0) + break; + } + if (fds[1].revents & POLLIN) { + char buf[8192]; + ssize_t n = read(master_fd, buf, sizeof(buf)); + if (n > 0) + write_all(STDOUT_FILENO, buf, n); + else + break; + } + if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + if (fds[1].revents & (POLLERR | POLLHUP | POLLNVAL)) + break; + } + close(master_fd); + int status; + while (waitpid(pid, &status, 0) == -1) + if (errno != EINTR) + break; +} } // namespace bed::internal::io diff --git a/src/internal/parser/address_promise.cc b/src/internal/parser/address_promise.cc index 7287f2f..bdc6e09 100644 --- a/src/internal/parser/address_promise.cc +++ b/src/internal/parser/address_promise.cc @@ -192,7 +192,19 @@ std::optional AddressPromise::get_range(BEd &ctx, std::vector(curr.base)) { - return ctx.prev(); + auto previous = ctx.prev(); + auto &buf = ctx.buffer(previous.buffername); + if (curr.offset < 0 && previous.start < (uint64_t)-curr.offset) + throw ed_error("Can't have negative addresses"); + previous.start += curr.offset; + if (previous.start > buf.lines()) + throw ed_error("Line number too high."); + if (curr.offset < 0 && previous.end < (uint64_t)-curr.offset) + throw ed_error("Can't have negative addresses"); + previous.end += curr.offset; + if (previous.end > buf.lines()) + throw ed_error("Line number too high."); + return previous; } if (prev_given) return buffer::Range(prev.resolve(ctx), curr.resolve(ctx)); diff --git a/src/internal/parser/operation.cc b/src/internal/parser/operation.cc index 8cf7c58..1cddf16 100644 --- a/src/internal/parser/operation.cc +++ b/src/internal/parser/operation.cc @@ -103,6 +103,8 @@ void Parser::operation() { command->argument = functions::Function::GlobalArg(delim, std::move(val)); } break; case functions::Function::ArgumentKind::File: + if (!(peek() == '\t' || peek() == ' ' || peek() == '\0')) + throw ed_error("Incorrect file input usage."); skip_ws(); switch (peek()) { case '!': diff --git a/src/internal/vase/vase.cc b/src/internal/vase/vase.cc index bc71ec9..41c5458 100644 --- a/src/internal/vase/vase.cc +++ b/src/internal/vase/vase.cc @@ -1,4 +1,5 @@ #include "internal/vase/vase.h" +#include "internal/io/io.h" namespace bed::internal::vase { std::string to_string(Shard *root) { @@ -128,7 +129,11 @@ uint64_t offset_of(Shard *root, Point point) { uint64_t offset_of(Shard *root, uint64_t line) { if (!root) return 0; - if (line > root->lines) + if (line == 0) + return 0; + if (line == root->lines + 1) + return root->length; + if (line > root->lines + 1) throw ed_error("line out of range"); uint64_t offset = 0; Shard *curr = root; @@ -207,10 +212,9 @@ Shard *erase(Shard *root, uint64_t start, uint64_t end) { if (start > end || end >= line_count) throw ed_error("line range out of bounds"); uint64_t start_offset = offset_of(root, start); - uint64_t end_offset = - (end + 1 == line_count) - ? root->length - : offset_of(root, end + 1); + uint64_t end_offset = offset_of(root, end + 1); + if (end == root->lines && start_offset) + start_offset--; auto [left, rest] = Shard::split(root, start_offset); auto [middle, right] = Shard::split(rest, end_offset - start_offset); Shard *new_root = Shard::concat(left, right); @@ -267,7 +271,7 @@ Shard *copy(Shard *root, uint64_t start, uint64_t end) { uint64_t end_offset = (end + 1 == line_count) ? root->length - : offset_of(root, end + 1); + : offset_of(root, end + 1) - 1; auto [left, rest] = Shard::split(root, start_offset); auto [middle, right] = Shard::split(rest, end_offset - start_offset); Shard::release(left); @@ -275,4 +279,49 @@ Shard *copy(Shard *root, uint64_t start, uint64_t end) { Shard::release(right); return middle; } + +void write_file(std::filesystem::path path, Shard *text) { + std::ofstream file(path, std::ios::binary); + if (!text) + return; + PetalIterator it(text, Direction::Forward); + it.seek_offset(0); + const char *data; + uint64_t len; + while (it.next(&data, &len)) { + if (!file) + throw ed_error("Failed while writing file: " + path.string()); + file.write(data, len); + } + file.write("\n", 1); + if (!file) + throw ed_error("Failed while writing file: " + path.string()); +} + +void write_command(const char *cmd, Shard *text) { + io::IO::cleanup(); + FILE *pipe = popen(cmd, "w"); + if (!pipe) { + io::IO::enable_raw(); + throw ed_error("Failed to start command: " + std::string(cmd)); + } + if (!text) { + int status = pclose(pipe); + if (status == -1) + throw ed_error("Failed to wait for command: " + std::string(cmd)); + return; + } + PetalIterator it(text, Direction::Forward); + it.seek_offset(0); + const char *data; + uint64_t len; + while (it.next(&data, &len)) + fwrite(data, 1, len, pipe); + fwrite("\n", 1, 1, pipe); + int status = pclose(pipe); + io::IO::enable_raw(); + if (status == -1) + throw ed_error("Failed to wait for command: " + std::string(cmd)); + return; +} } // namespace bed::internal::vase