From 881e2b3393d6f6e9a59adf2096dadf2ed7528c10 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Tue, 25 Nov 2025 00:04:58 -0800 Subject: [PATCH] Continue C++ rewrite. --- CMakeLists.txt | 10 + Makefile | 19 +- display.cc | 237 +++++++++++++++++ display.h | 42 +++ file_io.cc | 180 +++++++++++++ file_io.h | 29 +++ input_handler.cc | 122 +++++++++ input_handler.h | 29 +++ killring.cc | 262 +++++++++++++++++++ killring.h | 44 ++++ main.cc | 652 ++++++++++++++++++++++++++++++++--------------- terminal.cc | 111 ++++++++ terminal.h | 39 +++ 13 files changed, 1563 insertions(+), 213 deletions(-) create mode 100644 display.cc create mode 100644 display.h create mode 100644 file_io.cc create mode 100644 file_io.h create mode 100644 input_handler.cc create mode 100644 input_handler.h create mode 100644 killring.cc create mode 100644 killring.h create mode 100644 terminal.cc create mode 100644 terminal.h diff --git a/CMakeLists.txt b/CMakeLists.txt index e9f2516..5224958 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,6 +15,11 @@ set(KE_SOURCES main.cc abuf.cc erow.cc + terminal.cc + input_handler.cc + display.cc + file_io.cc + killring.cc ) # Header files @@ -22,6 +27,11 @@ set(KE_HEADERS ke_constants.h abuf.h erow.h + terminal.h + input_handler.h + display.h + file_io.h + killring.h ) # Build options diff --git a/Makefile b/Makefile index 823e92e..4a61178 100644 --- a/Makefile +++ b/Makefile @@ -18,8 +18,8 @@ CXXFLAGS += -fsanitize=address -fno-omit-frame-pointer LDFLAGS := -fsanitize=address -SOURCES := main.cc abuf.cc erow.cc -OBJECTS := main.o abuf.o erow.o +SOURCES := main.cc abuf.cc erow.cc terminal.cc input_handler.cc display.cc file_io.cc killring.cc +OBJECTS := main.o abuf.o erow.o terminal.o input_handler.o display.o file_io.o killring.o all: $(TARGET) test.txt @@ -35,6 +35,21 @@ abuf.o: abuf.cc abuf.h erow.o: erow.cc erow.h $(CXX) $(CXXFLAGS) -c erow.cc +terminal.o: terminal.cc terminal.h + $(CXX) $(CXXFLAGS) -c terminal.cc + +input_handler.o: input_handler.cc input_handler.h ke_constants.h + $(CXX) $(CXXFLAGS) -c input_handler.cc + +display.o: display.cc display.h ke_constants.h + $(CXX) $(CXXFLAGS) -c display.cc + +file_io.o: file_io.cc file_io.h + $(CXX) $(CXXFLAGS) -c file_io.cc + +killring.o: killring.cc killring.h + $(CXX) $(CXXFLAGS) -c killring.cc + .PHONY: install #install: $(TARGET) install: diff --git a/display.cc b/display.cc new file mode 100644 index 0000000..725f802 --- /dev/null +++ b/display.cc @@ -0,0 +1,237 @@ +#include "display.h" +#include "ke_constants.h" +#include "abuf.h" +#include "erow.h" +#include +#include +#include +#include +#include +#include + +// Need access to main.cc structures and functions +extern void erow_update(struct erow *row); +extern int erow_render_to_cursor(struct erow *row, int cx); + +// erow definition (needed for accessing row fields) +struct erow { + char *line; + char *render; + int size; + int rsize; + int cap; +}; + +// editor_t definition (needed for display operations) +struct editor_t { + struct termios entry_term; + int rows, cols; + int curx, cury; + int rx; + int mode; + int nrows; + int rowoffs, coloffs; + struct erow *row; + struct erow *killring; + int kill; + int no_kill; + char *filename; + int dirty; + int dirtyex; + char msg[80]; + int mark_set; + int mark_curx, mark_cury; + time_t msgtm; +}; + +namespace ke { + +void Display::clear(ke::abuf* ab) { + if (ab == nullptr) { + (void)write(STDOUT_FILENO, ESCSEQ "2J", 4); + (void)write(STDOUT_FILENO, ESCSEQ "H", 3); + } else { + ab->append(ESCSEQ "2J", 4); + ab->append(ESCSEQ "H", 3); + } +} + +void Display::draw_rows(editor_t* editor, ke::abuf* ab) { + assert(editor->cols >= 0); + + char* buf = new char[editor->cols]; + int buflen, filerow, padding; + int y; + + for (y = 0; y < editor->rows; y++) { + filerow = y + editor->rowoffs; + if (filerow >= editor->nrows) { + if ((editor->nrows == 0) && (y == editor->rows / 3)) { + buflen = snprintf(buf, + editor->cols, + "%s", + KE_VERSION); + padding = (editor->rows - buflen) / 2; + + if (padding) { + ab->append("|", 1); + padding--; + } + + while (padding--) + ab->append(" ", 1); + ab->append(buf, buflen); + } else { + ab->append("|", 1); + } + } else { + erow_update(&editor->row[filerow]); + buflen = editor->row[filerow].rsize - editor->coloffs; + if (buflen < 0) { + buflen = 0; + } + + if (buflen > editor->cols) { + buflen = editor->cols; + } + ab->append(editor->row[filerow].render + editor->coloffs, + buflen); + } + ab->append(ESCSEQ "K", 3); + ab->append("\r\n", 2); + } + delete[] buf; +} + +char Display::status_mode_char(int mode) { + switch (mode) { + case MODE_NORMAL: + return 'N'; + case MODE_KCOMMAND: + return 'K'; + case MODE_ESCAPE: + return 'E'; + default: + return '?'; + } +} + +void Display::draw_status_bar(editor_t* editor, ke::abuf* ab) { + char* status = new char[editor->cols]; + char* rstatus = new char[editor->cols]; + char* mstatus = new char[editor->cols]; + + int len, rlen; + + len = snprintf(status, + editor->cols, + "%c%cke: %.20s - %d lines", + status_mode_char(editor->mode), + editor->dirty ? '!' : '-', + editor->filename ? editor->filename : "[no file]", + editor->nrows); + + if (editor->mark_set) { + snprintf(mstatus, + editor->cols, + " | M: %d, %d ", + editor->mark_curx + 1, + editor->mark_cury + 1); + } else { + snprintf(mstatus, editor->cols, " | M:clear "); + } + + rlen = snprintf(rstatus, + editor->cols, + "L%d/%d C%d %s", + editor->cury + 1, + editor->nrows, + editor->curx + 1, + mstatus); + + if (len > editor->cols) { + len = editor->cols; + } + + ab->append(ESCSEQ "7m", 4); + ab->append(status, len); + while (len < editor->cols) { + if (editor->cols - len == rlen) { + ab->append(rstatus, rlen); + break; + } else { + ab->append(" ", 1); + len++; + } + len++; + } + ab->append(ESCSEQ "m", 3); + ab->append("\r\n", 2); + delete[] status; + delete[] rstatus; + delete[] mstatus; +} + +void Display::draw_message_line(editor_t* editor, ke::abuf* ab) { + int len = strlen(editor->msg); + + ab->append(ESCSEQ "K", 3); + if (len > editor->cols) { + len = editor->cols; + } + + if (len && ((time(nullptr) - editor->msgtm) < MSG_TIMEO)) { + ab->append(editor->msg, len); + } +} + +void Display::scroll(editor_t* editor) { + editor->rx = 0; + if (editor->cury < editor->nrows) { + editor->rx = erow_render_to_cursor( + &editor->row[editor->cury], + editor->curx); + } + + if (editor->cury < editor->rowoffs) { + editor->rowoffs = editor->cury; + } + + if (editor->cury >= editor->rowoffs + editor->rows) { + editor->rowoffs = editor->cury - editor->rows + 1; + } + + if (editor->rx < editor->coloffs) { + editor->coloffs = editor->rx; + } + + if (editor->rx >= editor->coloffs + editor->cols) { + editor->coloffs = editor->rx - editor->cols + 1; + } +} + +void Display::refresh(editor_t* editor) { + char buf[32]; + ke::abuf ab; + + scroll(editor); + + ab.append(ESCSEQ "?25l", 6); + clear(&ab); + + draw_rows(editor, &ab); + draw_status_bar(editor, &ab); + draw_message_line(editor, &ab); + + snprintf(buf, + sizeof(buf), + ESCSEQ "%d;%dH", + (editor->cury - editor->rowoffs) + 1, + (editor->rx - editor->coloffs) + 1); + ab.append(buf, strnlen(buf, 32)); + ab.append(ESCSEQ "?25h", 6); + + (void)write(STDOUT_FILENO, ab.data(), ab.size()); +} + +} // namespace ke diff --git a/display.h b/display.h new file mode 100644 index 0000000..bb5c3ae --- /dev/null +++ b/display.h @@ -0,0 +1,42 @@ +#ifndef DISPLAY_HPP +#define DISPLAY_HPP + +#include + +// Forward declarations +struct editor_t; + +namespace ke { + class abuf; // Forward declaration of ke::abuf + +/** + * Display class for screen rendering and refresh operations. + */ +class Display { +public: + Display() = default; + ~Display() = default; + + // Deleted copy constructor and assignment + Display(const Display&) = delete; + Display& operator=(const Display&) = delete; + + // Main display operations + static void refresh(editor_t* editor); + static void clear(ke::abuf* ab); + + // Drawing operations + static void draw_rows(editor_t* editor, ke::abuf* ab); + static void draw_status_bar(editor_t* editor, ke::abuf* ab); + static void draw_message_line(editor_t* editor, ke::abuf* ab); + + // Scrolling + static void scroll(editor_t* editor); + +private: + static char status_mode_char(int mode); +}; + +} // namespace ke + +#endif // DISPLAY_HPP diff --git a/file_io.cc b/file_io.cc new file mode 100644 index 0000000..f7f54a6 --- /dev/null +++ b/file_io.cc @@ -0,0 +1,180 @@ +#include "file_io.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Need access to main.cc structures and functions +extern void erow_insert(int at, const char *s, int len); +extern void reset_editor(); +extern void editor_set_status(const char *fmt, ...); +extern void die(const char *s); +extern char *editor_prompt(const char *, void (*cb)(char *, int16_t)); + +// erow definition +struct erow { + char *line; + char *render; + int size; + int rsize; + int cap; +}; + +// editor_t definition +struct editor_t { + struct termios entry_term; + int rows, cols; + int curx, cury; + int rx; + int mode; + int nrows; + int rowoffs, coloffs; + struct erow *row; + struct erow *killring; + int kill; + int no_kill; + char *filename; + int dirty; + int dirtyex; + char msg[80]; + int mark_set; + int mark_curx, mark_cury; + time_t msgtm; +}; + +namespace ke { + +void FileIO::open_file(editor_t* editor, const char* filename) { + char* line = nullptr; + size_t linecap = 0; + ssize_t linelen; + FILE* fp = nullptr; + + reset_editor(); + + editor->filename = strdup(filename); + assert(editor->filename != nullptr); + + editor->dirty = 0; + if ((fp = fopen(filename, "r")) == nullptr) { + if (errno == ENOENT) { + editor_set_status("[new file]"); + return; + } + die("fopen"); + } + + while ((linelen = getline(&line, &linecap, fp)) != -1) { + if (linelen != -1) { + while ((linelen > 0) && ((line[linelen - 1] == '\r') || + (line[linelen - 1] == '\n'))) { + linelen--; + } + + erow_insert(editor->nrows, line, linelen); + } + } + + free(line); + line = nullptr; + fclose(fp); +} + +char* FileIO::rows_to_buffer(editor_t* editor, int* buflen) { + int len = 0; + int j; + char* buf = nullptr; + char* p = nullptr; + + for (j = 0; j < editor->nrows; j++) { + /* extra byte for newline */ + len += editor->row[j].size + 1; + } + + if (len == 0) { + return nullptr; + } + + *buflen = len; + buf = static_cast(malloc(len)); + assert(buf != nullptr); + p = buf; + + for (j = 0; j < editor->nrows; j++) { + memcpy(p, editor->row[j].line, editor->row[j].size); + p += editor->row[j].size; + *p++ = '\n'; + } + + return buf; +} + +int FileIO::save_file(editor_t* editor) { + int fd = -1; + int len; + int status = 1; /* will be used as exit code */ + char* buf; + + if (!editor->dirty) { + editor_set_status("No changes to save."); + return 0; + } + + if (editor->filename == nullptr) { + editor->filename = editor_prompt("Filename: %s", nullptr); + if (editor->filename == nullptr) { + editor_set_status("Save aborted."); + return 0; + } + } + + buf = rows_to_buffer(editor, &len); + if ((fd = open(editor->filename, O_RDWR | O_CREAT, 0644)) == -1) { + goto save_exit; + } + + if (-1 == ftruncate(fd, len)) { + goto save_exit; + } + + if (len == 0) { + status = 0; + goto save_exit; + } + + if ((ssize_t) len != write(fd, buf, len)) { + goto save_exit; + } + + status = 0; + +save_exit: + if (fd) + close(fd); + if (buf) { + free(buf); + buf = nullptr; + } + + if (status != 0) { + buf = strerror(errno); + editor_set_status("Error writing %s: %s", + editor->filename, + buf); + } else { + editor_set_status("Wrote %d bytes to %s.", + len, + editor->filename); + editor->dirty = 0; + } + + return status; +} + +} // namespace ke diff --git a/file_io.h b/file_io.h new file mode 100644 index 0000000..bfa1501 --- /dev/null +++ b/file_io.h @@ -0,0 +1,29 @@ +#ifndef FILE_IO_HPP +#define FILE_IO_HPP + +// Forward declaration +struct editor_t; + +namespace ke { + +/** + * File I/O class for reading and writing files. + */ +class FileIO { +public: + FileIO() = default; + ~FileIO() = default; + + // Deleted copy constructor and assignment + FileIO(const FileIO&) = delete; + FileIO& operator=(const FileIO&) = delete; + + // File operations + static void open_file(editor_t* editor, const char* filename); + static int save_file(editor_t* editor); + static char* rows_to_buffer(editor_t* editor, int* buflen); +}; + +} // namespace ke + +#endif // FILE_IO_HPP diff --git a/input_handler.cc b/input_handler.cc new file mode 100644 index 0000000..9e4a230 --- /dev/null +++ b/input_handler.cc @@ -0,0 +1,122 @@ +#include "input_handler.h" +#include "ke_constants.h" +#include +#include +#include + +namespace ke { + +// Key codes for special keys +enum KeyPress { + TAB_KEY = 9, + ESC_KEY = 27, + BACKSPACE = 127, + ARROW_LEFT = 1000, + ARROW_RIGHT, + ARROW_UP, + ARROW_DOWN, + DEL_KEY, + HOME_KEY, + END_KEY, + PG_UP, + PG_DN, +}; + +int16_t InputHandler::get_keypress() { + char seq[3]; + /* read raw byte so UTF-8 bytes (>=0x80) are not sign-extended */ + unsigned char uc = 0; + int16_t c; + + if (read(STDIN_FILENO, &uc, 1) == -1) { + perror("get_keypress:read"); + exit(1); + } + + c = (int16_t) uc; + + if (c == 0x1b) { + if (read(STDIN_FILENO, &seq[0], 1) != 1) + return c; + if (read(STDIN_FILENO, &seq[1], 1) != 1) + return c; + + if (seq[0] == '[') { + if (seq[1] < 'A') { + if (read(STDIN_FILENO, &seq[2], 1) != 1) + return c; + if (seq[2] == '~') { + switch (seq[1]) { + case '1': + return HOME_KEY; + case '3': + return DEL_KEY; + case '4': + return END_KEY; + case '5': + return PG_UP; + case '6': + return PG_DN; + case '7': + return HOME_KEY; + case '8': + return END_KEY; + } + } + } else { + switch (seq[1]) { + case 'A': + return ARROW_UP; + case 'B': + return ARROW_DOWN; + case 'C': + return ARROW_RIGHT; + case 'D': + return ARROW_LEFT; + case 'F': + return END_KEY; + case 'H': + return HOME_KEY; + + default: + /* nada */ ; + } + } + } else if (seq[0] == 'O') { + switch (seq[1]) { + case 'F': + return END_KEY; + case 'H': + return HOME_KEY; + } + } + + return 0x1b; + } + + return c; +} + +bool InputHandler::is_arrow_key(int16_t c) { + switch (c) { + case ARROW_DOWN: + case ARROW_LEFT: + case ARROW_RIGHT: + case ARROW_UP: + case CTRL_KEY('p'): + case CTRL_KEY('n'): + case CTRL_KEY('f'): + case CTRL_KEY('b'): + case CTRL_KEY('a'): + case CTRL_KEY('e'): + case END_KEY: + case HOME_KEY: + case PG_DN: + case PG_UP: + return true; + } + + return false; +} + +} // namespace ke diff --git a/input_handler.h b/input_handler.h new file mode 100644 index 0000000..d73fc59 --- /dev/null +++ b/input_handler.h @@ -0,0 +1,29 @@ +#ifndef INPUT_HANDLER_HPP +#define INPUT_HANDLER_HPP + +#include + +namespace ke { + +/** + * Input handler class for reading and processing keyboard input. + */ +class InputHandler { +public: + InputHandler() = default; + ~InputHandler() = default; + + // Deleted copy constructor and assignment + InputHandler(const InputHandler&) = delete; + InputHandler& operator=(const InputHandler&) = delete; + + // Read a keypress from stdin + static int16_t get_keypress(); + + // Check if a key code is an arrow/navigation key + static bool is_arrow_key(int16_t c); +}; + +} // namespace ke + +#endif // INPUT_HANDLER_HPP diff --git a/killring.cc b/killring.cc new file mode 100644 index 0000000..2a44eef --- /dev/null +++ b/killring.cc @@ -0,0 +1,262 @@ +#include "killring.h" +#include +#include +#include +#include +#include + +// Need access to main.cc structures and functions +extern int erow_init(struct erow *row, int len); +extern void erow_update(struct erow *row); +extern void erow_free(struct erow *row); +extern void editor_set_status(const char *fmt, ...); +extern void insertch(int16_t c); +extern void newline(); +extern void move_cursor(int16_t c); +extern int cursor_at_eol(); +extern void swap_int(int *a, int *b); +extern void deletech(uint8_t op); + +// erow definition +struct erow { + char *line; + char *render; + int size; + int rsize; + int cap; +}; + +// editor_t definition +struct editor_t { + struct termios entry_term; + int rows, cols; + int curx, cury; + int rx; + int mode; + int nrows; + int rowoffs, coloffs; + struct erow *row; + struct erow *killring; + int kill; + int no_kill; + char *filename; + int dirty; + int dirtyex; + char msg[80]; + int mark_set; + int mark_curx, mark_cury; + time_t msgtm; +}; + +namespace ke { + +void Killring::flush(editor_t* editor) { + if (editor->killring != nullptr) { + erow_free(editor->killring); + free(editor->killring); + editor->killring = nullptr; + } +} + +void Killring::yank(editor_t* editor) { + if (editor->killring == nullptr) { + return; + } + /* + * Insert killring contents at the cursor without clearing the ring. + * Interpret '\n' as an actual newline() rather than inserting a raw 0x0A + * byte, so yanked content preserves lines correctly. + */ + for (int i = 0; i < editor->killring->size; i++) { + unsigned char ch = (unsigned char) editor->killring->line[i]; + if (ch == '\n') { + newline(); + } else { + insertch(ch); + } + } +} + +void Killring::start_with_char(editor_t* editor, unsigned char ch) { + erow* row = nullptr; + + if (editor->killring != nullptr) { + erow_free(editor->killring); + free(editor->killring); + editor->killring = nullptr; + } + + editor->killring = static_cast(malloc(sizeof(erow))); + assert(editor->killring != nullptr); + assert(erow_init(editor->killring, 0) == 0); + + /* append one char to empty killring without affecting editor.dirty */ + row = editor->killring; + + row->line = static_cast(realloc(row->line, row->size + 2)); + assert(row->line != nullptr); + row->line[row->size] = ch; + row->size++; + row->line[row->size] = '\0'; + erow_update(row); +} + +void Killring::append_char(editor_t* editor, unsigned char ch) { + erow* row = nullptr; + + if (editor->killring == nullptr) { + start_with_char(editor, ch); + return; + } + + row = editor->killring; + row->line = static_cast(realloc(row->line, row->size + 2)); + assert(row->line != nullptr); + row->line[row->size] = ch; + row->size++; + row->line[row->size] = '\0'; + erow_update(row); +} + +void Killring::prepend_char(editor_t* editor, unsigned char ch) { + if (editor->killring == nullptr) { + start_with_char(editor, ch); + return; + } + + erow* row = editor->killring; + row->line = static_cast(realloc(row->line, row->size + 2)); + assert(row->line != nullptr); + memmove(&row->line[1], &row->line[0], row->size + 1); + row->line[0] = ch; + row->size++; + erow_update(row); +} + +void Killring::toggle_markset(editor_t* editor) { + if (editor->mark_set) { + editor->mark_set = 0; + editor_set_status("Mark cleared."); + return; + } + + editor->mark_set = 1; + editor->mark_curx = editor->curx; + editor->mark_cury = editor->cury; + editor_set_status("Mark set."); +} + +int Killring::cursor_after_mark(editor_t* editor) { + if (editor->mark_cury < editor->cury) { + return 1; + } + + if (editor->mark_cury > editor->cury) { + return 0; + } + + return editor->curx >= editor->mark_curx; +} + +int Killring::count_chars_from_cursor_to_mark(editor_t* editor) { + int count = 0; + int curx = editor->curx; + int cury = editor->cury; + + int markx = editor->mark_curx; + int marky = editor->mark_cury; + + if (!cursor_after_mark(editor)) { + swap_int(&curx, &markx); + swap_int(&cury, &marky); + } + + editor->curx = markx; + editor->cury = marky; + + while (editor->cury != cury) { + while (!cursor_at_eol()) { + move_cursor(1000 + 3); // ARROW_RIGHT + count++; + } + + move_cursor(1000 + 3); // ARROW_RIGHT + count++; + } + + while (editor->curx != curx) { + count++; + move_cursor(1000 + 3); // ARROW_RIGHT + } + + return count; +} + +void Killring::kill_region(editor_t* editor) { + int curx = editor->curx; + int cury = editor->cury; + int markx = editor->mark_curx; + int marky = editor->mark_cury; + + if (!editor->mark_set) { + return; + } + + /* kill the current killring */ + flush(editor); + + if (!cursor_after_mark(editor)) { + swap_int(&curx, &markx); + swap_int(&cury, &marky); + } + + editor->curx = markx; + editor->cury = marky; + + while (editor->cury != cury) { + while (!cursor_at_eol()) { + append_char(editor, editor->row[editor->cury].line[editor->curx]); + move_cursor(1000 + 3); // ARROW_RIGHT + } + append_char(editor, '\n'); + move_cursor(1000 + 3); // ARROW_RIGHT + } + + while (editor->curx != curx) { + append_char(editor, editor->row[editor->cury].line[editor->curx]); + move_cursor(1000 + 3); // ARROW_RIGHT + } + + editor_set_status("Region killed."); +} + +void Killring::delete_region(editor_t* editor) { + int count = count_chars_from_cursor_to_mark(editor); + int killed = 0; + int curx = editor->curx; + int cury = editor->cury; + int markx = editor->mark_curx; + int marky = editor->mark_cury; + + if (!editor->mark_set) { + return; + } + + if (!cursor_after_mark(editor)) { + swap_int(&curx, &markx); + swap_int(&cury, &marky); + } + + editor->curx = markx; + editor->cury = marky; + + while (killed < count) { + deletech(0); // KILLRING_NO_OP + killed++; + } + + editor->kill = 1; + editor_set_status("Region killed."); +} + +} // namespace ke diff --git a/killring.h b/killring.h new file mode 100644 index 0000000..c188c92 --- /dev/null +++ b/killring.h @@ -0,0 +1,44 @@ +#ifndef KILLRING_HPP +#define KILLRING_HPP + +#include + +// Forward declarations +struct editor_t; + +namespace ke { + +/** + * Killring class for cut/paste (kill/yank) operations. + */ +class Killring { +public: + Killring() = default; + ~Killring() = default; + + // Deleted copy constructor and assignment + Killring(const Killring&) = delete; + Killring& operator=(const Killring&) = delete; + + // Killring operations + static void flush(editor_t* editor); + static void yank(editor_t* editor); + static void start_with_char(editor_t* editor, unsigned char ch); + static void append_char(editor_t* editor, unsigned char ch); + static void prepend_char(editor_t* editor, unsigned char ch); + + // Mark operations + static void toggle_markset(editor_t* editor); + static int cursor_after_mark(editor_t* editor); + + // Region operations + static void kill_region(editor_t* editor); + static void delete_region(editor_t* editor); + +private: + static int count_chars_from_cursor_to_mark(editor_t* editor); +}; + +} // namespace ke + +#endif // KILLRING_HPP diff --git a/main.cc b/main.cc index 2eb5c96..dd5d96a 100644 --- a/main.cc +++ b/main.cc @@ -2,14 +2,12 @@ * kyle's editor * * first version is a run-through of the kilo editor walkthrough as a - * set of guiderails. I've made a lot of changes and did some things - * differently. keep an eye for for kte, kyle's text editor - the - * rewrite that will be coming out... when it comes out. - * - * https://viewsourcecode.org/snaptoken/kilo/ + * set of guiderails. I've made a lot of changes. */ #include #include +#include +#include #include #include #include @@ -24,12 +22,12 @@ #include #include #include +#include #include #include "ke_constants.h" - /* * Function and struct declarations. */ @@ -41,8 +39,6 @@ struct abuf { int cap; }; -#define ABUF_INIT {NULL, 0, 0} - /* editor row */ struct erow { @@ -89,11 +85,11 @@ struct editor_t { .nrows = 0, .rowoffs = 0, .coloffs = 0, - .row = NULL, - .killring = NULL, + .row = nullptr, + .killring = nullptr, .kill = 0, .no_kill = 0, - .filename = NULL, + .filename = nullptr, .dirty = 0, .dirtyex = 0, .mark_set = 0, @@ -104,8 +100,8 @@ struct editor_t { int next_power_of_2(int n); int cap_growth(int cap, int sz); -void init_editor(void); -void reset_editor(void); +void init_editor(); +void reset_editor(); void ab_append(struct abuf *buf, const char *s, int len); void ab_free(struct abuf *buf); char nibble_to_hex(char c); @@ -117,53 +113,54 @@ void erow_insert(int at, const char *s, int len); void erow_free(struct erow *row); /* kill ring, marking, etc */ -void killring_flush(void); -void killring_yank(void); +void killring_flush(); +void killring_yank(); void killring_start_with_char(unsigned char ch); void killring_append_char(unsigned char ch); void killring_prepend_char(unsigned char ch); -void toggle_markset(void); -int cursor_after_mark(void); +void toggle_markset(); +int cursor_after_mark(); +void indent_region(); /* miscellaneous */ void die(const char *s); int get_winsz(int *rows, int *cols); -void goto_line(void); -int cursor_at_eol(void); +void goto_line(); +int cursor_at_eol(); void delete_row(int at); -void row_append_row(struct erow *row, char *s, int len); -void row_insert_ch(struct erow *row, int at, int16_t c); -void row_delete_ch(struct erow *row, int at); +void row_append_row(erow *row, char *s, int len); +void row_insert_ch(erow *row, int at, int16_t c); +void row_delete_ch(erow *row, int at); void insertch(int16_t c); void deletech(uint8_t op); void open_file(const char *filename); char *rows_to_buffer(int *buflen); -int save_file(void); +int save_file(); uint16_t is_arrow_key(int16_t c); -int16_t get_keypress(void); -void display_refresh(void); +int16_t get_keypress(); +void display_refresh(); void editor_find_callback(char *query, int16_t c); -void editor_find(void); +void editor_find(); char *editor_prompt(const char *, void (*cb)(char *, int16_t)); -void editor_openfile(void); +void editor_openfile(); void move_cursor(int16_t c); -void newline(void); +void newline(); void process_kcommand(int16_t c); void process_normal(int16_t c); void process_escape(int16_t c); -int process_keypress(void); -void enable_termraw(void); -void display_clear(struct abuf *ab); -void disable_termraw(void); -void setup_terminal(void); -void draw_rows(struct abuf *ab); -char status_mode_char(void); -void draw_status_bar(struct abuf *ab); -void draw_message_line(struct abuf *ab); -void scroll(void); -void display_refresh(void); +int process_keypress(); +void enable_termraw(); +void display_clear(abuf *ab); +void disable_termraw(); +void setup_terminal(); +void draw_rows(abuf *ab); +char status_mode_char(); +void draw_status_bar(abuf *ab); +void draw_message_line(abuf *ab); +void scroll(); +void display_refresh(); void editor_set_status(const char *fmt, ...); -void loop(void); +void loop(); void process_normal(int16_t c); @@ -220,7 +217,7 @@ enum KeyPress { * init_editor should set up the global editor struct. */ void -init_editor(void) +init_editor() { editor.cols = 0; editor.rows = 0; @@ -254,7 +251,7 @@ init_editor(void) * reset_editor presumes that editor has been initialized. */ void -reset_editor(void) +reset_editor() { for (int i = 0; i < editor.nrows; i++) { erow_free(&editor.row[i]); @@ -277,7 +274,7 @@ reset_editor(void) void -ab_append(struct abuf *buf, const char *s, int len) +ab_append(abuf *buf, const char *s, int len) { char *nc = buf->b; int sz = buf->len + len; @@ -301,7 +298,7 @@ ab_append(struct abuf *buf, const char *s, int len) void -ab_free(struct abuf *buf) +ab_free(abuf *buf) { free(buf->b); buf->b = NULL; @@ -331,7 +328,7 @@ swap_int(int *a, int *b) int -erow_render_to_cursor(struct erow *row, int cx) +erow_render_to_cursor(erow *row, int cx) { int rx = 0; size_t j = 0; @@ -387,7 +384,7 @@ erow_render_to_cursor(struct erow *row, int cx) int -erow_cursor_to_render(struct erow *row, int rx) +erow_cursor_to_render(erow *row, int rx) { int cur_rx = 0; size_t j = 0; @@ -444,8 +441,12 @@ erow_cursor_to_render(struct erow *row, int rx) int -erow_init(struct erow *row, int len) +erow_init(erow *row, int len) { + if (len < 0) { + return -1; + } + row->size = len; row->rsize = 0; row->render = NULL; @@ -464,7 +465,7 @@ erow_init(struct erow *row, int len) void -erow_update(struct erow *row) +erow_update(erow *row) { int i = 0, j; int tabs = 0; @@ -514,7 +515,7 @@ erow_update(struct erow *row) void erow_insert(int at, const char *s, int len) { - struct erow row; + erow row; if (at < 0 || at > editor.nrows) { return; @@ -524,14 +525,14 @@ erow_insert(int at, const char *s, int len) memcpy(row.line, s, len); row.line[len] = 0; - editor.row = static_cast(realloc(editor.row, - sizeof(struct erow) * (editor.nrows + 1))); + editor.row = static_cast(realloc(editor.row, + sizeof(erow) * (editor.nrows + 1))); assert(editor.row != NULL); if (at < editor.nrows) { memmove(&editor.row[at + 1], &editor.row[at], - sizeof(struct erow) * (editor.nrows - at)); + sizeof(erow) * (editor.nrows - at)); } editor.row[at] = row; @@ -541,7 +542,7 @@ erow_insert(int at, const char *s, int len) void -erow_free(struct erow *row) +erow_free(erow *row) { free(row->render); free(row->line); @@ -551,7 +552,7 @@ erow_free(struct erow *row) void -killring_flush(void) +killring_flush() { if (editor.killring != NULL) { erow_free(editor.killring); @@ -562,7 +563,7 @@ killring_flush(void) void -killring_yank(void) +killring_yank() { if (editor.killring == NULL) { return; @@ -586,7 +587,7 @@ killring_yank(void) void killring_start_with_char(unsigned char ch) { - struct erow *row = NULL; + erow *row = NULL; if (editor.killring != NULL) { erow_free(editor.killring); @@ -594,7 +595,7 @@ killring_start_with_char(unsigned char ch) editor.killring = NULL; } - editor.killring = static_cast(malloc(sizeof(struct erow))); + editor.killring = static_cast(malloc(sizeof(erow))); assert(editor.killring != NULL); assert(erow_init(editor.killring, 0) == 0); @@ -613,7 +614,7 @@ killring_start_with_char(unsigned char ch) void killring_append_char(unsigned char ch) { - struct erow *row = NULL; + erow *row = NULL; if (editor.killring == NULL) { killring_start_with_char(ch); @@ -638,7 +639,7 @@ killring_prepend_char(unsigned char ch) return; } - struct erow *row = editor.killring; + erow *row = editor.killring; row->line = static_cast(realloc(row->line, row->size + 2)); assert(row->line != NULL); memmove(&row->line[1], &row->line[0], row->size + 1); @@ -649,7 +650,7 @@ killring_prepend_char(unsigned char ch) void -toggle_markset(void) +toggle_markset() { if (editor.mark_set) { editor.mark_set = 0; @@ -665,7 +666,7 @@ toggle_markset(void) int -cursor_after_mark(void) +cursor_after_mark() { if (editor.mark_cury < editor.cury) { return 1; @@ -680,7 +681,7 @@ cursor_after_mark(void) int -count_chars_from_cursor_to_mark(void) +count_chars_from_cursor_to_mark() { int count = 0; int curx = editor.curx; @@ -717,7 +718,7 @@ count_chars_from_cursor_to_mark(void) void -kill_region(void) +kill_region() { int curx = editor.curx; int cury = editor.cury; @@ -759,9 +760,53 @@ kill_region(void) } +void +indent_region() +{ + int start_row, end_row; + int i; + + if (!editor.mark_set) { + return; + } + + if (editor.mark_cury < editor.cury) { + start_row = editor.mark_cury; + end_row = editor.cury; + } else if (editor.mark_cury > editor.cury) { + start_row = editor.cury; + end_row = editor.mark_cury; + } else { + start_row = end_row = editor.cury; + } + + /* Ensure bounds are valid */ + if (start_row < 0) { + start_row = 0; + } + + if (end_row >= editor.nrows) { + end_row = editor.nrows - 1; + } + + if (start_row >= editor.nrows || end_row < 0) { + return; + } + + /* Prepend a tab character to every row in the region */ + for (i = start_row; i <= end_row; i++) { + row_insert_ch(&editor.row[i], 0, '\t'); + } + + /* Move cursor to beginning of the line it was on */ + editor.curx = 0; + editor.dirty++; +} + + /* call after kill_region */ void -delete_region(void) +delete_region() { int count = count_chars_from_cursor_to_mark(); int killed = 0; @@ -830,7 +875,7 @@ get_winsz(int *rows, int *cols) void -goto_line(void) +goto_line() { int lineno = 0; char *query = editor_prompt("Line: %s", NULL); @@ -843,16 +888,18 @@ goto_line(void) if (lineno < 1 || lineno >= editor.nrows) { editor_set_status("Line number must be between 1 and %d.", editor.nrows); + free(query); return; } editor.cury = lineno - 1; editor.rowoffs = editor.cury - (editor.rows / 2); + free(query); } int -cursor_at_eol(void) +cursor_at_eol() { assert(editor.curx >= 0); assert(editor.cury >= 0); @@ -864,7 +911,7 @@ cursor_at_eol(void) void -find_next_word(void) +find_next_word() { while (cursor_at_eol()) { move_cursor(ARROW_RIGHT); @@ -890,7 +937,7 @@ find_next_word(void) void -delete_next_word(void) +delete_next_word() { while (cursor_at_eol()) { move_cursor(ARROW_RIGHT); @@ -919,7 +966,7 @@ delete_next_word(void) void -find_prev_word(void) +find_prev_word() { if (editor.cury == 0 && editor.curx == 0) { return; @@ -943,7 +990,7 @@ find_prev_word(void) } void -delete_prev_word(void) +delete_prev_word() { if (editor.cury == 0 && editor.curx == 0) { return; @@ -986,7 +1033,7 @@ delete_row(int at) * newline itself and we must NOT also push the entire row here. */ if (!editor.no_kill) { - struct erow *r = &editor.row[at]; + erow *r = &editor.row[at]; /* Start or continue the kill sequence based on editor.killing */ if (r->size > 0) { /* push raw bytes of the line */ @@ -1018,14 +1065,14 @@ delete_row(int at) erow_free(&editor.row[at]); memmove(&editor.row[at], &editor.row[at + 1], - sizeof(struct erow) * (editor.nrows - at - 1)); + sizeof(erow) * (editor.nrows - at - 1)); editor.nrows--; editor.dirty++; } void -row_append_row(struct erow *row, char *s, int len) +row_append_row(erow *row, char *s, int len) { row->line = static_cast(realloc(row->line, row->size + len + 1)); assert(row->line != NULL); @@ -1038,7 +1085,7 @@ row_append_row(struct erow *row, char *s, int len) void -row_insert_ch(struct erow *row, int at, int16_t c) +row_insert_ch(erow *row, int at, int16_t c) { /* * row_insert_ch just concerns itself with how to update a row. @@ -1059,7 +1106,7 @@ row_insert_ch(struct erow *row, int at, int16_t c) void -row_delete_ch(struct erow *row, int at) +row_delete_ch(erow *row, int at) { if (at < 0 || at >= row->size) { return; @@ -1098,7 +1145,7 @@ insertch(int16_t c) void deletech(uint8_t op) { - struct erow *row = NULL; + erow *row = NULL; unsigned char dch = 0; if (editor.cury >= editor.nrows) { @@ -1239,7 +1286,7 @@ rows_to_buffer(int *buflen) int -save_file(void) +save_file() { int fd = -1; int len; @@ -1329,7 +1376,7 @@ is_arrow_key(int16_t c) int16_t -get_keypress(void) +get_keypress() { char seq[3]; /* read raw byte so UTF-8 bytes (>=0x80) are not sign-extended */ @@ -1408,17 +1455,17 @@ get_keypress(void) char * editor_prompt(const char *prompt, void (*cb)(char *, int16_t)) { - size_t bufsz = 128; - char *buf = static_cast(malloc(bufsz)); - size_t buflen = 0; - int16_t c; + size_t bufsz = 128; + char *buf = static_cast(malloc(bufsz)); + size_t buflen = 0; + int16_t c; buf[0] = '\0'; while (1) { editor_set_status(prompt, buf); display_refresh(); - while ((c = get_keypress()) <= 0) ; + while ((c = get_keypress()) <= 0); if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) { if (buflen != 0) { buf[--buflen] = '\0'; @@ -1430,28 +1477,38 @@ editor_prompt(const char *prompt, void (*cb)(char *, int16_t)) } free(buf); return NULL; - } else if (c == '\r') { - if (buflen != 0) { - editor_set_status(""); - if (cb) { - cb(buf, c); - } - return buf; - } - } else if ((c == TAB_KEY) || (c >= 0x20 && c != 0x7f)) { - if (buflen == bufsz - 1) { - bufsz *= 2; - buf = static_cast(realloc(buf, bufsz)); - assert(buf != NULL); - } + } else if (c == '\r') { + if (buflen != 0) { + editor_set_status(""); + if (cb) { + cb(buf, c); + } + return buf; + } + } else if (c == TAB_KEY) { + /* TAB_KEY is handled by callback, don't insert it */ + } else if (c >= 0x20 && c < 0x7f) { + if (buflen == bufsz - 1) { + bufsz *= 2; + buf = static_cast(realloc(buf, bufsz)); + assert(buf != NULL); + } - buf[buflen++] = (char) (c & 0xff); - buf[buflen] = '\0'; - } + buf[buflen++] = (char) (c & 0xff); + buf[buflen] = '\0'; + } - if (cb) { - cb(buf, c); - } + if (cb) { + cb(buf, c); + /* Update buflen after callback may have modified buffer */ + buflen = strlen(buf); + /* Ensure buffer is large enough for modified content */ + if (buflen >= bufsz - 1) { + bufsz = buflen + 128; + buf = static_cast(realloc(buf, bufsz)); + assert(buf != NULL); + } + } } free(buf); @@ -1465,7 +1522,7 @@ editor_find_callback(char *query, int16_t c) static int dir = 1; int i, current; char *match; - struct erow *row; + erow *row; if (c == '\r' || c == ESC_KEY) { /* reset search */ @@ -1518,7 +1575,7 @@ editor_find_callback(char *query, int16_t c) void -editor_find(void) +editor_find() { char *query; int scx = editor.curx; @@ -1542,12 +1599,130 @@ editor_find(void) void -editor_openfile(void) +file_completion_callback(char *buf, int16_t key) +{ + static char** matches = NULL; + static int match_count = 0; + static int match_index = 0; + DIR* dir; + struct dirent* entry; + char* last_slash; + char dirpath[512]; + char prefix[256]; + int prefix_len; + int i; + + if (key != TAB_KEY) { + /* Reset state on non-TAB key */ + if (matches) { + for (i = 0; i < match_count; i++) { + free(matches[i]); + } + free(matches); + matches = NULL; + } + match_count = 0; + match_index = 0; + return; + } + + /* Find directory and prefix */ + last_slash = strrchr(buf, '/'); + if (last_slash) { + /* Extract directory path */ + size_t dir_len = last_slash - buf + 1; + if (dir_len >= sizeof(dirpath)) { + dir_len = sizeof(dirpath) - 1; + } + strncpy(dirpath, buf, dir_len); + dirpath[dir_len] = '\0'; + /* Extract prefix after last slash */ + strncpy(prefix, last_slash + 1, sizeof(prefix) - 1); + prefix[sizeof(prefix) - 1] = '\0'; + } else { + /* No slash - use current directory */ + strcpy(dirpath, "./"); + strncpy(prefix, buf, sizeof(prefix) - 1); + prefix[sizeof(prefix) - 1] = '\0'; + } + + prefix_len = strlen(prefix); + + /* Build matches list on first TAB press */ + if (matches == NULL) { + dir = opendir(dirpath); + if (!dir) { + return; + } + + /* Count matches first */ + match_count = 0; + while ((entry = readdir(dir)) != NULL) { + if (strncmp(entry->d_name, prefix, prefix_len) == 0) { + /* Skip . and .. unless explicitly typed */ + if (prefix_len == 0 && + (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0)) { + continue; + } + match_count++; + } + } + + if (match_count == 0) { + closedir(dir); + return; + } + + /* Allocate matches array */ + matches = static_cast(malloc(sizeof(char*) * match_count)); + if (!matches) { + closedir(dir); + return; + } + + /* Collect matches */ + rewinddir(dir); + i = 0; + while ((entry = readdir(dir)) != NULL && i < match_count) { + if (strncmp(entry->d_name, prefix, prefix_len) == 0) { + if (prefix_len == 0 && + (strcmp(entry->d_name, ".") == 0 || + strcmp(entry->d_name, "..") == 0)) { + continue; + } + matches[i] = static_cast(malloc(strlen(entry->d_name) + 1)); + strcpy(matches[i], entry->d_name); + i++; + } + } + closedir(dir); + match_index = 0; + } else { + /* Cycle to next match */ + match_index = (match_index + 1) % match_count; + } + + /* Complete with current match */ + if (match_count > 0 && matches[match_index]) { + if (last_slash) { + /* Keep directory path, replace filename */ + size_t dir_len = last_slash - buf + 1; + strcpy(buf + dir_len, matches[match_index]); + } else { + /* Replace entire buffer */ + strcpy(buf, matches[match_index]); + } + } +} + + +void +editor_openfile() { char *filename; - /* TODO(kyle): combine with dirutils for tab-completion */ - filename = editor_prompt("Load file: %s", NULL); + filename = editor_prompt("Load file: %s", file_completion_callback); if (filename == NULL) { return; } @@ -1556,95 +1731,141 @@ editor_openfile(void) } +int +first_nonwhitespace(struct erow *row) +{ + int pos; + wchar_t wc; + mbstate_t state; + size_t len; + + if (row == NULL) { + return 0; + } + + memset(&state, 0, sizeof(state)); + pos = 0; + while (pos < row->size) { + len = mbrtowc(&wc, &row->line[pos], row->size - pos, &state); + if (len == (size_t) -1 || len == (size_t) -2) { + /* Invalid or incomplete sequence, stop here */ + break; + } + if (len == 0) { + /* Null character, stop here */ + break; + } + if (!iswspace(wc)) { + /* Found non-whitespace character */ + break; + } + pos += len; + } + + return pos; +} + + void move_cursor(int16_t c) { - struct erow *row; + erow *row; int reps; row = (editor.cury >= editor.nrows) ? NULL : &editor.row[editor.cury]; switch (c) { - case ARROW_UP: - case CTRL_KEY('p'): - if (editor.cury > 0) { - editor.cury--; - } - break; - case ARROW_DOWN: - case CTRL_KEY('n'): - if (editor.cury < editor.nrows) { - editor.cury++; - } - break; - case ARROW_RIGHT: - case CTRL_KEY('f'): - if (row && editor.curx < row->size) { + case ARROW_UP: + case CTRL_KEY('p'): + if (editor.cury > 0) { + editor.cury--; + row = (editor.cury >= editor.nrows) + ? NULL + : &editor.row[editor.cury]; + editor.curx = first_nonwhitespace(row); + } + break; + case ARROW_DOWN: + case CTRL_KEY('n'): + if (editor.cury < editor.nrows) { + editor.cury++; + row = (editor.cury >= editor.nrows) + ? NULL + : &editor.row[editor.cury]; + editor.curx = first_nonwhitespace(row); + } + break; + case ARROW_RIGHT: + case CTRL_KEY('f'): + if (row && editor.curx < row->size) { + editor.curx++; + /* skip over UTF-8 continuation bytes */ + while (row && editor.curx < row->size && + ((unsigned char) row->line[editor.curx] & + 0xC0) == 0x80) { editor.curx++; - /* skip over UTF-8 continuation bytes */ - while (row && editor.curx < row->size && - ((unsigned char) row->line[editor.curx] & - 0xC0) == 0x80) { - editor.curx++; - } - } else if (row && editor.curx == row->size) { - editor.cury++; - editor.curx = 0; } - break; - case ARROW_LEFT: - case CTRL_KEY('b'): - if (editor.curx > 0) { + } else if (row && editor.curx == row->size) { + editor.cury++; + row = (editor.cury >= editor.nrows) + ? NULL + : &editor.row[editor.cury]; + editor.curx = first_nonwhitespace(row); + } + break; + case ARROW_LEFT: + case CTRL_KEY('b'): + if (editor.curx > 0) { + editor.curx--; + /* move to the start byte if we landed on a continuation */ + while (editor.curx > 0 && + ((unsigned char) row->line[editor.curx] & + 0xC0) == 0x80) { editor.curx--; - /* move to the start byte if we landed on a continuation */ - while (editor.curx > 0 && - ((unsigned char) row->line[editor.curx] & - 0xC0) == 0x80) { - editor.curx--; - } - } else if (editor.cury > 0) { - editor.cury--; - editor.curx = editor.row[editor.cury].size; - /* ensure at a codepoint boundary at end of previous line */ - row = &editor.row[editor.cury]; - while (editor.curx > 0 && - ((unsigned char) row->line[editor.curx] & - 0xC0) == 0x80) { - editor.curx--; - } - } - break; - case PG_UP: - case PG_DN: - if (c == PG_UP) { - editor.cury = editor.rowoffs; - } else if (c == PG_DN) { - editor.cury = editor.rowoffs + editor.rows - 1; - if (editor.cury > editor.nrows) { - editor.cury = editor.nrows; - } - } - - reps = editor.rows; - while (--reps) { - move_cursor(c == PG_UP ? ARROW_UP : ARROW_DOWN); - } - - break; - - case HOME_KEY: - case CTRL_KEY('a'): - editor.curx = 0; - break; - case END_KEY: - case CTRL_KEY('e'): - if (editor.nrows == 0) { - break; } + } else if (editor.cury > 0) { + editor.cury--; editor.curx = editor.row[editor.cury].size; + /* ensure at a codepoint boundary at end of previous line */ + row = &editor.row[editor.cury]; + while (editor.curx > 0 && + ((unsigned char) row->line[editor.curx] & + 0xC0) == 0x80) { + editor.curx--; + } + } + break; + case PG_UP: + case PG_DN: + if (c == PG_UP) { + editor.cury = editor.rowoffs; + } else if (c == PG_DN) { + editor.cury = editor.rowoffs + editor.rows - 1; + if (editor.cury > editor.nrows) { + editor.cury = editor.nrows; + } + } + + reps = editor.rows; + while (--reps) { + move_cursor(c == PG_UP ? ARROW_UP : ARROW_DOWN); + } + + break; + + case HOME_KEY: + case CTRL_KEY('a'): + editor.curx = 0; + break; + case END_KEY: + case CTRL_KEY('e'): + if (editor.nrows == 0) { break; - default: - break; + } + editor.curx = editor.row[editor.cury].size; + break; + default: + break; } @@ -1657,14 +1878,21 @@ move_cursor(int16_t c) void -newline(void) +newline() { - struct erow *row = NULL; + erow *row = NULL; - if (editor.curx == 0) { + if (editor.cury >= editor.nrows) { + /* At or past end of file, insert empty line */ + erow_insert(editor.cury, "", 0); + } else if (editor.curx == 0) { erow_insert(editor.cury, "", 0); } else { row = &editor.row[editor.cury]; + /* Ensure curx doesn't exceed row size to prevent negative len */ + if (editor.curx > row->size) { + editor.curx = row->size; + } erow_insert(editor.cury + 1, &row->line[editor.curx], row->size - editor.curx); @@ -1830,12 +2058,15 @@ process_kcommand(int16_t c) case 'u': editor_set_status("undo: todo"); break; - case 'y': - killring_yank(); - break; - default: - editor_set_status("unknown kcommand: %04x", c); - return; + case 'y': + killring_yank(); + break; + case TAB_KEY: + indent_region(); + break; + default: + editor_set_status("unknown kcommand: %04x", c); + return; } editor.dirtyex = 1; @@ -1888,12 +2119,11 @@ process_normal(int16_t c) case ESC_KEY: editor.mode = MODE_ESCAPE; break; - default: - /* Insert any printable byte: ASCII 0x20–0x7E and all bytes >=0x80. */ - if ((c == TAB_KEY) || (c >= 0x20 && c != 0x7f)) { - insertch(c); - } - break; + default: + if ((c == TAB_KEY) || (c >= 0x20 && c != 0x7f)) { + insertch(c); + } + break; } editor.dirtyex = 1; @@ -1944,7 +2174,7 @@ process_escape(int16_t c) int -process_keypress(void) +process_keypress() { int16_t c = get_keypress(); @@ -1981,7 +2211,7 @@ process_keypress(void) * is to be in canonical (cooked) mode, which is a buffered input mode. */ void -enable_termraw(void) +enable_termraw() { struct termios raw; @@ -2018,7 +2248,7 @@ enable_termraw(void) void -display_clear(struct abuf *ab) +display_clear(abuf *ab) { if (ab == NULL) { (void)write(STDOUT_FILENO, ESCSEQ "2J", 4); @@ -2031,7 +2261,7 @@ display_clear(struct abuf *ab) void -disable_termraw(void) +disable_termraw() { display_clear(NULL); @@ -2042,7 +2272,7 @@ disable_termraw(void) void -setup_terminal(void) +setup_terminal() { if (tcgetattr(STDIN_FILENO, &editor.entry_term) == -1) { die("can't snapshot terminal settings"); @@ -2053,7 +2283,7 @@ setup_terminal(void) void -draw_rows(struct abuf *ab) +draw_rows(abuf *ab) { assert(editor.cols >= 0); @@ -2104,7 +2334,7 @@ draw_rows(struct abuf *ab) char -status_mode_char(void) +status_mode_char() { switch (editor.mode) { case MODE_NORMAL: @@ -2120,7 +2350,7 @@ status_mode_char(void) void -draw_status_bar(struct abuf *ab) +draw_status_bar(abuf *ab) { char *status = new char[editor.cols]; char *rstatus = new char[editor.cols]; @@ -2173,7 +2403,7 @@ draw_status_bar(struct abuf *ab) void -draw_message_line(struct abuf *ab) +draw_message_line(abuf *ab) { int len = strlen(editor.msg); @@ -2189,7 +2419,7 @@ draw_message_line(struct abuf *ab) void -scroll(void) +scroll() { editor.rx = 0; if (editor.cury < editor.nrows) { @@ -2217,10 +2447,10 @@ scroll(void) void -display_refresh(void) +display_refresh() { char buf[32]; - struct abuf ab = ABUF_INIT; + abuf ab = ABUF_INIT; scroll(); @@ -2259,7 +2489,7 @@ editor_set_status(const char *fmt, ...) void -loop(void) +loop() { int up = 1; /* update on the first runthrough */ diff --git a/terminal.cc b/terminal.cc new file mode 100644 index 0000000..0434b67 --- /dev/null +++ b/terminal.cc @@ -0,0 +1,111 @@ +#include "terminal.h" +#include +#include +#include +#include + +namespace ke { + +static Terminal* g_terminal_instance = nullptr; + +static void cleanup_terminal_on_exit() { + if (g_terminal_instance) { + g_terminal_instance->disable_raw_mode(); + } +} + +Terminal::Terminal() : raw_mode_enabled_(false) { + g_terminal_instance = this; +} + +Terminal::~Terminal() { + if (raw_mode_enabled_) { + disable_raw_mode(); + } + g_terminal_instance = nullptr; +} + +void Terminal::setup() { + if (tcgetattr(STDIN_FILENO, &entry_term_) == -1) { + perror("can't snapshot terminal settings"); + exit(1); + } + atexit(cleanup_terminal_on_exit); + enable_raw_mode(); +} + +void Terminal::enable_raw_mode() { + if (raw_mode_enabled_) { + return; + } + + struct termios raw; + + /* Read the current terminal parameters for standard input. */ + if (tcgetattr(STDIN_FILENO, &raw) == -1) { + perror("tcgetattr while enabling raw mode"); + exit(1); + } + + /* + * Put the terminal into raw mode. + */ + cfmakeraw(&raw); + + /* + * Set timeout for read(2). + * + * VMIN: what is the minimum number of bytes required for read + * to return? + * + * VTIME: max time before read(2) returns in hundreds of milli- + * seconds. + */ + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + /* + * Now write the terminal parameters to the current terminal, + * after flushing any waiting input out. + */ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) { + perror("tcsetattr while enabling raw mode"); + exit(1); + } + + raw_mode_enabled_ = true; +} + +void Terminal::disable_raw_mode() { + if (!raw_mode_enabled_) { + return; + } + + clear_screen(); + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &entry_term_) == -1) { + perror("couldn't disable terminal raw mode"); + exit(1); + } + + raw_mode_enabled_ = false; +} + +int Terminal::get_window_size(int* rows, int* cols) { + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + return -1; + } + + *cols = ws.ws_col; + *rows = ws.ws_row; + return 0; +} + +void Terminal::clear_screen() { + (void)write(STDOUT_FILENO, "\x1b[2J", 4); + (void)write(STDOUT_FILENO, "\x1b[H", 3); +} + +} // namespace ke diff --git a/terminal.h b/terminal.h new file mode 100644 index 0000000..d4b3d1b --- /dev/null +++ b/terminal.h @@ -0,0 +1,39 @@ +#ifndef TERMINAL_HPP +#define TERMINAL_HPP + +#include + +namespace ke { + +/** + * Terminal management class for handling raw mode and terminal operations. + */ +class Terminal { +public: + Terminal(); + ~Terminal(); + + // Deleted copy constructor and assignment + Terminal(const Terminal&) = delete; + Terminal& operator=(const Terminal&) = delete; + + // Setup and teardown + void setup(); + void enable_raw_mode(); + void disable_raw_mode(); + + // Terminal operations + static int get_window_size(int* rows, int* cols); + static void clear_screen(); + + // Access to original terminal settings + const termios& entry_term() const { return entry_term_; } + +private: + termios entry_term_; + bool raw_mode_enabled_; +}; + +} // namespace ke + +#endif // TERMINAL_HPP