Add basic scripting support.

- Fix certain bugs in ruby parser
- And a lot more cleanup/minor fixes.
This commit is contained in:
2026-09-08 18:07:39 +01:00
parent a5794d9ab3
commit bb3ce7549a
20 changed files with 410 additions and 84 deletions
+18 -2
View File
@@ -203,7 +203,23 @@ uint64_t GenericBuffer::prune(int keep) {
bool GenericBuffer::waste() {
return save_path.empty()
&& root == nullptr
&& undo_stack.empty();
&& undo_stack.empty()
&& parse.lang == nullptr;
}
void GenericBuffer::language(BEd &ctx, std::string name) {
syntax::Language *lang = nullptr;
if (name.size()) {
auto it = ctx.languages.find(name);
if (it == ctx.languages.end())
throw ed_error("Language not found.");
lang = it->second;
}
if (lang == parse.lang)
return;
snapshot("Change buffer language.");
syntax::release(parse);
parse = syntax::make_parser(root, lines(), lang);
}
void GenericBuffer::load(BEd &ctx, vase::Shard *text) {
@@ -225,7 +241,7 @@ void GenericBuffer::load(BEd &ctx, vase::Shard *text) {
}
ctx.current() = {name, lines()};
syntax::release(parse);
parse = syntax::make_parser(root, lines(), ctx.languages["ruby"]);
parse = syntax::make_parser(root, lines(), nullptr);
}
void GenericBuffer::set_filename(std::filesystem::path path) {
+108 -4
View File
@@ -173,24 +173,95 @@ void Function::register_extented(BEd &ctx) {
ctx.functions.insert(
"`",
Function{
.address_kind = Function::AddressKind::None,
.address_kind = Function::AddressKind::Range,
.argument_kind = Function::ArgumentKind::Ruby,
.input_mode = Function::InputMode::None,
.desc = "Execute some ruby code",
.default_address = "",
.accept_zero = true,
.pre_text_mode = nullptr,
.handle = [](
BEd &ctx,
const buffer::Address &addr_,
vase::Shard *,
const Argument &arg_,
std::vector<buffer::Line> *
) {
auto &addr = std::get<buffer::Range>(addr_);
auto &arg = std::get<RubyArg>(arg_);
auto pre_code = "$START=" + std::to_string(addr.start)
+ ";$END=" + std::to_string(addr.end)
+ ";$BUFNAME=\"" + addr.buffername + "\"";
scripting::run(ctx, pre_code);
scripting::run(ctx, arg.cmd);
},
}
);
ctx.functions.insert(
"``",
Function{
.address_kind = Function::AddressKind::Range,
.argument_kind = Function::ArgumentKind::None,
.input_mode = Function::InputMode::None,
.desc = "Execute addressed lines as ruby code.",
.default_address = ".,.",
.accept_zero = false,
.pre_text_mode = nullptr,
.handle = [](
BEd &,
const buffer::Address &,
BEd &ctx,
const buffer::Address &addr_,
vase::Shard *,
const Argument &,
std::vector<buffer::Line> *
) {
// TODO: connect up to mruby.
auto &addr = std::get<buffer::Range>(addr_);
auto &buf = ctx.buffer(addr.buffername);
auto code = buf.copy(addr.start, addr.end);
ctx.prev().buffername = addr.buffername;
ctx.prev().start = addr.start;
ctx.prev().end = addr.end;
ctx.current() = {addr.buffername, addr.end};
auto pre_code = "$START=" + std::to_string(addr.start)
+ ";$END=" + std::to_string(addr.end)
+ ";$BUFNAME=\"" + addr.buffername + "\"";
scripting::run(ctx, pre_code);
scripting::run(ctx, vase::to_string(code));
vase::Shard::release(code);
},
}
);
ctx.functions.insert(
"```",
Function{
.address_kind = Function::AddressKind::Range,
.argument_kind = Function::ArgumentKind::None,
.input_mode = Function::InputMode::Text,
.desc = "Execute some ruby code (taken from text mode)",
.default_address = "",
.accept_zero = true,
.pre_text_mode = [](
BEd &ctx,
const buffer::Address &,
const Argument &
) -> std::tuple<vase::Shard *, syntax::Language *, void *> {
return {nullptr, ctx.languages["ruby"], nullptr};
},
.handle = [](
BEd &ctx,
const buffer::Address &addr_,
vase::Shard *code,
const Argument &,
std::vector<buffer::Line> *
) {
auto &addr = std::get<buffer::Range>(addr_);
auto pre_code = "$START=" + std::to_string(addr.start)
+ ";$END=" + std::to_string(addr.end)
+ ";$BUFNAME=\"" + addr.buffername + "\"";
scripting::run(ctx, pre_code);
scripting::run(ctx, vase::to_string(code));
vase::Shard::release(code); },
}
);
ctx.functions.insert(
"echo",
Function{
@@ -432,5 +503,38 @@ void Function::register_extented(BEd &ctx) {
}
}
);
ctx.functions.insert(
"lang",
Function{
.address_kind = Function::AddressKind::None,
.argument_kind = Function::ArgumentKind::Any,
.input_mode = Function::InputMode::None,
.desc = "Set buffer language",
.default_address = "",
.accept_zero = false,
.pre_text_mode = nullptr,
.handle = [](
BEd &ctx,
const buffer::Address &addr_,
vase::Shard *,
const Argument &arg_,
std::vector<buffer::Line> *
) {
auto &addr = std::get<std::string>(addr_);
auto name = std::get<std::string>(arg_);
const auto first = name.find_first_not_of(" \t");
const auto last = name.find_last_not_of(" \t");
if (first == std::string::npos)
name.clear();
else
name = name.substr(first, last - first + 1);
auto &buf_ = ctx.buffer(addr);
if (buf_.kind != buffer::Buffer::Kind::Generic)
throw ed_error("Can't set language to buffer.");
auto &buf = *(buffer::GenericBuffer *)&buf_;
buf.language(ctx, name);
}
}
);
}
} // namespace bed::internal::functions
+31 -9
View File
@@ -37,7 +37,19 @@ void Function::register_posix(BEd &ctx) {
.desc = "Append text to a line",
.default_address = ".",
.accept_zero = true,
.pre_text_mode = nullptr,
.pre_text_mode = [](
BEd &ctx,
const buffer::Address &addr_,
const Argument &
) -> std::tuple<vase::Shard *, syntax::Language *, void *> {
const auto &addr = std::get<buffer::Line>(addr_);
auto &buf_ = ctx.buffer(addr.buffername);
if (buf_.kind != buffer::Buffer::Kind::Generic)
return {nullptr, nullptr, nullptr};
auto &buf = *(buffer::GenericBuffer *)&buf_;
void *state = syntax::state_before(buf.parse, buf.root, addr.number);
return {nullptr, buf.parse.lang, state};
},
.handle = [](
BEd &ctx,
const buffer::Address &addr_,
@@ -47,8 +59,7 @@ void Function::register_posix(BEd &ctx) {
) {
auto addr = std::get<buffer::Line>(addr_);
ctx.buffer(addr.buffername).append(ctx, text, addr.number);
vase::Shard::release(text);
}
vase::Shard::release(text); }
}
);
ctx.functions.insert(
@@ -70,9 +81,7 @@ void Function::register_posix(BEd &ctx) {
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);
void *state = syntax::state_before(buf.parse, buf.root, addr.start - 1);
return {buf.copy(addr.start, addr.end), buf.parse.lang, state};
},
.handle = [](
@@ -291,7 +300,21 @@ void Function::register_posix(BEd &ctx) {
.desc = "Insert text before a line",
.default_address = ".",
.accept_zero = true,
.pre_text_mode = nullptr,
.pre_text_mode = [](
BEd &ctx,
const buffer::Address &addr_,
const Argument &
) -> std::tuple<vase::Shard *, syntax::Language *, void *> {
auto addr = std::get<buffer::Line>(addr_);
if (addr.number)
addr.number--;
auto &buf_ = ctx.buffer(addr.buffername);
if (buf_.kind != buffer::Buffer::Kind::Generic)
return {nullptr, nullptr, nullptr};
auto &buf = *(buffer::GenericBuffer *)&buf_;
void *state = syntax::state_before(buf.parse, buf.root, addr.number);
return {nullptr, buf.parse.lang, state};
},
.handle = [](
BEd &ctx,
const buffer::Address &addr_,
@@ -303,8 +326,7 @@ void Function::register_posix(BEd &ctx) {
if (addr.number)
addr.number--;
ctx.buffer(addr.buffername).append(ctx, text, addr.number);
vase::Shard::release(text);
}
vase::Shard::release(text); }
}
);
ctx.functions.insert(
+73
View File
@@ -0,0 +1,73 @@
#include "internal/scripting/ruby.h"
#include "bed.h"
namespace bed::internal::scripting {
static mrb_value mrb_bed_exit(mrb_state *mrb, mrb_value) {
mrb_raise(mrb, E_RUNTIME_ERROR, "Use `handle(\"q\")` to quit.");
return mrb_nil_value();
}
static void
raise_fatal(mrb_state *mrb, const fatal_error &e) {
struct RClass *klass = mrb_class_get(mrb, "FatalError");
mrb_value exc = mrb_exc_new_str(mrb, klass, mrb_str_new_cstr(mrb, e.what()));
mrb_iv_set(mrb, exc, mrb_intern_lit(mrb, "@code"), mrb_fixnum_value(e.code));
mrb_exc_raise(mrb, exc);
}
static mrb_value mrb_bed_handle(mrb_state *mrb, mrb_value) {
auto &ctx = *(BEd *)mrb->ud;
const char *command;
mrb_int len;
mrb_get_args(mrb, "s", &command, &len);
std::string_view cmd(command, len);
try {
ctx.handle(cmd, false);
} catch (const ed_error &e) {
mrb_raise(mrb, mrb_class_get(mrb, "EdError"), e.what());
} catch (const fatal_error &f) {
raise_fatal(mrb, f);
} catch (...) {
mrb_raise(mrb, E_RUNTIME_ERROR, "Unexpected error.");
}
return mrb_nil_value();
}
void register_basic(BEd &ctx) {
auto *bed_error =
mrb_define_class(ctx.mrb, "EdError", mrb_exc_get_id(ctx.mrb, MRB_ERROR_SYM(RuntimeError)));
mrb_define_class(ctx.mrb, "FatalError", bed_error);
mrb_define_method(ctx.mrb, ctx.mrb->kernel_module, "exit", mrb_bed_exit, MRB_ARGS_NONE());
mrb_define_method(ctx.mrb, ctx.mrb->kernel_module, "handle", mrb_bed_handle, MRB_ARGS_REQ(1));
}
void run(BEd &ctx, const std::string &str) {
mrb_state *mrb = ctx.mrb;
mrb_load_nstring(mrb, str.data(), str.size());
if (!mrb->exc)
return;
mrb_value exc = mrb_obj_value(mrb->exc);
mrb_value msg = mrb_funcall(mrb, exc, "message", 0);
std::string error;
if (mrb_string_p(msg))
error.assign(RSTRING_PTR(msg), RSTRING_LEN(msg));
auto *fatal_class = mrb_class_get(mrb, "FatalError");
if (mrb_obj_is_kind_of(mrb, exc, fatal_class)) {
mrb_value code =
mrb_iv_get(mrb, exc, mrb_intern_lit(mrb, "@code"));
mrb->exc = nullptr;
int c = 1;
if (mrb_fixnum_p(code))
c = mrb_fixnum(code);
throw fatal_error(error, c);
}
auto *ed_class = mrb_class_get(mrb, "EdError");
if (mrb_obj_is_kind_of(mrb, exc, ed_class)) {
mrb->exc = nullptr;
throw ed_error(error);
}
mrb->exc = nullptr;
throw ed_error("Ruby Exception: " + error);
}
} // namespace bed::internal::scripting
+6 -12
View File
@@ -2,19 +2,11 @@
namespace bed::internal::syntax {
Iterator::Iterator(uint64_t target, ParserSnapshot p, vase::Shard *vase)
: snap(p) {
uint64_t offset;
TreeCursor c = TreeCursor(*p.lang, p.root, target, &offset);
at = target - offset;
state = p.lang->copy(c.leaf->state);
: snap(p), at(target) {
at = target;
if (snap.lang)
state = ParseState::state_before(*snap.lang, snap.root, vase, at);
it = vase::Iterator(vase, at, Direction::Forward);
while (at < target) {
it->next();
tokens.clear();
events.clear();
p.lang->parse(&state, it->line, at == 0, &tokens, &events);
at++;
}
}
Iterator::~Iterator() {
@@ -46,6 +38,8 @@ Iterator &Iterator::operator=(Iterator &&other) {
}
void Iterator::next() {
if (!state)
return;
it->next();
tokens.clear();
events.clear();
+1 -1
View File
@@ -8,7 +8,7 @@ MiniParser::MiniParser(Language &lang, vase::Shard *vase, void *initial_state)
void *state = lang.copy(start_state);
std::vector<io::Token> tokens;
std::vector<ParseEvent> events;
const uint64_t count = vase ? vase->lines + 1 : 0;
const uint64_t count = vase ? vase->lines + 1 : 1;
lines.reserve(count);
for (uint64_t i = 0; i < count; ++i) {
it.next();
+6
View File
@@ -22,6 +22,12 @@ void release(ParserSnapshot &snap) {
snap.root = nullptr;
}
void *state_before(const ParserSnapshot &snap, vase::Shard *vase, uint64_t line) {
if (!snap.lang)
return nullptr;
return ParseState::state_before(*snap.lang, snap.root, vase, line);
}
uint64_t next_closing(const ParserSnapshot &snap, uint64_t line) {
if (!snap.root || !snap.lang)
return line + 10;
+26 -2
View File
@@ -108,7 +108,7 @@ ParseState *ParseState::splice(
void *state = lang.none_state();
std::vector<io::Token> tokens;
std::vector<ParseEvent> events;
ParsePieceBuilder builder(lang, 0);
ParsePieceBuilder builder(lang, 0, state);
for (uint64_t at = 0; at < final; ++at) {
it.next();
tokens.clear();
@@ -136,7 +136,7 @@ ParseState *ParseState::splice(
uint64_t end_extra;
TreeCursor c = TreeCursor(lang, root, end_in_tree, &end_extra);
uint64_t end_in_vase = line + final;
ParsePieceBuilder builder(lang, at);
ParsePieceBuilder builder(lang, at, state);
while (at < end_in_vase + end_extra) {
it.next();
tokens.clear();
@@ -192,4 +192,28 @@ ParseState *ParseState::concat(Language &lang, ParseState *a, ParseState *b) {
}
return balance(lang, new ParseStateBranch(a, b));
}
void *ParseState::state_before(Language &lang, ParseState *root, vase::Shard *vase, uint64_t line) {
uint64_t offset;
uint64_t at;
void *state;
if (!root) {
at = 0;
state = lang.none_state();
} else {
TreeCursor c = TreeCursor(lang, root, line, &offset);
at = line - offset;
state = lang.copy(c.leaf->state);
}
vase::Iterator it(vase, at, Direction::Forward);
std::vector<io::Token> tokens;
std::vector<ParseEvent> events;
for (; at < line; ++at) {
it.next();
tokens.clear();
events.clear();
lang.parse(&state, it.line, at == 0, &tokens, &events);
}
return state;
}
} // namespace bed::internal::syntax
+16 -9
View File
@@ -782,11 +782,6 @@ bool handle_syntax(RubyParser &p, std::vector<io::Token> *tokens, std::vector<Pa
p.ending = false;
p.advance();
return false;
case '\0':
if (p.ending)
p.current().flags |= RubyState::RubyInternalState::EXPECTING_EXPRESSION;
p.advance();
return false;
default:
if ('0' <= p.peek() && p.peek() <= '9') {
p.current().flags &= ~RubyState::RubyInternalState::EXPECTING_EXPRESSION;
@@ -981,6 +976,11 @@ bool handle_syntax(RubyParser &p, std::vector<io::Token> *tokens, std::vector<Pa
| RubyState::RubyInternalState::DEF_NAME;
return false;
}
if (j > 3 && p.peek_str(3) == "to_") {
p.current().flags &= ~RubyState::RubyInternalState::EXPECTING_EXPRESSION;
tokens->push_back({p.i, p.i + j, io::Token::Function});
p.advance(j);
}
uint32_t start = p.i;
if (p.peek(j) == ':') {
p.advance(j);
@@ -1008,8 +1008,9 @@ bool handle_syntax(RubyParser &p, std::vector<io::Token> *tokens, std::vector<Pa
return false;
if (p.peek(j) == '&'
|| p.peek(j) == '%'
|| p.peek(j) == ':') {
if (p.peek(j + 1) == ' ' || p.peek(j + 1) == '>')
|| p.peek(j) == ':'
|| p.peek(j) == '?') {
if (p.peek(j + 1) == ' ' || p.peek(j + 1) == '\0')
return false;
} else if (p.peek(j) == '-') {
if (p.peek(j + 1) != '>')
@@ -1025,7 +1026,6 @@ bool handle_syntax(RubyParser &p, std::vector<io::Token> *tokens, std::vector<Pa
|| p.peek(j) == '*'
|| p.peek(j) == '/'
|| p.peek(j) == '='
|| p.peek(j) == '?'
|| p.peek(j) == '|'
|| p.peek(j) == '^'
|| p.peek(j) == '<'
@@ -1061,7 +1061,7 @@ void ruby_parse(
std::vector<ParseEvent> *events
) {
RubyParser p(v_state, line);
while (p.i <= p.len()) {
while (p.i < p.len()) {
p.op_last = p.set_op_last;
p.set_op_last = false;
if (p.current().state == RubyState::RubyInternalState::END)
@@ -1099,6 +1099,13 @@ void ruby_parse(
return;
if (!handle_syntax(p, tokens, events))
p.current().flags &= ~RubyState::RubyInternalState::NEWLINE;
if (p.peek() == '\0') {
if (p.ending)
p.current().flags |= RubyState::RubyInternalState::EXPECTING_EXPRESSION;
p.advance();
p.current().flags &= ~RubyState::RubyInternalState::NEWLINE;
break;
}
continue;
}
if (p.ending)
+23 -15
View File
@@ -206,11 +206,13 @@ std::pair<vase::Shard *, bool> TextMode::run_terminal() {
}
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);
if (parser) {
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;
}
@@ -224,10 +226,12 @@ std::pair<vase::Shard *, bool> TextMode::run_terminal() {
}
}
vase = vase::insert(&bed.append, vase, &cursor, '\n');
parser->insert(vase, cursor.row - 1, 1);
if (parser)
parser->insert(vase, cursor.row - 1, 1);
} else {
vase = vase::insert(&bed.append, vase, &cursor, res.text.data(), res.text.size());
parser->dirty(vase, cursor.row, 1);
if (parser)
parser->dirty(vase, cursor.row, 1);
}
break;
}
@@ -235,11 +239,13 @@ std::pair<vase::Shard *, bool> TextMode::run_terminal() {
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);
if (parser) {
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:
@@ -304,9 +310,11 @@ std::pair<vase::Shard *, bool> TextMode::run_terminal() {
}
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);
if (parser) {
if (cursor.row != next.row)
parser->erase(vase, cursor.row + 1, 1);
parser->dirty(vase, cursor.row, 1);
}
}
break;
}