ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit 7892e89a47ccb35549b25fe558245b9e8f639daf
parent 22ffb413e9e0be6a5764de4bcc951e6a0480bad3
Author: lumidify <nobody@lumidify.org>
Date:   Sat, 18 Dec 2021 21:44:47 +0100

Start cleaning up and adding documentation

Diffstat:
MIDEAS | 7+++++++
MMakefile | 7+++++--
ANOTES | 3+++
DQUIRKS | 14--------------
MREADME | 15++++++++++++++-
MTODO | 3+++
Massert.c | 17-----------------
Mbuffer.c | 138+++++++++++++++++++++++++++++++++++++------------------------------------------
Mbuffer.h | 30+++++++++++++++---------------
Mcache.c | 9+++------
Mcache.h | 9+++++++++
Mcleanup.h | 5+++++
Mcommon.h | 11+++++++++--
Adraw_util.c | 43+++++++++++++++++++++++++++++++++++++++++++
Adraw_util.h | 34++++++++++++++++++++++++++++++++++
Mkeys.c | 2+-
Mkeys.h | 32+++++++++-----------------------
Mkeys_basic.c | 163++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mkeys_basic.h | 8++++++++
Mkeys_basic_config.h | 18+++++++++---------
Mkeys_command.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Mkeys_command.h | 8++++++++
Mkeys_command_config.h | 6+++---
Akeys_config.h | 31+++++++++++++++++++++++++++++++
Aledit.1 | 796+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mledit.c | 45+++++++++++++++++++--------------------------
Mmacros.h | 5+++++
Mmemory.c | 126+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mmemory.h | 16++++++++++++++++
Mpango-compat.h | 7+++++++
Msearch.c | 49+++++++++++++++++++------------------------------
Msearch.h | 22+++++++++++++++++-----
Mtheme.h | 8++++++++
Mtxtbuf.c | 22++++++++--------------
Mtxtbuf.h | 15++++++++-------
Mundo.c | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mundo.h | 25+++++++++++++++++++++----
Mutil.c | 77++++++++++++++++++++++++++++++-----------------------------------------------
Mutil.h | 34++++++++++++++--------------------
Mview.c | 119++++++++++++++++++++++++++++++++++---------------------------------------------
Mview.h | 26++++++++++++++------------
Mwindow.c | 28+++++++++++++---------------
Mwindow.h | 28++++++++++++++++++++++------
43 files changed, 1642 insertions(+), 589 deletions(-)

