Major updates/cleanup.

This commit is contained in:
2026-09-08 07:00:13 +01:00
parent 0df03f418a
commit 0760209417
23 changed files with 668 additions and 441 deletions
+318 -114
View File
@@ -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 <typename F>
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<std::pair<size_t, size_t>> wrap_line(std::string_view line, size_t avail) {
std::vector<std::pair<size_t, size_t>> 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<vase::Shard *, bool> TextMode::run() {
@@ -13,31 +136,32 @@ std::pair<vase::Shard *, bool> TextMode::run() {
}
std::pair<vase::Shard *, bool> 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<vase::Shard *, bool> 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<vase::Shard *, bool> 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<io::Token> *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<size_t>(
token.start,
row_start
);
const size_t end = std::min<size_t>(
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