From e56f784c9a5a834cf50c34166025c2bc2e96d7a7 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Sun, 9 Aug 2026 21:44:02 +0100 Subject: [PATCH] Add more ed commands. --- README.md | 2 +- include/commands/ed/ed.h | 14 +++- include/internal/vase/shard.h | 5 +- include/internal/vase/vase.h | 6 ++ src/commands/ed/ed.cc | 8 +- src/commands/ed/handle.cc | 94 ++++++++++++++++++++-- src/commands/ed/parsers/command.cc | 37 +++++++++ src/internal/vase/shard.cc | 125 +++++++++++++++++++++++++++-- src/internal/vase/vase.cc | 79 +++++++++++++++++- 9 files changed, 340 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index eee5fdc..b8547c6 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ done. Ed is an ed (the posix line editor) implementation using `Vase`.
-Not done yet. +Mostly done. #### `hl` diff --git a/include/commands/ed/ed.h b/include/commands/ed/ed.h index d41b383..190b4b6 100644 --- a/include/commands/ed/ed.h +++ b/include/commands/ed/ed.h @@ -19,9 +19,13 @@ struct Command { Print, Number, Append, + Insert, Change, Delete, Write, + Edit, + ForceEdit, + Filename, Dump, None } type; @@ -56,22 +60,24 @@ struct Ed { uint64_t line = 0; bool modified = false; bool quitting = false; + bool editing = false; uint64_t marks[26]{0}; std::string last_regex = ""; bool help_mode = false; bool suppress_mode = false; bool prompt_mode = false; std::string prompt = "*"; + std::filesystem::path save_path = ""; std::string last_message = ""; - Ed(std::filesystem::path file, bool suppress_mode, std::string prompt_) - : vase(file, "/tmp"), suppress_mode(suppress_mode), prompt(prompt_) { + Ed(std::string file_path, bool suppress_mode, std::string prompt_) + : vase("/tmp"), suppress_mode(suppress_mode), prompt(prompt_) { if (prompt == "") prompt = "*"; else prompt_mode = true; - line = vase.lines(); - std::cout << vase.length() << std::endl; + if (file_path != "") + handle("E " + file_path, false); } void parse_address(std::string_view cmd, uint64_t &i, Command::Address &addr); diff --git a/include/internal/vase/shard.h b/include/internal/vase/shard.h index d0a5991..81e31c8 100644 --- a/include/internal/vase/shard.h +++ b/include/internal/vase/shard.h @@ -25,8 +25,9 @@ struct Shard { static void retain(Shard *n); static void release(Shard *n); - static Shard *from_file(std::filesystem::path path, OriginalBuffer *b, bool posix_ending); - static std::vector from_swap(std::filesystem::path path, OriginalBuffer *b); + static Shard *from_file(std::filesystem::path &path, OriginalBuffer *b, bool posix_ending); + static Shard *from_command(const char *cmd, OriginalBuffer *o, bool posix_ending); + static std::vector from_swap(std::filesystem::path &path, OriginalBuffer *b); static std::pair split(Shard *n, uint64_t offset); static Shard *concat(Shard *left, Shard *right); diff --git a/include/internal/vase/vase.h b/include/internal/vase/vase.h index bbaf0d7..0d673f8 100644 --- a/include/internal/vase/vase.h +++ b/include/internal/vase/vase.h @@ -56,7 +56,13 @@ struct Vase { std::filesystem::path swapdir; Vase(std::filesystem::path path, std::filesystem::path swapdir); + Vase(std::string cmd, std::filesystem::path swapdir); + Vase(std::filesystem::path swapdir); ~Vase(); + Vase(Vase &&other) noexcept; + Vase &operator=(Vase &&other) noexcept; + Vase(const Vase &) = delete; + Vase &operator=(const Vase &) = delete; uint64_t length(); uint64_t lines(); diff --git a/src/commands/ed/ed.cc b/src/commands/ed/ed.cc index d9919b0..7f05e4b 100644 --- a/src/commands/ed/ed.cc +++ b/src/commands/ed/ed.cc @@ -10,7 +10,7 @@ void help() {} void run(std::vector args) { std::string prompt = ""; - std::filesystem::path filepath; + std::string file; bool suppress = false; for (size_t i = 1; i < args.size(); i++) { if (args[i] == "-p") { @@ -21,12 +21,12 @@ void run(std::vector args) { } else if (args[i] == "-s") { suppress = true; } else { - if (filepath.string().size()) + if (file.size()) throw crib::cli::cli_error("Invalid arguments given.", 1); - filepath = args[i]; + file = args[i]; } } - Ed ed(filepath, suppress, prompt); + Ed ed(file, suppress, prompt); std::string command; while (true) { std::string cmd; diff --git a/src/commands/ed/handle.cc b/src/commands/ed/handle.cc index 5e9c1a1..9e3c163 100644 --- a/src/commands/ed/handle.cc +++ b/src/commands/ed/handle.cc @@ -9,6 +9,8 @@ bool Ed::handle(std::string cmd, bool eof) { Command command = parse(cmd, eof); bool was_quitting = quitting; quitting = false; + bool was_editing = editing; + editing = false; switch (command.type) { case Command::Type::Quit: if (modified && !was_quitting) { @@ -97,8 +99,6 @@ bool Ed::handle(std::string cmd, bool eof) { text.append(cline); text.push_back('\n'); } - if (std::cin.eof()) - throw ed_error("EOF reached before '.' during text input."); if (command.start.type != Command::Address::Type::None) { if (command.address_flags & Command::RANGE) resolve_address(command.end, &line); @@ -115,6 +115,32 @@ bool Ed::handle(std::string cmd, bool eof) { modified = true; line += line_count; } break; + case Command::Type::Insert: { + std::string text; + std::string cline; + uint64_t line_count = 0; + while (std::getline(std::cin, cline)) { + if (cline == ".") + break; + line_count++; + text.append(cline); + text.push_back('\n'); + } + if (command.start.type != Command::Address::Type::None) { + if (command.address_flags & Command::RANGE) + resolve_address(command.end, &line); + else + resolve_address(command.start, &line); + } + if (line == 0) + line = 1; + if (text.empty()) + break; + vase.snapshot(); + append(text, line - 1); + modified = true; + line += line_count - 1; + } break; case Command::Type::Change: { uint64_t line_start = line; uint64_t line_end = line; @@ -138,8 +164,6 @@ bool Ed::handle(std::string cmd, bool eof) { text.append(cline); text.push_back('\n'); } - if (std::cin.eof()) - throw ed_error("EOF reached before '.' during text input."); vase.snapshot(); remove(line_start, line_end); line = line_start; @@ -172,9 +196,9 @@ bool Ed::handle(std::string cmd, bool eof) { case Command::Type::Write: { std::string path = command.argument; if (path.empty()) { - path = vase.path; + path = save_path; if (path.empty()) - throw ed_error("Need filename to write to."); + throw ed_error("Need file to write to."); } uint64_t line_start = 1; uint64_t line_end = vase.lines(); @@ -207,7 +231,6 @@ bool Ed::handle(std::string cmd, bool eof) { if (pclose(pipe) == -1) throw ed_error("Error closing command."); } else { - vase.path = path; std::ofstream file(path, std::ios::out | std::ios::trunc); if (!file) throw ed_error("Error writing to file."); @@ -218,11 +241,68 @@ bool Ed::handle(std::string cmd, bool eof) { } if (!file) throw ed_error("Error writing to file."); + save_path = path; modified = false; } if (!suppress_mode) std::cout << bytes << std::endl; } break; + case Command::Type::Edit: { + if (modified && !was_editing) { + editing = true; + throw ed_error("Buffer modified."); + } + std::string path = command.argument; + if (path.empty()) { + path = save_path; + if (path.empty()) + throw ed_error("Need file to read from."); + } + if (path[0] == '!') { + path.erase(1); + Vase new_vase(std::string(path), "/tmp"); + vase = std::move(new_vase); + modified = true; + } else { + Vase new_vase(std::filesystem::path(path), "/tmp"); + vase = std::move(new_vase); + save_path = path; + modified = false; + } + line = vase.lines(); + if (!suppress_mode) + std::cout << vase.length() << std::endl; + } break; + case Command::Type::ForceEdit: { + std::string path = command.argument; + if (path.empty()) { + path = save_path; + if (path.empty()) + throw ed_error("Need file to read from."); + } + if (path[0] == '!') { + path.erase(0, 1); + Vase new_vase(std::string(path), "/tmp"); + vase = std::move(new_vase); + modified = true; + } else { + Vase new_vase(std::filesystem::path(path), "/tmp"); + vase = std::move(new_vase); + save_path = path; + modified = false; + } + line = vase.lines(); + std::fill(std::begin(marks), std::end(marks), '\0'); + if (!suppress_mode) + std::cout << vase.length() << std::endl; + } break; + case Command::Type::Filename: { + std::string path = command.argument; + if (path.empty()) + throw ed_error("No filename given."); + save_path = path; + std::cout << save_path.string() << std::endl; + } break; case Command::Type::Dump: Shard::dump(vase.root); break; diff --git a/src/commands/ed/parsers/command.cc b/src/commands/ed/parsers/command.cc index 4063ba4..dd8d503 100644 --- a/src/commands/ed/parsers/command.cc +++ b/src/commands/ed/parsers/command.cc @@ -79,6 +79,10 @@ Command Ed::parse(std::string cmd, bool eof) { command.type = Command::Type::Append; i++; break; + case 'i': + command.type = Command::Type::Insert; + i++; + break; case 'c': command.type = Command::Type::Change; i++; @@ -98,6 +102,39 @@ Command Ed::parse(std::string cmd, bool eof) { throw ed_error("Invalid command."); command.argument = cmd.substr(i); return command; + case 'e': + command.type = Command::Type::Edit; + i++; + if (i >= cmd.size()) + return command; + if (cmd[i] == ' ' || cmd[i] == '\t') + skip_space(); + else + throw ed_error("Invalid command."); + command.argument = cmd.substr(i); + return command; + case 'E': + command.type = Command::Type::ForceEdit; + i++; + if (i >= cmd.size()) + return command; + if (cmd[i] == ' ' || cmd[i] == '\t') + skip_space(); + else + throw ed_error("Invalid command."); + command.argument = cmd.substr(i); + return command; + case 'f': + command.type = Command::Type::Filename; + i++; + if (i >= cmd.size()) + return command; + if (cmd[i] == ' ' || cmd[i] == '\t') + skip_space(); + else + throw ed_error("Invalid command."); + command.argument = cmd.substr(i); + return command; case '#': command.type = Command::Type::Dump; i++; diff --git a/src/internal/vase/shard.cc b/src/internal/vase/shard.cc index 65abb2c..0132774 100644 --- a/src/internal/vase/shard.cc +++ b/src/internal/vase/shard.cc @@ -205,7 +205,112 @@ Shard *Shard::build(Shard **pieces, uint64_t lo, uint64_t hi) { return node; } -Shard *Shard::from_file(std::filesystem::path path, OriginalBuffer *o, bool posix_ending) { +static bool write_all(int fd, const void *data, size_t len) { + const char *p = static_cast(data); + while (len > 0) { + ssize_t n = write(fd, p, len); + if (n > 0) { + p += n; + len -= (size_t)n; + continue; + } + if (n == -1 && errno == EINTR) + continue; + return false; + } + return true; +} + +Shard *Shard::from_command(const char *cmd, OriginalBuffer *o, bool posix_ending) { + int dest_fd = o->fd; + if (dest_fd == -1) + return nullptr; + + FILE *pipe = popen(cmd, "r"); + if (!pipe) + return nullptr; + + std::vector pieces; + pieces.reserve(16); + + uint64_t pos = 0; + char buf[PETAL_SIZE_MAX]; + uint64_t buf_cursor = 0; + + char ending[2] = {'\0', '\0'}; + + while (true) { + size_t got = fread(buf + buf_cursor, 1, sizeof(buf) - buf_cursor, pipe); + buf_cursor += got; + + if (buf_cursor == PETAL_SIZE_MAX || feof(pipe)) { + if (buf_cursor == 0) + break; + uint64_t lines = 0; + const char *p = buf; + const char *end = p + buf_cursor; + while (p < end) { + const void *nl = memchr(p, '\n', end - p); + if (!nl) + break; + lines++; + p = (const char *)nl + 1; + } + if (buf_cursor >= 2) { + ending[0] = buf[buf_cursor - 2]; + ending[1] = buf[buf_cursor - 1]; + } else if (buf_cursor == 1) { + ending[0] = ending[1]; + ending[1] = buf[0]; + } + if (!write_all(dest_fd, buf, buf_cursor)) { + pclose(pipe); + return nullptr; + } + pieces.push_back(new Petal(buf_cursor, lines, o, pos)); + pos += buf_cursor; + } + if (feof(pipe)) + break; + if (ferror(pipe)) + return nullptr; + } + + int status = pclose(pipe); + if (status == -1) + return nullptr; + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) + return nullptr; + + if (pieces.empty()) + return nullptr; + + if (posix_ending) { + if (ending[1] == '\n') { + Petal *last = (Petal *)pieces.back(); + last->lines--; + last->length--; + if (last->length == 0) { + Shard::release(last); + pieces.pop_back(); + last = nullptr; + if (!pieces.empty()) + last = (Petal *)pieces.back(); + } + if (last && ending[0] == '\r') + last->length--; + } + } + + o->initialize(); + + if (pieces.size() == 1) + return pieces[0]; + + return build(pieces.data(), 0, pieces.size()); +} + +Shard *Shard::from_file(std::filesystem::path &path, OriginalBuffer *o, bool posix_ending) { int dest_fd = o->fd; if (dest_fd == -1) return nullptr; @@ -216,17 +321,22 @@ Shard *Shard::from_file(std::filesystem::path path, OriginalBuffer *o, bool posi uint64_t total = std::filesystem::file_size(path); if (posix_ending && total > 0) { char last; - char s_last; if (pread(src_fd, &last, 1, (off_t)(total - 1)) != 1) return nullptr; - if (pread(src_fd, &s_last, 1, (off_t)(total - 2)) != 1) - return nullptr; if (last == '\n') { total--; - if (s_last == '\r') - total--; + if (total > 0) { + char s_last; + if (pread(src_fd, &s_last, 1, (off_t)(total - 1)) != 1) + return nullptr; + if (s_last == '\r') + total--; + } } } + if (total == 0) + return nullptr; + std::vector pieces; uint64_t pos = 0; pieces.reserve((total + PETAL_SIZE_MAX - 1) / PETAL_SIZE_MAX); @@ -250,8 +360,7 @@ Shard *Shard::from_file(std::filesystem::path path, OriginalBuffer *o, bool posi lines++; p = (const char *)nl + 1; } - ssize_t written = write(dest_fd, buf, take); - if (written != (ssize_t)take) { + if (!write_all(dest_fd, buf, take)) { close(src_fd); return nullptr; } diff --git a/src/internal/vase/vase.cc b/src/internal/vase/vase.cc index 86d923a..0a9ea3b 100644 --- a/src/internal/vase/vase.cc +++ b/src/internal/vase/vase.cc @@ -16,12 +16,78 @@ Vase::Vase(std::filesystem::path path, std::filesystem::path swapdir) Shard::retain(root); } +Vase::Vase(std::string cmd, std::filesystem::path swapdir) + : path(""), swapdir(swapdir) { + if (!std::filesystem::exists(swapdir) || !std::filesystem::is_directory(swapdir)) + throw std::runtime_error("Swap directory does not exist or is not a directory."); + append = new AppendBuffer(swapdir); + original = new OriginalBuffer(swapdir); + root = Shard::from_command(cmd.c_str(), original, posix_ending); + history_top = 0; + history.push_back(root); + Shard::retain(root); +} + +Vase::Vase(std::filesystem::path swapdir) + : path(""), swapdir(swapdir) { + if (!std::filesystem::exists(swapdir) || !std::filesystem::is_directory(swapdir)) + throw std::runtime_error("Swap directory does not exist or is not a directory."); + append = new AppendBuffer(swapdir); + original = new OriginalBuffer(swapdir); + root = nullptr; + history_top = 0; + history.push_back(root); + Shard::retain(root); +} + Vase::~Vase() { Shard::release(root); + for (auto s : history) + Shard::release(s); + if (original) + delete original; + if (append) + delete append; +} + +Vase::Vase(Vase &&other) noexcept + : original(other.original), + append(other.append), + root(other.root), + posix_ending(other.posix_ending), + using_crlf(other.using_crlf), + path(std::move(other.path)), + swapdir(std::move(other.swapdir)), + history(std::move(other.history)), + history_top(other.history_top) { + other.original = nullptr; + other.append = nullptr; + other.root = nullptr; + other.history_top = 0; +} + +Vase &Vase::operator=(Vase &&other) noexcept { + if (this == &other) + return *this; + Shard::release(root); for (auto s : history) Shard::release(s); delete original; delete append; + original = other.original; + append = other.append; + root = other.root; + posix_ending = other.posix_ending; + using_crlf = other.using_crlf; + path = std::move(other.path); + swapdir = std::move(other.swapdir); + history = std::move(other.history); + history_top = other.history_top; + other.original = nullptr; + other.append = nullptr; + other.root = nullptr; + other.history_top = 0; + return *this; } uint64_t Vase::length() { @@ -108,16 +174,21 @@ void Vase::snapshot() { } void Vase::prune_history(uint64_t n) { - n = std::min(n, history_top); - for (uint64_t i = 0; i < n; ++i) + uint64_t keep = std::min(history.size(), n + 1); + if (keep == history.size()) + return; + uint64_t remove = history.size() - keep; + for (uint64_t i = 0; i < remove; ++i) Shard::release(history[i]); - history.erase(history.begin(), history.begin() + n); - history_top -= n; + history.erase(history.begin(), history.begin() + remove); + history_top -= remove; } bool Vase::save() { if (!root) return true; + if (path == "") + return false; std::ofstream file(path, std::ios::binary); if (!file) return false;