Add selection + clipboard support (cut/copy/paste)

This commit is contained in:
2025-12-13 17:50:44 +00:00
parent a59111162c
commit 5153770ee9
7 changed files with 167 additions and 22 deletions

View File

@@ -6,8 +6,6 @@ A TUI IDE.
# TODO
- [ ] Make function to get selected text. (selection itself is done)
- [ ] Add support for copy/cut/paste.
- [ ] Add support for ctrl + arrow key for moving words.
- [ ] Add support for ctrl + backspace / delete for deleting words.
- [ ] Add underline highlight for current word and all occurences.

View File

@@ -109,6 +109,8 @@ void render_editor(Editor *editor);
void fold(Editor *editor, uint32_t start_line, uint32_t end_line);
void cursor_up(Editor *editor, uint32_t number);
void cursor_down(Editor *editor, uint32_t number);
Coord move_left(Editor *editor, Coord cursor, uint32_t number);
Coord move_right(Editor *editor, Coord cursor, uint32_t number);
void cursor_left(Editor *editor, uint32_t number);
void cursor_right(Editor *editor, uint32_t number);
void scroll_up(Editor *editor, uint32_t number);
@@ -120,5 +122,6 @@ void apply_edit(std::vector<Span> &spans, uint32_t x, int64_t y);
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);
#endif

View File

