This commit is contained in:
Piotr Kocia 2025-03-31 11:02:24 +02:00
parent 23c95f1dea
commit 26266dff3b
4 changed files with 341 additions and 62 deletions

View file

@ -87,6 +87,10 @@ static inline bool hasc32upper(const char32_t *s) {
return false;
}
static inline bool isc32punct(char32_t c32) {
return iswpunct((wint_t)c32);
}
static inline int c32width(char32_t c) {
#if defined(FOOT_GRAPHEME_CLUSTERING)
return utf8proc_charwidth((utf8proc_int32_t)c);

View file

@ -176,6 +176,13 @@ static const char *const vimode_binding_action_map[] = {
[BIND_ACTION_VIMODE_DOWN_LINE] = "vimode-down-line",
[BIND_ACTION_VIMODE_FIRST_LINE] = "vimode-first-line",
[BIND_ACTION_VIMODE_LAST_LINE] = "vimode-last-line",
[BIND_ACTION_VIMODE_LINE_BEGIN] = "vimode-line-begin",
[BIND_ACTION_VIMODE_LINE_END] = "vimode-line-end",
[BIND_ACTION_VIMODE_TEXT_BEGIN] = "vimode-text-begin",
[BIND_ACTION_VIMODE_NEXT_WORD_BEGIN] = "vimode-next-word-begin",
[BIND_ACTION_VIMODE_PREV_WORD_END] = "vimode-prev-word-end",
[BIND_ACTION_VIMODE_WORD_BEGIN] = "vimode-word-begin",
[BIND_ACTION_VIMODE_WORD_END] = "vimode-word-end",
[BIND_ACTION_VIMODE_CANCEL] = "vimode-cancel",
[BIND_ACTION_VIMODE_START_SEARCH_FORWARD] = "vimode-start-search-forward",
[BIND_ACTION_VIMODE_START_SEARCH_BACKWARD] = "vimode-start-search-backward",
@ -3282,6 +3289,14 @@ add_default_vimode_bindings(struct config *conf)
{BIND_ACTION_VIMODE_DOWN_LINE, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_e}}},
{BIND_ACTION_VIMODE_FIRST_LINE, m("none"), {{XKB_KEY_g}}},
{BIND_ACTION_VIMODE_LAST_LINE, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_g}}},
{BIND_ACTION_VIMODE_LINE_BEGIN, m("none"), {{XKB_KEY_0}}},
{BIND_ACTION_VIMODE_LINE_END, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_4}}},
{BIND_ACTION_VIMODE_TEXT_BEGIN, m(XKB_MOD_NAME_SHIFT), {{XKB_KEY_6}}},
{BIND_ACTION_VIMODE_NEXT_WORD_BEGIN, m("none"), {{XKB_KEY_w}}},
// TODO (kociap): PREV_WORD_END currently unbound. By default 'ge' in vim.
// {BIND_ACTION_VIMODE_PREV_WORD_END, m("none"), {{XKB_KEY_}}},
{BIND_ACTION_VIMODE_WORD_BEGIN, m("none"), {{XKB_KEY_b}}},
{BIND_ACTION_VIMODE_WORD_END, m("none"), {{XKB_KEY_e}}},
{BIND_ACTION_VIMODE_CANCEL, m(XKB_MOD_NAME_CTRL), {{XKB_KEY_c}}},
{BIND_ACTION_VIMODE_CANCEL, m("none"), {{XKB_KEY_Escape}}},
{BIND_ACTION_VIMODE_START_SEARCH_FORWARD, m("none"), {{XKB_KEY_slash}}},

View file

