Major updates/cleanup.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user