From 49bf5a2e2d23b83f6cc9cb22f83fcf4b10faae51 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Sun, 30 Aug 2026 17:02:34 +0100 Subject: [PATCH] Make substitutions work and other minor fixes. --- include/internal/syntax/parser.h | 10 ++++ include/internal/vase/vase.h | 9 ++- src/internal/buffer/buffer.cc | 45 ++++++++++++--- src/internal/functions/functions.cc | 58 ++++++++++++++++++- src/internal/parser/operation.cc | 11 ++-- src/internal/syntax/parser.cc | 90 ++++++++++++++++++++--------- src/internal/vase/regex/api.cc | 76 ++++++++++++++++-------- src/internal/vase/regex/core.cc | 6 +- src/internal/vase/vase.cc | 1 - 9 files changed, 233 insertions(+), 73 deletions(-) diff --git a/include/internal/syntax/parser.h b/include/internal/syntax/parser.h index a39e571..345e714 100644 --- a/include/internal/syntax/parser.h +++ b/include/internal/syntax/parser.h @@ -10,6 +10,10 @@ struct Parser { ParseState *root; Language lang; + bool in_edit = false; + bool dirty = false; + uint64_t dirty_start = 0; + uint64_t dirty_end = 0; Parser(vase::Shard *, uint64_t, Language); ~Parser(); @@ -21,6 +25,12 @@ struct Parser { void insert(vase::Shard *, uint64_t, uint64_t); void modify(vase::Shard *, uint64_t, uint64_t); + void begin_edit(); + void erase(uint64_t start, uint64_t count); + void insert(uint64_t start, uint64_t count); + void end_edit(vase::Shard *vase); + void mark_dirty(uint64_t start, uint64_t end); + uint64_t next_closing(uint64_t line); uint64_t prev_opening(uint64_t line); diff --git a/include/internal/vase/vase.h b/include/internal/vase/vase.h index 8d5f12f..490c7fd 100644 --- a/include/internal/vase/vase.h +++ b/include/internal/vase/vase.h @@ -20,8 +20,8 @@ struct Range { }; struct RegexGroup { - uint64_t start{0}; - uint64_t end{0}; + uint64_t start{UINT64_MAX}; + uint64_t end{UINT64_MAX}; }; struct RegexMatch { @@ -57,7 +57,10 @@ uint64_t find_prev(Shard *root, std::string_view pattern, uint64_t start); Shard *substitute( AppendStorage *ap, Shard *root, std::string_view pattern, uint64_t start, uint64_t end, - std::string_view replace, std::string_view options + std::string_view replace, std::string_view options, + const std::function &on_edit = nullptr ); // internal diff --git a/src/internal/buffer/buffer.cc b/src/internal/buffer/buffer.cc index 1794ade..ef0484a 100644 --- a/src/internal/buffer/buffer.cc +++ b/src/internal/buffer/buffer.cc @@ -22,11 +22,20 @@ uint64_t Buffer::bytes() { return 0; } -void Buffer::load(BEd &, vase::Shard *text) { +void Buffer::load(BEd &ctx, vase::Shard *text) { try { vase::Shard::release(root); root = text; state = buffer::Buffer::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); @@ -71,13 +80,35 @@ void Buffer::substitute( BEd &ctx, uint64_t start_line, uint64_t end_line, std::string ®ex, std::string &replacement, std::string &options ) { - // TODO: make substitue return a list of modifications made. - root = vase::substitute(&ctx.append, root, regex, start_line, end_line, replacement, options); - /*prev_range.start = start_line; - prev_range.end = start_line; - marks.collapse(start_line, end_line - start_line); + ctx.prev().buffername = name; + ctx.prev().start = start_line; + ctx.prev().end = end_line; if (parser) - parser->erase(vase, start_line, end_line - start_line);*/ + parser->begin_edit(); + root = vase::substitute( + &ctx.append, + root, + regex, + start_line, + end_line, + replacement, + options, + [&](uint64_t line, uint64_t old_lines, uint64_t new_lines) { + if (old_lines) { + ctx.marks.erase(name, line, old_lines); + if (parser) + parser->erase(line, old_lines); + } + if (new_lines) { + ctx.marks.insert(name, line, line + new_lines - 1); + if (parser) + parser->insert(line, line + new_lines - 1); + } + } + ); + if (parser) + parser->end_edit(root); + ctx.prev().buffername = name; state = Modified; } diff --git a/src/internal/functions/functions.cc b/src/internal/functions/functions.cc index 28e4239..86e1e01 100644 --- a/src/internal/functions/functions.cc +++ b/src/internal/functions/functions.cc @@ -79,7 +79,7 @@ void Function::register_posix(BEd &ctx) { .input_mode = Function::InputMode::None, .desc = "Delete set of lines.", .default_address = ".,.", - .accept_zero = true, + .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_); @@ -88,6 +88,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( "j", Function{ @@ -186,7 +205,7 @@ void Function::register_posix(BEd &ctx) { .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_); + auto &addr = std::get(addr_); if (addr.start == addr.end) std::cout << ':' << addr.buffername << ':' << addr.start << "\n"; else @@ -206,11 +225,44 @@ void Function::register_posix(BEd &ctx) { .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 &addr = std::get(addr_); ctx.mark(std::get(arg), addr); } } ); + ctx.functions.insert( + "s", + Function{ + .address_kind = Function::AddressKind::Range, + .argument_kind = Function::ArgumentKind::Regex, + .input_mode = Function::InputMode::None, + .desc = "Substitute regex.", + .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.expression == "") + arg.expression = ctx.last_regex; + else + ctx.last_regex = arg.expression; + if (arg.replacement == "%") + arg.replacement = ctx.last_replacement; + else + ctx.last_replacement = arg.replacement; + ctx.buffer(addr.buffername) + .substitute( + ctx, + addr.start, + addr.end, + arg.expression, + arg.replacement, + arg.options + ); + } + } + ); ctx.functions.insert( "e", Function{ diff --git a/src/internal/parser/operation.cc b/src/internal/parser/operation.cc index c90d65e..8cf7c58 100644 --- a/src/internal/parser/operation.cc +++ b/src/internal/parser/operation.cc @@ -132,7 +132,7 @@ void Parser::operation() { if (delim == '\0') throw ed_error("regex expected"); advance(); - uint64_t j = 0; + uint16_t j = 0; while (true) { if (peek(j) == '\0') throw ed_error("Unterminated regex"); @@ -141,7 +141,7 @@ void Parser::operation() { else if (peek(j) == '\\') j += 2; else if (peek(j) == '[') - while (peek(j) != '\0' && cmd[i] != ']') + while (peek(j) != '\0' && peek(j) != ']') j++; else j++; @@ -157,15 +157,16 @@ void Parser::operation() { else if (peek(j) == '\\') j += 2; else if (peek(j) == '[') - while (peek(j) != '\0' && cmd[i] != ']') + while (peek(j) != '\0' && peek(j) != ']') j++; else j++; } - std::string replacement; + std::string replacement(peek_str(j)); if (peek(j) != '\0') { - replacement = peek_str(j); advance(j + 1); + } else { + advance(j); } std::string options; if (peek() != '\0') { diff --git a/src/internal/syntax/parser.cc b/src/internal/syntax/parser.cc index 18dbfa3..0e0e234 100644 --- a/src/internal/syntax/parser.cc +++ b/src/internal/syntax/parser.cc @@ -123,36 +123,15 @@ ParseState *Parser::join_tree(ParseState *a, ParseState *b) { } void Parser::erase(vase::Shard *vase, uint64_t start, uint64_t count) { - if (count == 0 || !root) - return; - auto [a, remaining] = split_tree(root, start); - auto [waste, b] = split_tree(remaining, count); - destroy_tree(waste, lang); - root = join_tree(a, b); - modify(vase, start, 1); + begin_edit(); + erase(start, count); + end_edit(vase); } void Parser::insert(vase::Shard *vase, uint64_t start, uint64_t count) { - if (count == 0) - return; - std::vector leaves; - leaves.reserve((count + MAX_CHUNK - 1) / MAX_CHUNK); - uint64_t consumed = 0; - while (consumed < count) { - auto *leaf = (ParseStateLeaf *)malloc(sizeof(ParseStateLeaf)); - leaf->state = nullptr; - leaf->blocks = nullptr; - leaf->n = 0; - leaf->cap = 0; - uint64_t chunk = std::min(MAX_CHUNK, count - consumed); - leaf->header = chunk; - consumed += chunk; - leaves.push_back(leaf); - } - ParseState *subtree = build_tree(leaves, 0, leaves.size()); - auto [left, right] = split_tree(root, start); - root = join_tree(join_tree(left, subtree), right); - modify(vase, start, count); + begin_edit(); + insert(start, count); + end_edit(vase); } void Parser::modify(vase::Shard *vase, uint64_t target, uint64_t count) { @@ -219,6 +198,63 @@ void Parser::modify(vase::Shard *vase, uint64_t target, uint64_t count) { lang.destroy(state); } +void Parser::begin_edit() { + in_edit = true; +} + +void Parser::mark_dirty(uint64_t start, uint64_t end) { + if (!dirty) { + dirty_start = start; + dirty_end = end; + dirty = true; + } else { + dirty_start = std::min(dirty_start, start); + dirty_end = std::max(dirty_end, end); + } +} + +void Parser::erase(uint64_t start, uint64_t count) { + if (count == 0 || !root) + return; + auto [a, remaining] = split_tree(root, start); + auto [waste, b] = split_tree(remaining, count); + destroy_tree(waste, lang); + root = join_tree(a, b); + mark_dirty(start, start + 1); +} + +void Parser::insert(uint64_t start, uint64_t count) { + if (count == 0) + return; + std::vector leaves; + leaves.reserve((count + MAX_CHUNK - 1) / MAX_CHUNK); + uint64_t consumed = 0; + while (consumed < count) { + auto *leaf = (ParseStateLeaf *)malloc(sizeof(ParseStateLeaf)); + leaf->state = nullptr; + leaf->blocks = nullptr; + leaf->n = 0; + leaf->cap = 0; + uint64_t chunk = std::min(MAX_CHUNK, count - consumed); + leaf->header = chunk; + consumed += chunk; + leaves.push_back(leaf); + } + ParseState *subtree = build_tree(leaves, 0, leaves.size()); + auto [left, right] = split_tree(root, start); + root = join_tree(join_tree(left, subtree), right); + mark_dirty(start, start + count); +} + +void Parser::end_edit(vase::Shard *vase) { + in_edit = false; + if (!dirty) + return; + uint64_t count = dirty_end > dirty_start ? dirty_end - dirty_start : 1; + modify(vase, dirty_start, count); + dirty = false; +} + uint64_t Parser::next_closing(uint64_t line) { if (!root) return UINT64_MAX; diff --git a/src/internal/vase/regex/api.cc b/src/internal/vase/regex/api.cc index c6a441e..e36447d 100644 --- a/src/internal/vase/regex/api.cc +++ b/src/internal/vase/regex/api.cc @@ -25,12 +25,7 @@ std::vector parse_replace(AppendStorage *ap, std::string_view s) { }; for (size_t i = 0; i < s.size(); ++i) { char c = s[i]; - if (c == '\\' && i + 1 < s.size() && s[i + 1] == '$') { - constant.push_back('$'); - ++i; - continue; - } - if (c == '$' && i + 1 < s.size()) { + if (c == '\\' && i + 1 < s.size()) { char next = s[i + 1]; if (next == '0') { flush_constant(); @@ -40,10 +35,7 @@ std::vector parse_replace(AppendStorage *ap, std::string_view s) { .value = (uint8_t)0 } ); - ++i; - continue; - } - if (next >= '1' && next <= '9') { + } else if (next >= '1' && next <= '9') { flush_constant(); parts.push_back( ReplacePart{ @@ -51,9 +43,22 @@ std::vector parse_replace(AppendStorage *ap, std::string_view s) { .value = (uint8_t)(next - '0') } ); - ++i; - continue; + } else if (next == 'n') { + constant.push_back('\n'); + } else { + constant.push_back(next); } + ++i; + continue; + } + if (c == '&') { + flush_constant(); + parts.push_back( + ReplacePart{ + .type = ReplacePart::PartType::FullMatch, + .value = (uint8_t)0 + } + ); } constant.push_back(c); } @@ -64,7 +69,8 @@ std::vector parse_replace(AppendStorage *ap, std::string_view s) { Shard *substitute( AppendStorage *ap, Shard *root, std::string_view pattern, uint64_t start, uint64_t end, - std::string_view replace, std::string_view options + std::string_view replace, std::string_view options, + const std::function &on_edit ) { if (!start || !end) throw ed_error("Invalid range."); @@ -79,11 +85,18 @@ Shard *substitute( uint64_t end_offset = (end + 1 == line_count) ? root->length - : offset_of(root, end + 1); + : offset_of(root, end + 1) - 1; std::vector matches = _regex_search(root, pattern, start_offset, end_offset, options); if (matches.empty()) return root; std::vector replace_parts = parse_replace(ap, replace); + struct Edit { + uint64_t line; + uint64_t old_lines; + uint64_t new_lines; + }; + uint64_t orig_line = start; + int64_t line_delta = 0; std::vector pieces; pieces.reserve(matches.size() * 2 + 1); Shard *remaining = root; @@ -96,26 +109,33 @@ Shard *substitute( Shard::release(remaining); pieces.push_back(keep); remaining = rest; + orig_line += keep ? keep->lines : 0; } auto [dropped, rest2] = Shard::split(remaining, match.end - match.start); Shard::release(remaining); remaining = rest2; + uint64_t old_lines = dropped ? dropped->lines : 0; + uint64_t new_lines = 0; for (size_t i = 0; i < replace_parts.size(); ++i) { const ReplacePart &part = replace_parts[i]; switch (part.type) { - case ReplacePart::PartType::Constant: - Shard::retain(std::get(part.value)); - pieces.push_back(std::get(part.value)); + case ReplacePart::PartType::Constant: { + Shard *c = std::get(part.value); + Shard::retain(c); + pieces.push_back(c); + new_lines += c ? c->lines : 0; break; + } case ReplacePart::PartType::FullMatch: Shard::retain(dropped); pieces.push_back(dropped); + new_lines += dropped ? dropped->lines : 0; break; case ReplacePart::PartType::CaptureGroup: { uint8_t idx = std::get(part.value); if (idx <= 9) { const RegexGroup &group = match.groups[idx - 1]; - if (group.start > group.end) { + if (group.start != UINT64_MAX) { uint64_t ls = group.start - match.start; uint64_t le = group.end - match.start; auto [a, b] = Shard::split(dropped, ls); @@ -124,6 +144,7 @@ Shard *substitute( Shard::release(b); Shard::release(c); pieces.push_back(g); + new_lines += g ? g->lines : 0; } } break; @@ -131,6 +152,13 @@ Shard *substitute( } } Shard::release(dropped); + if (old_lines || new_lines) { + uint64_t report_line = (uint64_t)((int64_t)orig_line + line_delta); + if (on_edit) + on_edit(report_line, old_lines, new_lines); + line_delta += (int64_t)new_lines - (int64_t)old_lines; + } + orig_line += old_lines; cursor = match.end; } pieces.push_back(remaining); @@ -151,7 +179,7 @@ Shard *substitute( } uint64_t find_next(Shard *root, std::string_view pattern, uint64_t start) { - if (start == 0 || start > root->lines) + if (start == 0 || start > root->lines + 1) throw ed_error("Invalid line number."); start--; std::vector results; @@ -180,7 +208,7 @@ uint64_t find_next(Shard *root, std::string_view pattern, uint64_t start) { if (rc >= 0) { pcre2_match_data_free(match_data); pcre2_code_free(re); - return at; + return at + 1; } if (rc != PCRE2_ERROR_NOMATCH) { pcre2_match_data_free(match_data); @@ -198,7 +226,7 @@ uint64_t find_next(Shard *root, std::string_view pattern, uint64_t start) { if (rc >= 0) { pcre2_match_data_free(match_data); pcre2_code_free(re); - return at; + return at + 1; } if (rc != PCRE2_ERROR_NOMATCH) { pcre2_match_data_free(match_data); @@ -213,7 +241,7 @@ 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) { - if (start == 0 || start > root->lines) + if (start == 0 || start > root->lines + 1) throw ed_error("Invalid line number."); start--; std::vector results; @@ -242,7 +270,7 @@ uint64_t find_prev(Shard *root, std::string_view pattern, uint64_t start) { if (rc >= 0) { pcre2_match_data_free(match_data); pcre2_code_free(re); - return at; + return at + 1; } if (rc != PCRE2_ERROR_NOMATCH) { pcre2_match_data_free(match_data); @@ -260,7 +288,7 @@ uint64_t find_prev(Shard *root, std::string_view pattern, uint64_t start) { if (rc >= 0) { pcre2_match_data_free(match_data); pcre2_code_free(re); - return at; + return at + 1; } if (rc != PCRE2_ERROR_NOMATCH) { pcre2_match_data_free(match_data); diff --git a/src/internal/vase/regex/core.cc b/src/internal/vase/regex/core.cc index 94247a0..4f78b1a 100644 --- a/src/internal/vase/regex/core.cc +++ b/src/internal/vase/regex/core.cc @@ -64,7 +64,7 @@ std::vector _regex_search( uint64_t offset = UINT64_MAX; auto record_match = [&](int rc, PCRE2_SIZE *ovector) { - if (global_offset + (uint64_t)ovector[1] > end_offset || ovector[0] == ovector[1]) + if (global_offset + (uint64_t)ovector[1] > end_offset) return; RegexMatch match{ .start = global_offset + (uint64_t)ovector[0], @@ -74,8 +74,8 @@ std::vector _regex_search( PCRE2_SIZE s = ovector[2 * i]; PCRE2_SIZE e = ovector[2 * i + 1]; if (s != PCRE2_UNSET) { - match.groups[i].start = global_offset + (uint64_t)s; - match.groups[i].end = global_offset + (uint64_t)e; + match.groups[i - 1].start = global_offset + (uint64_t)s; + match.groups[i - 1].end = global_offset + (uint64_t)e; } } results.push_back(std::move(match)); diff --git a/src/internal/vase/vase.cc b/src/internal/vase/vase.cc index 4ed9272..e000cf5 100644 --- a/src/internal/vase/vase.cc +++ b/src/internal/vase/vase.cc @@ -232,7 +232,6 @@ Shard *join(Shard *root, uint64_t start, uint64_t end) { uint64_t line_count = root->lines + 1; if (start >= end || end >= line_count) throw ed_error("line range out of bounds"); - uint64_t offset = offset_of(root, start); std::vector pieces; pieces.reserve(end - start + 3);