#include "internal/ui/text_mode.h" #include "bed.h" namespace bed::internal::ui::text_mode { // TODO: make this into a proper layout engine, in order to avoid so many expensive calculations repeatedly. 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() { if (!bed.io.interactive()) return run_pipe(); return run_terminal(); } std::pair TextMode::run_pipe() { vase::Shard::release(vase); vase = nullptr; while (true) { auto [str, eof] = bed.io.read_pipe(); if (str == "." || eof) break; vase = vase::insert(&bed.append, vase, &cursor, str.data(), str.length()); vase = vase::insert(&bed.append, vase, &cursor, '\n'); } 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 ed_error("Invalid cursor position."); start = row; height = rows - row + 1; term_width = cols; term_height = rows; redraw(); bool running = true; bool cancelled = false; while (running) { redraw(); io::KeyEvent res = bed.io.read_key(); switch (res.type) { case io::KeyEvent::KeyType::EOF_: running = false; break; case io::KeyEvent::KeyType::MOUSE: case io::KeyEvent::KeyType::RESIZE: break; case io::KeyEvent::KeyType::CHAR: switch (res.modifier) { case io::KeyEvent::Modifier::SHIFT: case io::KeyEvent::Modifier::ALT: case io::KeyEvent::Modifier::CTRL_ALT: case io::KeyEvent::Modifier::CTRL: if (res.text[0] == 'c') { cancelled = true; running = false; } break; case io::KeyEvent::Modifier::NONE: if (res.text[0] == '\b' || res.text[0] == 0x7f) { 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 (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; } } else if (res.text[0] == '\n') { 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'); if (parser) parser->insert(vase, cursor.row - 1, 1); } else { vase = vase::insert(&bed.append, vase, &cursor, res.text.data(), res.text.size()); if (parser) parser->dirty(vase, cursor.row, 1); } break; } 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 (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: if (!vase) break; switch (res.special_key) { case io::KeyEvent::SpecialKey::UNKNOWN: 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: { if (!cursor.row) 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: { if (cursor.row >= vase->lines) 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: 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 < vase->lines) { ++next.row; next.col = 0; } else { break; } vase::Range r = {cursor, next}; vase = vase::erase(vase, r); if (parser) { if (cursor.row != next.row) parser->erase(vase, cursor.row + 1, 1); parser->dirty(vase, cursor.row, 1); } } break; } } 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"); return {vase, cancelled}; } void TextMode::grow() { 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; if (required_height > height) height = required_height; long overflow = long(start) + long(height) - 1 - long(rows); if (overflow <= 0) return; bed.io.move_cursor(rows, 1); for (long i = 0; i < overflow; ++i) bed.io.write("\n"); start -= overflow; if (start < 1) start = 1; } void TextMode::redraw() { 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); } uint16_t screen_line = start; 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; 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; } 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::text_mode