diff --git a/flake.nix b/flake.nix index d7744cb..325e892 100644 --- a/flake.nix +++ b/flake.nix @@ -113,6 +113,7 @@ export MRBC=${mruby}/bin/mrbc export MRUBY_CFLAGS=-I${mruby}/include export MRUBY_LIBS="-L${mruby}/lib -lmruby" + export PATH="$PWD/result/bin:$PATH" ''; }; }; diff --git a/include/bed.h b/include/bed.h index 8a4f1fa..e479d3c 100644 --- a/include/bed.h +++ b/include/bed.h @@ -26,6 +26,7 @@ struct BEd { bool prompt_mode = true; std::function prompt = nullptr; bool suppress_mode = false; + bool color_mode = false; bool temporary_current = false; std::string last_help = ""; std::string last_regex = ""; diff --git a/include/internal/io/tokens.h b/include/internal/io/tokens.h index 1392f4c..e509821 100644 --- a/include/internal/io/tokens.h +++ b/include/internal/io/tokens.h @@ -17,7 +17,6 @@ struct Token { RubyFunction, RubyArg, Function, - Any, Shell, File, Suffix, diff --git a/include/internal/parser/parser.h b/include/internal/parser/parser.h index a2a9372..e56ed2b 100644 --- a/include/internal/parser/parser.h +++ b/include/internal/parser/parser.h @@ -93,6 +93,9 @@ struct Parser { std::vector *tokens, CompletionContext *completion ); + void token(io::Token::Kind); + void end_token(); + char peek(uint16_t = 0); // == \0 if at eof. std::string_view peek_str(uint16_t = UINT16_MAX); void advance(uint16_t = 1); diff --git a/include/internal/syntax/miniparser.h b/include/internal/syntax/miniparser.h new file mode 100644 index 0000000..eed26b3 --- /dev/null +++ b/include/internal/syntax/miniparser.h @@ -0,0 +1,21 @@ +#pragma once + +#include "decl.h" + +namespace bed::internal::syntax { +struct MiniParser { + Language ⟨ + void *start_state; + std::vector>> lines; + + explicit MiniParser(Language &lang, vase::Shard *vase, void *initial_state); + ~MiniParser(); + + MiniParser(const MiniParser &) = delete; + MiniParser &operator=(const MiniParser &) = delete; + + void dirty(vase::Shard *vase, uint64_t start, uint64_t count); + void insert(vase::Shard *vase, uint64_t start, uint64_t count); + void erase(vase::Shard *vase, uint64_t start, uint64_t count); +}; +} // namespace bed::internal::syntax diff --git a/include/internal/ui/text_mode.h b/include/internal/ui/text_mode.h index 097e970..44a8e39 100644 --- a/include/internal/ui/text_mode.h +++ b/include/internal/ui/text_mode.h @@ -1,23 +1,31 @@ #pragma once -#include "definitions.h" +#include "internal/syntax/miniparser.h" #include "pch.h" -namespace bed::internal::ui { +namespace bed::internal::ui::text_mode { struct TextMode { - std::string cmd; - uint16_t cursor; + struct Row { + uint64_t start; + uint64_t length; + }; + using Lines = std::vector; + vase::Shard *vase; + vase::Point cursor; + std::optional parser; + std::vector lines; uint16_t start; uint16_t height; uint16_t term_height; uint16_t term_width; BEd &bed; - TextMode(BEd &); + TextMode(BEd &, vase::Shard *, syntax::Language *, void *); std::pair run(); std::pair run_pipe(); std::pair run_terminal(); void grow(); void redraw(); + void layout_lines(); }; -} // namespace bed::internal::ui +} // namespace bed::internal::ui::text_mode diff --git a/include/internal/vase/vase.h b/include/internal/vase/vase.h index d9d6d87..99b8a5a 100644 --- a/include/internal/vase/vase.h +++ b/include/internal/vase/vase.h @@ -41,7 +41,7 @@ struct ReplacePart { uint64_t offset_of(Shard *root, uint64_t line); uint64_t offset_of(Shard *root, Point point); -Point point_of(Shard *root, uint64_t offset); +Point eof_point(Shard *root); std::string to_string(Shard *root); std::string to_string(Shard *root, Range range); diff --git a/src/bed/run.cc b/src/bed/run.cc index 688fbba..20f65be 100644 --- a/src/bed/run.cc +++ b/src/bed/run.cc @@ -7,6 +7,7 @@ BEd::BEd(std::vector args) std::string prompt_ = ""; std::string file = ""; bool suppress = false; + bool color = true; for (size_t i = 1; i < args.size(); i++) { if (args[i] == "-p") { i++; @@ -15,6 +16,8 @@ BEd::BEd(std::vector args) prompt_ = args[i]; } else if (args[i] == "-s") { suppress = true; + } else if (args[i] == "--no-color") { + color = false; } else if (args[i] == "-v" || args[i] == "--verbose") { help_mode = true; } else if (args[i] == "-h" || args[i] == "--help") { @@ -31,6 +34,15 @@ BEd::BEd(std::vector args) else prompt_mode = false; suppress_mode = suppress; + const char *no_color = getenv("NO_COLOR"); + if (no_color && *no_color != '\0') + color = false; + const char *colorterm = getenv("COLORTERM"); + if (colorterm + && strcmp(colorterm, "truecolor") != 0 + && strcmp(colorterm, "24bit") != 0) + color = false; + color_mode = color; internal::functions::Function::register_posix(*this); internal::functions::Function::register_extented(*this); internal::functions::Suffix::register_suffixes(*this); @@ -77,7 +89,7 @@ void BEd::run() { } void BEd::handle(std::string_view cmd, bool eof) { - if (eof) { + if (eof && cmd.empty()) { eof_op.handle(*this, "", nullptr, std::monostate(), nullptr); return; } @@ -149,10 +161,17 @@ void BEd::handle(std::string_view cmd, bool eof) { } internal::vase::Shard *text = nullptr; if (c.function->input_mode == internal::functions::Function::InputMode::Text) { - internal::ui::TextMode tm(*this); + internal::vase::Shard *vase = nullptr; + internal::syntax::Language *lang = nullptr; + void *state = nullptr; + if (c.function->pre_text_mode) + std::tie(vase, lang, state) = c.function->pre_text_mode(*this, address, c.argument); + internal::ui::text_mode::TextMode tm(*this, vase, lang, state); auto [a, b] = tm.run(); if (!b) text = a; + else + internal::vase::Shard::release(a); } if (c.function->handle) c.function->handle(*this, address, text, c.argument, nullptr); diff --git a/src/internal/buffer/special/clip.cc b/src/internal/buffer/special/clip.cc index afa9e68..8fccc00 100644 --- a/src/internal/buffer/special/clip.cc +++ b/src/internal/buffer/special/clip.cc @@ -94,7 +94,6 @@ void ClipBuffer::replace(BEd &ctx, vase::Shard *text, uint64_t start_line, uint6 uint64_t diff = old_count - new_count; ctx.marks.collapse(name, start_line + new_count - 1, diff); } - state = Modified; } void ClipBuffer::join(BEd &ctx, uint64_t start_line, uint64_t end_line) { diff --git a/src/internal/functions/posix.cc b/src/internal/functions/posix.cc index 6da7794..c606e2a 100644 --- a/src/internal/functions/posix.cc +++ b/src/internal/functions/posix.cc @@ -60,7 +60,21 @@ void Function::register_posix(BEd &ctx) { .desc = "Change a range of lines", .default_address = ".,.", .accept_zero = false, - .pre_text_mode = nullptr, + .pre_text_mode = []( + BEd &ctx, + const buffer::Address &addr_, + const Argument & + ) -> std::tuple { + const auto &addr = std::get(addr_); + auto &buf_ = ctx.buffer(addr.buffername); + if (buf_.kind != buffer::Buffer::Kind::Generic) + return {buf_.copy(addr.start, addr.end), nullptr, nullptr}; + auto &buf = *(buffer::GenericBuffer *)&buf_; + if (!buf.parse.lang) + return {buf.copy(addr.start, addr.end), nullptr, nullptr}; + void *state = nullptr; // TODO: buf.parse.root.get_at(addr.start); + return {buf.copy(addr.start, addr.end), buf.parse.lang, state}; + }, .handle = []( BEd &ctx, const buffer::Address &addr_, @@ -68,10 +82,9 @@ void Function::register_posix(BEd &ctx) { const Argument &, std::vector * ) { - auto addr = std::get(addr_); + const auto &addr = std::get(addr_); ctx.buffer(addr.buffername).replace(ctx, text, addr.start, addr.end); - vase::Shard::release(text); - } + vase::Shard::release(text); } } ); ctx.functions.insert( diff --git a/src/internal/io/input.cc b/src/internal/io/input.cc index 7c8193f..f506a9a 100644 --- a/src/internal/io/input.cc +++ b/src/internal/io/input.cc @@ -16,9 +16,11 @@ std::pair IO::read_pipe() { continue; } if (n == 0) { + if (pipe_input.empty()) + return {"", true}; std::string line = std::move(pipe_input); pipe_input.clear(); - return {line, true}; + return {line, false}; } if (errno == EINTR) continue; diff --git a/src/internal/io/io.cc b/src/internal/io/io.cc index c99efd4..ba50e51 100644 --- a/src/internal/io/io.cc +++ b/src/internal/io/io.cc @@ -35,7 +35,7 @@ IO::~IO() { } void IO::apply(const io::Token::Kind &t) { - if (bed.suppress_mode) + if (!bed.color_mode) return; write("\x1b[0m"); const auto &hl = bed.theme.get(t); @@ -60,7 +60,7 @@ void IO::apply(const io::Token::Kind &t) { } void IO::reset() { - if (bed.suppress_mode) + if (!bed.color_mode) return; write("\x1b[0m"); } diff --git a/src/internal/parser/address.cc b/src/internal/parser/address.cc index ed69381..92a9eb1 100644 --- a/src/internal/parser/address.cc +++ b/src/internal/parser/address.cc @@ -6,74 +6,49 @@ void Parser::locator(AddressPromise &addr) { addr.base = AddressPromise::None{}; switch (peek()) { case '.': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Current{}; break; case '$': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Last{}; break; case '%': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::LastRange{}; break; case '[': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Block{Direction::Backward}; break; case ']': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Block{Direction::Forward}; break; case '^': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Diagnostic{Direction::Backward}; break; case '~': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Diagnostic{Direction::Forward}; break; case '\'': - tokens->push_back( - {.start = i, - .end = i + (uint64_t)2, - .type = io::Token::Mark} - ); + token(io::Token::Mark); advance(); if (('a' <= peek() && peek() <= 'z') || ('A' <= peek() && peek() <= 'Z')) @@ -81,17 +56,16 @@ void Parser::locator(AddressPromise &addr) { else throw ed_error("Valid mark needed after \'"); advance(); + end_token(); break; case '{': { - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); uint16_t j = 0; std::string func; std::string arg; + token(io::Token::RubyFunction); while (peek(j) != '}') { if (peek(j) == '\0') throw ed_error("Scripted address not terminated"); @@ -100,6 +74,8 @@ void Parser::locator(AddressPromise &addr) { if (peek(j) == ':') { func = peek_str(j); advance(j + 1); + end_token(); + token(io::Token::RubyArg); j = 0; continue; } @@ -109,26 +85,15 @@ void Parser::locator(AddressPromise &addr) { arg = peek_str(j); else func = peek_str(j); - tokens->push_back( - {.start = i, - .end = i + (uint64_t)func.size(), - .type = io::Token::RubyFunction} - ); - if (arg.size()) - tokens->push_back( - {.start = i + (uint64_t)func.size() + 1, - .end = i + (uint64_t)func.size() + 1 + (uint64_t)arg.size(), - .type = io::Token::RubyFunction} - ); addr.base = AddressPromise::Scripted{std::move(func), std::move(arg)}; - advance(j + 1); - tokens->push_back( - {.start = i, - .end = i - (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + advance(j); + end_token(); + token(io::Token::AddressSymbol); + advance(); + end_token(); } break; case '/': { + token(io::Token::Regexp); advance(); uint64_t j = 0; while (true) { @@ -148,19 +113,11 @@ void Parser::locator(AddressPromise &addr) { Direction::Forward, std::string(peek_str(j)) ); - tokens->push_back( - {.start = (uint64_t)i - 1, - .end = (uint64_t)i + j + 1, - .type = io::Token::Regexp} - ); advance(j + 1); + end_token(); } break; case '?': { - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::Regexp); advance(); uint16_t j = 0; while (true) { @@ -180,14 +137,11 @@ void Parser::locator(AddressPromise &addr) { Direction::Backward, std::string(peek_str(j)) ); - tokens->push_back( - {.start = (uint64_t)i - 1, - .end = (uint64_t)i + j + 1, - .type = io::Token::Regexp} - ); advance(j + 1); + end_token(); } break; case '<': { + token(io::Token::Label); advance(); uint16_t j = 0; while (peek(j) != '>' @@ -208,14 +162,11 @@ void Parser::locator(AddressPromise &addr) { }; break; } - tokens->push_back( - {.start = (uint64_t)i - 1, - .end = (uint64_t)i + j + 1, - .type = io::Token::AddressSymbol} - ); advance(j + 1); + end_token(); } break; case '>': { + token(io::Token::Label); advance(); uint16_t j = 0; while (peek(j) != '>' && peek(j) != '\0') @@ -230,20 +181,13 @@ void Parser::locator(AddressPromise &addr) { }; break; } - tokens->push_back( - {.start = (uint64_t)i - 1, - .end = (uint64_t)i + j + 1, - .type = io::Token::AddressSymbol} - ); advance(j + 1); + end_token(); } break; case '+': { - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); addr.base = AddressPromise::Current{}; uint16_t j = 0; uint64_t num = 0; @@ -253,21 +197,15 @@ void Parser::locator(AddressPromise &addr) { } if (j == 0) num = 1; - tokens->push_back( - {.start = (uint64_t)i, - .end = (uint64_t)i + j, - .type = io::Token::Number} - ); - advance(j); addr.offset += num; + advance(j); + end_token(); } break; case '-': { - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); advance(); + end_token(); + token(io::Token::Number); addr.base = AddressPromise::Current{}; uint16_t j = 0; uint64_t num = 0; @@ -277,28 +215,20 @@ void Parser::locator(AddressPromise &addr) { } if (j == 0) num = 1; - tokens->push_back( - {.start = (uint64_t)i, - .end = (uint64_t)i + j, - .type = io::Token::Number} - ); - advance(j); addr.offset -= num; + advance(j); + end_token(); } break; default: if ('0' <= peek() && peek() <= '9') { - uint16_t start = i; + token(io::Token::Number); uint64_t num = 0; while ('0' <= peek() && peek() <= '9') { num = num * 10 + (peek() - '0'); advance(); } addr.base = AddressPromise::Number{num}; - tokens->push_back( - {.start = (uint64_t)start, - .end = (uint64_t)i, - .type = io::Token::Number} - ); + end_token(); } } } @@ -308,26 +238,19 @@ int64_t Parser::offset() { while (peek() == '+' || peek() == '-' || ('0' <= peek() && peek() <= '9')) { bool positive = peek() != '-'; - if (peek() == '+' || peek() == '-') { - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSymbol} - ); + token(io::Token::AddressSymbol); + if (peek() == '+' || peek() == '-') advance(); - } + end_token(); + token(io::Token::Number); uint16_t j = 0; int64_t num = 0; while ('0' <= peek(j) && peek(j) <= '9') num = num * 10 + (peek(j++) - '0'); if (j == 0) num = 1; - tokens->push_back( - {.start = (uint64_t)i, - .end = (uint64_t)i + j, - .type = io::Token::Number} - ); advance(j); + end_token(); offset += positive ? num : -num; skip_ws(); } @@ -337,7 +260,7 @@ int64_t Parser::offset() { void Parser::address(AddressPromise &addr) { if (peek() == ':') { - uint16_t start = i; + token(io::Token::BufferName); advance(); uint16_t j = 0; while (peek(j) != ':' && peek(j) != '\0') @@ -346,11 +269,7 @@ void Parser::address(AddressPromise &addr) { advance(j); if (peek() == ':') advance(); - tokens->push_back( - {.start = start, - .end = i, - .type = io::Token::BufferName} - ); + end_token(); } skip_ws(); if (peek() == '\0') @@ -367,11 +286,7 @@ void Parser::addresses(std::vector &addresses) { address(*addr); while (peek() == ',' || peek() == ';') { addr->jumping = peek() == ';'; - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::AddressSeperator} - ); + token(io::Token::AddressSeperator); advance(); skip_ws(); addresses.push_back({}); diff --git a/src/internal/parser/address_promise.cc b/src/internal/parser/address_promise.cc index bdc6e09..85b54ef 100644 --- a/src/internal/parser/address_promise.cc +++ b/src/internal/parser/address_promise.cc @@ -81,16 +81,10 @@ std::optional AddressPromise::get_line(BEd &ctx, std::vectorempty()) { - bufname = ctx.current().buffername; - curr.bufname = bufname; - } else { - bufname = *curr.bufname; - } - } else { + if (!curr.bufname.has_value()) curr.bufname = bufname; - } + else if (curr.bufname->empty()) + curr.bufname = bufname = ctx.current().buffername; bool is_final = idx + 1 == list.size(); if (!is_final) { if (std::holds_alternative(curr.base)) { @@ -107,15 +101,15 @@ std::optional AddressPromise::get_line(BEd &ctx, std::vector(curr.base)) { if (prev_set) { @@ -144,16 +138,10 @@ std::optional AddressPromise::get_range(BEd &ctx, std::vectorempty()) { - bufname = ctx.current().buffername; - curr.bufname = bufname; - } else { - bufname = *curr.bufname; - } - } else { + if (!curr.bufname.has_value()) curr.bufname = bufname; - } + else if (curr.bufname->empty()) + curr.bufname = bufname = ctx.current().buffername; bool is_final = idx + 1 == list.size(); if (!is_final) { if (std::holds_alternative(curr.base)) { @@ -170,15 +158,15 @@ std::optional AddressPromise::get_range(BEd &ctx, std::vector(curr.base)) { if (prev_set) { diff --git a/src/internal/parser/operation.cc b/src/internal/parser/operation.cc index 40facfa..c3d5d15 100644 --- a/src/internal/parser/operation.cc +++ b/src/internal/parser/operation.cc @@ -11,12 +11,9 @@ void Parser::operation() { if (len == 0) throw ed_error("Function not found."); functions::Function *function = bed.functions.get_ptr(peek_str(len)); - tokens->push_back( - {.start = i, - .end = i + len, - .type = io::Token::Function} - ); + token(io::Token::Function); advance(len); + end_token(); command->function = function; char suffix = '\0'; switch (command->function->argument_kind) { @@ -32,21 +29,15 @@ void Parser::operation() { command->argument = peek(); else throw ed_error("Valid mark needed."); - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::Mark} - ); + token(io::Token::Mark); advance(); + end_token(); break; case functions::Function::ArgumentKind::Any: command->argument = std::string(peek_str()); - tokens->push_back( - {.start = i, - .end = i + peek_str().size(), - .type = io::Token::Any} - ); + token(io::Token::Data); advance(peek_str().size()); + end_token(); break; case functions::Function::ArgumentKind::Global: { char delim; @@ -123,31 +114,22 @@ void Parser::operation() { skip_ws(); switch (peek()) { case '!': - tokens->push_back( - {.start = i, - .end = (uint64_t)i + 1, - .type = io::Token::Error} - ); + token(io::Token::Error); advance(); + end_token(); + token(io::Token::Shell); command->argument = functions::Function::ShellArg(std::string(peek_str())); - tokens->push_back( - {.start = i, - .end = i + peek_str().size(), - .type = io::Token::Shell} - ); advance(peek_str().size()); + end_token(); break; case '\0': command->argument = std::monostate(); break; default: + token(io::Token::File); command->argument = std::filesystem::path(peek_str()); - tokens->push_back( - {.start = i, - .end = i + peek_str().size(), - .type = io::Token::File} - ); advance(peek_str().size()); + end_token(); break; } break; @@ -163,7 +145,7 @@ void Parser::operation() { char delim = peek(); if (delim == '\0') throw ed_error("regex expected"); - uint16_t start = i; + token(io::Token::Regexp); advance(); uint16_t j = 0; while (true) { @@ -203,20 +185,13 @@ void Parser::operation() { advance(j); options = "p"; } - tokens->push_back( - {.start = start, - .end = i, - .type = io::Token::Regexp} - ); + end_token(); + token(io::Token::Suffix); if (peek() != '\0') { options = std::string(peek_str()); - tokens->push_back( - {.start = i, - .end = i + peek_str().size(), - .type = io::Token::Suffix} - ); advance(peek_str().size()); } + end_token(); std::erase_if(options, [&](char c) { if (bed.suffixes[c - 'a'].has_value()) { suffix = c; @@ -243,22 +218,16 @@ void Parser::operation() { } break; case functions::Function::ArgumentKind::Shell: command->argument = functions::Function::ShellArg(std::string(peek_str())); - tokens->push_back( - {.start = i, - .end = i + peek_str().size(), - .type = io::Token::Shell} - ); + token(io::Token::Shell); advance(peek_str().size()); + end_token(); break; } if (!suffix) { suffix = peek(); - tokens->push_back( - {.start = i, - .end = i + (uint64_t)1, - .type = io::Token::Suffix} - ); + token(io::Token::Suffix); advance(); + end_token(); } if (suffix) { auto &s = bed.suffixes[suffix - 'a']; diff --git a/src/internal/parser/parser.cc b/src/internal/parser/parser.cc index 21ae3d2..987edb7 100644 --- a/src/internal/parser/parser.cc +++ b/src/internal/parser/parser.cc @@ -2,6 +2,19 @@ #include "bed.h" namespace bed::internal::parser { +void Parser::token(io::Token::Kind k) { + end_token(); + tokens->push_back({.start = i, .end = 0, .type = k}); +} + +void Parser::end_token() { + if (tokens->empty()) + return; + auto &b = tokens->back(); + if (!b.end) + b.end = i; +} + char Parser::peek(uint16_t o) { return i + o < cmd.size() ? cmd[i + o] : '\0'; } @@ -15,29 +28,36 @@ void Parser::advance(uint16_t c) { } void Parser::skip_ws() { + end_token(); while (peek() == ' ' || peek() == '\t') advance(); } void Parser::parse() { - skip_ws(); - if (peek() == '@') { - tokens->push_back( - {.start = 0, - .end = 1, - .type = io::Token::TempCurrent} - ); - advance(); - command->temp_address = true; - } else { - command->temp_address = false; + try { + skip_ws(); + if (peek() == '@') { + token(io::Token::TempCurrent); + advance(); + command->temp_address = true; + } else { + command->temp_address = false; + } + skip_ws(); + addresses(command->addresses); + operation(); + skip_ws(); + if (peek() != '\0') { + token(io::Token::Error); + throw ed_error("Malformed command"); + } + advance(cmd.size() - i); + end_token(); + } catch (const ed_error &e) { + advance(cmd.size() - i); + end_token(); + throw; } - skip_ws(); - addresses(command->addresses); - operation(); - skip_ws(); - if (peek() != '\0') - throw ed_error("Malformed command"); } Parser::Parser( diff --git a/src/internal/syntax/miniparser.cc b/src/internal/syntax/miniparser.cc new file mode 100644 index 0000000..954499a --- /dev/null +++ b/src/internal/syntax/miniparser.cc @@ -0,0 +1,120 @@ +#include "internal/syntax/miniparser.h" + +namespace bed::internal::syntax { +MiniParser::MiniParser(Language &lang, vase::Shard *vase, void *initial_state) + : lang(lang), + start_state(initial_state ? lang.copy(initial_state) : lang.none_state()) { + vase::Iterator it(vase, 0, Direction::Forward); + void *state = lang.copy(start_state); + std::vector tokens; + std::vector events; + const uint64_t count = vase ? vase->lines + 1 : 0; + lines.reserve(count); + for (uint64_t i = 0; i < count; ++i) { + it.next(); + tokens.clear(); + events.clear(); + lang.parse(&state, it.line, i == 0, &tokens, &events); + lines.emplace_back(lang.copy(state), std::move(tokens)); + } + lang.destroy(state); +} + +MiniParser::~MiniParser() { + if (start_state) + lang.destroy(start_state); + for (auto &[state, tokens] : lines) + if (state) + lang.destroy(state); +} + +void MiniParser::dirty(vase::Shard *vase, uint64_t start, uint64_t count) { + vase::Iterator it(vase, start, Direction::Forward); + void *state = start ? lang.copy(lines[start - 1].first) + : lang.copy(start_state); + std::vector events; + uint64_t i = start; + for (; i < start + count; i++) { + it.next(); + auto &line = lines[i]; + line.second.clear(); + events.clear(); + lang.parse(&state, it.line, i == 0, &line.second, &events); + lang.destroy(line.first); + line.first = lang.copy(state); + } + while (it.next()) { + auto &line = lines[i++]; + line.second.clear(); + events.clear(); + lang.parse(&state, it.line, false, &line.second, &events); + if (lang.equal(line.first, state)) + break; + lang.destroy(line.first); + line.first = lang.copy(state); + } + lang.destroy(state); +} + +void MiniParser::insert(vase::Shard *vase, uint64_t start, uint64_t count) { + vase::Iterator it(vase, start, Direction::Forward); + void *state = start ? lang.copy(lines[start - 1].first) + : lang.copy(start_state); + std::vector events; + std::vector>> lines_new; + std::vector tokens; + lines_new.reserve(count); + for (uint64_t i = 0; i < count; ++i) { + it.next(); + tokens.clear(); + events.clear(); + lang.parse(&state, it.line, start + i == 0, &tokens, &events); + lines_new.emplace_back(lang.copy(state), std::move(tokens)); + } + uint64_t i = start; + while (it.next()) { + auto &line = lines[i++]; + line.second.clear(); + events.clear(); + lang.parse(&state, it.line, i == 0, &line.second, &events); + if (lang.equal(line.first, state)) + break; + lang.destroy(line.first); + line.first = lang.copy(state); + } + lines.insert( + lines.begin() + start, + std::make_move_iterator(lines_new.begin()), + std::make_move_iterator(lines_new.end()) + ); + lang.destroy(state); +} + +void MiniParser::erase(vase::Shard *vase, uint64_t start, uint64_t count) { + if (count == 0) + return; + void *state = start ? lang.copy(lines[start - 1].first) + : lang.copy(start_state); + std::vector events; + for (uint64_t i = start; i < start + count; ++i) + if (lines[i].first) + lang.destroy(lines[i].first); + lines.erase( + lines.begin() + start, + lines.begin() + start + count + ); + vase::Iterator it(vase, start, Direction::Forward); + uint64_t i = start; + while (i < lines.size() && it.next()) { + auto &line = lines[i]; + line.second.clear(); + events.clear(); + lang.parse(&state, it.line, i++ == 0, &line.second, &events); + if (lang.equal(line.first, state)) + break; + lang.destroy(line.first); + line.first = lang.copy(state); + } + lang.destroy(state); +} +} // namespace bed::internal::syntax diff --git a/src/internal/syntax/ruby/parse.cc b/src/internal/syntax/ruby/parse.cc index 4abe831..0694353 100644 --- a/src/internal/syntax/ruby/parse.cc +++ b/src/internal/syntax/ruby/parse.cc @@ -1030,6 +1030,7 @@ bool handle_syntax(RubyParser &p, std::vector *tokens, std::vector' + || p.peek(j) == '#' ) { return false; } diff --git a/src/internal/theme/theme.cc b/src/internal/theme/theme.cc index 997a1d6..e0c1c5d 100644 --- a/src/internal/theme/theme.cc +++ b/src/internal/theme/theme.cc @@ -63,11 +63,6 @@ Theme Theme::default_theme() { .bg = 0x000000, .flags = io::Highlight::None, }; - theme.hl[io::Token::Any] = { - .fg = GREY, - .bg = 0x000000, - .flags = io::Highlight::None, - }; theme.hl[io::Token::Shell] = { .fg = BRIGHT_GREEN, .bg = 0x000000, diff --git a/src/internal/ui/command/command.cc b/src/internal/ui/command/command.cc index a81cfcf..03869df 100644 --- a/src/internal/ui/command/command.cc +++ b/src/internal/ui/command/command.cc @@ -3,66 +3,6 @@ #include "internal/parser/parser.h" namespace bed::internal::ui { -/*template -static void for_each_cluster(std::string_view s, F &&f) { - unicode_width_state_t state; - unicode_width_init(&state); - size_t i = 0; - while (i < s.size()) { - unsigned char c = static_cast(s[i]); - size_t bytes = 1; - int width = 0; - if (c < 128) { - width = unicode_width_process(&state, c); - } else { - uint_least32_t cp; - size_t decoded = grapheme_decode_utf8(s.data() + i, s.size() - i, &cp); - bytes = decoded > 0 ? decoded : 1; - width = unicode_width_process(&state, cp); - } - if (width < 0) - width = 0; - f(i, bytes, width); - i += bytes; - } -} - -static int display_width(std::string_view s) { - int w = 0; - for_each_cluster(s, [&](size_t, size_t, int cw) { w += cw; }); - return w; -} - -static uint16_t count_clusters(std::string_view s) { - uint16_t n = 0; - for_each_cluster(s, [&](size_t, size_t, int) { ++n; }); - return n; -} - -static std::vector wrap_offsets(std::string_view line, uint16_t avail) { - std::vector offsets{0}; - int col = 0; - for_each_cluster(line, [&](uint16_t i, uint16_t, int w) { - if (col + w > avail && col > 0) { - offsets.push_back(i); - col = 0; - } - col += w; - }); - return offsets; -} - -// The word under/before `byte_pos`, split on plain ASCII spaces. Used to -// pick what prefix to hand the suggestion trie. -// TODO: change to use libgrapheme word break here. -static std::string current_word(const std::string &line, size_t byte_pos) { - size_t start = (byte_pos == 0) ? std::string::npos : line.rfind(' ', byte_pos - 1); - start = (start == std::string::npos) ? 0 : start + 1; - if (byte_pos < start) - byte_pos = start; - return line.substr(start, byte_pos - start); -}*/ - CommandIO::CommandIO(BEd &bed) : bed(bed) { if (bed.prompt_mode) prompt = bed.prompt(bed); diff --git a/src/internal/ui/text_mode/layout.cc b/src/internal/ui/text_mode/layout.cc new file mode 100644 index 0000000..e69de29 diff --git a/src/internal/ui/text_mode/text_mode.cc b/src/internal/ui/text_mode/text_mode.cc index b4b53de..303d25a 100644 --- a/src/internal/ui/text_mode/text_mode.cc +++ b/src/internal/ui/text_mode/text_mode.cc @@ -1,9 +1,132 @@ #include "internal/ui/text_mode.h" #include "bed.h" -namespace bed::internal::ui { -TextMode::TextMode(BEd &bed) : bed(bed) { - cursor = 0; +namespace bed::internal::ui::text_mode { +template +static void for_each_cluster(std::string_view s, F &&f) { + size_t cluster_start = 0; + while (cluster_start < s.size()) { + size_t cluster_len = + grapheme_next_character_break_utf8( + s.data() + cluster_start, + s.size() - cluster_start + ); + if (cluster_len == 0) + break; + size_t cluster_end = cluster_start + cluster_len; + unicode_width_state_t state; + unicode_width_init(&state); + size_t i = cluster_start; + int width = 0; + while (i < cluster_end) { + uint_least32_t cp; + size_t decoded = + grapheme_decode_utf8(s.data() + i, cluster_end - i, &cp); + if (decoded == 0) + decoded = 1; + int w = unicode_width_process(&state, cp); + if (w > width) + width = w; + i += decoded; + } + if (width < 0) + width = 0; + if (!f(cluster_start, cluster_len, width)) + return; + cluster_start = cluster_end; + } +} + +static size_t previous_cluster(std::string_view line, size_t col) { + size_t previous = 0; + for_each_cluster(line, [&](size_t start, size_t len, int) { + if (start >= col) + return false; + previous = start; + return start + len < col; + }); + return previous; +} + +static size_t next_cluster(std::string_view line, size_t col) { + size_t next = line.size(); + for_each_cluster(line, [&](size_t start, size_t len, int) { + if (start >= col) { + next = start + len; + return false; + } + return true; + }); + return next; +} + +static size_t visual_column(std::string_view line, size_t col) { + size_t width = 0; + for_each_cluster(line, [&](size_t start, size_t, int cluster_width) { + if (start >= col) + return false; + width += cluster_width; + return true; + }); + return width; +} + +static size_t byte_column(std::string_view line, size_t target_width) { + size_t width = 0; + size_t col = 0; + for_each_cluster(line, [&](size_t start, size_t len, int cluster_width) { + if (width + cluster_width > target_width) + return false; + width += cluster_width; + col = start + len; + if (width == target_width) + return false; + return true; + }); + return col; +} + +static std::vector> wrap_line(std::string_view line, size_t avail) { + std::vector> result; + if (avail == 0) { + result.push_back({0, line.size()}); + return result; + } + size_t begin = 0; + size_t end = 0; + int col = 0; + for_each_cluster(line, [&](size_t i, size_t len, int width) { + if (col > 0 && col + width > int(avail)) { + result.push_back({begin, i}); + begin = i; + col = 0; + } + col += width; + end = i + len; + return true; + }); + if (begin < line.size() || result.empty()) + result.push_back({begin, end}); + return result; +} + +void TextMode::layout_lines() { + vase::Iterator it(vase, 0, Direction::Forward); + lines.clear(); + while (it.next()) { + auto wrapped = wrap_line(it.line, term_width); + lines.push_back({}); + for (auto [begin, end] : wrapped) + lines.back().push_back({begin, end - begin}); + } +} + +TextMode::TextMode( + BEd &bed, vase::Shard *vase, syntax::Language *lang, void *state +) : vase(vase), bed(bed) { + if (lang) + parser.emplace(*lang, vase, state); + cursor = vase::eof_point(vase); } std::pair TextMode::run() { @@ -13,31 +136,32 @@ std::pair TextMode::run() { } std::pair TextMode::run_pipe() { - cmd.clear(); + vase::Shard::release(vase); + vase = nullptr; while (true) { auto [str, eof] = bed.io.read_pipe(); if (str == "." || eof) break; - cmd += str + "\n"; + vase = vase::insert(&bed.append, vase, &cursor, str.data(), str.length()); + vase = vase::insert(&bed.append, vase, &cursor, '\n'); } - return {vase::Shard::from_string(cmd.data(), cmd.length(), true), false}; + return {vase, false}; } std::pair TextMode::run_terminal() { auto [row, col] = bed.io.cursor_position(); auto [rows, cols] = bed.io.terminal_size(); if (row > rows) - throw fatal_error("Invalid cursor position.", 1); + throw ed_error("Invalid cursor position."); start = row; height = rows - row + 1; term_width = cols; term_height = rows; - cmd.clear(); - cursor = 0; redraw(); bool running = true; while (running) { + redraw(); io::KeyEvent res = bed.io.read_key(); switch (res.type) { case io::KeyEvent::KeyType::EOF_: @@ -52,110 +176,151 @@ std::pair TextMode::run_terminal() { case io::KeyEvent::Modifier::ALT: case io::KeyEvent::Modifier::CTRL_ALT: case io::KeyEvent::Modifier::CTRL: + if (res.text[0] == 'c') + return {vase, true}; break; case io::KeyEvent::Modifier::NONE: if (res.text[0] == '\b' || res.text[0] == 0x7f) { - if (cursor > 0) { - cmd.erase(--cursor, 1); + if (!vase) + break; + if (cursor.row || cursor.col) { + auto last = cursor; + if (last.col) { + vase::Iterator it(vase, last.row, Direction::Forward); + if (!it.next()) + throw ed_error("Invalid cursor position."); + last.col = previous_cluster(it.line, last.col); + } else if (last.row) { + --last.row; + vase::Iterator it(vase, last.row, Direction::Forward); + if (!it.next()) + throw ed_error("Invalid cursor position."); + last.col = it.line.size(); + } else { + break; + } + vase::Range r = {last, cursor}; + vase = vase::erase(vase, r); + if (cursor.row == last.row) { + parser->dirty(vase, cursor.row, 1); + } else { + parser->erase(vase, cursor.row, 1); + parser->dirty(vase, last.row, 1); + } + cursor = last; } } else if (res.text[0] == '\n') { - cmd.insert(cursor++, 1, '\n'); - grow(); + if (vase && cursor.row == vase->lines) { + vase::Iterator it(vase, vase->lines, Direction::Backward); + if (it.next() && it.line == ".") { + vase = vase::erase(vase, vase->lines + 1, vase->lines + 1); + running = false; + break; + } + } + vase = vase::insert(&bed.append, vase, &cursor, '\n'); + parser->insert(vase, cursor.row - 1, 1); } else { - cmd.insert(cursor, res.text); - cursor += res.text.size(); + vase = vase::insert(&bed.append, vase, &cursor, res.text.data(), res.text.size()); + parser->dirty(vase, cursor.row, 1); } break; } break; - case io::KeyEvent::KeyType::PASTE: - cmd.insert(cursor, res.text); - cursor += res.text.size(); - grow(); - break; + case io::KeyEvent::KeyType::PASTE: { + auto lines = std::count(res.text.begin(), res.text.end(), '\n'); + vase = vase::insert(&bed.append, vase, &cursor, res.text.data(), res.text.size()); + if (lines) { + parser->insert(vase, cursor.row - lines, lines); + parser->dirty(vase, cursor.row - lines, 1); + } else { + parser->dirty(vase, cursor.row, 1); + } + } break; case io::KeyEvent::KeyType::SPECIAL: + if (!vase) + break; switch (res.special_key) { case io::KeyEvent::SpecialKey::UNKNOWN: break; - case io::KeyEvent::SpecialKey::RIGHT: - if (cursor < cmd.size()) - ++cursor; - break; - case io::KeyEvent::SpecialKey::LEFT: - if (cursor > 0) - --cursor; - break; + case io::KeyEvent::SpecialKey::RIGHT: { + vase::Iterator it(vase, cursor.row, Direction::Forward); + if (!it.next()) + throw ed_error("Invalid cursor position."); + cursor.col = next_cluster(it.line, cursor.col); + } break; + case io::KeyEvent::SpecialKey::LEFT: { + vase::Iterator it(vase, cursor.row, Direction::Forward); + if (!it.next()) + throw ed_error("Invalid cursor position."); + cursor.col = previous_cluster(it.line, cursor.col); + } break; case io::KeyEvent::SpecialKey::UP: { - size_t line_start = - cmd.rfind('\n', cursor == 0 ? 0 : cursor - 1); - if (line_start == std::string::npos) - line_start = 0; - else - ++line_start; - size_t col = cursor - line_start; - if (line_start == 0) + if (!cursor.row) break; - size_t prev_end = line_start - 1; - size_t prev_start = - cmd.rfind('\n', prev_end == 0 ? 0 : prev_end - 1); - if (prev_start == std::string::npos) - prev_start = 0; - else - ++prev_start; - size_t prev_len = prev_end - prev_start; - cursor = prev_start + std::min(col, prev_len); - break; - } + vase::Iterator current(vase, cursor.row, Direction::Forward); + if (!current.next()) + throw ed_error("Invalid cursor position."); + size_t wanted = visual_column(current.line, cursor.col); + --cursor.row; + vase::Iterator previous(vase, cursor.row, Direction::Forward); + if (!previous.next()) + throw ed_error("Invalid cursor position."); + cursor.col = byte_column(previous.line, wanted); + } break; case io::KeyEvent::SpecialKey::DOWN: { - size_t line_start = - cmd.rfind('\n', cursor == 0 ? 0 : cursor - 1); - if (line_start == std::string::npos) - line_start = 0; - else - ++line_start; - size_t col = cursor - line_start; - size_t line_end = cmd.find('\n', cursor); - if (line_end == std::string::npos) - line_end = cmd.size(); - if (line_end == cmd.size()) + if (cursor.row + 1 >= vase->lines) break; - size_t next_start = line_end + 1; - size_t next_end = cmd.find('\n', next_start); - if (next_end == std::string::npos) - next_end = cmd.size(); - size_t next_len = next_end - next_start; - cursor = next_start + std::min(col, next_len); - break; - } + vase::Iterator current(vase, cursor.row, Direction::Forward); + if (!current.next()) + throw ed_error("Invalid cursor position."); + size_t wanted = visual_column(current.line, cursor.col); + ++cursor.row; + vase::Iterator previous(vase, cursor.row, Direction::Forward); + if (!previous.next()) + throw ed_error("Invalid cursor position."); + cursor.col = byte_column(previous.line, wanted); + } break; case io::KeyEvent::SpecialKey::DELETE: - if (cursor < cmd.size()) - cmd.erase(cursor, 1); - break; + auto next = cursor; + vase::Iterator it(vase, cursor.row, Direction::Forward); + if (!it.next()) + break; + if (cursor.col < it.line.size()) { + vase::Iterator it(vase, next.row, Direction::Forward); + if (!it.next()) + throw ed_error("Invalid cursor position."); + next.col = next_cluster(it.line, cursor.col); + } else if (cursor.row + 1 < vase->lines) { + ++next.row; + next.col = 0; + } else { + break; + } + vase::Range r = {cursor, next}; + vase = vase::erase(vase, r); + if (cursor.row != next.row) + parser->erase(vase, cursor.row + 1, 1); + parser->dirty(vase, cursor.row, 1); } break; } - redraw(); - if (cmd.size() >= 3 && cmd.compare(cmd.size() - 3, 3, "\n.\n") == 0) { - cmd.erase(cmd.size() - 3); - cursor = cmd.size(); - running = false; - } - if (cmd.size() == 2 && cmd.compare(0, 2, ".\n") == 0) { - cmd.clear(); - cursor = 0; - running = false; - } } - size_t total_lines = cmd.size() ? 1 + std::count(cmd.begin(), cmd.end(), '\n') : 0; - uint16_t last_row = start + total_lines; + layout_lines(); + size_t required_height = 0; + for (const auto &elem : lines) + required_height += elem.size(); + uint16_t last_row = start + required_height; bed.io.move_cursor(last_row, 1); - bed.io.write("\n", 1); - return {vase::Shard::from_string(cmd.data(), cmd.length(), true), false}; + bed.io.write("\n"); + return {vase, false}; } void TextMode::grow() { - size_t required_height = 1 + std::count(cmd.begin(), cmd.end(), '\n'); + size_t required_height = 0; + for (const auto &elem : lines) + required_height += elem.size(); auto [rows, cols] = bed.io.terminal_size(); term_height = rows; term_width = cols; @@ -166,46 +331,85 @@ void TextMode::grow() { return; bed.io.move_cursor(rows, 1); for (long i = 0; i < overflow; ++i) - bed.io.write("\n", 1); + bed.io.write("\n"); start -= overflow; if (start < 1) start = 1; } void TextMode::redraw() { - auto [rows, cols] = bed.io.terminal_size(); - term_height = rows; - term_width = cols; + layout_lines(); + grow(); + bed.io.write("\x1b[?25l", 6); for (uint16_t i = 0; i < height; ++i) { bed.io.move_cursor(start + i, 1); bed.io.write("\x1b[2K", 4); } - size_t line = 0; - size_t line_start = 0; - for (size_t i = 0; i < cursor; ++i) { - if (cmd[i] == '\n') { - ++line; - line_start = i + 1; - } - } - size_t col = cursor - line_start; - size_t pos = 0; uint16_t screen_line = start; - while (pos <= cmd.size()) { - size_t end = cmd.find('\n', pos); - if (end == std::string::npos) - end = cmd.size(); - if (screen_line < start + height) { - size_t len = std::min(end - pos, size_t(term_width)); - bed.io.move_cursor(screen_line, 1); - bed.io.write(cmd.substr(pos, len)); - } - if (end == cmd.size()) + uint64_t line = 0; + size_t cursor_screen_row = start; + size_t cursor_screen_col = 0; + vase::Iterator it(vase, line, Direction::Forward); + while (it.next()) { + if (screen_line >= start + height) break; - pos = end + 1; - ++screen_line; + std::string_view l(it.line); + const std::vector *tokens = nullptr; + if (parser && parser->lines.size() > line) + tokens = &parser->lines[line].second; + size_t token_index = 0; + for (auto &row : lines[line]) { + const size_t row_start = row.start; + const size_t row_end = row.start + row.length; + bed.io.move_cursor(screen_line, 1); + if (tokens) { + while (token_index < tokens->size() && (*tokens)[token_index].end <= row_start) + ++token_index; + size_t pos = row_start; + for (size_t i = token_index; i < tokens->size(); ++i) { + const auto &token = (*tokens)[i]; + if (token.start >= row_end) + break; + const size_t begin = std::max( + token.start, + row_start + ); + const size_t end = std::min( + token.end, + row_end + ); + if (begin > pos) + bed.io.write(l.data() + pos, begin - pos); + if (begin < end) { + bed.io.apply(token.type); + bed.io.write(l.data() + begin, end - begin); + bed.io.reset(); + pos = end; + } + } + if (pos < row_end) + bed.io.write(l.data() + pos, row_end - pos); + bed.io.reset(); + } else { + bed.io.write(l.substr(row_start, row.length)); + } + if (line == cursor.row && cursor.col >= row_start && cursor.col <= row_end) { + cursor_screen_row = screen_line; + cursor_screen_col = 0; + for_each_cluster( + l.substr(row_start, cursor.col - row_start), + [&](size_t, size_t, int width) { + cursor_screen_col += width; + return true; + } + ); + } + ++screen_line; + } + ++line; } - size_t vis_col = std::min(col, size_t(term_width - 1)); - bed.io.move_cursor(start + line, vis_col + 1); + cursor_screen_col = std::min(cursor_screen_col, size_t(term_width - 1)); + bed.io.move_cursor(cursor_screen_row, cursor_screen_col + 1); + bed.io.write("\x1b[?25h", 6); } -} // namespace bed::internal::ui +} // namespace bed::internal::ui::text_mode diff --git a/src/internal/vase/vase.cc b/src/internal/vase/vase.cc index 9765718..45675e5 100644 --- a/src/internal/vase/vase.cc +++ b/src/internal/vase/vase.cc @@ -74,14 +74,7 @@ void _insert(AppendStorage *ap, Shard **root, Point *point, const char *data, ui ++lines; last_line = ++data; } - uint64_t col = 0; - uint64_t remaining = end - last_line; - while (remaining) { - uint64_t n = grapheme_next_character_break_utf8(last_line, remaining); - last_line += n; - remaining -= n; - ++col; - } + uint64_t col = end - last_line; if (lines) { point->row += lines; point->col = col; @@ -122,6 +115,22 @@ Shard *replace(AppendStorage *ap, Shard *root, Range range, const char *data, ui return insert(ap, root, &range.start, data, len); } +Point eof_point(Shard *root) { + if (!root) + return {0, 0}; + Point result{ + .row = root->lines, + .col = 0 + }; + PetalIterator it(root, Direction::Forward); + it.seek_line(root->lines); + const char *data; + uint64_t len; + while (it.next(&data, &len)) + result.col += len; + return result; +} + uint64_t offset_of(Shard *root, Point point) { return offset_of(root, point.row) + point.col; }