@ -78,6 +78,13 @@ enum bind_action_vimode {
BIND_ACTION_VIMODE_DOWN_LINE,
BIND_ACTION_VIMODE_FIRST_LINE,
BIND_ACTION_VIMODE_LAST_LINE,
BIND_ACTION_VIMODE_LINE_BEGIN,
BIND_ACTION_VIMODE_LINE_END,
BIND_ACTION_VIMODE_TEXT_BEGIN,
BIND_ACTION_VIMODE_NEXT_WORD_BEGIN,
BIND_ACTION_VIMODE_PREV_WORD_END,
BIND_ACTION_VIMODE_WORD_BEGIN,
BIND_ACTION_VIMODE_WORD_END,
BIND_ACTION_VIMODE_CANCEL,
BIND_ACTION_VIMODE_START_SEARCH_FORWARD,
BIND_ACTION_VIMODE_START_SEARCH_BACKWARD,

377
vimode.c
View file

@ -436,7 +436,7 @@ static ssize_t matches_cell(const struct terminal *term,
if (composed == NULL && base == 0 && buf[search_ofs] == U' ')
return 1;
if (c32ncasecmp(&base, &buf[search_ofs], 1) != 0)
if (c32ncasecmp(&base, buf, 1) != 0)
return -1;
if (composed != NULL) {
@ -738,59 +738,6 @@ void search_add_chars(struct terminal *term, const char *src, size_t count) {
add_wchars(term, c32s, chars);
}
// static size_t distance_next_word(const struct terminal *term) {
// size_t cursor = term->vimode.search.cursor;
//
// /* First eat non-whitespace. This is the word we're skipping past */
// while (cursor < term->vimode.search.len) {
// if (isc32space(term->vimode.search.buf[cursor++]))
// break;
// }
//
// xassert(cursor == term->vimode.search.len ||
// isc32space(term->vimode.search.buf[cursor - 1]));
//
// /* Now skip past whitespace, so that we end up at the beginning of
// * the next word */
// while (cursor < term->vimode.search.len) {
// if (!isc32space(term->vimode.search.buf[cursor++]))
// break;
// }
//
// xassert(cursor == term->vimode.search.len ||
// !isc32space(term->vimode.search.buf[cursor - 1]));
//
// if (cursor < term->vimode.search.len &&
// !isc32space(term->vimode.search.buf[cursor]))
// cursor--;
//
// return cursor - term->vimode.search.cursor;
// }
// static size_t distance_prev_word(const struct terminal *term) {
// int cursor = term->vimode.search.cursor;
//
// /* First, eat whitespace prefix */
// while (cursor > 0) {
// if (!isc32space(term->vimode.search.buf[--cursor]))
// break;
// }
//
// xassert(cursor == 0 || !isc32space(term->vimode.search.buf[cursor]));
//
// /* Now eat non-whitespace. This is the word we're skipping past */
// while (cursor > 0) {
// if (isc32space(term->vimode.search.buf[--cursor]))
// break;
// }
//
// xassert(cursor == 0 || isc32space(term->vimode.search.buf[cursor]));
// if (cursor > 0 && isc32space(term->vimode.search.buf[cursor]))
// cursor++;
//
// return term->vimode.search.cursor - cursor;
// }
void vimode_view_down(struct terminal *const term, int const delta) {
if (!term->vimode.active) {
return;
@ -860,6 +807,254 @@ static void move_cursor_horizontal(struct terminal *const term,
move_cursor_delta(term, (struct coord){.row = 0, .col = count});
}
enum c32_class {
CLASS_BLANK,
CLASS_PUNCTUATION,
CLASS_WORD,
};
enum c32_class get_class(char32_t const c) {
if (c == '\0') {
return CLASS_BLANK;
}
// We consider an underscore to be a word character instead of
// punctuation.
if (c == '_') {
return CLASS_WORD;
}
bool const whitespace = isc32space(c);
if (whitespace) {
return CLASS_BLANK;
}
// TODO (kociap): unsure whether this handles all possible
// punctuation, a subset of it or just the latin1 characters.
bool const punctuation = isc32punct(c);
if (punctuation) {
return CLASS_PUNCTUATION;
}
// Most other characters may be considered "word" characters.
return CLASS_WORD;
}
char32_t cursor_char(struct terminal *const term) {
struct row *const row = grid_row(term->grid, term->vimode.cursor.row);
return row->cells[term->vimode.cursor.col].wc;
}
enum c32_class cursor_class(struct terminal *const term) {
char32_t const c = cursor_char(term);
return get_class(c);
}
int row_length(struct terminal *const term, int const row_index) {
struct row *const row = grid_row(term->grid, row_index);
int length = 0;
while (length < term->grid->num_cols) {
if (row->cells[length].wc == '\0') {
break;
}
length += 1;
}
return length;
}
// increment_cursor
//
// Move the cursor to the next character, moving to the next row as
// necessary.
//
// Returns:
// false when at the end of the scrollback.
// true otherwise
//
bool increment_cursor(struct terminal *const term) {
char32_t const c = cursor_char(term);
// Within a row, move to the next column.
if (c != '\0') {
term->vimode.cursor.col += 1;
struct row const *const row = grid_row(term->grid, term->vimode.cursor.row);
// If the row does not contain a linebreak, we want to move to the
// next row immediately.
if (row->linebreak || term->vimode.cursor.col < term->cols) {
return true;
}
}
// Not in the last row.
if (term->vimode.cursor.row != term->rows - 1) {
term->vimode.cursor.row += 1;
term->vimode.cursor.col = 0;
return true;
}
return false;
}
// decrement_cursor
//
// Move the cursor to the previous character, moving to the previous
// row as necessary.
//
// Returns:
// false when at the start of the scrollback.
// true otherwise
//
bool decrement_cursor(struct terminal *const term) {
// Within a row, move to the previous column.
if (term->vimode.cursor.col > 0) {
term->vimode.cursor.col -= 1;
return true;
}
// TODO (kociap): this seems like a terrible way (performance-wise)
// to figure out the current scrollback position. Could maybe store
// it in the grid instead of recalculating it repeatedly?
int const sb_start =
grid_sb_start_ignore_uninitialized(term->grid, term->rows);
int const sb_row = grid_row_abs_to_sb_precalc_sb_start(
term->grid, sb_start,
grid_row_absolute(term->grid, term->vimode.cursor.row));
// Not in the first row.
if (sb_row > 0) {
term->vimode.cursor.row -= 1;
term->vimode.cursor.col = row_length(term, term->vimode.cursor.row) - 1;
return true;
}
return false;
}
bool skip_chars_forward(struct terminal *const term,
enum c32_class const class) {
while (cursor_class(term) == class) {
if (increment_cursor(term) == false) {
return false;
}
}
return true;
}
bool skip_chars_backward(struct terminal *const term,
enum c32_class const class) {
while (cursor_class(term) == class) {
if (decrement_cursor(term) == false) {
return false;
}
}
return true;
}
// MOTIONS
void motion_begin_word(struct terminal *const term) {
if (decrement_cursor(term) == false) {
return;
}
// Skip whitespace. If we encounter an empty row, stop.
while (cursor_class(term) == CLASS_BLANK) {
bool const row_empty = row_length(term, term->vimode.cursor.row) == 0;
if (row_empty && term->vimode.cursor.col == 0) {
return;
}
if (decrement_cursor(term) == false) {
return;
}
}
// Go to the start of the next word.
enum c32_class const current_class = cursor_class(term);
if (skip_chars_backward(term, current_class) == false) {
return;
}
// We overshot. Move forward one character.
increment_cursor(term);
}
void motion_end_word(struct terminal *const term) {
if (increment_cursor(term) == false) {
return;
}
// Skip whitespace. If we encounter an empty row, stop.
while (cursor_class(term) == CLASS_BLANK) {
bool const row_empty = row_length(term, term->vimode.cursor.row) == 0;
if (row_empty && term->vimode.cursor.col == 0) {
return;
}
if (increment_cursor(term) == false) {
return;
}
}
// Go to the end of the next word.
enum c32_class current_class = cursor_class(term);
if (skip_chars_forward(term, current_class) == false) {
return;
}
// We overshot. Go back one character.
decrement_cursor(term);
}
void motion_fwd_begin_word(struct terminal *const term) {
enum c32_class const starting_class = cursor_class(term);
if (increment_cursor(term) == false) {
return;
}
// Move to the end of this word.
if (starting_class != CLASS_BLANK) {
if (skip_chars_forward(term, starting_class) == false) {
return;
}
}
// Skip whitespace. If we encounter an empty row, stop.
while (cursor_class(term) == CLASS_BLANK) {
bool const row_empty = row_length(term, term->vimode.cursor.row) == 0;
if (row_empty && term->vimode.cursor.col == 0) {
return;
}
if (increment_cursor(term) == false) {
return;
}
}
}
void motion_back_end_word(struct terminal *const term) {
enum c32_class const starting_class = cursor_class(term);
if (decrement_cursor(term) == false) {
return;
}
// Move to before the start of this word.
if (starting_class != CLASS_BLANK) {
if (skip_chars_backward(term, starting_class) == false) {
return;
}
}
// Skip whitespace. If we encounter an empty row, stop.
while (cursor_class(term) == CLASS_BLANK) {
bool const row_empty = row_length(term, term->vimode.cursor.row) == 0;
if (row_empty && term->vimode.cursor.col == 0) {
return;
}
if (decrement_cursor(term) == false) {
return;
}
}
}
static void execute_vimode_binding(struct seat *seat, struct terminal *term,
const struct key_binding *binding,
uint32_t serial) {
@ -946,8 +1141,7 @@ static void execute_vimode_binding(struct seat *seat, struct terminal *term,
damage_cursor_cell(term);
update_selection(term);
update_highlights(term);
break;
}
} break;
case BIND_ACTION_VIMODE_LAST_LINE:
cmd_scrollback_down(term, term->grid->num_rows);
@ -958,6 +1152,68 @@ static void execute_vimode_binding(struct seat *seat, struct terminal *term,
update_highlights(term);
break;
case BIND_ACTION_VIMODE_LINE_BEGIN:
damage_cursor_cell(term);
term->vimode.cursor.col = 0;
damage_cursor_cell(term);
break;
case BIND_ACTION_VIMODE_LINE_END: {
damage_cursor_cell(term);
struct row const *const row = grid_row(term->grid, term->vimode.cursor.row);
int col = term->cols - 1;
while (col > 0) {
if (row->cells[col].wc != '\0') {
break;
}
col -= 1;
}
term->vimode.cursor.col = col;
damage_cursor_cell(term);
} break;
case BIND_ACTION_VIMODE_TEXT_BEGIN: {
damage_cursor_cell(term);
struct row const *const row = grid_row(term->grid, term->vimode.cursor.row);
int col = 0;
while (col < term->cols - 1) {
if (isc32graph(row->cells[col].wc)) {
break;
}
col += 1;
}
term->vimode.cursor.col = col;
damage_cursor_cell(term);
} break;
case BIND_ACTION_VIMODE_WORD_BEGIN:
damage_cursor_cell(term);
motion_begin_word(term);
damage_cursor_cell(term);
update_selection(term);
break;
case BIND_ACTION_VIMODE_WORD_END:
damage_cursor_cell(term);
motion_end_word(term);
damage_cursor_cell(term);
update_selection(term);
break;
case BIND_ACTION_VIMODE_NEXT_WORD_BEGIN:
damage_cursor_cell(term);
motion_fwd_begin_word(term);
damage_cursor_cell(term);
update_selection(term);
break;
case BIND_ACTION_VIMODE_PREV_WORD_END:
damage_cursor_cell(term);
motion_back_end_word(term);
damage_cursor_cell(term);
update_selection(term);
break;
case BIND_ACTION_VIMODE_CANCEL: {
// We handle multiple actions here (in that exact order):
// - clear search (handled by vimode-search bindings),
@ -969,8 +1225,7 @@ static void execute_vimode_binding(struct seat *seat, struct terminal *term,
} else {
vimode_cancel(term);
}
break;
}
} break;
case BIND_ACTION_VIMODE_START_SEARCH_FORWARD:
start_search(term, SEARCH_FORWARD);
@ -1004,8 +1259,7 @@ static void execute_vimode_binding(struct seat *seat, struct terminal *term,
update_selection(term);
}
update_highlights(term);
break;
}
} break;
case BIND_ACTION_VIMODE_ENTER_VISUAL:
case BIND_ACTION_VIMODE_ENTER_VLINE:
@ -1041,8 +1295,7 @@ static void execute_vimode_binding(struct seat *seat, struct terminal *term,
term->vimode.selection.start = cursor;
term->vimode.mode = mode;
}
break;
}
} break;
case BIND_ACTION_VIMODE_YANK:
// TODO (kociap): Should yank executed in non-visual mode copy the