Add better indentation and pasting

This commit is contained in:
2025-12-22 14:56:48 +00:00
parent 7307387f64
commit a12e2fb1c4
8 changed files with 184 additions and 24 deletions

View File

@@ -6,18 +6,8 @@ A TUI IDE.
# TODO # 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 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. - [ ] Add support for LSP & autocomplete / snippets.
- First research - First research
- `textDocument/documentHighlight` - for highlighting stuff (probably tree-sitter is enough) - `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) 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) 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 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 support for virtual cursor where edits apply at all the places.
- [ ] Add alt + click to set multiple cursors. - [ ] Add alt + click to set multiple cursors.
- [ ] Add search / replace along with search / virtual cursors are searched pos. - [ ] Add search / replace along with search / virtual cursors are searched pos.

View File

@@ -177,6 +177,9 @@ inline uint32_t prev_unfolded_row(const Editor *editor, uint32_t row) {
} }
void apply_edit(std::vector<Span> &spans, uint32_t x, int64_t y); void apply_edit(std::vector<Span> &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); Editor *new_editor(const char *filename, Coord position, Coord size);
void free_editor(Editor *editor); void free_editor(Editor *editor);
void render_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_up(Editor *editor, int32_t number);
void scroll_down(Editor *editor, uint32_t number); void scroll_down(Editor *editor, uint32_t number);
void ensure_cursor(Editor *editor); 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 ensure_scroll(Editor *editor);
void handle_editor_event(Editor *editor, KeyEvent event); void handle_editor_event(Editor *editor, KeyEvent event);
void edit_erase(Editor *editor, Coord pos, int64_t len); void edit_erase(Editor *editor, Coord pos, int64_t len);
void edit_insert(Editor *editor, Coord pos, char *data, uint32_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); 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 editor_worker(Editor *editor);
void move_line_down(Editor *editor); void move_line_down(Editor *editor);
void move_line_up(Editor *editor); void move_line_up(Editor *editor);

View File

@@ -15,7 +15,8 @@
#define KEY_CHAR 0 #define KEY_CHAR 0
#define KEY_SPECIAL 1 #define KEY_SPECIAL 1
#define KEY_MOUSE 2 #define KEY_MOUSE 2
#define KEY_NONE 3 #define KEY_PASTE 3
#define KEY_NONE 4
#define KEY_UP 0 #define KEY_UP 0
#define KEY_DOWN 1 #define KEY_DOWN 1

View File

@@ -131,6 +131,8 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) {
uint32_t target_visual_row = y; uint32_t target_visual_row = y;
uint32_t visual_row = 0; uint32_t visual_row = 0;
uint32_t line_index = editor->scroll.row; 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; bool first_visual_line = true;
std::shared_lock knot_lock(editor->knot_mtx); std::shared_lock knot_lock(editor->knot_mtx);
LineIterator *it = begin_l_iter(editor->root, line_index); 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; break;
line_index++; line_index++;
} }
last_line_index = fold->end;
last_col = 0;
continue; continue;
} }
uint32_t line_len; uint32_t line_len;
@@ -163,6 +167,8 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) {
break; break;
if (line_len && line[line_len - 1] == '\n') if (line_len && line[line_len - 1] == '\n')
line_len--; line_len--;
last_line_index = line_index;
last_col = line_len;
uint32_t offset = first_visual_line ? editor->scroll.col : 0; uint32_t offset = first_visual_line ? editor->scroll.col : 0;
first_visual_line = false; first_visual_line = false;
while (offset < line_len || (line_len == 0 && offset == 0)) { 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; left -= g;
col += w; col += w;
} }
last_col = last_good_offset;
if (visual_row == target_visual_row) { if (visual_row == target_visual_row) {
free(it->buffer); free(it->buffer);
free(it); free(it);
@@ -204,7 +211,7 @@ Coord editor_hit_test(Editor *editor, uint32_t x, uint32_t y) {
} }
free(it->buffer); free(it->buffer);
free(it); free(it);
return editor->scroll; return {last_line_index, last_col};
} }
Coord move_right_pure(Editor *editor, Coord cursor, uint32_t number) { 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; ++row;
} }
col = 0;
if (line_len > 0 && line[line_len - 1] == '\n') if (line_len > 0 && line[line_len - 1] == '\n')
--line_len; --line_len;
col = 0;
--number;
continue; continue;
} else { } else {
uint32_t inc = 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 start_row = point.row;
uint32_t end_row = pos.row; uint32_t end_row = pos.row;
apply_line_deletion(editor, start_row + 1, end_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); std::unique_lock lock_2(editor->knot_mtx);
editor->root = erase(editor->root, start, byte_pos - start); editor->root = erase(editor->root, start, byte_pos - start);
lock_2.unlock(); 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 start_row = pos.row;
uint32_t end_row = point.row; uint32_t end_row = point.row;
apply_line_deletion(editor, start_row + 1, end_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); std::unique_lock lock_2(editor->knot_mtx);
editor->root = erase(editor->root, byte_pos, end - byte_pos); editor->root = erase(editor->root, byte_pos, end - byte_pos);
lock_2.unlock(); 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_line_insertion(editor, pos.row, rows);
apply_hook_insertion(editor, pos.row, rows);
if (editor->tree) { if (editor->tree) {
TSInputEdit edit = { TSInputEdit edit = {
.start_byte = byte_pos, .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); 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); std::shared_lock lock(editor->knot_mtx);
Coord start, end; Coord start, end;
if (editor->cursor >= editor->selection) { if (editor->cursor >= editor->selection) {
@@ -832,6 +843,8 @@ char *get_selection(Editor *editor, uint32_t *out_len) {
break; break;
} }
} }
if (out_start)
*out_start = start;
uint32_t start_byte = uint32_t start_byte =
line_to_byte(editor->root, start.row, nullptr) + start.col; line_to_byte(editor->root, start.row, nullptr) + start.col;
uint32_t end_byte = line_to_byte(editor->root, end.row, nullptr) + end.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); 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;
}

View File

@@ -1,6 +1,7 @@
#include "../include/editor.h" #include "../include/editor.h"
#include "../include/main.h" #include "../include/main.h"
#include "../include/ts.h" #include "../include/ts.h"
#include <cstdint>
void handle_editor_event(Editor *editor, KeyEvent event) { void handle_editor_event(Editor *editor, KeyEvent event) {
static std::chrono::steady_clock::time_point last_click_time = static std::chrono::steady_clock::time_point last_click_time =
@@ -203,6 +204,7 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
switch (mode) { switch (mode) {
case NORMAL: case NORMAL:
if (event.key_type == KEY_CHAR && event.len == 1) { if (event.key_type == KEY_CHAR && event.len == 1) {
Coord start = editor->cursor;
switch (event.c[0]) { switch (event.c[0]) {
case 'u': case 'u':
if (editor->root->line_count > 0) { if (editor->root->line_count > 0) {
@@ -230,6 +232,8 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
case 'a': case 'a':
mode = INSERT; mode = INSERT;
cursor_right(editor, 1); cursor_right(editor, 1);
if (start.row != editor->cursor.row)
cursor_left(editor, 1);
break; break;
case 'i': case 'i':
mode = INSERT; mode = INSERT;
@@ -282,6 +286,14 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
scroll_up(editor, 1); scroll_up(editor, 1);
ensure_cursor(editor); ensure_cursor(editor);
break; break;
case '>':
case '.':
indent_line(editor, editor->cursor.row);
break;
case '<':
case ',':
dedent_line(editor, editor->cursor.row);
break;
case 'p': case 'p':
uint32_t len; uint32_t len;
char *text = get_from_clipboard(&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); edit_erase(editor, editor->cursor, -1);
} }
} else if (event.c[0] == 0x1B) { } else if (event.c[0] == 0x1B) {
Coord prev_pos = editor->cursor;
mode = NORMAL; mode = NORMAL;
cursor_left(editor, 1); cursor_left(editor, 1);
if (prev_pos.row != editor->cursor.row)
cursor_right(editor, 1);
} }
} else if (event.len > 1) { } else if (event.len > 1) {
edit_insert(editor, editor->cursor, event.c, event.len); 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); edit_erase(editor, editor->cursor, next_col_cluster);
break; 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; break;
case SELECT: case SELECT:
if (event.key_type == KEY_CHAR && event.len == 1) { if (event.key_type == KEY_CHAR && event.len == 1) {
uint32_t len; uint32_t len;
char *text; char *text;
Coord start;
switch (event.c[0]) { switch (event.c[0]) {
case 'f': case 'f':
if (editor->cursor.row != editor->selection.row) { if (editor->cursor.row != editor->selection.row) {
@@ -430,16 +453,17 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
mode = NORMAL; mode = NORMAL;
break; break;
case 'y': case 'y':
text = get_selection(editor, &len); text = get_selection(editor, &len, nullptr);
copy_to_clipboard(text, len); copy_to_clipboard(text, len);
free(text); free(text);
editor->selection_active = false; editor->selection_active = false;
mode = NORMAL; mode = NORMAL;
break; break;
case 'x': case 'x':
text = get_selection(editor, &len); text = get_selection(editor, &len, &start);
copy_to_clipboard(text, len); 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); free(text);
editor->selection_active = false; editor->selection_active = false;
mode = NORMAL; mode = NORMAL;
@@ -502,7 +526,7 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
break; break;
} }
ensure_scroll(editor); 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); free(event.c);
} }

View File

@@ -23,6 +23,8 @@ uint32_t get_indent(Editor *editor, Coord cursor) {
if (line_len == 0) if (line_len == 0)
continue; continue;
uint32_t indent = leading_indent(line, line_len); uint32_t indent = leading_indent(line, line_len);
free(it->buffer);
free(it);
return indent; return indent;
} }
free(it->buffer); free(it->buffer);
@@ -38,3 +40,47 @@ bool closing_after_cursor(const char *line, uint32_t len, uint32_t col) {
return false; return false;
return line[i] == '}' || line[i] == ']' || line[i] == ')'; 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);
}

View File

@@ -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 read_key() {
KeyEvent ret; KeyEvent ret;
char *buf; char *buf;
@@ -145,10 +199,21 @@ KeyEvent read_key() {
ret.key_type = KEY_NONE; ret.key_type = KEY_NONE;
return ret; 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; ret.key_type = KEY_MOUSE;
capture_mouse(buf, &ret); 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; ret.key_type = KEY_SPECIAL;
int using_modifiers = buf[3] == ';'; int using_modifiers = buf[3] == ';';
int pos; int pos;

View File

@@ -9,7 +9,7 @@ std::mutex screen_mutex;
termios orig_termios; termios orig_termios;
void disable_raw_mode() { 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()); write(STDOUT_FILENO, os.c_str(), os.size());
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) { if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1) {
perror("tcsetattr"); perror("tcsetattr");
@@ -30,7 +30,7 @@ void enable_raw_mode() {
raw.c_cc[VTIME] = 0; raw.c_cc[VTIME] = 0;
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
exit(EXIT_FAILURE); 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()); write(STDOUT_FILENO, os.c_str(), os.size());
} }