From a12e2fb1c4102ffa9ca58f790094c35554c110f9 Mon Sep 17 00:00:00 2001 From: Syed Daanish Date: Mon, 22 Dec 2025 14:56:48 +0000 Subject: [PATCH] Add better indentation and pasting --- README.md | 15 +++------- include/editor.h | 7 ++++- include/ui.h | 3 +- src/editor_ctrl.cc | 32 ++++++++++++++++++-- src/editor_events.cc | 32 +++++++++++++++++--- src/editor_indents.cc | 46 +++++++++++++++++++++++++++++ src/input.cc | 69 +++++++++++++++++++++++++++++++++++++++++-- src/renderer.cc | 4 +-- 8 files changed, 184 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3a27f69..686a6fc 100644 --- a/README.md +++ b/README.md @@ -6,18 +6,8 @@ A TUI IDE. # TODO -- [ ] Add tab/untab with [,<] and [.>] -- [ ] Add something that works like `p` but doesnt move cursor. -- [ ] Fix the fact that hooks arent updated when a line is deleted/added. -- [ ] also use this sorta rules apart from previous lines indentation for the indentor. - - if previous line ends with { or similar → indent +1 - - (i.e. just check the last line that is not string or comment or empty) - - language-specific rules later if you want - - ignore comments that dedent etc. - anything else should be handled by LSP. -- [ ] Fix bug where clicking outside eof sets to top. -- [ ] Add this thing where select at end of screen scrolls down. (and vice versa) - [ ] Add a virtual text support (text that is rendered in the editor but not in the actual text) - - Add a whitespace highlighter (nerd font). for spaces and tabs at start/end of line. + - Add a whitespace highlighter (nerd font). for spaces and tabs at start/end of line. as a test. - [ ] Add support for LSP & autocomplete / snippets. - First research - `textDocument/documentHighlight` - for highlighting stuff (probably tree-sitter is enough) @@ -39,6 +29,9 @@ A TUI IDE. 3. One for Warnings/errors and inlay hints etc. (stuff that adds virtual text to the editor) 4. One for fromatting and stuff like that. (stuff that edits the buffer text) - [ ] Add snippets from wherever i get them. (like luasnip or vsnip) +- [ ] Add this thing where select at end of screen scrolls down. (and vice versa) + - Can be acheived by updating `main.cc` to send drag events to the selected editor instead of just under cursor. + - Then a drag event above or below will scroll the selected editor. - [ ] Add support for virtual cursor where edits apply at all the places. - [ ] Add alt + click to set multiple cursors. - [ ] Add search / replace along with search / virtual cursors are searched pos. diff --git a/include/editor.h b/include/editor.h index 77277b4..0cead51 100644 --- a/include/editor.h +++ b/include/editor.h @@ -177,6 +177,9 @@ inline uint32_t prev_unfolded_row(const Editor *editor, uint32_t row) { } void apply_edit(std::vector &spans, uint32_t x, int64_t y); +void apply_hook_insertion(Editor *editor, uint32_t line, uint32_t rows); +void apply_hook_deletion(Editor *editor, uint32_t removal_start, + uint32_t removal_end); Editor *new_editor(const char *filename, Coord position, Coord size); void free_editor(Editor *editor); void render_editor(Editor *editor); @@ -190,12 +193,14 @@ void cursor_right(Editor *editor, uint32_t number); void scroll_up(Editor *editor, int32_t number); void scroll_down(Editor *editor, uint32_t number); void ensure_cursor(Editor *editor); +void indent_line(Editor *editor, uint32_t row); +void dedent_line(Editor *editor, uint32_t row); void ensure_scroll(Editor *editor); void handle_editor_event(Editor *editor, KeyEvent event); void edit_erase(Editor *editor, Coord pos, int64_t len); void edit_insert(Editor *editor, Coord pos, char *data, uint32_t len); Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y); -char *get_selection(Editor *editor, uint32_t *out_len); +char *get_selection(Editor *editor, uint32_t *out_len, Coord *out_start); void editor_worker(Editor *editor); void move_line_down(Editor *editor); void move_line_up(Editor *editor); diff --git a/include/ui.h b/include/ui.h index d3131a8..feddae6 100644 --- a/include/ui.h +++ b/include/ui.h @@ -15,7 +15,8 @@ #define KEY_CHAR 0 #define KEY_SPECIAL 1 #define KEY_MOUSE 2 -#define KEY_NONE 3 +#define KEY_PASTE 3 +#define KEY_NONE 4 #define KEY_UP 0 #define KEY_DOWN 1 diff --git a/src/editor_ctrl.cc b/src/editor_ctrl.cc index 3731ee7..96ffc2c 100644 --- a/src/editor_ctrl.cc +++ b/src/editor_ctrl.cc @@ -131,6 +131,8 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) { uint32_t target_visual_row = y; uint32_t visual_row = 0; uint32_t line_index = editor->scroll.row; + uint32_t last_line_index = editor->scroll.row; + uint32_t last_col = editor->scroll.col; bool first_visual_line = true; std::shared_lock knot_lock(editor->knot_mtx); LineIterator *it = begin_l_iter(editor->root, line_index); @@ -155,6 +157,8 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) { break; line_index++; } + last_line_index = fold->end; + last_col = 0; continue; } uint32_t line_len; @@ -163,6 +167,8 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) { break; if (line_len && line[line_len - 1] == '\n') line_len--; + last_line_index = line_index; + last_col = line_len; uint32_t offset = first_visual_line ? editor->scroll.col : 0; first_visual_line = false; while (offset < line_len || (line_len == 0 && offset == 0)) { @@ -186,6 +192,7 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) { left -= g; col += w; } + last_col = last_good_offset; if (visual_row == target_visual_row) { free(it->buffer); free(it); @@ -204,7 +211,7 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) { } free(it->buffer); free(it); - return editor->scroll; + return {last_line_index, last_col}; } Coord move_right_pure(Editor *editor, Coord cursor, uint32_t number) { @@ -353,9 +360,10 @@ Coord move_right(Editor *editor, Coord cursor, uint32_t number) { } ++row; } - col = 0; if (line_len > 0 && line[line_len - 1] == '\n') --line_len; + col = 0; + --number; continue; } else { uint32_t inc = @@ -672,6 +680,7 @@ void edit_erase(Editor *editor, Coord pos, int64_t len) { uint32_t start_row = point.row; uint32_t end_row = pos.row; apply_line_deletion(editor, start_row + 1, end_row); + apply_hook_deletion(editor, start_row + 1, end_row); std::unique_lock lock_2(editor->knot_mtx); editor->root = erase(editor->root, start, byte_pos - start); lock_2.unlock(); @@ -715,6 +724,7 @@ void edit_erase(Editor *editor, Coord pos, int64_t len) { uint32_t start_row = pos.row; uint32_t end_row = point.row; apply_line_deletion(editor, start_row + 1, end_row); + apply_hook_deletion(editor, start_row + 1, end_row); std::unique_lock lock_2(editor->knot_mtx); editor->root = erase(editor->root, byte_pos, end - byte_pos); lock_2.unlock(); @@ -766,6 +776,7 @@ void edit_insert(Editor *editor, Coord pos, char *data, uint32_t len) { } } apply_line_insertion(editor, pos.row, rows); + apply_hook_insertion(editor, pos.row, rows); if (editor->tree) { TSInputEdit edit = { .start_byte = byte_pos, @@ -786,7 +797,7 @@ void edit_insert(Editor *editor, Coord pos, char *data, uint32_t len) { apply_edit(editor->def_spans.spans, byte_pos, len); } -char *get_selection(Editor *editor, uint32_t *out_len) { +char *get_selection(Editor *editor, uint32_t *out_len, Coord *out_start) { std::shared_lock lock(editor->knot_mtx); Coord start, end; if (editor->cursor >= editor->selection) { @@ -832,6 +843,8 @@ char *get_selection(Editor *editor, uint32_t *out_len) { break; } } + if (out_start) + *out_start = start; uint32_t start_byte = line_to_byte(editor->root, start.row, nullptr) + start.col; uint32_t end_byte = line_to_byte(editor->root, end.row, nullptr) + end.col; @@ -953,3 +966,16 @@ void apply_line_deletion(Editor *editor, uint32_t removal_start, } editor->folds.swap(updated); } + +void apply_hook_insertion(Editor *editor, uint32_t line, uint32_t rows) { + for (auto &hook : editor->hooks) + if (hook > line) + hook += rows; +} + +void apply_hook_deletion(Editor *editor, uint32_t removal_start, + uint32_t removal_end) { + for (auto &hook : editor->hooks) + if (hook > removal_start) + hook -= removal_end - removal_start + 1; +} diff --git a/src/editor_events.cc b/src/editor_events.cc index 02c32fe..f38f19b 100644 --- a/src/editor_events.cc +++ b/src/editor_events.cc @@ -1,6 +1,7 @@ #include "../include/editor.h" #include "../include/main.h" #include "../include/ts.h" +#include void handle_editor_event(Editor *editor, KeyEvent event) { static std::chrono::steady_clock::time_point last_click_time = @@ -203,6 +204,7 @@ void handle_editor_event(Editor *editor, KeyEvent event) { switch (mode) { case NORMAL: if (event.key_type == KEY_CHAR && event.len == 1) { + Coord start = editor->cursor; switch (event.c[0]) { case 'u': if (editor->root->line_count > 0) { @@ -230,6 +232,8 @@ void handle_editor_event(Editor *editor, KeyEvent event) { case 'a': mode = INSERT; cursor_right(editor, 1); + if (start.row != editor->cursor.row) + cursor_left(editor, 1); break; case 'i': mode = INSERT; @@ -282,6 +286,14 @@ void handle_editor_event(Editor *editor, KeyEvent event) { scroll_up(editor, 1); ensure_cursor(editor); break; + case '>': + case '.': + indent_line(editor, editor->cursor.row); + break; + case '<': + case ',': + dedent_line(editor, editor->cursor.row); + break; case 'p': uint32_t len; char *text = get_from_clipboard(&len); @@ -382,8 +394,11 @@ void handle_editor_event(Editor *editor, KeyEvent event) { edit_erase(editor, editor->cursor, -1); } } else if (event.c[0] == 0x1B) { + Coord prev_pos = editor->cursor; mode = NORMAL; cursor_left(editor, 1); + if (prev_pos.row != editor->cursor.row) + cursor_right(editor, 1); } } else if (event.len > 1) { edit_insert(editor, editor->cursor, event.c, event.len); @@ -405,12 +420,20 @@ void handle_editor_event(Editor *editor, KeyEvent event) { edit_erase(editor, editor->cursor, next_col_cluster); break; } + } else if (event.key_type == KEY_PASTE) { + if (event.c) { + edit_insert(editor, editor->cursor, event.c, event.len); + uint32_t grapheme_len = + count_clusters(event.c, event.len, 0, event.len); + cursor_right(editor, grapheme_len); + } } break; case SELECT: if (event.key_type == KEY_CHAR && event.len == 1) { uint32_t len; char *text; + Coord start; switch (event.c[0]) { case 'f': if (editor->cursor.row != editor->selection.row) { @@ -430,16 +453,17 @@ void handle_editor_event(Editor *editor, KeyEvent event) { mode = NORMAL; break; case 'y': - text = get_selection(editor, &len); + text = get_selection(editor, &len, nullptr); copy_to_clipboard(text, len); free(text); editor->selection_active = false; mode = NORMAL; break; case 'x': - text = get_selection(editor, &len); + text = get_selection(editor, &len, &start); copy_to_clipboard(text, len); - edit_erase(editor, MIN(editor->cursor, editor->selection), len); + len = count_clusters(text, len, 0, len); + edit_erase(editor, start, len); free(text); editor->selection_active = false; mode = NORMAL; @@ -502,7 +526,7 @@ void handle_editor_event(Editor *editor, KeyEvent event) { break; } ensure_scroll(editor); - if (event.key_type == KEY_CHAR && event.c) + if ((event.key_type == KEY_CHAR || event.key_type == KEY_PASTE) && event.c) free(event.c); } diff --git a/src/editor_indents.cc b/src/editor_indents.cc index 7e1af77..7653777 100644 --- a/src/editor_indents.cc +++ b/src/editor_indents.cc @@ -23,6 +23,8 @@ uint32_t get_indent(Editor *editor, Coord cursor) { if (line_len == 0) continue; uint32_t indent = leading_indent(line, line_len); + free(it->buffer); + free(it); return indent; } free(it->buffer); @@ -38,3 +40,47 @@ bool closing_after_cursor(const char *line, uint32_t len, uint32_t col) { return false; return line[i] == '}' || line[i] == ']' || line[i] == ')'; } + +void indent_line(Editor *editor, uint32_t row) { + if (!editor) + return; + LineIterator *it = begin_l_iter(editor->root, row); + uint32_t line_len; + char *line = next_line(it, &line_len); + if (!line) { + free(it->buffer); + free(it); + return; + } + char *spaces = (char *)malloc(INDENT_WIDTH); + memset(spaces, ' ', INDENT_WIDTH); + Coord cursor = editor->cursor; + edit_insert(editor, {row, 0}, spaces, INDENT_WIDTH); + editor->cursor = cursor; + free(spaces); + free(it->buffer); + free(it); +} + +void dedent_line(Editor *editor, uint32_t row) { + if (!editor) + return; + LineIterator *it = begin_l_iter(editor->root, row); + uint32_t line_len; + char *line = next_line(it, &line_len); + if (!line) { + free(it->buffer); + free(it); + return; + } + uint32_t indent = leading_indent(line, line_len); + if (indent == 0) { + free(it->buffer); + free(it); + return; + } + uint32_t remove = indent >= INDENT_WIDTH ? INDENT_WIDTH : indent; + edit_erase(editor, {row, 0}, remove); + free(it->buffer); + free(it); +} diff --git a/src/input.cc b/src/input.cc index 8646907..38c870b 100644 --- a/src/input.cc +++ b/src/input.cc @@ -137,6 +137,60 @@ void capture_mouse(char *buf, KeyEvent *ret) { } } +bool read_bracketed_paste(char **out, int *out_len) { + size_t cap = 256, len = 0; + char *buf = (char *)malloc(cap); + if (!buf) + return false; + char window[6] = {0}; + size_t wlen = 0; + auto push_byte = [&](char c) { + if (len + 1 >= cap) { + cap *= 2; + buf = (char *)realloc(buf, cap); + } + buf[len++] = c; + }; + while (true) { + char c; + if (!get_next_byte(&c)) { + free(buf); + return false; + } + if (wlen < 5) { + window[wlen++] = c; + } else { + memmove(window, window + 1, 4); + window[4] = c; + wlen = 5; + } + if (wlen == 5 && window[0] == '\x1b' && window[1] == '[' && + window[2] == '2' && window[3] == '0' && window[4] == '1') { + char tilde; + if (!get_next_byte(&tilde)) { + free(buf); + return false; + } + if (tilde == '~') + break; + for (int i = 0; i < 5; i++) + push_byte(window[i]); + push_byte(tilde); + wlen = 0; + continue; + } + if (wlen == 5) { + push_byte(window[0]); + memmove(window, window + 1, 4); + wlen = 4; + } + } + buf[len] = '\0'; + *out = buf; + *out_len = (int)len; + return true; +} + KeyEvent read_key() { KeyEvent ret; char *buf; @@ -145,10 +199,21 @@ KeyEvent read_key() { ret.key_type = KEY_NONE; return ret; } - if (buf[0] == '\x1b' && buf[1] == '[' && buf[2] == 'M') { + if (n >= 6 && buf[0] == '\x1b' && buf[1] == '[' && buf[2] == '2' && + buf[3] == '0' && buf[4] == '0' && buf[5] == '~') { + char *pbuf = nullptr; + int plen = 0; + if (read_bracketed_paste(&pbuf, &plen)) { + free(buf); + ret.key_type = KEY_PASTE; + ret.c = pbuf; + ret.len = plen; + return ret; + } + } else if (n >= 3 && buf[0] == '\x1b' && buf[1] == '[' && buf[2] == 'M') { ret.key_type = KEY_MOUSE; capture_mouse(buf, &ret); - } else if (buf[0] == '\x1b' && buf[1] == '[') { + } else if (n >= 2 && buf[0] == '\x1b' && buf[1] == '[') { ret.key_type = KEY_SPECIAL; int using_modifiers = buf[3] == ';'; int pos; diff --git a/src/renderer.cc b/src/renderer.cc index 870f858..b18a010 100644 --- a/src/renderer.cc +++ b/src/renderer.cc @@ -9,7 +9,7 @@ std::mutex screen_mutex; termios orig_termios; void disable_raw_mode() { - std::string os = "\x1b[?1049l\x1b[2 q\x1b[?1002l\x1b[?25h"; + std::string os = "\x1b[?1049l\x1b[2 q\x1b[?1002l\x1b[?25h\x1b[?2004l"; write(STDOUT_FILENO, os.c_str(), os.size()); if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) { perror("tcsetattr"); @@ -30,7 +30,7 @@ void enable_raw_mode() { raw.c_cc[VTIME] = 0; if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) exit(EXIT_FAILURE); - std::string os = "\x1b[?1049h\x1b[2 q\x1b[?1002h\x1b[?25l"; + std::string os = "\x1b[?1049h\x1b[2 q\x1b[?1002h\x1b[?25l\x1b[?2004h"; write(STDOUT_FILENO, os.c_str(), os.size()); }