@@ -34,9 +34,21 @@ template <typename T> struct Queue {
struct Coord {
uint32_t row;
uint32_t col;
bool operator<(const Coord &other) const {
return row < other.row || (row == other.row && col < other.col);
}
bool operator<=(const Coord &other) const {
return *this < other || *this == other;
}
bool operator==(const Coord &other) const {
return row == other.row && col == other.col;
}
bool operator!=(const Coord &other) const { return !(*this == other); }
bool operator>(const Coord &other) const { return other < *this; }
bool operator>=(const Coord &other) const { return !(*this < other); }
};
// std::vector<uint32_t> visual_width(const char *s, uint32_t max_width);
int display_width(const char *str, size_t len);
uint32_t get_visual_col_from_bytes(const char *line, uint32_t len,
uint32_t byte_limit);
@@ -47,5 +59,7 @@ std::string get_exe_dir();
char *load_file(const char *path, uint32_t *out_len);
char *detect_file_type(const char *filename);
Language language_for_file(const char *filename);
void copy_to_clipboard(const char *text, size_t len);
char *get_from_clipboard(uint32_t *out_len);
#endif

View File

@@ -75,12 +75,16 @@ void render_editor(Editor *editor) {
uint32_t render_width = editor->size.col - numlen;
uint32_t render_x = editor->position.col + numlen;
if (editor->selection_active) {
uint32_t sel1 = line_to_byte(editor->root, editor->cursor.row, nullptr) +
editor->cursor.col;
uint32_t sel2 = line_to_byte(editor->root, editor->selection.row, nullptr) +
editor->selection.col;
sel_start = MIN(sel1, sel2);
sel_end = MAX(sel1, sel2);
Coord start, end;
if (editor->cursor >= editor->selection) {
start = editor->selection;
end = move_right(editor, editor->cursor, 1);
} else {
start = editor->cursor;
end = move_right(editor, editor->selection, 1);
}
sel_start = line_to_byte(editor->root, start.row, nullptr) + start.col;
sel_end = line_to_byte(editor->root, end.row, nullptr) + end.col;
}
Coord cursor = {UINT32_MAX, UINT32_MAX};
uint32_t line_index = editor->scroll.row;

View File

@@ -1,3 +1,4 @@
#include <cstdint>
extern "C" {
#include "../libs/libgrapheme/grapheme.h"
}
@@ -7,6 +8,9 @@ extern "C" {
#include <cmath>
void handle_editor_event(Editor *editor, KeyEvent event) {
static std::chrono::steady_clock::time_point last_click_time =
std::chrono::steady_clock::now();
static Coord last_click_pos = {UINT32_MAX, UINT32_MAX};
if (event.key_type == KEY_SPECIAL) {
switch (event.special_key) {
case KEY_DOWN:
@@ -24,6 +28,10 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
}
}
if (event.key_type == KEY_MOUSE) {
auto now = std::chrono::steady_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
now - last_click_time)
.count();
switch (event.mouse_state) {
case SCROLL:
switch (event.mouse_direction) {
@@ -35,17 +43,31 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
scroll_down(editor, 10);
ensure_cursor(editor);
break;
case SCROLL_LEFT:
cursor_left(editor, 10);
break;
case SCROLL_RIGHT:
cursor_right(editor, 10);
break;
}
break;
case PRESS:
if (event.mouse_button == LEFT_BTN) {
Coord p = editor_hit_test(editor, event.mouse_x, event.mouse_y);
editor->cursor = p;
editor->cursor_preffered = UINT32_MAX;
editor->selection = p;
if (mode == SELECT) {
mode = NORMAL;
editor->selection_active = false;
if (duration < 250 &&
last_click_pos == (Coord){event.mouse_x, event.mouse_y}) {
mode = SELECT;
editor->selection_active = true;
} else {
Coord p = editor_hit_test(editor, event.mouse_x, event.mouse_y);
editor->cursor = p;
editor->cursor_preffered = UINT32_MAX;
editor->selection = p;
if (mode == SELECT) {
mode = NORMAL;
editor->selection_active = false;
}
last_click_pos = (Coord){event.mouse_x, event.mouse_y};
last_click_time = now;
}
}
break;
@@ -143,6 +165,8 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
break;
case SELECT:
if (event.key_type == KEY_CHAR && event.len == 1) {
uint32_t len;
char *text;
switch (event.c[0]) {
case 0x1B:
case 's':
@@ -150,6 +174,43 @@ void handle_editor_event(Editor *editor, KeyEvent event) {
editor->selection_active = false;
mode = NORMAL;
break;
case 'y':
text = get_selection(editor, &len);
copy_to_clipboard(text, len);
free(text);
editor->selection_active = false;
mode = NORMAL;
break;
case 'x':
text = get_selection(editor, &len);
copy_to_clipboard(text, len);
edit_erase(editor, MIN(editor->cursor, editor->selection), len);
free(text);
editor->selection_active = false;
mode = NORMAL;
break;
case 'p':
text = get_from_clipboard(&len);
if (text) {
Coord start, end;
if (editor->cursor >= editor->selection) {
start = editor->selection;
end = move_right(editor, editor->cursor, 1);
} else {
start = editor->cursor;
end = move_right(editor, editor->selection, 1);
}
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;
edit_erase(editor, start, end_byte - start_byte);
edit_insert(editor, editor->cursor, text, len);
free(text);
}
editor->selection_active = false;
mode = NORMAL;
break;
}
}
break;
@@ -562,6 +623,25 @@ void edit_insert(Editor *editor, Coord pos, char *data, uint32_t len) {
editor->spans.edits.push({byte_pos, len});
}
char *get_selection(Editor *editor, uint32_t *out_len) {
std::shared_lock lock(editor->knot_mtx);
Coord start, end;
if (editor->cursor >= editor->selection) {
start = editor->selection;
end = move_right(editor, editor->cursor, 1);
} else {
start = editor->cursor;
end = move_right(editor, editor->selection, 1);
}
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;
char *text = read(editor->root, start_byte, end_byte - start_byte);
if (out_len)
*out_len = end_byte - start_byte;
return text;
}
void apply_edit(std::vector<Span> &spans, uint32_t x, int64_t y) {
Span key{.start = x, .end = 0, .hl = nullptr};
auto it = std::lower_bound(

View File

@@ -184,9 +184,7 @@ void scroll_down(Editor *editor, uint32_t number) {
void ensure_cursor(Editor *editor) {
std::shared_lock knot_lock(editor->knot_mtx);
if (editor->cursor.row < editor->scroll.row ||
(editor->cursor.row == editor->scroll.row &&
editor->cursor.col < editor->scroll.col)) {
if (editor->cursor < editor->scroll) {
editor->cursor = editor->scroll;
return;
}
@@ -275,9 +273,7 @@ void ensure_scroll(Editor *editor) {
uint32_t numlen =
2 + static_cast<int>(std::log10(editor->root->line_count + 1));
uint32_t render_width = editor->size.col - numlen;
if (editor->cursor.row < editor->scroll.row ||
(editor->cursor.row == editor->scroll.row &&
editor->cursor.col < editor->scroll.col)) {
if (editor->cursor < editor->scroll) {
LineIterator *it = begin_l_iter(editor->root, editor->cursor.row);
if (!it)
return;

View File

@@ -7,6 +7,7 @@ extern "C" {
#include <cstdarg>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <limits.h>
@@ -16,6 +17,55 @@ extern "C" {
#include <unistd.h>
#include <unordered_map>
char *get_from_clipboard(uint32_t *out_len) {
FILE *pipe = popen("xclip -selection clipboard -o", "r");
if (!pipe) {
*out_len = 0;
return nullptr;
}
size_t capacity = 4096;
size_t length = 0;
char *buffer = (char *)malloc(capacity);
if (!buffer) {
pclose(pipe);
*out_len = 0;
return nullptr;
}
size_t n;
while ((n = fread(buffer + length, 1, capacity - length, pipe)) > 0) {
length += n;
if (length == capacity) {
capacity *= 2;
char *tmp = (char *)realloc(buffer, capacity);
if (!tmp) {
free(buffer);
pclose(pipe);
*out_len = 0;
return nullptr;
}
buffer = tmp;
}
}
pclose(pipe);
char *result = (char *)realloc(buffer, length + 1);
if (result) {
result[length] = '\0';
buffer = result;
} else {
buffer[length] = '\0';
}
*out_len = length;
return buffer;
}
void copy_to_clipboard(const char *text, size_t len) {
FILE *pipe = popen("xclip -selection clipboard", "w");
if (!pipe)
return;
fwrite(text, sizeof(char), len, pipe);
pclose(pipe);
}
int display_width(const char *str, size_t len) {
if (!str || !*str)
return 0;