diff --git a/IDEAS b/IDEAS @@ -3,3 +3,10 @@ * basic macros * add different (more basic) text backend * https://drewdevault.com/2021/06/27/You-cant-capture-the-nuance.html +* maybe somehow allow to define one keyboard layout that can be easily + be switched to and from - this would make typing commands easier + because they can't really be translated into other languages like the + key bindings + -> I'm not sure it that's even possible in a portable way, though, + since the keyboard layouts can be set in many different ways, so + the entire state would somehow have to be saved to restore it again. diff --git a/Makefile b/Makefile @@ -24,6 +24,7 @@ OBJ = \ txtbuf.o \ undo.o \ util.o \ + draw_util.o \ window.o \ pango-compat.o @@ -42,6 +43,7 @@ HDR = \ txtbuf.h \ undo.h \ util.h \ + draw_util.h \ window.h \ cleanup.h \ pango-compat.h @@ -53,8 +55,9 @@ all: ${BIN} ledit.o window.o : config.h theme.o : theme_config.h -keys_basic.o : keys_basic_config.h -keys_command.o : keys_command_config.h +keys_basic.o : keys_basic_config.h keys_config.h +keys_command.o : keys_command_config.h keys_config.h +keys.o : keys_config.h ${OBJ} : ${HDR} diff --git a/NOTES b/NOTES @@ -0,0 +1,3 @@ +* I originally wanted to avoid putting includes in header files, but + I eventually decided that it just becomes a huge mess that way. + Maybe I'll change my mind sometime, though. diff --git a/QUIRKS b/QUIRKS @@ -1,14 +0,0 @@ -* Undo with multiple views: - Since a new mode group is started each time insert is entered, when text - is typed in one view in insert, then in another view, and then again in - the first one, the last two inserts will be undone in one go since both - views were in insert already. I'm not sure how to make this more logical, - though. - Maybe it could be "improved" by also saving view in undo stack, but that - would cause problems because views can be added and removed, and it would - maybe not even be more logical. - -* Scroll offset is stored as pixel value, so a view may scroll when text is - added or deleted in another view. Additionally, when a new view is created, - the scroll offset from the old view is taken, which may be weird if the - window of the new view is a different size. diff --git a/README b/README @@ -1 +1,14 @@ -Work in progress. Nothing to see here. +WARNING: This is work in progress! A lot of bugs still need to be fixed +before this can be used as a real text editor. + +ledit is a vi-like text editor for people who switch between keyboard +layouts frequently and/or work with languages that require complex text +layout. + +The documentation can be viewed in ledit.1 or at the following locations: + +gopher://lumidify.org/0/doc/ledit/ledit-current.txt +gopher://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/0/doc/ledit/ledit-current.txt +http://lumidify.org/doc/ledit/ledit-current.html +http://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/doc/ledit/ledit-current.html +https://lumidify.org/doc/ledit/ledit-current.html diff --git a/TODO b/TODO @@ -1,3 +1,6 @@ +* Use proper gap buffer implementation +* Somehow clean up overflow handling +* File locking (lockf(3) or flock(2)) * Load file in background so text is already shown while still loading the rest of a big file. * Try to copy vi behavior where the cursor jumps back to the original diff --git a/assert.c b/assert.c @@ -1,22 +1,5 @@ -/* FIXME: sort out the stupid includes */ #include <stdio.h> #include <stdlib.h> - -#include <X11/Xlib.h> -#include <X11/Xutil.h> -#include <X11/Xatom.h> -#include <pango/pangoxft.h> -#include <X11/extensions/Xdbe.h> - -#include "pango-compat.h" -#include "memory.h" -#include "common.h" -#include "txtbuf.h" -#include "undo.h" -#include "cache.h" -#include "theme.h" -#include "window.h" -#include "buffer.h" #include "cleanup.h" void diff --git a/buffer.c b/buffer.c @@ -1,5 +1,3 @@ -/* FIXME: shrink buffers when text length less than a fourth of the size */ -/* FIXME: handle all undo within buffer to keep it consistent */ /* FIXME: maybe use separate unicode grapheme library so all functions that need grapheme boundaries can be included here instead of in the views */ @@ -16,16 +14,17 @@ #include <pango/pangoxft.h> #include <X11/extensions/Xdbe.h> -#include "pango-compat.h" -#include "memory.h" -#include "common.h" -#include "txtbuf.h" +#include "util.h" #include "undo.h" #include "cache.h" #include "theme.h" +#include "memory.h" +#include "common.h" +#include "txtbuf.h" #include "window.h" #include "buffer.h" #include "assert.h" +#include "pango-compat.h" /* * Important notes: @@ -43,11 +42,6 @@ */ /* - * Move the gap of a line so it is at byte position 'index' - */ -static void move_text_gap(ledit_line *line, size_t index); - -/* * Move the gap of the line gap buffer to 'index'. */ static void move_line_gap(ledit_buffer *buffer, size_t index); @@ -150,27 +144,18 @@ static void buffer_delete_line_entries_base(ledit_buffer *buffer, size_t index1, */ static void buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start, size_t length); +/* + * Copy text range into given buffer. + * - dst is null-terminated + * - dst must be large enough to contain the text and NUL (only use this together with buffer_textlen) + * - the range must be sorted already + */ +static void buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2); + static void marklist_destroy(ledit_buffer_marklist *marklist); static ledit_buffer_marklist *marklist_create(void); static void -swap_sz(size_t *a, size_t *b) { - size_t tmp = *a; - *a = *b; - *b = tmp; -} - -static void -sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2) { - if (*l1 == *l2 && *b1 > *b2) { - swap_sz(b1, b2); - } else if (*l1 > *l2) { - swap_sz(l1, l2); - swap_sz(b1, b2); - } -} - -static void marklist_destroy(ledit_buffer_marklist *marklist) { for (size_t i = 0; i < marklist->len; i++) { free(marklist->marks[i].text); @@ -190,7 +175,7 @@ buffer_insert_mark(ledit_buffer *buffer, char *mark, size_t len, size_t line, si } } if (marklist->len == marklist->alloc) { - size_t new_alloc = marklist->alloc > 0 ? marklist->alloc * 2 : 4; + size_t new_alloc = ideal_array_size(marklist->alloc, add_sz(marklist->len, 1)); marklist->marks = ledit_reallocarray( marklist->marks, new_alloc, sizeof(ledit_buffer_mark) ); @@ -299,10 +284,8 @@ buffer_set_hard_line_based(ledit_buffer *buffer, int hl) { } void -buffer_add_view(ledit_buffer *buffer, ledit_theme *theme, enum ledit_mode mode, size_t line, size_t pos, long scroll_offset) { - size_t new_num = buffer->views_num + 1; - if (new_num <= buffer->views_num) - err_overflow(); +buffer_add_view(ledit_buffer *buffer, ledit_theme *theme, ledit_mode mode, size_t line, size_t pos, long scroll_offset) { + size_t new_num = add_sz(buffer->views_num, 1); buffer->views = ledit_reallocarray(buffer->views, new_num, sizeof(ledit_view *)); buffer->views[buffer->views_num] = view_create(buffer, theme, mode, line, pos); set_view_hard_line_text(buffer, buffer->views[buffer->views_num]); @@ -354,10 +337,11 @@ buffer_load_file(ledit_buffer *buffer, char *filename, size_t line, char **errst len = ftell(file); if (len < 0) goto errorclose; if (fseek(file, 0, SEEK_SET)) goto errorclose; + size_t lenz = add_sz((size_t)len, 2); ll = buffer_get_line(buffer, line); /* FIXME: insert in chunks instead of allocating huge buffer */ - file_contents = ledit_malloc(len + 2); + file_contents = ledit_malloc(lenz); /* mimic nvi (or at least the openbsd version) - if the line is empty, insert directly, otherwise insert after the line */ if (ll->len > 0) { @@ -485,8 +469,9 @@ buffer_insert_text_from_line_base( txtbuf *text_ret) { ledit_assert(dst_line != src_line); ledit_line *ll = buffer_get_line(buffer, src_line); + ledit_assert(add_sz(src_index, src_len) <= ll->len); if (text_ret != NULL) { - txtbuf_grow(text_ret, src_len); + txtbuf_resize(text_ret, src_len); text_ret->len = src_len; } if (src_index >= ll->gap) { @@ -547,16 +532,6 @@ buffer_insert_text_from_line_base( } static void -move_text_gap(ledit_line *line, size_t index) { - /* yes, I know sizeof(char) == 1 anyways */ - move_gap( - line->text, sizeof(char), index, - line->gap, line->cap, line->len, - &line->gap - ); -} - -static void move_line_gap(ledit_buffer *buffer, size_t index) { move_gap( buffer->lines, sizeof(ledit_line), index, @@ -593,12 +568,9 @@ resize_and_move_line_gap(ledit_buffer *buffer, size_t min_size, size_t index) { static void buffer_insert_text_base(ledit_buffer *buffer, size_t line_index, size_t index, char *text, size_t len) { ledit_line *line = buffer_get_line(buffer, line_index); + ledit_assert(index <= line->len); /* \0 is not included in line->len */ - /* FIXME: this if should be redundant now because resize_and_move... includes a check */ - if (line->len + len + 1 > line->cap || line->text == NULL) - resize_and_move_text_gap(line, line->len + len + 1, index); - else - move_text_gap(line, index); + resize_and_move_text_gap(line, add_sz3(line->len, len, 1), index); /* the gap is now located at 'index' and least large enough to hold the new text */ memcpy(line->text + index, text, len); line->gap += len; @@ -660,7 +632,7 @@ buffer_insert_text_with_newlines_base( rem_len -= cur - last + 1; last = cur + 1; } - /* FIXME: check how legal this casting between pointers and ints is */ + /* FIXME: check how legal this casting between pointers and size_t's is */ buffer_insert_text_base(buffer, cur_line, cur_index, last, text + len - last); if (end_line_ret) *end_line_ret = cur_line; @@ -678,15 +650,10 @@ init_line(ledit_buffer *buffer, ledit_line *line) { line->len = 0; } -/* FIXME: error checking (index out of bounds, etc.) */ static void buffer_append_line_base(ledit_buffer *buffer, size_t line_index, size_t text_index, int break_text) { - size_t new_len = buffer->lines_num + 1; - if (new_len <= buffer->lines_num) - err_overflow(); - size_t insert_index = line_index + 1; - if (insert_index <= line_index) - err_overflow(); + size_t new_len = add_sz(buffer->lines_num, 1); + size_t insert_index = add_sz(line_index, 1); resize_and_move_line_gap(buffer, new_len, insert_index); buffer->lines_num++; buffer->lines_gap++; @@ -721,6 +688,11 @@ buffer_delete_line_entries_base(ledit_buffer *buffer, size_t index1, size_t inde } move_line_gap(buffer, index1); buffer->lines_num -= index2 - index1 + 1; + /* possibly decrease size of array - this needs to be after + actually deleting the lines so the length is already less */ + size_t min_size = ideal_array_size(buffer->lines_cap, buffer->lines_num); + if (min_size != buffer->lines_cap) + resize_and_move_line_gap(buffer, buffer->lines_num, buffer->lines_gap); for (size_t i = 0; i < buffer->views_num; i++) { view_notify_delete_lines(buffer->views[i], index1, index2); } @@ -760,14 +732,24 @@ buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, s ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2)); size_t len = 0; ledit_line *ll = buffer_get_line(buffer, line1); + ledit_line *ll2 = buffer_get_line(buffer, line2); + ledit_assert(byte1 <= ll->len); + ledit_assert(byte2 <= ll2->len); if (line1 == line2) { len = byte2 - byte1; } else { /* + 1 for newline */ - len = ll->len - byte1 + byte2 + 1; + len = add_sz3(ll->len - byte1, byte2, 1); for (size_t i = line1 + 1; i < line2; i++) { ll = buffer_get_line(buffer, i); - len += ll->len + 1; + /* ll->len + 1 should be valid anyways + because there *should* always be + space for '\0' at the end, i.e. ll->cap + should be at least ll->len + 1 */ + /* FIXME: also, this overflow checking is + probably completely useless (it definitely + is really ugly) */ + len += add_sz(ll->len, 1); } } return len; @@ -779,7 +761,7 @@ buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, s only done when it is re-rendered (and thus normalized because of pango's requirements). If a more efficient rendering backend is added, it would be good to optimize this, though. */ -void +static void buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2) { ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2)); ledit_line *ll1 = buffer_get_line(buffer, line1); @@ -817,7 +799,7 @@ buffer_copy_text_to_txtbuf( size_t line2, size_t byte2) { ledit_assert(line1 < line2 || (line1 == line2 && byte1 <= byte2)); size_t len = buffer_textlen(buffer, line1, byte1, line2, byte2); - txtbuf_grow(buf, len + 1); + txtbuf_resize(buf, len); buffer_copy_text(buffer, buf->text, line1, byte1, line2, byte2); buf->len = len; } @@ -831,7 +813,6 @@ line_prev_utf8(ledit_line *line, size_t index) { return 0; size_t i = index - 1; /* find valid utf8 char - this probably needs to be improved */ - /* FIXME: don't go off end or beginning */ while (i > 0 && ((LINE_CHAR(line, i) & 0xC0) == 0x80)) i--; return i; @@ -865,6 +846,8 @@ line_byte_to_char(ledit_line *line, size_t byte) { static void buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start, size_t length) { ledit_line *l = buffer_get_line(buffer, line); + (void)add_sz(start, length); /* just check that no overflow */ + ledit_assert(start + length <= l->len); if (start <= l->gap && start + length >= l->gap) { l->gap = start; } else if (start < l->gap && start + length < l->gap) { @@ -882,6 +865,10 @@ buffer_delete_line_section_base(ledit_buffer *buffer, size_t line, size_t start, ); } l->len -= length; + /* possibly decrease size of line */ + size_t cap = ideal_array_size(l->cap, add_sz(l->len, 1)); + if (cap != l->cap) + resize_and_move_text_gap(l, l->len + 1, l->gap); for (size_t i = 0; i < buffer->views_num; i++) { view_notify_delete_text(buffer->views[i], line, start, length); } @@ -915,6 +902,8 @@ delete_range_base( } ledit_line *line1 = buffer_get_line(buffer, line_index1); ledit_line *line2 = buffer_get_line(buffer, line_index2); + ledit_assert(byte_index1 <= line1->len); + ledit_assert(byte_index2 <= line2->len); buffer_delete_line_section_base( buffer, line_index1, byte_index1, line1->len - byte_index1 ); @@ -942,26 +931,26 @@ undo_delete_helper(void *data, size_t line1, size_t byte1, size_t line2, size_t delete_range_base(buffer, line1, byte1, line2, byte2, NULL); } -void -buffer_undo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte) { +undo_status +buffer_undo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte) { size_t min_line; - ledit_undo( + undo_status s = ledit_undo( buffer->undo, mode, buffer, &undo_insert_helper, &undo_delete_helper, cur_line, cur_byte, &min_line ); - /* FIXME: why is this check here? */ if (min_line < buffer->lines_num) { buffer_recalc_all_views_from_line( buffer, min_line > 0 ? min_line - 1 : min_line ); } buffer->modified = 1; + return s; } -void -buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte) { +undo_status +buffer_redo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte) { size_t min_line; - ledit_redo( + undo_status s = ledit_redo( buffer->undo, mode, buffer, &undo_insert_helper, &undo_delete_helper, cur_line, cur_byte, &min_line ); @@ -971,12 +960,13 @@ buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t ); } buffer->modified = 1; + return s; } void buffer_delete_with_undo_base( ledit_buffer *buffer, ledit_range cur_range, - int start_undo_group, enum ledit_mode mode, /* for undo */ + int start_undo_group, ledit_mode mode, /* for undo */ size_t line_index1, size_t byte_index1, size_t line_index2, size_t byte_index2, txtbuf *text_ret) { @@ -1001,7 +991,7 @@ buffer_delete_with_undo_base( void buffer_delete_with_undo( ledit_buffer *buffer, ledit_range cur_range, - int start_undo_group, enum ledit_mode mode, /* for undo */ + int start_undo_group, ledit_mode mode, /* for undo */ size_t line_index1, size_t byte_index1, size_t line_index2, size_t byte_index2, txtbuf *text_ret) { @@ -1020,7 +1010,7 @@ void buffer_insert_with_undo_base( ledit_buffer *buffer, ledit_range cur_range, int set_range_end, - int start_undo_group, enum ledit_mode mode, + int start_undo_group, ledit_mode mode, size_t line, size_t byte, char *text, size_t len, size_t *line_ret, size_t *byte_ret) { @@ -1053,7 +1043,7 @@ void buffer_insert_with_undo( ledit_buffer *buffer, ledit_range cur_range, int set_range_end, - int start_undo_group, enum ledit_mode mode, + int start_undo_group, ledit_mode mode, size_t line, size_t byte, char *text, size_t len, size_t *line_ret, size_t *byte_ret) { diff --git a/buffer.h b/buffer.h @@ -1,6 +1,14 @@ #ifndef _LEDIT_BUFFER_H_ #define _LEDIT_BUFFER_H_ +#include <time.h> +#include <stdio.h> +#include <stddef.h> + +#include "common.h" +#include "txtbuf.h" +#include "undo.h" + typedef struct ledit_buffer ledit_buffer; #include "view.h" @@ -72,7 +80,7 @@ void buffer_set_hard_line_based(ledit_buffer *buffer, int hl); */ void buffer_add_view( ledit_buffer *buffer, ledit_theme *theme, - enum ledit_mode mode, size_t line, size_t pos, long scroll_offset + ledit_mode mode, size_t line, size_t pos, long scroll_offset ); /* @@ -165,14 +173,6 @@ void buffer_recalc_all_lines(ledit_buffer *buffer); size_t buffer_textlen(ledit_buffer *buffer, size_t line1, size_t byte1, size_t line2, size_t byte2); /* - * Copy text range into given buffer. - * - dst is null-terminated - * - dst must be large enough to contain the text and NUL (only use this together with buffer_textlen) - * - the range must be sorted already - */ -void buffer_copy_text(ledit_buffer *buffer, char *dst, int line1, int byte1, int line2, int byte2); - -/* * Copy text range into given buffer and resize it if necessary. * - the range must be sorted already */ @@ -219,12 +219,12 @@ int buffer_get_mark(ledit_buffer *buffer, char *mark, size_t len, size_t *line_r * 'cur_line' and 'cur_byte' are filled with the new line and cursor * position after the undo. */ -void buffer_undo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte); +undo_status buffer_undo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte); /* * Same as 'buffer_undo', but for redo. */ -void buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, size_t *cur_byte); +undo_status buffer_redo(ledit_buffer *buffer, ledit_mode mode, size_t *cur_line, size_t *cur_byte); /* * Delete the given range (which does not need to be sorted yet) and @@ -237,7 +237,7 @@ void buffer_redo(ledit_buffer *buffer, enum ledit_mode mode, size_t *cur_line, s */ void buffer_delete_with_undo_base( ledit_buffer *buffer, ledit_range cur_range, - int start_undo_group, enum ledit_mode mode, /* for undo */ + int start_undo_group, ledit_mode mode, /* for undo */ size_t line_index1, size_t byte_index1, size_t line_index2, size_t byte_index2, txtbuf *text_ret @@ -249,7 +249,7 @@ void buffer_delete_with_undo_base( */ void buffer_delete_with_undo( ledit_buffer *buffer, ledit_range cur_range, - int start_undo_group, enum ledit_mode mode, /* for undo */ + int start_undo_group, ledit_mode mode, /* for undo */ size_t line_index1, size_t byte_index1, size_t line_index2, size_t byte_index2, txtbuf *text_ret @@ -268,7 +268,7 @@ void buffer_delete_with_undo( void buffer_insert_with_undo_base( ledit_buffer *buffer, ledit_range cur_range, int set_range_end, - int start_undo_group, enum ledit_mode mode, + int start_undo_group, ledit_mode mode, size_t line, size_t byte, char *text, size_t len, size_t *line_ret, size_t *byte_ret @@ -281,7 +281,7 @@ void buffer_insert_with_undo_base( void buffer_insert_with_undo( ledit_buffer *buffer, ledit_range cur_range, int set_range_end, - int start_undo_group, enum ledit_mode mode, + int start_undo_group, ledit_mode mode, size_t line, size_t byte, char *text, size_t len, size_t *line_ret, size_t *byte_ret diff --git a/cache.c b/cache.c @@ -6,6 +6,7 @@ #include <pango/pangoxft.h> #include <X11/extensions/Xdbe.h> +#include "util.h" #include "common.h" #include "memory.h" #include "cache.h" @@ -94,9 +95,6 @@ cache_get_layout(ledit_cache *cache, size_t index) { return &cache->layouts[index]; } -/* FIXME: decide on int or size_t, but not both */ -/* or maybe ssize_t */ - /* FIXME: max pixmap cache size */ void cache_assign_pixmap_index( @@ -126,10 +124,9 @@ cache_assign_pixmap_index( } /* no free entry found, increase cache size */ - /* FIXME: what is the ideal size to resize to? */ - /* FIXME: overflow */ /* FIXME: maybe have maximum cache size */ - cache->pixmaps = ledit_reallocarray(cache->pixmaps, cache->num_pixmaps * 2, sizeof(cache_pixmap)); + size_t new_alloc = ideal_array_size(cache->num_pixmaps, add_sz(cache->num_pixmaps, 1)); + cache->pixmaps = ledit_reallocarray(cache->pixmaps, new_alloc, sizeof(cache_pixmap)); entry_index = cache->num_pixmaps; for (size_t i = cache->num_pixmaps; i < cache->num_pixmaps * 2; i++) { cache->pixmaps[i].line = 0; diff --git a/cache.h b/cache.h @@ -1,3 +1,10 @@ +#ifndef _CACHE_H_ +#define _CACHE_H_ + +#include <stddef.h> +#include <X11/Xlib.h> +#include <pango/pangoxft.h> + /* *The maximum number of layouts in the cache. */ @@ -129,3 +136,5 @@ void cache_assign_layout_index( void (*set_layout_line)(void *, size_t, size_t), void (*invalidate_layout_line)(void *, size_t) ); + +#endif diff --git a/cleanup.h b/cleanup.h @@ -1,4 +1,9 @@ +#ifndef _CLEANUP_H_ +#define _CLEANUP_H_ + /* This is here so it can be called from other places even though the function definition is in ledit.c */ void ledit_cleanup(void); void ledit_emergencydump(void); + +#endif diff --git a/common.h b/common.h @@ -1,3 +1,8 @@ +#ifndef _COMMON_H_ +#define _COMMON_H_ + +#include <X11/Xlib.h> + typedef struct { size_t line1; size_t byte1; @@ -5,11 +10,11 @@ typedef struct { size_t byte2; } ledit_range; -enum ledit_mode { +typedef enum { NORMAL = 1, INSERT = 2, VISUAL = 4 -}; +} ledit_mode; typedef struct { Display *dpy; @@ -19,3 +24,5 @@ typedef struct { int depth; int redraw; } ledit_common; + +#endif diff --git a/draw_util.c b/draw_util.c @@ -0,0 +1,43 @@ +#include <X11/Xlib.h> +#include <X11/Xft/Xft.h> + +#include "memory.h" +#include "window.h" +#include "draw_util.h" + +ledit_draw * +draw_create(ledit_window *window, int w, int h) { + ledit_draw *draw = ledit_malloc(sizeof(ledit_draw)); + draw->w = w; + draw->h = h; + draw->pixmap = XCreatePixmap( + window->common->dpy, window->drawable, w, h, window->common->depth + ); + draw->xftdraw = XftDrawCreate( + window->common->dpy, draw->pixmap, window->common->vis, window->common->cm + ); + return draw; +} + +void +draw_grow(ledit_window *window, ledit_draw *draw, int w, int h) { + /* FIXME: sensible default pixmap sizes here */ + /* FIXME: maybe shrink the pixmaps at some point */ + if (draw->w < w || draw->h < h) { + draw->w = w > draw->w ? w + 10 : draw->w; + draw->h = h > draw->h ? h + 10 : draw->h; + XFreePixmap(window->common->dpy, draw->pixmap); + draw->pixmap = XCreatePixmap( + window->common->dpy, window->drawable, + draw->w, draw->h, window->common->depth + ); + XftDrawChange(draw->xftdraw, draw->pixmap); + } +} + +void +draw_destroy(ledit_window *window, ledit_draw *draw) { + XFreePixmap(window->common->dpy, draw->pixmap); + XftDrawDestroy(draw->xftdraw); + free(draw); +} diff --git a/draw_util.h b/draw_util.h @@ -0,0 +1,34 @@ +#ifndef _DRAW_UTIL_H_ +#define _DRAW_UTIL_H_ + +#include <X11/Xlib.h> +#include <X11/Xft/Xft.h> + +#include "window.h" + +/* + * This is just a basic wrapper for XftDraws and Pixmaps + * that is used by the window for its text display at the bottom. + */ +typedef struct { + XftDraw *xftdraw; + Pixmap pixmap; + int w, h; +} ledit_draw; + +/* + * Create a draw with the specified width and height. + */ +ledit_draw *draw_create(ledit_window *window, int w, int h); + +/* + * Make sure the size of the draw is at least the given width and height. + */ +void draw_grow(ledit_window *window, ledit_draw *draw, int w, int h); + +/* + * Destroy a draw. + */ +void draw_destroy(ledit_window *window, ledit_draw *draw); + +#endif diff --git a/keys.c b/keys.c @@ -13,6 +13,7 @@ #include "theme.h" #include "window.h" #include "keys.h" +#include "keys_config.h" KEY_LANGS; @@ -32,7 +33,6 @@ get_language_index(char *lang) { /* FIXME: The Mod*Masks can be remapped, so it isn't really clear what is what */ /* most are disabled now to avoid issues with e.g. numlock */ static unsigned int importantmod = ShiftMask | ControlMask | Mod1Mask; -#define XK_ANY_MOD UINT_MAX int match_key(unsigned int mask, unsigned int state) diff --git a/keys.h b/keys.h @@ -1,29 +1,13 @@ -#define LENGTH(X) (sizeof(X) / sizeof(X[0])) +#ifndef _KEYS_H_ +#define _KEYS_H_ -/* - * These are the language strings compared with the language strings that - * xkb gives in order to change the key mapping on layout change events. - */ -#define KEY_LANGS \ -static char *key_langs[] = { \ - "English (US)", \ - "German", \ - "Urdu (Pakistan)", \ - "Hindi (Bolnagri)" \ -} +#include <X11/Xlib.h> +#include "window.h" -#define GEN_KEY_ARRAY(key_struct, en, de, ur, hi) \ -static struct { \ - key_struct *keys; \ - int num_keys; \ -} keys[] = { \ - {en, LENGTH(en)}, \ - {de, LENGTH(de)}, \ - {ur, LENGTH(ur)}, \ - {hi, LENGTH(hi)} \ -} +#define LENGTH(X) (sizeof(X) / sizeof(X[0])) -#define LANG_KEYS(index) &keys[index] +#define XK_ANY_MOD UINT_MAX +#define XK_NO_MOD 0 /* get the index of a language with the given name, or -1 if none exists */ int get_language_index(char *lang); @@ -34,3 +18,5 @@ void preprocess_key( ledit_window *window, XEvent *event, KeySym *sym_ret, char *buf_ret, int buf_size, int *buf_len_ret ); + +#endif diff --git a/keys_basic.c b/keys_basic.c @@ -7,8 +7,8 @@ -> space is hidden when e.g. ltr text left and rtl text on right is wrapped */ /* FIXME: some weird things still happen with selections staying as "ghosts" and being deleted at some later time even though they're not shown anymore */ -/* FIXME: there seem to be some issues with undo, but I couldn't reproduce - them reliably yet */ +/* FIXME: delete everything concerned with selections in insert mode since + they are now not allowed at all */ #include <stdio.h> #include <stdlib.h> @@ -20,6 +20,7 @@ #include <X11/XF86keysym.h> #include <X11/cursorfont.h> +#include "util.h" #include "memory.h" #include "common.h" #include "txtbuf.h" @@ -32,6 +33,7 @@ #include "search.h" #include "keys.h" +#include "keys_config.h" #include "keys_basic.h" #include "keys_command.h" #include "keys_basic_config.h" @@ -108,7 +110,7 @@ void clear_key_stack(void); static void move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index); static void move_cursor_up_down(ledit_view *view, int dir); -static void push_num(int num); +static void push_num(ledit_view *view, int num); static void delete_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type); static void yank_cb(ledit_view *view, size_t line, size_t char_pos, enum key_type type); static void get_new_line_softline( @@ -129,14 +131,6 @@ view_locked_error(ledit_view *view) { #define CHECK_VIEW_LOCKED(view) if (view->lock_text) return view_locked_error(view) #define CHECK_VIEW_LOCKED_NORETURN(view) if (view->lock_text) (void)view_locked_error(view) -/* FIXME: move to common */ -static void -swap_sz(size_t *a, size_t *b) { - size_t tmp = *a; - *a = *b; - *b = tmp; -} - static int key_stack_empty(void) { return key_stack.len == 0; @@ -146,7 +140,7 @@ static struct key_stack_elem * push_key_stack(void) { struct key_stack_elem *e; if (key_stack.len >= key_stack.alloc) { - size_t new_alloc = key_stack.alloc > 0 ? key_stack.alloc * 2 : 4; + size_t new_alloc = ideal_array_size(key_stack.alloc, add_sz(key_stack.len, 1)); key_stack.stack = ledit_reallocarray( key_stack.stack, new_alloc, sizeof(struct key_stack_elem) ); @@ -207,7 +201,7 @@ err_invalid_key(ledit_view *view) { * possibly a second one if the top one was a number key. */ static int -get_key_repeat_and_motion_cb(motion_callback *cb_ret) { +get_key_repeat_and_motion_cb(ledit_view *view, motion_callback *cb_ret) { int num = 1; struct key_stack_elem *e = pop_key_stack(); if (e != NULL) { @@ -220,7 +214,10 @@ get_key_repeat_and_motion_cb(motion_callback *cb_ret) { if (e != NULL) { int new_count = e->count > 0 ? e->count : 1; if (INT_MAX / new_count < num) { - /* FIXME: show error */ + window_show_message( + view->window, + "Integer overflow in key repetition", -1 + ); num = INT_MAX; } num *= new_count; @@ -274,7 +271,7 @@ static struct repetition_stack_elem * push_repetition_stack(void) { struct repetition_stack_elem *e; if (repetition_stack.tmp_len >= repetition_stack.tmp_alloc) { - size_t new_alloc = repetition_stack.tmp_alloc > 0 ? repetition_stack.tmp_alloc * 2 : 4; + size_t new_alloc = ideal_array_size(repetition_stack.tmp_alloc, add_sz(repetition_stack.tmp_len, 1)); repetition_stack.tmp_stack = ledit_reallocarray( repetition_stack.tmp_stack, new_alloc, sizeof(struct repetition_stack_elem) @@ -507,6 +504,7 @@ delete_chars_forwards(ledit_view *view, char *text, size_t len) { view, view->cur_line, view->cur_index ); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); + finalize_repetition_stack(); return (struct action){ACTION_NONE, NULL}; } @@ -537,6 +535,7 @@ delete_chars_backwards(ledit_view *view, char *text, size_t len) { view, view->cur_line, start_index ); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); + finalize_repetition_stack(); return (struct action){ACTION_NONE, NULL}; } @@ -615,7 +614,7 @@ move_to_line(ledit_view *view, char *text, size_t len) { (void)text; (void)len; motion_callback cb = NULL; - int repeat = get_key_repeat_and_motion_cb(&cb); + int repeat = get_key_repeat_and_motion_cb(view, &cb); size_t line; if (repeat > 0) line = (size_t)repeat > view->lines_num ? view->lines_num : (size_t)repeat; @@ -624,7 +623,11 @@ move_to_line(ledit_view *view, char *text, size_t len) { else return err_invalid_key(view); if (cb != NULL) { - cb(view, line - 1, 0, KEY_MOTION_LINE); + /* this is a bit of a hack - because move_to_line always works + with hard lines, it sets the index to ll->len so e.g. the delete + callback deletes until the end of the line even in soft line mode */ + ledit_line *ll = buffer_get_line(view->buffer, line - 1); + cb(view, line - 1, ll->len, KEY_MOTION_LINE); } else { view_wipe_line_cursor_attrs(view, view->cur_line); view->cur_line = line - 1; @@ -822,6 +825,7 @@ screen_up(ledit_view *view, char *text, size_t len) { (void)text; (void)len; int repeat = get_key_repeat(); + /* FIXME: overflow */ if (repeat >= 0) move_half_screen(view, -(repeat == 0 ? 2 : repeat*2)); else @@ -903,7 +907,7 @@ change(ledit_view *view, char *text, size_t len) { (void)len; CHECK_VIEW_LOCKED(view); motion_callback cb = NULL; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == -1) return err_invalid_key(view); if (view->mode == VISUAL) { @@ -971,7 +975,7 @@ yank(ledit_view *view, char *text, size_t len) { if (!paste_buffer) paste_buffer = txtbuf_new(); if (view->mode == VISUAL) { - view_sort_selection( + sort_range( &view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2 ); buffer_copy_text_to_txtbuf( @@ -990,7 +994,7 @@ yank(ledit_view *view, char *text, size_t len) { clear_key_stack(); } else { motion_callback cb = NULL; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == 0) num = 1; if (cb == &yank_cb) { @@ -1086,7 +1090,7 @@ delete(ledit_view *view, char *text, size_t len) { (void)len; CHECK_VIEW_LOCKED(view); motion_callback cb = NULL; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == -1) return err_invalid_key(view); if (delete_selection(view)) { @@ -1255,20 +1259,27 @@ paste_normal_backwards(ledit_view *view, char *text, size_t len) { } static void -push_num(int num) { +push_num(ledit_view *view, int num) { struct key_stack_elem *e = peek_key_stack(); if (!e || !(e->key & KEY_NUMBER)) { e = push_key_stack(); e->key = KEY_NUMBER; e->followup = KEY_NUMBER|KEY_NUMBERALLOWED; } - /* FIXME: error messages */ if (INT_MAX / 10 < e->count) { + window_show_message( + view->window, + "Integer overflow in key repetition", -1 + ); clear_key_stack(); return; } e->count *= 10; if (INT_MAX - num < e->count) { + window_show_message( + view->window, + "Integer overflow in key repetition", -1 + ); clear_key_stack(); return; } @@ -1280,7 +1291,7 @@ push_0(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(0); + push_num(view, 0); return (struct action){ACTION_NONE, NULL}; } @@ -1289,7 +1300,7 @@ push_1(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(1); + push_num(view, 1); return (struct action){ACTION_NONE, NULL}; } @@ -1298,7 +1309,7 @@ push_2(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(2); + push_num(view, 2); return (struct action){ACTION_NONE, NULL}; } @@ -1307,7 +1318,7 @@ push_3(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(3); + push_num(view, 3); return (struct action){ACTION_NONE, NULL}; } @@ -1316,7 +1327,7 @@ push_4(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(4); + push_num(view, 4); return (struct action){ACTION_NONE, NULL}; } @@ -1325,7 +1336,7 @@ push_5(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(5); + push_num(view, 5); return (struct action){ACTION_NONE, NULL}; } @@ -1334,7 +1345,7 @@ push_6(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(6); + push_num(view, 6); return (struct action){ACTION_NONE, NULL}; } @@ -1343,7 +1354,7 @@ push_7(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(7); + push_num(view, 7); return (struct action){ACTION_NONE, NULL}; } @@ -1352,7 +1363,7 @@ push_8(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(8); + push_num(view, 8); return (struct action){ACTION_NONE, NULL}; } @@ -1361,7 +1372,7 @@ push_9(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; - push_num(9); + push_num(view, 9); return (struct action){ACTION_NONE, NULL}; } @@ -1413,7 +1424,7 @@ move_to_eol(ledit_view *view, char *text, size_t len) { (void)len; CHECK_VIEW_LOCKED(view); motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == -1) return err_invalid_key(view); if (num == 0) @@ -1461,7 +1472,7 @@ name(ledit_view *view, char *text, size_t len) { (void)text; \ (void)len; \ motion_callback cb; \ - int num = get_key_repeat_and_motion_cb(&cb); \ + int num = get_key_repeat_and_motion_cb(view, &cb); \ if (num == -1) \ return err_invalid_key(view); \ if (num == 0) \ @@ -1510,7 +1521,7 @@ GEN_WORD_MOVEMENT(prev_bigword, view_prev_bigword) static void move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index) { motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == -1) (void)err_invalid_key(view); if (num == 0) @@ -1540,6 +1551,8 @@ move_cursor_left_right(ledit_view *view, int dir, int allow_illegal_index) { if (view->mode == VISUAL) { view_set_selection(view, view->sel.line1, view->sel.byte1, view->cur_line, new_index); } else if (view->mode == INSERT && view->sel_valid) { + /* FIXME: I guess this is unnecessary now that no + selection is allowed in insert mode */ view_wipe_selection(view); } else if (view->mode == NORMAL) { view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); @@ -1618,8 +1631,11 @@ enter_insert(ledit_view *view, char *text, size_t len) { (void)text; (void)len; CHECK_VIEW_LOCKED(view); - if (view->mode == NORMAL) + if (view->mode == NORMAL) { view_wipe_line_cursor_attrs(view, view->cur_line); + } else if (view->mode == VISUAL) { + view_wipe_selection(view); + } view_set_mode(view, INSERT); clear_key_stack(); return (struct action){ACTION_NONE, NULL}; @@ -1632,7 +1648,7 @@ move_cursor_up_down(ledit_view *view, int dir) { int new_softline; motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num == -1) (void)err_invalid_key(view); if (num == 0) @@ -1649,8 +1665,6 @@ move_cursor_up_down(ledit_view *view, int dir) { view_get_softline_bounds(view, new_line, new_softline, &start, &end); cb(view, new_line, start, KEY_MOTION_LINE); } else { - /* FIXME: when selecting on last line, moving down moves the cursor back - one (when it stays on the same line because it's the last one) */ int lineno, x; view_pos_to_x_softline(view, view->cur_line, view->cur_index, &x, &lineno); view->cur_index = view_x_softline_to_pos(view, new_line, x, new_softline); @@ -1744,7 +1758,7 @@ cursor_to_first_non_ws(ledit_view *view, char *text, size_t len) { (void)text; (void)len; motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num != 0) return err_invalid_key(view); size_t new_index = 0; @@ -1778,7 +1792,7 @@ cursor_to_beginning(ledit_view *view, char *text, size_t len) { (void)text; (void)len; motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num != 0) return err_invalid_key(view); /* FIXME: should anything be done with num? */ @@ -1831,13 +1845,11 @@ switch_selection_end(ledit_view *view, char *text, size_t len) { return (struct action){ACTION_NONE, NULL}; } -#define XK_ANY_MOD UINT_MAX -#define XK_NO_MOD 0 - static struct action enter_commandedit(ledit_view *view, char *text, size_t len) { (void)text; (void)len; + /* FIXME: wipe selection? */ char *str = view->sel_valid ? ":'<,'>" : ":"; window_set_bottom_bar_text(view->window, str, -1); window_set_bottom_bar_cursor(view->window, strlen(str)); @@ -1888,7 +1900,7 @@ static struct action jump_to_mark_cb(ledit_view *view, char *text, size_t len) { grab_char_cb = NULL; motion_callback cb; - int num = get_key_repeat_and_motion_cb(&cb); + int num = get_key_repeat_and_motion_cb(view, &cb); if (num > 0) return err_invalid_key(view); size_t line = 0, index = 0; @@ -1962,23 +1974,30 @@ static struct action show_line(ledit_view *view, char *text, size_t len) { (void)text; (void)len; - int textlen = snprintf(NULL, 0, "Line %zu of %zu", view->cur_line + 1, view->lines_num); - char *str = ledit_malloc(textlen + 1); - snprintf(str, textlen + 1, "Line %zu of %zu", view->cur_line + 1, view->lines_num); - window_show_message(view->window, str, textlen); + window_show_message_fmt( + view->window, + "%s: %s: line %zu of %zu", + view->buffer->filename ? view->buffer->filename : "(no filename)", + view->buffer->modified ? "modified" : "unmodified", + add_sz(view->cur_line, 1), view->lines_num + ); discard_repetition_stack(); return (struct action){ACTION_NONE, NULL}; } -/* FIXME: return status! */ static struct action undo(ledit_view *view, char *text, size_t len) { (void)text; (void)len; CHECK_VIEW_LOCKED(view); + int num = get_key_repeat(); + if (num == -1) + return err_invalid_key(view); + if (num == 0) + num = 1; view_wipe_selection(view); view_wipe_line_cursor_attrs(view, view->cur_line); - view_undo(view); + view_undo(view, num); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); finalize_repetition_stack(); return (struct action){ACTION_NONE, NULL}; @@ -1989,9 +2008,14 @@ redo(ledit_view *view, char *text, size_t len) { (void)text; (void)len; CHECK_VIEW_LOCKED(view); + int num = get_key_repeat(); + if (num == -1) + return err_invalid_key(view); + if (num == 0) + num = 1; view_wipe_selection(view); view_wipe_line_cursor_attrs(view, view->cur_line); - view_redo(view); + view_redo(view, num); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); finalize_repetition_stack(); return (struct action){ACTION_NONE, NULL}; @@ -2081,7 +2105,7 @@ dummy_cursor_helper(ledit_view *view, size_t line, size_t index, int num) { static struct action \ name##_cb(ledit_view *view, char *text, size_t len) { \ motion_callback cb = NULL; \ - int num = get_key_repeat_and_motion_cb(&cb); \ + int num = get_key_repeat_and_motion_cb(view, &cb); \ if (num == -1) \ return err_invalid_key(view); \ if (num == 0) \ @@ -2249,15 +2273,26 @@ repeat_command(ledit_view *view, char *text, size_t len) { (void)view; (void)text; (void)len; + int num = get_key_repeat(); + if (num == -1) + return err_invalid_key(view); + if (num == 0) + num = 1; + if (repetition_stack.len == 0) { + window_show_message(view->window, "No previous command", -1); + discard_repetition_stack(); + return (struct action){ACTION_NONE, NULL}; + } int found; repetition_stack.replaying = 1; - clear_key_stack(); - unwind_repetition_stack(); - struct repetition_stack_elem *e = get_cur_repetition_stack_elem(); - while (e) { - (void)handle_key(view, e->key_text, e->len, e->sym, e->key_state, e->lang_index, &found); - advance_repetition_stack(); - e = get_cur_repetition_stack_elem(); + for (int i = 0; i < num; i++) { + unwind_repetition_stack(); + struct repetition_stack_elem *e = get_cur_repetition_stack_elem(); + while (e) { + (void)handle_key(view, e->key_text, e->len, e->sym, e->key_state, e->lang_index, &found); + advance_repetition_stack(); + e = get_cur_repetition_stack_elem(); + } } repetition_stack.replaying = 0; discard_repetition_stack(); @@ -2290,11 +2325,13 @@ basic_key_handler(ledit_view *view, XEvent *event, int lang_index) { struct action act = handle_key(view, buf, (size_t)n, sym, key_state, lang_index, &found); if (found && n > 0 && !view->window->message_shown) window_hide_message(view->window); - else + else if (msg_shown) view->window->message_shown = msg_shown; + /* FIXME: add attribute for this to keys - this doesn't take e.g. cursor keys into account! */ if (found && n > 0) view_ensure_cursor_shown(view); - /* FIXME: maybe show error if not found */ + if (!found && n > 0) + window_show_message(view->window, "Invalid key", -1); return act; } diff --git a/keys_basic.h b/keys_basic.h @@ -1,3 +1,11 @@ +#ifndef _KEYS_BASIC_H_ +#define _KEYS_BASIC_H_ + +#include <X11/Xlib.h> +#include "view.h" + /* perform cleanup of global data */ void basic_key_cleanup(void); struct action basic_key_handler(ledit_view *view, XEvent *event, int lang_index); + +#endif diff --git a/keys_basic_config.h b/keys_basic_config.h @@ -20,7 +20,7 @@ struct key { char *text; /* for keys that correspond with text */ unsigned int mods; /* modifier mask */ KeySym keysym; /* for other keys, e.g. arrow keys */ - enum ledit_mode modes; /* modes in which this keybinding is functional */ + ledit_mode modes; /* modes in which this keybinding is functional */ enum key_type prev_keys; /* allowed previous keys */ enum key_type key_types; /* key types - used to determine if the key is allowed */ struct action (*func)(ledit_view *, char *, size_t); /* callback function */ @@ -109,7 +109,7 @@ static struct key keys_en[] = { {NULL, 0, XK_Right, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_right}, {NULL, 0, XK_Up, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_up}, {NULL, 0, XK_Down, VISUAL|INSERT|NORMAL, KEY_ANY, KEY_ANY, &cursor_down}, - {NULL, 0, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key}, + {NULL, XK_ANY_MOD, XK_Return, INSERT, KEY_ANY, KEY_ANY, &return_key}, {NULL, 0, XK_Delete, INSERT, KEY_ANY, KEY_ANY, &delete_key}, {NULL, 0, XK_Escape, NORMAL|VISUAL|INSERT, KEY_ANY, KEY_ANY, &escape_key}, {"i", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_insert}, @@ -142,16 +142,16 @@ static struct key keys_en[] = { {"c", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_MOTION|KEY_NUMBERALLOWED, &change}, {"v", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_visual}, {"o", 0, 0, VISUAL, KEY_ANY, KEY_ANY, &switch_selection_end}, - {"c", ControlMask, 0, INSERT|VISUAL, KEY_ANY, KEY_ANY, &clipcopy}, + {"c", ControlMask, 0, VISUAL, KEY_ANY, KEY_ANY, &clipcopy}, {"v", ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &clippaste}, {"g", ControlMask, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &show_line}, {":", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_commandedit}, - {"?", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_backward}, - {"/", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward}, - {"n", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &key_search_next}, - {"N", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &key_search_prev}, - {"u", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &undo}, - {"U", 0, 0, NORMAL|VISUAL, KEY_ANY, KEY_ANY, &redo}, + {"?", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_searchedit_backward}, + {"/", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &enter_searchedit_forward}, + {"n", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &key_search_next}, + {"N", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &key_search_prev}, + {"u", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &undo}, + {"U", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &redo}, {".", 0, 0, NORMAL, KEY_ANY, KEY_ANY, &repeat_command}, /* FIXME: only allow after finished key sequence */ {"z", ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &undo}, {"y", ControlMask, 0, INSERT, KEY_ANY, KEY_ANY, &redo}, /* FIXME: this is confusing with ctrl+y in normal mode */ diff --git a/keys_command.c b/keys_command.c @@ -27,6 +27,7 @@ #include "util.h" #include "keys.h" +#include "keys_config.h" #include "keys_command.h" #include "keys_command_config.h" @@ -57,9 +58,7 @@ history searchhistory = {0, 0, 0, NULL}; static void push_history(history *hist, char *cmd, size_t len) { if (hist->len >= hist->cap) { - size_t cap = hist->cap * 2 > hist->cap + 2 ? hist->cap * 2 : hist->cap + 2; - if (cap <= hist->len) - exit(1); /* FIXME: overflow */ + size_t cap = ideal_array_size(hist->cap, add_sz(hist->cap, 1)); hist->cmds = ledit_reallocarray(hist->cmds, cap, sizeof(char *)); hist->cap = cap; } @@ -100,15 +99,17 @@ view_locked_error(ledit_view *view) { #define CHECK_VIEW_LOCKED(view) if (view->lock_text) return view_locked_error(view) -/* FIXME: history for search and commands */ - static int create_view(ledit_view *view, char *cmd, size_t l1, size_t l2); static int close_view(ledit_view *view, char *cmd, size_t l1, size_t l2); static int handle_write(ledit_view *view, char *cmd, size_t l1, size_t l2); static int handle_quit(ledit_view *view, char *cmd, size_t l1, size_t l2); static int handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2); static int handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2); -static int parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid); +static int parse_range( + ledit_view *view, char *cmd, size_t len, char **cmd_ret, + size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid, + char **errstr_ret +); static int handle_cmd(ledit_view *view, char *cmd, size_t len); /* FIXME: remove command name before passing to handlers */ @@ -217,12 +218,7 @@ handle_write_quit(ledit_view *view, char *cmd, size_t l1, size_t l2) { static void show_num_substituted(ledit_view *view) { - char buf[30]; - if (snprintf(buf, sizeof(buf), "%d substitution(s)", sub_state.num) < 0) { - window_show_message(view->window, "This message is a bug, tell lumidify about it", -1); - } else { - window_show_message(view->window, buf, -1); - } + window_show_message_fmt(view->window, "%d substitution(s)", sub_state.num); } /* returns 1 when match was found, 0 otherwise */ @@ -311,12 +307,11 @@ substitute_all_remaining(ledit_view *view) { } if (min_line < view->lines_num) buffer_recalc_all_views_from_line(view->buffer, min_line); - /* FIXME: show number replaced */ + window_show_message_fmt(view->window, "Replaced %d occurrences", sub_state.num); /* this doesn't need to be added to the undo stack since it's called on undo/redo anyways */ view->cur_index = view_get_legal_normal_pos(view, view->cur_line, view->cur_index); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); view_ensure_cursor_shown(view); - show_num_substituted(view); buffer_unlock_all_views(view->buffer); } @@ -369,6 +364,11 @@ handle_substitute(ledit_view *view, char *cmd, size_t l1, size_t l2) { sub_state.num = 0; sub_state.start_group = 1; + /* trying to perform substitution in visual mode would make + it unnecessarily complicated */ + if (view->mode == VISUAL) + view_wipe_selection(view); + view_set_mode(view, NORMAL); if (confirm) { buffer_lock_all_views_except(view->buffer, view, "Ongoing substitution in other view."); view->cur_command_type = CMD_SUBSTITUTE; @@ -412,8 +412,12 @@ $ last line /* NOTE: Marks are only recognized here if they are one unicode character! */ /* NOTE: Only the line range of the selection is used at the moment. */ static int -parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid) { +parse_range( + ledit_view *view, char *cmd, size_t len, char **cmd_ret, + size_t *line1_ret, size_t *line2_ret, int *l1_valid, int *l2_valid, + char **errstr_ret) { (void)len; + *errstr_ret = ""; enum { START_LINENO = 1, START_RANGE = 2, @@ -426,14 +430,22 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin char *c = cmd; while (*c != '\0') { if (isdigit(*c)) { - /* FIXME: integer overflow */ if (s & IN_LINENO) { - if (*l2_valid) { - l2 = l2 * 10 + (*c - '0'); - } else { - l1 = l1 * 10 + (*c - '0'); + size_t *final = &l2; + if (!*l2_valid) { + final = &l1; *l1_valid = 1; } + if (SIZE_MAX / 10 < *final) { + *errstr_ret = "Integer overflow in range"; + return 1; + } + *final *= 10; + if (SIZE_MAX - (*c - '0') < *final) { + *errstr_ret = "Integer overflow in range"; + return 1; + } + *final += (*c - '0'); } else if ((s & START_LINENO) && (s & START_RANGE)) { l1 = *c - '0'; *l1_valid = 1; @@ -444,8 +456,10 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin s = IN_LINENO; } } else if (*c == '\'' && (s & START_LINENO)) { - if (c[1] == '\0' || c[2] == '\0') + if (c[1] == '\0' || c[2] == '\0') { + *errstr_ret = "Invalid range"; return 1; + } char *aftermark = next_utf8(c + 2); size_t marklen = aftermark - (c + 1); size_t l, b; @@ -455,7 +469,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin l = view->sel.line1 > view->sel.line2 ? view->sel.line1 : view->sel.line2; } else { if (buffer_get_mark(view->buffer, c + 1, marklen, &l, &b)) { - /* FIXME: show better error message */ + *errstr_ret = "Invalid mark"; return 1; } } @@ -471,6 +485,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin continue; } else if (*c == ',' && !(s & START_RANGE)) { if (*l1_valid && *l2_valid) { + *errstr_ret = "Invalid range"; return 1; } else { s = START_LINENO; @@ -481,11 +496,13 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin l2 = view->lines_num; *l1_valid = *l2_valid = 1; c++; + s = 0; break; } else { + *errstr_ret = "Invalid range"; return 1; } - } else if (*c == '$') { + } else if (*c == '.') { if (s & START_LINENO) { if (!*l1_valid) { l1 = view->cur_line + 1; @@ -496,6 +513,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin } s = 0; } else { + *errstr_ret = "Invalid range"; return 1; } } else if (*c == '$') { @@ -509,6 +527,7 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin } s = 0; } else { + *errstr_ret = "Invalid range"; return 1; } } else { @@ -516,10 +535,14 @@ parse_range(ledit_view *view, char *cmd, size_t len, char **cmd_ret, size_t *lin } c++; } - if ((!*l1_valid || !*l2_valid) && !(s & START_RANGE)) + if ((!*l1_valid || !*l2_valid) && !(s & START_RANGE)) { + *errstr_ret = "Invalid range"; return 1; - if ((*l1_valid || *l2_valid) && (l1 == 0 || l2 == 0 || l1 > view->lines_num || l2 > view->lines_num)) - return 1; /* FIXME: better error messages */ + } + if ((*l1_valid || *l2_valid) && (l1 == 0 || l2 == 0 || l1 > view->lines_num || l2 > view->lines_num)) { + *errstr_ret = "Invalid line number in range"; + return 1; + } *cmd_ret = c; /* ranges are given 1-indexed by user */ *line1_ret = l1 - 1; @@ -535,8 +558,9 @@ handle_cmd(ledit_view *view, char *cmd, size_t len) { char *c; size_t l1, l2; int l1_valid, l2_valid; - if (parse_range(view, cmd, len, &c, &l1, &l2, &l1_valid, &l2_valid)) { - window_show_message(view->window, "Error parsing command", -1); + char *errstr; + if (parse_range(view, cmd, len, &c, &l1, &l2, &l1_valid, &l2_valid, &errstr)) { + window_show_message(view->window, errstr, -1); return 0; } int range_given = l1_valid && l2_valid; @@ -743,7 +767,7 @@ edit_nextsearch(ledit_view *view, char *key_text, size_t len) { void search_next(ledit_view *view) { view_wipe_line_cursor_attrs(view, view->cur_line); - enum ledit_search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index); + search_state ret = ledit_search_next(view, &view->cur_line, &view->cur_index); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); view_ensure_cursor_shown(view); if (ret != SEARCH_NORMAL) @@ -753,7 +777,7 @@ search_next(ledit_view *view) { void search_prev(ledit_view *view) { view_wipe_line_cursor_attrs(view, view->cur_line); - enum ledit_search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index); + search_state ret = ledit_search_prev(view, &view->cur_line, &view->cur_index); view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); view_ensure_cursor_shown(view); if (ret != SEARCH_NORMAL) diff --git a/keys_command.h b/keys_command.h @@ -1,6 +1,14 @@ +#ifndef _KEYS_COMMAND_H_ +#define _KEYS_COMMAND_H_ + +#include <X11/Xlib.h> +#include "view.h" + /* these are only here so they can also be used by keys_basic */ void search_next(ledit_view *view); void search_prev(ledit_view *view); void command_key_cleanup(void); struct action command_key_handler(ledit_view *view, XEvent *event, int lang_index); + +#endif diff --git a/keys_command_config.h b/keys_command_config.h @@ -38,9 +38,9 @@ static struct key keys_en[] = { {"Y", 0, 0, CMD_SUBSTITUTE, &substitute_yes_all}, {"n", 0, 0, CMD_SUBSTITUTE, &substitute_no}, {"N", 0, 0, CMD_SUBSTITUTE, &substitute_no_all}, - {NULL, 0, XK_Return, CMD_EDIT, &edit_submit}, - {NULL, 0, XK_Return, CMD_EDITSEARCH, &editsearch_submit}, - {NULL, 0, XK_Return, CMD_EDITSEARCHB, &editsearchb_submit}, + {NULL, XK_ANY_MOD, XK_Return, CMD_EDIT, &edit_submit}, + {NULL, XK_ANY_MOD, XK_Return, CMD_EDITSEARCH, &editsearch_submit}, + {NULL, XK_ANY_MOD, XK_Return, CMD_EDITSEARCHB, &editsearchb_submit}, {NULL, 0, XK_Left, CMD_EDIT, &edit_cursor_left}, {NULL, 0, XK_Left, CMD_EDITSEARCH, &edit_cursor_left}, {NULL, 0, XK_Left, CMD_EDITSEARCHB, &edit_cursor_left}, diff --git a/keys_config.h b/keys_config.h @@ -0,0 +1,31 @@ +#ifndef _KEYS_CONFIG_H_ +#define _KEYS_CONFIG_H_ + +#include "keys.h" + +/* + * These are the language strings compared with the language strings that + * xkb gives in order to change the key mapping on layout change events. + */ +#define KEY_LANGS \ +static char *key_langs[] = { \ + "English (US)", \ + "German", \ + "Urdu (Pakistan)", \ + "Hindi (Bolnagri)" \ +} + +#define GEN_KEY_ARRAY(key_struct, en, de, ur, hi) \ +static struct { \ + key_struct *keys; \ + int num_keys; \ +} keys[] = { \ + {en, LENGTH(en)}, \ + {de, LENGTH(de)}, \ + {ur, LENGTH(ur)}, \ + {hi, LENGTH(hi)} \ +} + +#define LANG_KEYS(index) &keys[index] + +#endif diff --git a/ledit.1 b/ledit.1 @@ -0,0 +1,796 @@ +.\" WARNING: Some parts of this are stolen shamelessly from OpenBSD's +.\" vi(1) manpage! +.Dd December 18, 2021 +.Dt LEDIT 1 +.Os +.Sh NAME +.Nm ledit +.Nd weird text editor +.Sh SYNOPSIS +.Nm +.Op Ar file +.Sh DESCRIPTION +.Nm +is a vi-like text editor for people who switch between keyboard layouts +frequently and/or work with languages that require complex text layout. +.Pp +It is assumed that readers of this manual page are already familiar +with +.Xr vi 1 . +Differences with +.Xr vi 1 +are documented, but it is very likely that many have been missed. +If you find an important difference that is not documented, please +contact me. +.Sh ANTI-DESCRIPTION +.Bl -tag -width Ds +.It Nm +is not a code editor. +Features for code editing may be added in the future, but the main +purpose is currently to edit other text. +.It Nm +is not a general-purpose text editor. +It is content to be useful for some tasks and does not feel insulted when +other editors are used for other tasks. +.It Nm +is not a minimalistic text editor. +It probably counts as reasonably minimalistic in the modern world, but +that is not the main goal. +.It Nm +is not a good text editor. +.El +.Sh KEY BINDINGS +The key bindings listed here are given as the default English bindings. +These will, of course, not be accurate for other languages or if +the configuration is changed. +.Pp +Some commands change their behavior depending on whether the mode is set +to soft line or hard line. +This is sometimes a bit inconsistent, but at least it might help when +editing longer paragraphs with no manual line breaking. +.Pp +Note that this list is currently not sorted in any logical way. +That will hopefully be fixed in the future. +.Ss NORMAL MODE +.Bl -tag -width Ds -compact +.It Xo +.Op Ar count +.Aq Cm arrow down +.Xc +.It Xo +.Op Ar count +.Aq Cm control-j +.Xc +.It Xo +.Op Ar count +.Aq Cm control-n +.Xc +.It Xo +.Op Ar count +.Cm j +.Xc +Move the cursor down +.Ar count +lines. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Aq Cm arrow up +.Xc +.It Xo +.Op Ar count +.Aq Cm control-p +.Xc +.It Xo +.Op Ar count +.Cm k +.Xc +Move the cursor up +.Ar count +lines. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Aq Cm arrow right +.Xc +.It Xo +.Op Ar count +.Aq Cm space +.Xc +.It Xo +.Op Ar count +.Cm l +.Xc +Move the cursor right +.Ar count +characters in the current line. +Note that this is a visual operation, i.e. the cursor will still move right +if the text is right-to-left. +.Pp +.It Xo +.Op Ar count +.Aq Cm arrow left +.Xc +.It Xo +.Op Ar count +.Aq Cm control-h +.Xc +.It Xo +.Op Ar count +.Cm h +.Xc +Move the cursor left +.Ar count +characters in the current line. +Note that this is a visual operation, i.e. the cursor will still move left +if the text is right-to-left. +.Pp +.It Aq Cm escape +Clear the key stack (i.e. cancel multi-key command). +.Pp +.It Cm i +Enter insert mode. +.Pp +.It Cm v +Enter visual mode. +.Pp +.It Aq Cm control-t +Toggle mode between hard line and soft line based. +.Pp +.It Cm 0 +Move cursor to beginning of line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm x +.Xc +Delete +.Ar count +characters after the cursor on the current line and copy the +deleted text into the paste buffer. +.Pp +.It Xo +.Op Ar count +.Cm X +.Xc +Delete +.Ar count +characters before the cursor on the current line and copy the +deleted text into the paste buffer. +.Pp +.It Xo +.Op Ar count +.Cm d +.Ar motion +.Xc +Delete the region of text described by +.Ar count +and +.Ar motion +and copy the deleted text into the paste buffer. +If +.Ar motion +is +.Cm d +again, +.Ar count +lines are deleted, starting with the current line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Cm D +Delete all text from the current cursor position to the end of +the line and copy the deleted text into the paste buffer. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm y +.Ar motion +.Xc +Copy the region of text described by +.Ar count +and +.Ar motion +into the paste buffer. +If +.Ar motion +is +.Cm y +again, +.Ar count +lines are copied, starting with the current line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm Y +.Xc +Copy +.Ar count +lines into the paste buffer. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm c +.Ar motion +.Xc +Change the region described by +.Ar count +and +.Ar motion +and copy the changed text into the paste buffer. +.Pp +.It Cm C +Change all text from the current cursor position to the end of +the line and copy the changed text into the paste buffer. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Aq Cm control-g +Show the current filename, whether the buffer has been modified since the +last write, and the current line. +.Pp +.It Cm \&: +Enter the line-editing mode for running a command. +.Pp +.It Cm / +Search forward for a search term. +Note that no regex is currently supported. +.Pp +.It Cm \&? +Search backwards for a search term. +Note that no regex is currently supported. +.Pp +.It Cm n +Move to the next match of the last search term in the direction of the +last search. +.Pp +.It Cm N +Move to the next match of the last search term in the direction opposite +to the direction of the last search. +.Pp +.It Xo +.Op Ar count +.Cm u +.Xc +Undo +.Ar count +operations. +Note that an entire session in insert mode is considered as one operation +when in normal mode. +.Pp +.It Xo +.Op Ar count +.Cm U +.Xc +Redo +.Ar count +operations. +Note that an entire session in insert mode is considered as one operation +when in normal mode. +.Pp +.It Xo +.Op Ar count +.Cm \&. +.Xc +Repeat the last command +.Ar count +times. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-b +.Xc +Move +.Ar count +screens up. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-f +.Xc +Move +.Ar count +screens down. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-y +.Xc +Move +.Ar count +lines up, attemting to leave the cursor in its current line and +character position. +Note that this command works with soft lines, regardless of the +current mode. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-e +.Xc +Move +.Ar count +lines down, attemting to leave the cursor in its current line and +character position. +Note that this command works with soft lines, regardless of the +current mode. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-u +.Xc +Move +.Ar count +lines up. +If +.Ar count +is not given, scroll up the number of lines specified by the last +.Aq Cm control-d +or +.Aq Cm control-u +command. +If this is the first such command, scroll up half a screen. +Note that this command works with soft lines, regardless of the +current mode. +.Pp +.It Xo +.Op Ar count +.Aq Cm control-d +.Xc +Move +.Ar count +lines down. +If +.Ar count +is not given, scroll down the number of lines specified by the last +.Aq Cm control-d +or +.Aq Cm control-u +command. +If this is the first such command, scroll down half a screen. +Note that this command works with soft lines, regardless of the +current mode. +.Pp +.It Cm $ +Move to the last cursor position on the current line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm w +.Xc +Move forward +.Ar count +words. +.Pp +.It Xo +.Op Ar count +.Cm W +.Xc +Move forward +.Ar count +bigwords. +.Pp +.It Xo +.Op Ar count +.Cm e +.Xc +Move forward +.Ar count +end-of-words. +.Pp +.It Xo +.Op Ar count +.Cm E +.Xc +Move forward +.Ar count +end-of-bigwords. +.Pp +.It Xo +.Op Ar count +.Cm b +.Xc +Move backwards +.Ar count +words. +.Pp +.It Xo +.Op Ar count +.Cm B +.Xc +Move backwards +.Ar count +bigwords. +.Pp +.It Xo +.Op Ar count +.Cm G +.Xc +Move to the line number given by +.Ar count . +If +.Ar count +is not given, move to the last line in the buffer. +.Pp +.It Xo +.Op Ar count +.Cm J +.Xc +Join the current line with the next one +.Ar count +times. +Note that this command always works on hard lines, regardless +of the current mode. +Also note that this currently does not compress whitespace between +the lines as other vi-like editors do. +This is due to the author's laziness. +.Pp +.It Cm I +Move cursor to beginning of line and enter insert mode. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Cm p +Paste text from the paste buffer after the current cursor position if the +buffer is character-based and after the current line if it is line-based. +Note that this does take into account the hard line/soft line mode, but +it behaves a bit weirdly when in soft line mode - it inserts the text +after the current soft line but adds newlines on both sides. +This behavior may be changed in the future if it turns out there's a more +logical behavior for soft line mode. +.Pp +.It Cm P +Paste text from the paste buffer before the current cursor position if the +buffer is character-based and before the current line if it is line-based. +The quirk for +.Cm p +applies here as well. +.Pp +.It Cm A +Append text after the current line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Cm a +Append text after the current cursor position. +.Pp +.It Cm o +Append a new line after the current line and enter insert mode there. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Cm O +Append a new line before the current line and enter insert mode there. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Cm m +.Aq Cm character +.Xc +Mark the current current cursor position as +.Aq Cm character . +.Pp +.It Xo +.Cm ' +.Aq Cm character +.Xc +Jump to a position previously marked as +.Aq Cm character +with +.Cm m . +.Pp +.It Xo +.Cm r +.Aq Cm character +.Xc +Replace the character at the current cursor position with +.Aq Cm character . +.Pp +.It Cm ^ +Move to the first non-whitespace character on the current line. +This changes behavior depending on the hard/soft line mode. +.Pp +.It Xo +.Op Ar count +.Cm t +.Aq Cm character +.Xc +Search forward, +.Op Ar count +times, through the current line for the cursor position immediately before +.Aq Cm character . +.Pp +.It Xo +.Op Ar count +.Cm T +.Aq Cm character +.Xc +Search backwards, +.Op Ar count +times, through the current line for the cursor position after +.Aq Cm character . +.Pp +.It Xo +.Op Ar count +.Cm f +.Aq Cm character +.Xc +Search forward, +.Op Ar count +times, through the current line for +.Aq Cm character . +.Pp +.It Xo +.Op Ar count +.Cm F +.Aq Cm character +.Xc +Search backwards, +.Op Ar count +times, through the current line for +.Aq Cm character . +.El +.Ss VISUAL MODE +The movement keys generally work the same in visual mode but change the +selection instead of just moving to the new position, so they are not +listed here separately. +.Pp +The +.Cm d , +.Cm y , +and +.Cm c +keys also work similarly, but operate on the range given by the selection. +.Pp +The +.Cm \&: +key automatically pastes the selection range into the line editor so a +command can be run over the range specified by the selection. +.Pp +Additionally, these keys are supported: +.Pp +.Bl -tag -width Ds -compact +.It Cm o +Switch the end of the selection that is modified by the movement keys. +.Pp +.It Aq Cm control-c +Copy the current selection to the clipboard. +.Pp +.It Aq Cm escape +Return to normal mode. +.El +.Ss INSERT MODE +All regular keys simply insert the corresponding text at the current cursor +position. +.Pp +The cursor keys, backspace, delete, and return all work as expected. +.Pp +Additionally, the following keys are supported: +.Pp +.Bl -tag -width Ds -compact +.It Aq Cm control-v +Paste text from the clipboard at the current cursor position. +.Pp +.It Aq Cm control-z +Undo one operation. +Note that this, in contrast to the undo in normal mode, does not consider +an entire insert session to be one operation. +.Pp +.It Aq Cm control-y +Redo one operation. +Note that this, in contrast to the redo in normal mode, does not consider +an entire insert session to be one operation. +.Pp +.It Aq Cm escape +Return to normal mode. +.El +Note that many keys that are common in other editors are not recognized currently. +That will hopefully be fixed in the future. +.Ss LINE EDITING MODE +These key bindings work in the line editor that is used for searching or +running commands. +.Pp +.Bl -tag -width DS -compact +.It Aq Cm return +Submit the search or command. +.Pp +.It Aq Cm arrow left +Move the cursor one to the left. +.Pp +.It Aq Cm arrow right +Move the cursor one to the right. +.Pp +.It Aq Cm arrow up +.It Aq Cm arrow down +Move through the search or command history. +Note that the search and command histories are separate. +.Pp +.It Aq Cm backspace +Delete one unicode character before the cursor. +.Pp +.It Aq Cm delete +Delete one unicode character after the cursor. +.Pp +.It Aq Cm end +Move the cursor to the end of the line. +.Pp +.It Aq Cm home +Move the cursor to the beginning of the line. +.Pp +.It Aq Cm escape +Cancel the search or command. +.El +.Ss MISCELLANEOUS +.Bl -tag -width DS +.It Keys while performing substitution with confirmation: +.Bl -tag -width Ds +.It Cm y +Confirm the current substitution. +.It Cm n +Reject the current substitution. +.It Cm Y +Confirm the current substitution and all further ones. +.It Cm N +Reject the current substitution and all further ones. +.El +.Pp +Note that these keys are also displayed during the substitution, but only +the default English bindings are shown because implementing anything else +would require work. +.El +.Sh COMMANDS +Note: The terminology is currently a bit inconsistent. +Sometimes, +.Dq commands +refers to the key commands, sometimes to the commands +written in the line editor, which are documented in this section. +.Pp +Note that the commands which take filenames currently use the entire rest of +the line as the filename instead of doing any string parsing. +This may be changed in the future. +.Pp +.Bl -tag -width Ds -compact +.It Xo +.Cm :w +.Op Ar filename +.Xc +Write the buffer to +.Op Ar filename , +or, if no filename is given, to the file the buffer was read from. +.Pp +.It Xo +.Cm :w\&! +.Op Ar filename +.Xc +Same as +.Cm :w , +but the file will be attempted to be written to even if there +is something blocking it (e.g. the modified date of the file is newer +than it was when it was opened). +.Pp +.It Cm :q +Quit. +.Pp +.It Cm :q\&! +Quit, even if there are unsaved changes. +.Pp +.It Xo +.Cm :wq +.Op Ar filename +.Xc +.It Xo +.Cm :wq\&! +.Op Ar filename +.Xc +Write and quit afterwards. +The +.Cm \&! +is interpreted as for normal writing. +.Pp +.It Xo +.Sm off +.Op Ar range +.Cm s / Ar pattern Cm / Ar replace Cm / +.Op Ar options +.Sm on +.Xc +Substitute +.Ar pattern +with +.Ar replace +in the given line range. +If no range is given, substitution is only performed on the current line. +Note that no regex is currently supported. +.Pp +The range consists of two line numbers separated by a comma or the special value +.Cm % , +which refers to the entire file. +The following special values are possible instead of writing a line number directly: +.Bl -tag -width Ds +.It Cm $ +The last line in the file. +.It Xo +.Sm off +.Cm ' Aq Cm mark +.Sm on +.Xc +The line of the previously set mark +.Aq Cm mark . +Note that even though marks can theoretically be any string of characters, +they are only allowed to be one unicode character if they are used in a range. +The special values +.Cm < +and +.Cm > +are possible, which refer to the first and last line, respectively, in the +current selection. +.It Cm \&. +The current line. +.El +.Pp +The +.Ar options +may be a combination of the following: +.Bl -tag -width Ds +.It Cm g +Perform substitution for all occurrences in the given lines instead of just +the first one on each line. +.It Cm c +Confirm each substitution before performing it. +.El +.Pp +.It Cm :v +Open a new view. +Each view is a window that shows the text in the current buffer, +which is synced between the views. +.El +.Sh MOUSE ACTIONS +There currently are not many mouse actions. +Clicking and dragging with the left mouse button enters visual mode and +selects text, which is always copied into the X11 primary selection. +.Pp +Note that text selection currently does not work in the line editor +because the author is too lazy to implement that. +.Sh CONFIGURATION +(Todo) - also document weirdness with xkb language strings +.Sh MISCELLANEOUS +(Todo) - document emergency dumps +.Sh EXIT STATUS +.Ex -std +.Sh QUIRKS +The cursor movement commands try to move left/right visually instead of moving +through the text logically. +This causes weird cursor jumps when working with bidirectional text in normal mode. +This may be fixed in the future, but it currently is not clear how to make the +behavior more logical. +.Pp +Since a new mode group is started each time insert is entered, when text +is typed in one view in insert, then in another view, and then again in +the first one, the last two inserts will be undone in one go since both +views were in insert already. +I'm not sure how to make this more logical, though. +Maybe it could be +.Dq improved +by also saving the view in the undo stack, +but that would cause problems because views can be added and removed, +and it would maybe not even be more logical. +.Pp +Scroll offset is stored as a pixel value, so a view may scroll when text is +added or deleted in another view. +Additionally, when a new view is created, the scroll offset from the old view +is taken, which may be weird if the window of the new view is a different size. +.Pp +(Todo) - document weirdness with spaces at end of line in normal mode +.Sh SEE ALSO +.Xr ed 1 , +.Xr vi 1 , +.Xr vim 1 +.Sh AUTHORS +.An lumidify Aq Mt nobody@lumidify.org +.Sh BUGS +Too many to count. +See +.Sx TINY SUBSET OF BUGS . +.Sh TINY SUBSET OF BUGS +(Todo) diff --git a/ledit.c b/ledit.c @@ -1,4 +1,3 @@ -/* FIXME: clean up asserts a bit; clean up includes */ /* FIXME: On large files, expose event takes a long time for some reason -> but somehow only sometimes... */ /* FIXME: generally optimize redrawing */ @@ -7,50 +6,38 @@ /* FIXME: Document that everything is assumed to be utf8 */ /* FIXME: Only redraw part of screen if needed */ /* FIXME: overflow in repeated commands */ -/* FIXME: Fix lag when scrolling - combine repeated mouse motion events */ -/* FIXME: Fix lag when selecting with mouse */ /* FIXME: Use PANGO_PIXELS() */ /* FIXME: Fix cursor movement, especially buffer->trailing and writing at end of line */ /* FIXME: horizontal scrolling (also need cache to avoid too large pixmaps) */ -/* FIXME: sort out types for indices (currently just int, but that might overflow) */ /* TODO: allow extending selection with shift+mouse like in e.g. gtk */ -#include <math.h> + #include <time.h> -#include <stdio.h> #include <errno.h> -#include <string.h> +#include <stdio.h> #include <stdlib.h> -#include <limits.h> -#include <unistd.h> +#include <string.h> #include <locale.h> +#include <unistd.h> +#include <sys/stat.h> #include <X11/Xlib.h> -#include <X11/Xatom.h> -#include <X11/Xutil.h> -#include <X11/keysym.h> -#include <X11/XF86keysym.h> -#include <X11/cursorfont.h> -#include <pango/pangoxft.h> #include <X11/XKBlib.h> -#include <X11/extensions/XKBrules.h> #include <X11/extensions/Xdbe.h> +#include <X11/extensions/XKBrules.h> -#include "config.h" -#include "memory.h" -#include "common.h" -#include "txtbuf.h" +#include "view.h" #include "theme.h" -#include "window.h" -#include "cache.h" -#include "undo.h" #include "buffer.h" -#include "view.h" +#include "common.h" +#include "window.h" #include "search.h" +#include "macros.h" +#include "memory.h" +#include "config.h" +#include "cleanup.h" #include "keys.h" #include "keys_basic.h" #include "keys_command.h" -#include "macros.h" -#include "cleanup.h" static void mainloop(void); static void setup(int argc, char *argv[]); @@ -326,6 +313,12 @@ ledit_emergencydump(void) { /* FIXME: maybe just leave the file in case at least part of it was written? */ unlink(template); + } else { + fprintf( + stderr, + "Wrote emergency dump to %s\n", + template + ); } free(template); } diff --git a/macros.h b/macros.h @@ -1,3 +1,6 @@ +#ifndef _MACROS_H_ +#define _MACROS_H_ + /* stolen from OpenBSD */ #define ledit_timespecsub(tsp, usp, vsp) \ do { \ @@ -8,3 +11,5 @@ (vsp)->tv_nsec += 1000000000L; \ } \ } while (0) + +#endif diff --git a/memory.c b/memory.c @@ -3,8 +3,9 @@ #include <stdlib.h> #include <string.h> -#include "cleanup.h" #include "assert.h" +#include "memory.h" +#include "cleanup.h" static void fatal_err(const char *msg) { @@ -15,14 +16,18 @@ fatal_err(const char *msg) { void err_overflow(void) { - fprintf(stderr, "Integer overflow.\n"); - ledit_cleanup(); - exit(1); + (void)fprintf(stderr, "Integer overflow.\n"); + ledit_emergencydump(); + abort(); } +/* FIXME: should these perform emergencydump instead of just + fatal_err? It probably isn't of much use when there isn't + even any memory left. */ char * ledit_strdup(const char *s) { char *str = strdup(s); + ledit_assert(str && "Out of memory."); if (!str) fatal_err("Out of memory.\n"); return str; @@ -93,7 +98,7 @@ ledit_reallocarray(void *optr, size_t nmemb, size_t size) { if ((nmemb >= MUL_NO_OVERFLOW || size >= MUL_NO_OVERFLOW) && nmemb > 0 && SIZE_MAX / nmemb < size) { - fatal_err("Integer overflow in reallocarray.\n"); + err_overflow(); } return realloc(optr, size * nmemb); } @@ -141,57 +146,76 @@ resize_and_move_gap( size_t *new_gap_ret, size_t *new_cap_ret) { ledit_assert(index <= len); ledit_assert(len <= old_cap); + ledit_assert(old_gap <= len); size_t gap_size = old_cap - len; - size_t new_cap = old_cap; - /* FIXME: read up on what the best values are here */ - if (new_cap < min_size) - new_cap = old_cap * 2 > min_size ? old_cap * 2 : min_size; - if (new_cap < min_size) - err_overflow(); - if (new_cap != old_cap) - array = ledit_reallocarray(array, new_cap, elem_size); - char *carray = (char*)array; /* cast to char to do pointer arithmetic */ - /* we already know new_cap * elem_size does not wrap around because array - is of that size, so all the other multiplications here should be safe - (at least that's what I think, but I may be wrong) */ - if (index > old_gap) { - /* move piece between end of original gap and index to - beginning of original gap */ - memmove( - carray + old_gap * elem_size, - carray + (old_gap + gap_size) * elem_size, - (index - old_gap) * elem_size - ); - /* move piece after index to end of buffer */ - memmove( - carray + (new_cap - (len - index)) * elem_size, - carray + (index + gap_size) * elem_size, - (len - index) * elem_size - ); - } else if (index < old_gap) { - /* move piece after original gap to end of buffer */ - memmove( - carray + (new_cap - (len - old_gap)) * elem_size, - carray + (old_gap + gap_size) * elem_size, - (len - old_gap) * elem_size - ); - /* move piece between index and original gap to end */ - memmove( - carray + (new_cap - len + index) * elem_size, - carray + index * elem_size, - (old_gap - index) * elem_size - ); + size_t new_cap = ideal_array_size(old_cap, min_size);; + if (new_cap >= old_cap) { + if (new_cap > old_cap) + array = ledit_reallocarray(array, new_cap, elem_size); + char *carray = (char*)array; /* cast to char to do pointer arithmetic */ + /* we already know new_cap * elem_size does not wrap around because array + is of that size, so all the other multiplications here should be safe + (at least that's what I think, but I may be wrong) */ + if (index > old_gap) { + /* move piece between end of original gap and index to + beginning of original gap */ + memmove( + carray + old_gap * elem_size, + carray + (old_gap + gap_size) * elem_size, + (index - old_gap) * elem_size + ); + /* move piece after index to end of buffer */ + memmove( + carray + (new_cap - (len - index)) * elem_size, + carray + (index + gap_size) * elem_size, + (len - index) * elem_size + ); + } else if (index < old_gap) { + /* move piece after original gap to end of buffer */ + memmove( + carray + (new_cap - (len - old_gap)) * elem_size, + carray + (old_gap + gap_size) * elem_size, + (len - old_gap) * elem_size + ); + /* move piece between index and original gap to end */ + memmove( + carray + (new_cap - len + index) * elem_size, + carray + index * elem_size, + (old_gap - index) * elem_size + ); + } else { + /* move piece after original gap to end of buffer */ + memmove( + carray + (new_cap - (len - old_gap)) * elem_size, + carray + (old_gap + gap_size) * elem_size, + (len - old_gap) * elem_size + ); + } } else { - /* move piece after original gap to end of buffer */ - memmove( - carray + (new_cap - (len - old_gap)) * elem_size, - carray + (old_gap + gap_size) * elem_size, - (len - old_gap) * elem_size - ); + /* otherwise, parts may be cut off */ + ledit_assert(min_size >= len); + /* FIXME: optimize this */ + move_gap(array, elem_size, len, old_gap, old_cap, len, NULL); + array = ledit_reallocarray(array, new_cap, elem_size); + move_gap(array, elem_size, index, len, new_cap, len, NULL); } if (new_gap_ret) *new_gap_ret = index; if (new_cap_ret) *new_cap_ret = new_cap; - return carray; + return array; +} + +/* FIXME: maybe don't double when already very large? */ +/* FIXME: better start size when old == 0? */ +size_t +ideal_array_size(size_t old, size_t needed) { + size_t ret = old; + if (old < needed) + ret = old * 2 > needed ? old * 2 : needed; + else if (needed * 4 < old) + ret = old / 2; + if (ret == 0) + ret = 1; /* not sure if this is necessary */ + return ret; } diff --git a/memory.h b/memory.h @@ -1,3 +1,9 @@ +#ifndef _MEMORY_H_ +#define _MEMORY_H_ + +#include <stddef.h> +#include <stdint.h> + /* * These functions all wrap the regular functions but exit on error. */ @@ -28,6 +34,7 @@ void move_gap( /* * Resize a generic gap buffer with elements of size 'elem_size' to hold at least * 'min_size' elements and move the gap to element position 'index'. + * The array size may be increased or decreased. * 'old_gap' is the old index of the gap buffer, 'old_cap' is the total length of the * array (number of elements, not bytes), and 'len' is the number of valid elements. * 'index' is also written to 'new_gap_ret' if it is not NULL. This is just @@ -45,3 +52,12 @@ void *resize_and_move_gap( /* FIXME: not sure if this really belongs here */ void err_overflow(void); + +/* + * Return the ideal new size for an array of size 'old' when resizing it + * so it fits at least 'needed' elements. The return value may be smaller + * than 'old' if 'needed' is smaller. + */ +size_t ideal_array_size(size_t old, size_t needed); + +#endif diff --git a/pango-compat.h b/pango-compat.h @@ -1,4 +1,11 @@ +#ifndef _PANGO_COMPAT_H_ +#define _PANGO_COMPAT_H_ + +#include <pango/pangoxft.h> + //#if !PANGO_VERSION_CHECK(1, 46, 0) #if 1 PangoDirection ledit_pango_layout_get_direction(PangoLayout *layout, int index); #endif + +#endif diff --git a/search.c b/search.c @@ -1,22 +1,9 @@ -/* FIXME: split buffer into pure text part and graphical part so this - * doesn't depend on all the graphics stuff */ - #include <string.h> -#include <X11/Xlib.h> -#include <X11/Xutil.h> -#include <pango/pangoxft.h> -#include <X11/extensions/Xdbe.h> - -#include "memory.h" -#include "common.h" -#include "txtbuf.h" -#include "undo.h" -#include "cache.h" -#include "theme.h" -#include "window.h" +#include "view.h" #include "buffer.h" #include "search.h" +#include "memory.h" /* FIXME: make sure only whole utf8 chars are matched */ static char *last_search = NULL; @@ -44,7 +31,7 @@ set_search_backward(char *pattern) { last_search = ledit_strdup(pattern); } -static enum ledit_search_state +static search_state search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) { *line_ret = view->cur_line; *byte_ret = view->cur_index; @@ -93,7 +80,7 @@ search_forward(ledit_view *view, size_t *line_ret, size_t *byte_ret) { /* FIXME: this is insanely inefficient */ /* FIXME: just go backwards char-by-char and compare */ -static enum ledit_search_state +static search_state search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) { *line_ret = view->cur_line; *byte_ret = view->cur_index; @@ -157,7 +144,7 @@ search_backward(ledit_view *view, size_t *line_ret, size_t *byte_ret) { return SEARCH_NOT_FOUND; } -enum ledit_search_state +search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret) { if (last_dir == FORWARD) return search_forward(view, line_ret, byte_ret); @@ -165,7 +152,7 @@ ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret) { return search_backward(view, line_ret, byte_ret); } -enum ledit_search_state +search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret) { if (last_dir == FORWARD) return search_backward(view, line_ret, byte_ret); @@ -174,16 +161,18 @@ ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret) { } char * -search_state_to_str(enum ledit_search_state state) { - switch (state) { - case SEARCH_WRAPPED: - return "Search wrapped"; - case SEARCH_NOT_FOUND: - return "Pattern not found"; - case SEARCH_NO_PATTERN: - return "No previous search pattern"; - default: - return "This message should not be shown. " - "Please bug lumidify about it."; +search_state_to_str(search_state s) { + switch (s) { + case SEARCH_NORMAL: + return "Found match"; + case SEARCH_WRAPPED: + return "Search wrapped"; + case SEARCH_NOT_FOUND: + return "Pattern not found"; + case SEARCH_NO_PATTERN: + return "No previous search pattern"; + default: + return "This message should not be shown. " + "Please bug lumidify about it."; } } diff --git a/search.h b/search.h @@ -1,13 +1,25 @@ -enum ledit_search_state { +#ifndef _SEARCH_H_ +#define _SEARCH_H_ + +#include <stddef.h> + +typedef enum { SEARCH_NORMAL, SEARCH_WRAPPED, SEARCH_NOT_FOUND, SEARCH_NO_PATTERN -}; +} search_state; void search_cleanup(void); void set_search_forward(char *pattern); void set_search_backward(char *pattern); -enum ledit_search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret); -enum ledit_search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret); -char *search_state_to_str(enum ledit_search_state state); +search_state ledit_search_next(ledit_view *view, size_t *line_ret, size_t *byte_ret); +search_state ledit_search_prev(ledit_view *view, size_t *line_ret, size_t *byte_ret); + +/* + * Get a string corresponding to a search_state. + * This string should not be freed. + */ +char *search_state_to_str(search_state s); + +#endif diff --git a/theme.h b/theme.h @@ -1,3 +1,9 @@ +#ifndef _THEME_H_ +#define _THEME_H_ + +#include <X11/Xft/Xft.h> +#include "common.h" + typedef struct { int scrollbar_width; int scrollbar_step; @@ -14,3 +20,5 @@ typedef struct { ledit_theme *theme_create(ledit_common *common); void theme_destroy(ledit_common *common, ledit_theme *theme); + +#endif diff --git a/txtbuf.c b/txtbuf.c @@ -1,6 +1,7 @@ #include <stdlib.h> #include <string.h> +#include "util.h" #include "memory.h" #include "txtbuf.h" @@ -13,20 +14,13 @@ txtbuf_new(void) { } void -txtbuf_grow(txtbuf *buf, size_t sz) { +txtbuf_resize(txtbuf *buf, size_t sz) { /* always leave room for extra \0 */ - if (sz + 1 > buf->cap) { - /* FIXME: what are the best values here? */ - buf->cap = buf->cap * 2 > sz + 1 ? buf->cap * 2 : sz + 1; - buf->text = ledit_realloc(buf->text, buf->cap); - } -} - -void -txtbuf_shrink(txtbuf *buf) { - if ((buf->len + 1) * 4 < buf->cap) { - buf->cap /= 2; - buf->text = ledit_realloc(buf->text, buf->cap); + /* FIXME: '\0' isn't actually used anywhere */ + size_t cap = ideal_array_size(buf->cap, add_sz(sz, 1)); + if (cap != buf->cap) { + buf->text = ledit_realloc(buf->text, cap); + buf->cap = cap; } } @@ -40,7 +34,7 @@ txtbuf_destroy(txtbuf *buf) { void txtbuf_copy(txtbuf *dst, txtbuf *src) { - txtbuf_grow(dst, src->len); + txtbuf_resize(dst, src->len); memcpy(dst->text, src->text, src->len); dst->len = src->len; } diff --git a/txtbuf.h b/txtbuf.h @@ -1,3 +1,8 @@ +#ifndef _TXTBUF_H_ +#define _TXTBUF_H_ + +#include <stddef.h> + /* * txtbuf is really just a string data type that is badly named. */ @@ -16,13 +21,7 @@ txtbuf *txtbuf_new(void); * Make sure the txtbuf has space for at least the given size, * plus '\0' at the end. */ -void txtbuf_grow(txtbuf *buf, size_t sz); - -/* - * Shrink a textbuf, if the allocated space is much larger than the text. - */ -/* FIXME: actually use this */ -void txtbuf_shrink(txtbuf *buf); +void txtbuf_resize(txtbuf *buf, size_t sz); /* * Destroy a txtbuf. @@ -38,3 +37,5 @@ void txtbuf_copy(txtbuf *dst, txtbuf *src); * Duplicate txtbuf 'src'. */ txtbuf *txtbuf_dup(txtbuf *src); + +#endif diff --git a/undo.c b/undo.c @@ -6,6 +6,7 @@ #include <X11/Xutil.h> #include <pango/pangoxft.h> +#include "util.h" #include "memory.h" #include "common.h" #include "txtbuf.h" @@ -33,7 +34,7 @@ enum operation { typedef struct { txtbuf *text; enum operation type; - enum ledit_mode mode; + ledit_mode mode; ledit_range op_range; ledit_range cursor_range; int group; @@ -41,9 +42,9 @@ typedef struct { } undo_elem; struct undo_stack { - /* FIXME: size_t? */ - int len, cur, cap; + size_t len, cur, cap; undo_elem *stack; + int cur_valid; int change_mode_group; }; @@ -51,7 +52,8 @@ undo_stack * undo_stack_create(void) { undo_stack *undo = ledit_malloc(sizeof(undo_stack)); undo->len = undo->cap = 0; - undo->cur = -1; + undo->cur = 0; + undo->cur_valid = 0; undo->stack = NULL; undo->change_mode_group = 0; return undo; @@ -59,6 +61,10 @@ undo_stack_create(void) { void undo_stack_destroy(undo_stack *undo) { + for (size_t i = 0; i < undo->cap; i++) { + if (undo->stack[i].text != NULL) + txtbuf_destroy(undo->stack[i].text); + } free(undo->stack); free(undo); } @@ -66,12 +72,14 @@ undo_stack_destroy(undo_stack *undo) { /* FIXME: resize text buffers when they aren't needed anymore */ static undo_elem * push_undo_elem(undo_stack *undo) { - ledit_assert(undo->cur >= -1); - undo->cur++; - undo->len = undo->cur + 1; + if (undo->cur_valid) + undo->cur++; + else + undo->cur = 0; + undo->cur_valid = 1; + undo->len = add_sz(undo->cur, 1); if (undo->len > undo->cap) { - /* FIXME: wait, why is it size_t here already? */ - size_t cap = undo->len * 2; + size_t cap = ideal_array_size(undo->cap, undo->len); undo->stack = ledit_reallocarray(undo->stack, cap, sizeof(undo_elem)); for (size_t i = undo->cap; i < cap; i++) { undo->stack[i].text = NULL; @@ -83,7 +91,7 @@ push_undo_elem(undo_stack *undo) { static undo_elem * peek_undo_elem(undo_stack *undo) { - if (undo->cur < 0) + if (!undo->cur_valid) return NULL; return &undo->stack[undo->cur]; } @@ -99,7 +107,7 @@ push_undo( ledit_range insert_range, /* maybe not the best name */ ledit_range cursor_range, int start_group, - enum operation type, enum ledit_mode mode) { + enum operation type, ledit_mode mode) { undo_elem *old = peek_undo_elem(undo); int last_group = old == NULL ? 0 : old->group; int last_mode_group = old == NULL ? 0 : old->mode_group; @@ -121,7 +129,7 @@ void undo_push_insert( undo_stack *undo, txtbuf *text, ledit_range insert_range, ledit_range cursor_range, - int start_group, enum ledit_mode mode) { + int start_group, ledit_mode mode) { push_undo( undo, text, insert_range, cursor_range, start_group, UNDO_INSERT, mode @@ -132,7 +140,7 @@ void undo_push_delete( undo_stack *undo, txtbuf *text, ledit_range delete_range, ledit_range cursor_range, - int start_group, enum ledit_mode mode) { + int start_group, ledit_mode mode) { push_undo( undo, text, delete_range, cursor_range, start_group, UNDO_DELETE, mode @@ -140,15 +148,18 @@ undo_push_delete( } undo_status -ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data, +ledit_undo(undo_stack *undo, ledit_mode mode, void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb, size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) { undo_elem *e; /* skip empty elements */ - while (undo->cur >= 0 && undo->stack[undo->cur].text->len == 0) { - undo->cur--; + while (undo->cur_valid && undo->stack[undo->cur].text->len == 0) { + if (undo->cur == 0) + undo->cur_valid = 0; + else + undo->cur--; } - if (undo->cur < 0) + if (!undo->cur_valid) return UNDO_OLDEST_CHANGE; int group = undo->stack[undo->cur].group; int mode_group = undo->stack[undo->cur].mode_group; @@ -156,7 +167,7 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data, int mode_group_same = 0; size_t cur_line = 0; size_t cur_index = 0; - while (undo->cur >= 0 && + while (undo->cur_valid && (undo->stack[undo->cur].group == group || (mode_group_same = ((mode == NORMAL || mode == VISUAL) && @@ -193,7 +204,10 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data, /* FIXME: make sure this is always sorted already */ if (e->op_range.line1 < min_line) min_line = e->op_range.line1; - undo->cur--; + if (undo->cur == 0) + undo->cur_valid = 0; + else + undo->cur--; cur_line = e->cursor_range.line1; cur_index = e->cursor_range.byte1; } @@ -210,17 +224,24 @@ ledit_undo(undo_stack *undo, enum ledit_mode mode, void *callback_data, } undo_status -ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data, +ledit_redo(undo_stack *undo, ledit_mode mode, void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb, size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret) { undo_elem *e; + if (undo->len == 0) + return UNDO_NEWEST_CHANGE; /* skip elements where no text is changed */ while (undo->cur < undo->len - 1 && undo->stack[undo->cur + 1].text->len == 0) { undo->cur++; } - if (undo->cur >= undo->len - 1) + if (undo->cur_valid && undo->cur >= undo->len - 1) return UNDO_NEWEST_CHANGE; - undo->cur++; + if (!undo->cur_valid) { + undo->cur_valid = 1; + undo->cur = 0; + } else { + undo->cur++; + } int group = undo->stack[undo->cur].group; int mode_group = undo->stack[undo->cur].mode_group; size_t min_line = SIZE_MAX; @@ -263,7 +284,9 @@ ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data, } *cur_line_ret = cur_line; *cur_index_ret = cur_index; - undo->cur--; + /* it should theoretically never be 0 anyways, but whatever */ + if (undo->cur > 0) + undo->cur--; *min_line_ret = min_line; return UNDO_NORMAL; } @@ -271,5 +294,20 @@ ledit_redo(undo_stack *undo, enum ledit_mode mode, void *callback_data, void undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range) { undo_elem *e = peek_undo_elem(undo); - e->cursor_range = cur_range; + if (e != NULL) + e->cursor_range = cur_range; +} + +char * +undo_state_to_str(undo_status s) { + switch (s) { + case UNDO_NORMAL: + return "Performed undo/redo"; + case UNDO_OLDEST_CHANGE: + return "Already at oldest change"; + case UNDO_NEWEST_CHANGE: + return "Already at newest change"; + default: + return "This is a bug. Tell lumidify about it."; + } } diff --git a/undo.h b/undo.h @@ -1,3 +1,10 @@ +#ifndef _UNDO_H_ +#define _UNDO_H_ + +#include <stddef.h> +#include "common.h" +#include "txtbuf.h" + /* * This handles undo and redo. * @@ -70,7 +77,7 @@ void undo_change_mode_group(undo_stack *undo); void undo_push_insert( undo_stack *undo, txtbuf *text, ledit_range insert_range, ledit_range cursor_range, - int start_group, enum ledit_mode mode + int start_group, ledit_mode mode ); /* @@ -80,7 +87,7 @@ void undo_push_insert( void undo_push_delete( undo_stack *undo, txtbuf *text, ledit_range delete_range, ledit_range cursor_range, - int start_group, enum ledit_mode mode + int start_group, ledit_mode mode ); /* @@ -88,9 +95,10 @@ void undo_push_delete( * 'cur_line_ret' and 'cur_index_ret' are set to the new cursor position. * 'min_line_ret' is set to the minimum line that was touched so the lines * can be recalculated properly. + * WARNING: If nothing was changed, 'min_line_ret' will be SIZE_MAX. */ undo_status ledit_undo( - undo_stack *undo, enum ledit_mode mode, + undo_stack *undo, ledit_mode mode, void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb, size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret ); @@ -100,9 +108,10 @@ undo_status ledit_undo( * 'cur_line_ret' and 'cur_index_ret' are set to the new cursor position. * 'min_line_ret' is set to the minimum line that was touched so the lines * can be recalculated properly. + * WARNING: If nothing was changed, 'min_line_ret' will be SIZE_MAX. */ undo_status ledit_redo( - undo_stack *undo, enum ledit_mode mode, + undo_stack *undo, ledit_mode mode, void *callback_data, undo_insert_callback insert_cb, undo_delete_callback delete_cb, size_t *cur_line_ret, size_t *cur_index_ret, size_t *min_line_ret ); @@ -115,3 +124,11 @@ undo_status ledit_redo( * Fails silently if the stack is empty. */ void undo_change_last_cur_range(undo_stack *undo, ledit_range cur_range); + +/* + * Get a string corresponding to an undo_status. + * This string should not be freed. + */ +char *undo_state_to_str(undo_status s); + +#endif diff --git a/util.c b/util.c @@ -1,57 +1,40 @@ -#include <stdlib.h> - -#include <X11/Xlib.h> -#include <X11/Xutil.h> -#include <pango/pangoxft.h> -#include <X11/extensions/Xdbe.h> - +#include <stddef.h> #include "memory.h" -#include "common.h" -#include "txtbuf.h" -#include "theme.h" -#include "window.h" -#include "util.h" -ledit_draw * -draw_create(ledit_window *window, int w, int h) { - ledit_draw *draw = ledit_malloc(sizeof(ledit_draw)); - draw->w = w; - draw->h = h; - draw->pixmap = XCreatePixmap( - window->common->dpy, window->drawable, w, h, window->common->depth - ); - draw->xftdraw = XftDrawCreate( - window->common->dpy, draw->pixmap, window->common->vis, window->common->cm - ); - return draw; +char * +next_utf8(char *str) { + while ((*str & 0xC0) == 0x80) + str++; + return str; } -void -draw_grow(ledit_window *window, ledit_draw *draw, int w, int h) { - /* FIXME: sensible default pixmap sizes here */ - /* FIXME: maybe shrink the pixmaps at some point */ - if (draw->w < w || draw->h < h) { - draw->w = w > draw->w ? w + 10 : draw->w; - draw->h = h > draw->h ? h + 10 : draw->h; - XFreePixmap(window->common->dpy, draw->pixmap); - draw->pixmap = XCreatePixmap( - window->common->dpy, window->drawable, - draw->w, draw->h, window->common->depth - ); - XftDrawChange(draw->xftdraw, draw->pixmap); - } +size_t +add_sz(size_t a, size_t b) { + if (a > SIZE_MAX - b) + err_overflow(); + return a + b; +} + +size_t +add_sz3(size_t a, size_t b, size_t c) { + if (b > SIZE_MAX - c || a > SIZE_MAX - (b + c)) + err_overflow(); + return a + b + c; } void -draw_destroy(ledit_window *window, ledit_draw *draw) { - XFreePixmap(window->common->dpy, draw->pixmap); - XftDrawDestroy(draw->xftdraw); - free(draw); +swap_sz(size_t *a, size_t *b) { + size_t tmp = *a; + *a = *b; + *b = tmp; } -char * -next_utf8(char *str) { - while ((*str & 0xC0) == 0x80) - str++; - return str; +void +sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2) { + if (*l1 == *l2 && *b1 > *b2) { + swap_sz(b1, b2); + } else if (*l1 > *l2) { + swap_sz(l1, l2); + swap_sz(b1, b2); + } } diff --git a/util.h b/util.h @@ -1,28 +1,22 @@ -/* FIXME: rename this to draw_util.h and rename macros.h to util.h */ +#ifndef _UTIL_H_ +#define _UTIL_H_ /* - * This is just a basic wrapper for XftDraws and Pixmaps - * that is used by the window for its text display at the bottom. + * Return the position of the next start of a utf8 character. + * If there is none, the position of the terminating NUL is + * returned. */ -typedef struct { - XftDraw *xftdraw; - Pixmap pixmap; - int w, h; -} ledit_draw; +char *next_utf8(char *str); /* - * Create a draw with the specified width and height. + * Add size_t values and abort if overflow would occur. + * FIXME: Maybe someone with actual experience could tell me + * if this overflow checking actually works. */ -ledit_draw *draw_create(ledit_window *window, int w, int h); +size_t add_sz(size_t a, size_t b); +size_t add_sz3(size_t a, size_t b, size_t c); -/* - * Make sure the size of the draw is at least the given width and height. - */ -void draw_grow(ledit_window *window, ledit_draw *draw, int w, int h); +void swap_sz(size_t *a, size_t *b); +void sort_range(size_t *l1, size_t *b1, size_t *l2, size_t *b2); -/* - * Destroy a draw. - */ -void draw_destroy(ledit_window *window, ledit_draw *draw); - -char *next_utf8(char *str); +#endif diff --git a/view.c b/view.c @@ -1,6 +1,3 @@ -/* FIXME: shrink buffers when text length less than a fourth of the size */ -/* FIXME: handle all undo within buffer to keep it consistent */ - #include <stdio.h> #include <errno.h> #include <string.h> @@ -13,6 +10,7 @@ #include <pango/pangoxft.h> #include <X11/extensions/Xdbe.h> +#include "util.h" #include "pango-compat.h" #include "memory.h" #include "common.h" @@ -75,9 +73,6 @@ static PangoAttrList *get_pango_attributes(ledit_view *view, size_t start_byte, */ static void set_line_layout_attrs(ledit_view *view, size_t line, PangoLayout *layout); -/* Swap size_t values. */ -static void swap_sz(size_t *a, size_t *b); - /* Move the gap of the line gap buffer to index 'index'. */ static void move_line_gap(ledit_view *view, size_t index); @@ -90,14 +85,14 @@ static void resize_and_move_line_gap(ledit_view *view, size_t min_size, size_t i /* FIXME: This is weird because mode is per-view but the undo mode group is changed for the entire buffer. */ void -view_set_mode(ledit_view *view, enum ledit_mode mode) { +view_set_mode(ledit_view *view, ledit_mode mode) { view->mode = mode; undo_change_mode_group(view->buffer->undo); window_set_mode(view->window, mode); } ledit_view * -view_create(ledit_buffer *buffer, ledit_theme *theme, enum ledit_mode mode, size_t line, size_t pos) { +view_create(ledit_buffer *buffer, ledit_theme *theme, ledit_mode mode, size_t line, size_t pos) { if (basic_attrs == NULL) { basic_attrs = pango_attr_list_new(); #if PANGO_VERSION_CHECK(1, 44, 0) @@ -179,7 +174,6 @@ move_line_gap(ledit_view *view, size_t index) { static void resize_and_move_line_gap(ledit_view *view, size_t min_size, size_t index) { - /* FIXME: Add to common bug list: used sizeof(ledit_line) instead of sizeof(ledit_view_line) */ view->lines = resize_and_move_gap( view->lines, sizeof(ledit_view_line), view->lines_gap, view->lines_cap, view->lines_num, @@ -235,10 +229,7 @@ view_notify_delete_text(ledit_view *view, size_t line, size_t index, size_t len) void view_notify_append_line(ledit_view *view, size_t line) { - size_t new_len = view->lines_num + 1; - if (new_len <= view->lines_num) - err_overflow(); - resize_and_move_line_gap(view, new_len, line + 1); + resize_and_move_line_gap(view, add_sz(view->lines_num, 1), line + 1); if (line < view->cur_line) view->cur_line++; if (view->sel_valid) @@ -278,6 +269,11 @@ view_notify_delete_lines(ledit_view *view, size_t index1, size_t index2) { ); move_line_gap(view, index1); view->lines_num -= index2 - index1 + 1; + /* possibly decrease size of array - this needs to be after + actually deleting the lines so the length is already less */ + size_t min_size = ideal_array_size(view->lines_cap, view->lines_num); + if (min_size != view->lines_cap) + resize_and_move_line_gap(view, view->lines_num, view->lines_gap); /* force first entry to offset 0 if first line was deleted */ if (index1 == 0) { ledit_view_line *vl = view_get_line(view, 0); @@ -329,7 +325,7 @@ set_line_layout_attrs(ledit_view *view, size_t line, PangoLayout *layout) { PangoAttrList *list = NULL; if (view->sel_valid) { ledit_range sel = view->sel; - view_sort_selection(&sel.line1, &sel.byte1, &sel.line2, &sel.byte2); + sort_range(&sel.line1, &sel.byte1, &sel.line2, &sel.byte2); if (sel.line1 < line && sel.line2 > line) { list = get_pango_attributes(view, 0, ll->len); } else if (sel.line1 == line && sel.line2 == line) { @@ -486,6 +482,7 @@ view_recalc_line(ledit_view *view, size_t line) { * and adjust offsets of all lines following it */ if (l->h_dirty) { l->h_dirty = 0; + /* FIXME: maybe also check overflow for offset? */ long off = l->y_offset + l->h; for (size_t i = line + 1; i < view->lines_num; i++) { l = view_get_line(view, i); @@ -554,10 +551,8 @@ view_next_cursor_pos(ledit_view *view, size_t line, size_t byte, int num) { PangoLayout *layout = get_pango_layout(view, line); const PangoLogAttr *attrs = pango_layout_get_log_attrs_readonly(layout, &nattrs); - if (num < 0) - return 0; /* FIXME: error */ for (size_t i = 0; i < (size_t)num; i++) { - cur_byte = line_next_utf8(ll, byte); + cur_byte = line_next_utf8(ll, cur_byte); for (c++; c < (size_t)nattrs; c++) { if (attrs[c].is_cursor_position) break; @@ -578,8 +573,6 @@ view_prev_cursor_pos(ledit_view *view, size_t line, size_t byte, int num) { PangoLayout *layout = get_pango_layout(view, line); const PangoLogAttr *attrs = pango_layout_get_log_attrs_readonly(layout, &nattrs); - if (num < 0) - return 0; /* FIXME: error */ for (int i = 0; i < num; i++) { cur_byte = line_prev_utf8(ll, cur_byte); for (; c > 0; c--) { @@ -1069,7 +1062,6 @@ get_pango_layout(ledit_view *view, size_t line) { return cl->layout; } -/* FIXME: document what works with pango units and what not */ void view_pos_to_x_softline(ledit_view *view, size_t line, size_t pos, int *x_ret, int *softline_ret) { ledit_view_line *vl = view_get_line(view, line); @@ -1114,9 +1106,9 @@ view_x_softline_to_pos(ledit_view *view, size_t line, int x, int softline) { pango_line, x_relative, &tmp_pos, &trailing ); size_t pos = (size_t)tmp_pos; - /* if in insert mode, snap to the nearest border between graphemes */ + /* if in insert or visual mode, snap to the nearest border between graphemes */ /* FIXME: add parameter for this instead of checking mode */ - if (view->mode == INSERT) { + if (view->mode == INSERT || view->mode == VISUAL) { ledit_line *ll = buffer_get_line(view->buffer, line); while (trailing > 0) { trailing--; @@ -1176,9 +1168,6 @@ view_delete_range( /* line_index1, byte_index1 are used as the cursor position in order to determine the new cursor position */ /* FIXME: use at least somewhat sensible variable names */ -/* FIXME: I once noticed a bug where using 'dG' to delete to the end of - the file caused a line index way larger than buffer->lines_num to be - given, but I couldn't reproduce this bug */ void view_delete_range_base( ledit_view *view, @@ -1194,7 +1183,7 @@ view_delete_range_base( -> FIXME: why not just use view->cur_line, view->cur_index here? */ size_t cur_line = line_index1; size_t cur_byte = byte_index1; - view_sort_selection(&line_index1, &byte_index1, &line_index2, &byte_index2); + sort_range(&line_index1, &byte_index1, &line_index2, &byte_index2); size_t new_line = 0, new_byte = 0; ledit_assert(line_index1 < view->lines_num); ledit_assert(line_index2 < view->lines_num); @@ -1682,24 +1671,6 @@ view_ensure_cursor_shown(ledit_view *view) { } } -static void -swap_sz(size_t *a, size_t *b) { - size_t tmp = *a; - *a = *b; - *b = tmp; -} - -/* FIXME: this is generic, so it doesn't need to be in view.c */ -void -view_sort_selection(size_t *line1, size_t *byte1, size_t *line2, size_t *byte2) { - if (*line1 > *line2) { - swap_sz(line1, line2); - swap_sz(byte1, byte2); - } else if (*line1 == *line2 && *byte1 > *byte2) { - swap_sz(byte1, byte2); - } -} - /* FIXME: don't reset selection when selection is clicked away */ /* FIXME: when selecting with mouse, only call this when button is released */ /* lines and bytes need to be sorted already! */ @@ -1740,8 +1711,8 @@ view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t line2, s } size_t l1_new = line1, l2_new = line2; size_t b1_new = byte1, b2_new = byte2; - view_sort_selection(&l1_new, &b1_new, &l2_new, &b2_new); - view_sort_selection(&view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2); + sort_range(&l1_new, &b1_new, &l2_new, &b2_new); + sort_range(&view->sel.line1, &view->sel.byte1, &view->sel.line2, &view->sel.byte2); /* FIXME: make this a bit nicer and optimize it */ if (view->sel.line1 > l2_new || view->sel.line2 < l1_new) { for (size_t i = view->sel.line1; i <= view->sel.line2; i++) { @@ -1772,6 +1743,7 @@ view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t line2, s view->sel.line2 = line2; view->sel.byte2 = byte2; view->sel_valid = 1; + view->redraw = 1; } static void @@ -1815,8 +1787,8 @@ view_button_handler(void *data, XEvent *event) { view_wipe_line_cursor_attrs(view, view->cur_line); /* FIXME: return to old mode afterwards? */ /* should change_mode_group even be called here? */ - view_set_mode(view, VISUAL); } + view_set_mode(view, VISUAL); if (!view->sel_valid) { /* the selection has just started, so the current position is already set to the beginning of the @@ -1962,32 +1934,43 @@ view_redraw(ledit_view *view) { } void -view_undo(ledit_view *view) { - /* FIXME: maybe wipe selection */ - size_t old_line = view->cur_line; - buffer_undo(view->buffer, view->mode, &view->cur_line, &view->cur_index); - if (view->mode == NORMAL) { - view->cur_index = view_get_legal_normal_pos( - view, view->cur_line, view->cur_index - ); +view_undo(ledit_view *view, int num) { + /* FIXME: maybe wipe selection (although I guess this + currently isn't possible in visual mode anyways) */ + for (int i = 0; i < num; i++) { + size_t old_line = view->cur_line; + undo_status s = buffer_undo(view->buffer, view->mode, &view->cur_line, &view->cur_index); + if (view->mode == NORMAL) { + view->cur_index = view_get_legal_normal_pos( + view, view->cur_line, view->cur_index + ); + } + view_wipe_line_cursor_attrs(view, old_line); + view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); + if (s != UNDO_NORMAL) { + window_show_message(view->window, undo_state_to_str(s), -1); + break; + } } - view_wipe_line_cursor_attrs(view, old_line); - view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); - /* FIXME: show undo message */ } void -view_redo(ledit_view *view) { - size_t old_line = view->cur_line; - buffer_redo(view->buffer, view->mode, &view->cur_line, &view->cur_index); - if (view->mode == NORMAL) { - view->cur_index = view_get_legal_normal_pos( - view, view->cur_line, view->cur_index - ); +view_redo(ledit_view *view, int num) { + for (int i = 0; i < num; i++) { + size_t old_line = view->cur_line; + undo_status s = buffer_redo(view->buffer, view->mode, &view->cur_line, &view->cur_index); + if (view->mode == NORMAL) { + view->cur_index = view_get_legal_normal_pos( + view, view->cur_line, view->cur_index + ); + } + view_wipe_line_cursor_attrs(view, old_line); + view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); + if (s != UNDO_NORMAL) { + window_show_message(view->window, undo_state_to_str(s), -1); + break; + } } - view_wipe_line_cursor_attrs(view, old_line); - view_set_line_cursor_attrs(view, view->cur_line, view->cur_index); - /* FIXME: show undo message */ } static void diff --git a/view.h b/view.h @@ -6,6 +6,13 @@ #ifndef _LEDIT_VIEW_H_ #define _LEDIT_VIEW_H_ +#include <stddef.h> +#include "common.h" +#include "txtbuf.h" +#include "window.h" +#include "theme.h" +#include "cache.h" + typedef struct ledit_view ledit_view; #include "buffer.h" @@ -69,7 +76,7 @@ struct ledit_view { long total_height; /* total pixel height of all lines */ long display_offset; /* current pixel offset of viewport */ ledit_range sel; /* current selection */ - enum ledit_mode mode; /* current mode of this view */ + ledit_mode mode; /* current mode of this view */ char selecting; /* whether user is currently selecting text with mouse */ char sel_valid; /* whether there is currently a valid selection */ char redraw; /* whether something has changed so the view needs to be redrawn */ @@ -86,7 +93,7 @@ enum delete_mode { * This changes the mode group of the associated buffer's * undo stack and changes the mode display in the window. */ -void view_set_mode(ledit_view *view, enum ledit_mode mode); +void view_set_mode(ledit_view *view, ledit_mode mode); /* * Create a view with associated buffer 'buffer' and theme 'theme'. @@ -95,7 +102,7 @@ void view_set_mode(ledit_view *view, enum ledit_mode mode); */ ledit_view *view_create( ledit_buffer *buffer, ledit_theme *theme, - enum ledit_mode mode, size_t line, size_t pos + ledit_mode mode, size_t line, size_t pos ); /* @@ -442,11 +449,6 @@ void view_scroll_to_pos_bottom(ledit_view *view, size_t line, size_t byte); void view_ensure_cursor_shown(ledit_view *view); /* - * Sort the given range so that (*line1 < *line2) or (*line1 == *line2 && *byte1 <= *byte2). - */ -void view_sort_selection(size_t *line1, size_t *byte1, size_t *line2, size_t *byte2); - -/* * Clear the selection. */ void view_wipe_selection(ledit_view *view); @@ -465,20 +467,20 @@ void view_set_selection(ledit_view *view, size_t line1, size_t byte1, size_t lin void view_redraw(ledit_view *view); /* - * Perform an undo step. + * Perform up to num undo steps. * The cursor position of the view is set to the stored position * in the undo stack. * The line heights and offsets are recalculated. */ -void view_undo(ledit_view *view); +void view_undo(ledit_view *view, int num); /* - * Perform a redo step. + * Perform up to num redo steps. * The cursor position of the view is set to the stored position * in the undo stack. * The line heights and offsets are recalculated. */ -void view_redo(ledit_view *view); +void view_redo(ledit_view *view, int num); /* * Paste the X11 clipboard at the current cursor position. diff --git a/window.c b/window.c @@ -9,19 +9,18 @@ #include <X11/Xatom.h> #include <X11/Xutil.h> #include <pango/pangoxft.h> -#include <X11/XKBlib.h> -#include <X11/extensions/XKBrules.h> #include <X11/extensions/Xdbe.h> +#include "util.h" +#include "theme.h" #include "memory.h" #include "common.h" #include "txtbuf.h" -#include "theme.h" #include "window.h" -#include "util.h" #include "macros.h" #include "config.h" #include "assert.h" +#include "draw_util.h" /* FIXME: Everything to do with the bottom bar is extremely hacky */ struct bottom_bar { @@ -106,14 +105,14 @@ recalc_text_size(ledit_window *window) { static void resize_line_text(ledit_window *window, int min_size) { - if (min_size > window->bb->line_alloc || window->bb->line_text == NULL) { - /* FIXME: read up on what the best values are here */ - /* FIXME: overflow */ - window->bb->line_alloc = - window->bb->line_alloc * 2 > min_size ? - window->bb->line_alloc * 2 : - min_size; - window->bb->line_text = ledit_realloc(window->bb->line_text, window->bb->line_alloc); + /* FIXME: use size_t everywhere */ + ledit_assert(min_size >= 0); + size_t cap = ideal_array_size(window->bb->line_alloc, min_size); + if (cap > INT_MAX) + err_overflow(); + if (cap != (size_t)window->bb->line_alloc) { + window->bb->line_alloc = (int)cap; + window->bb->line_text = ledit_realloc(window->bb->line_text, cap); } } @@ -337,7 +336,7 @@ window_hide_message(ledit_window *window) { } void -window_set_mode(ledit_window *window, enum ledit_mode mode) { +window_set_mode(ledit_window *window, ledit_mode mode) { window->mode = mode; char *text; switch (mode) { @@ -516,7 +515,7 @@ xximspot(ledit_window *window, int x, int y) { } ledit_window * -window_create(ledit_common *common, ledit_theme *theme, enum ledit_mode mode) { +window_create(ledit_common *common, ledit_theme *theme, ledit_mode mode) { XSetWindowAttributes attrs; XGCValues gcv; @@ -659,7 +658,6 @@ window_destroy(ledit_window *window) { /*g_object_unref(window->context);*/ g_object_unref(window->fontmap); - /* FIXME: is gc, etc. destroyed automatically when destroying window? */ if (window->spotlist) XFree(window->spotlist); XDestroyWindow(window->common->dpy, window->xwin); diff --git a/window.h b/window.h @@ -1,3 +1,6 @@ +#ifndef _WINDOW_H_ +#define _WINDOW_H_ + /* * A window is associated with exactly one view and is responsible everything * other than the actual text handling (of the text in the buffer). @@ -5,10 +8,17 @@ * partially here and partially in keys_command, but that's the way it is for now. */ -#ifndef _WINDOW_H_ -#define _WINDOW_H_ - +#include <time.h> #include <stdarg.h> +#include <X11/Xlib.h> +#include <X11/Xatom.h> +#include <X11/Xutil.h> +#include <X11/extensions/Xdbe.h> +#include <pango/pangoxft.h> + +#include "theme.h" +#include "common.h" +#include "txtbuf.h" typedef struct bottom_bar bottom_bar; @@ -42,7 +52,7 @@ typedef struct { int message_shown; /* whether a readonly message is shown at the bottom */ bottom_bar *bb; /* encapsulates the text at the bottom */ int redraw; /* whether something has changed and the window has to be redrawn */ - enum ledit_mode mode; /* mode of the view - a bit ugly to duplicate this here... */ + ledit_mode mode; /* mode of the view - a bit ugly to duplicate this here... */ /* stuff for filtering events so not too many have to be handled */ struct timespec last_scroll; @@ -83,7 +93,7 @@ typedef struct { /* * Create a window with initial mode 'mode'. */ -ledit_window *window_create(ledit_common *common, ledit_theme *theme, enum ledit_mode mode); +ledit_window *window_create(ledit_common *common, ledit_theme *theme, ledit_mode mode); /* * Destroy a window. @@ -171,6 +181,8 @@ void window_set_bottom_bar_realtext(ledit_window *window, char *text, int len); /* * Get the text of the editable line. + * WARNING: this is a direct pointer to the internal storage, + * it is not copied. */ char *window_get_bottom_bar_text(ledit_window *window); @@ -183,6 +195,10 @@ void window_show_message(ledit_window *window, char *text, int len); * Show a non-editable message that is given as a * format string and arguments as interpreted by * vsnprintf(3) + * WARNING: This may reallocate the internal text storage for the + * bottom bar before actually using the format arguments, so don't + * ever pass parts of the text returned by window_get_bottom_bar_text + * to this function without first copying it. */ void window_show_message_fmt(ledit_window *window, char *fmt, ...); @@ -199,7 +215,7 @@ int window_message_shown(ledit_window *window); /* * Set the displayed mode of the window. */ -void window_set_mode(ledit_window *window, enum ledit_mode mode); +void window_set_mode(ledit_window *window, ledit_mode mode); /* * Set extra text that is shown to the right of the mode.