Emacs style case conversions (#5898)
This PR introduces the following changes to rizin shell: - Use Meta-L to lowercase the following word - Use Meta-U to uppercase the following word - Use Meta-C to capitalize the following word The behavior of these actions is the same as in zsh.
This commit is contained in:
parent
70fdd67278
commit
9f85d684a1
5 changed files with 401 additions and 1 deletions
|
|
@ -6,6 +6,7 @@
|
|||
#include <rz_core.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include "i/private.h"
|
||||
|
||||
#if __WINDOWS__
|
||||
#include <windows.h>
|
||||
|
|
@ -1555,6 +1556,8 @@ RZ_API const char *rz_line_readline_cb(RZ_NONNULL RzLine *line, RzLineReadCallba
|
|||
int prev_buflen = -1;
|
||||
bool enable_yank_pop = false;
|
||||
bool gcomp_is_rev = true;
|
||||
RzEmacsModeModifyOpts em_opts;
|
||||
rz_emacs_mode_modify_opts_reset(&em_opts);
|
||||
|
||||
RzCons *cons = rz_cons_singleton();
|
||||
|
||||
|
|
@ -1885,6 +1888,47 @@ RZ_API const char *rz_line_readline_cb(RZ_NONNULL RzLine *line, RzLineReadCallba
|
|||
line->buffer.index = line->buffer.length;
|
||||
}
|
||||
break;
|
||||
case 'C':
|
||||
case 'c':
|
||||
em_opts.op = EMACS_MODIFY_CAPITALIZE;
|
||||
rz_emacs_mode_modify(&em_opts, line);
|
||||
rz_emacs_mode_modify_opts_reset(&em_opts);
|
||||
break;
|
||||
case 'L':
|
||||
case 'l':
|
||||
em_opts.op = EMACS_MODIFY_TOLOWER;
|
||||
rz_emacs_mode_modify(&em_opts, line);
|
||||
rz_emacs_mode_modify_opts_reset(&em_opts);
|
||||
break;
|
||||
case 'U':
|
||||
case 'u':
|
||||
em_opts.op = EMACS_MODIFY_TOUPPER;
|
||||
rz_emacs_mode_modify(&em_opts, line);
|
||||
rz_emacs_mode_modify_opts_reset(&em_opts);
|
||||
break;
|
||||
case '-':
|
||||
em_opts.move_cursor = !em_opts.move_cursor;
|
||||
if (em_opts.word_count_provided) {
|
||||
// word count must be provided after '-'
|
||||
rz_emacs_mode_modify_opts_reset(&em_opts);
|
||||
}
|
||||
break;
|
||||
case '0':
|
||||
case '1':
|
||||
case '2':
|
||||
case '3':
|
||||
case '4':
|
||||
case '5':
|
||||
case '6':
|
||||
case '7':
|
||||
case '8':
|
||||
case '9': {
|
||||
const ut32 d = buf[0] - '0';
|
||||
em_opts.word_count = em_opts.word_count <= (UT32_MAX - d) / 10
|
||||
? em_opts.word_count * 10 + d
|
||||
: UT32_MAX;
|
||||
em_opts.word_count_provided = true;
|
||||
} break;
|
||||
case 63: // ^[? Meta-/
|
||||
case 95: // ^[_ Meta-_
|
||||
if (!line->gcomp && !line->hud && !line->sel_widget) {
|
||||
|
|
|
|||
322
librz/cons/emacs_mode.c
Normal file
322
librz/cons/emacs_mode.c
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ehab-24 <ehabs1775@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#include <wctype.h>
|
||||
#include "i/private.h"
|
||||
|
||||
static bool is_word_constituent(RzCodePoint cp) {
|
||||
return iswalnum((wint_t)cp);
|
||||
}
|
||||
|
||||
static char *unicode_mapping_append_to_buf(RZ_NONNULL const RzUnicodeCaseMapping *um, RZ_NONNULL char *p, RZ_NONNULL const char *end) {
|
||||
rz_return_val_if_fail(um && p && end, NULL);
|
||||
if (rz_unicode_case_mapping_is_empty(um)) {
|
||||
const size_t len = rz_utf8_encode((ut8 *)p, um->key);
|
||||
if (len > 0) {
|
||||
if ((size_t)(end - p) < len) {
|
||||
return NULL;
|
||||
}
|
||||
p += len;
|
||||
}
|
||||
} else {
|
||||
for (int j = 0; j < 3; ++j) {
|
||||
RzCodePoint v = um->val[j];
|
||||
if (v == 0) {
|
||||
continue;
|
||||
}
|
||||
const size_t len = rz_utf8_encode((ut8 *)p, v);
|
||||
if (len < 1) {
|
||||
continue;
|
||||
}
|
||||
if ((size_t)(end - p) < len) {
|
||||
return NULL;
|
||||
}
|
||||
p += len;
|
||||
}
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Convert a vector of RzUnicodeCaseMapping to an encoded UTF-8 string.
|
||||
* NOTE: Empty mappings are encoded using their `key` insted of value(s).
|
||||
*
|
||||
* \param map RzVector <RzUnicodeCaseMapping>.
|
||||
* \param maxlen The length of encoded string in bytes.
|
||||
*
|
||||
* \return Encoded UTF-8 (null-terminated) string.
|
||||
*/
|
||||
static RZ_OWN RZ_NULLABLE char *unicode_mapping_to_str(RZ_NONNULL RzUnicodeCaseMappings *map, size_t utf8_len) {
|
||||
rz_return_val_if_fail(map, NULL);
|
||||
char *utf8_buf = RZ_NEWS0(char, utf8_len + 1);
|
||||
if (!utf8_buf) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *it;
|
||||
char *ptr = utf8_buf;
|
||||
char *const endptr = utf8_buf + utf8_len;
|
||||
rz_vector_foreach (map, it) {
|
||||
const RzUnicodeCaseMapping *um = (RzUnicodeCaseMapping *)it;
|
||||
ptr = unicode_mapping_append_to_buf(um, ptr, endptr);
|
||||
if (!ptr) {
|
||||
RZ_FREE(utf8_buf);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return utf8_buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Find index of the first (unicode) character in the following word in \p buffer.
|
||||
*/
|
||||
static ssize_t emacs_mode_find_word_start(RZ_NONNULL const RzCodePoints *buffer, ssize_t start) {
|
||||
rz_return_val_if_fail(buffer && rz_vector_len(buffer), -1);
|
||||
while (start < rz_vector_len(buffer)) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, start);
|
||||
if (is_word_constituent(*cp)) {
|
||||
break;
|
||||
}
|
||||
++start;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Find index of the last (unicode) character in the following word in \p buffer.
|
||||
*/
|
||||
static ssize_t emacs_mode_find_word_end(RZ_NONNULL const RzCodePoints *buffer, ssize_t start) {
|
||||
rz_return_val_if_fail(buffer && rz_vector_len(buffer), -1);
|
||||
ssize_t end = RZ_MIN(start + 1, rz_vector_len(buffer));
|
||||
while (end < rz_vector_len(buffer)) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, end);
|
||||
if (!is_word_constituent(*cp)) {
|
||||
break;
|
||||
}
|
||||
++end;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Find the starting byte index of a unicode code point in \p buffer.
|
||||
*
|
||||
* \param buffer RzVector <RzUnicodePoint>.
|
||||
* \param cp_index Index of the unicode code point.
|
||||
*
|
||||
* \return ssize_t -1 in case of failure.
|
||||
*/
|
||||
static ssize_t emacs_mode_find_byte_index(RZ_NONNULL const RzCodePoints *buffer, size_t cp_index) {
|
||||
rz_return_val_if_fail(cp_index <= rz_vector_len(buffer), -1);
|
||||
ssize_t bytei = 0;
|
||||
for (size_t i = 0; i < cp_index; ++i) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
|
||||
const size_t len = rz_utf8_byte_length(*cp);
|
||||
bytei += len ? len : 1;
|
||||
}
|
||||
return bytei;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Capitalize the current or next word in \p buffer.
|
||||
*
|
||||
* \param buffer RzVector <RzCodePoint>.
|
||||
* \param start Starting index of the word in \p buffer.
|
||||
* \param end Ending index of the word in \p buffer.
|
||||
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
|
||||
*
|
||||
* \return Capitalized word as UTF-8 encoded (null-terminated) string.
|
||||
*/
|
||||
static RZ_OWN RZ_NULLABLE char *emacs_mode_capitalize(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
|
||||
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
|
||||
|
||||
RzUnicodeCaseMappings *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
|
||||
ssize_t i = start;
|
||||
for (; i < end; ++i) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
|
||||
if (iswalpha((wint_t)(*cp))) {
|
||||
RzUnicodeCaseMapping um = rz_unicode_code_point_find_upper(*cp);
|
||||
rz_vector_push(map, &um);
|
||||
break;
|
||||
} else {
|
||||
RzUnicodeCaseMapping um = rz_unicode_case_mapping_default(*cp);
|
||||
rz_vector_push(map, &um);
|
||||
}
|
||||
}
|
||||
for (++i; i < end; ++i) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
|
||||
RzUnicodeCaseMapping um = rz_unicode_code_point_find_lower(*cp);
|
||||
rz_vector_push(map, &um);
|
||||
}
|
||||
|
||||
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
|
||||
rz_vector_free(map);
|
||||
return utf8_buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Lowercase the current or next word in \p buffer.
|
||||
*
|
||||
* \param buffer RzVector <RzCodePoint>.
|
||||
* \param start Starting index of the word in \p buffer.
|
||||
* \param end Ending index of the word in \p buffer.
|
||||
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
|
||||
*
|
||||
* \return Lowercase word as UTF-8 encoded (null-terminated) string.
|
||||
*/
|
||||
static char *emacs_mode_tolower(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
|
||||
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
|
||||
|
||||
RzVector *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
|
||||
for (ssize_t i = start; i < end; ++i) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
|
||||
RzUnicodeCaseMapping um = rz_unicode_code_point_find_lower(*cp);
|
||||
rz_vector_push(map, &um);
|
||||
}
|
||||
|
||||
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
|
||||
rz_vector_free(map);
|
||||
return utf8_buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Uppercase the current or next word in \p buffer.
|
||||
*
|
||||
* \param buffer RzVector <RzCodePoint>.
|
||||
* \param start Starting index of the word in \p buffer.
|
||||
* \param end Ending index of the word in \p buffer.
|
||||
* \param utf8_len Number of bytes between \p start (inclusive) and \p end (exclusive) in \p buffer.
|
||||
*
|
||||
* \return Uppercase word as UTF-8 encoded (null-terminated) string.
|
||||
*/
|
||||
static RZ_OWN RZ_NULLABLE char *emacs_mode_toupper(RZ_NONNULL const RzCodePoints *buffer, ssize_t start, ssize_t end, size_t utf8_len) {
|
||||
rz_return_val_if_fail(buffer && rz_vector_len(buffer) > 0 && utf8_len > 0 && start <= end && end <= rz_vector_len(buffer), NULL);
|
||||
|
||||
RzVector *map = rz_vector_new(sizeof(RzUnicodeCaseMapping), NULL, NULL);
|
||||
for (ssize_t i = start; i < end; ++i) {
|
||||
const RzCodePoint *cp = rz_vector_index_ptr(buffer, i);
|
||||
RzUnicodeCaseMapping um = rz_unicode_code_point_find_upper(*cp);
|
||||
rz_vector_push(map, &um);
|
||||
}
|
||||
|
||||
char *utf8_buf = unicode_mapping_to_str(map, utf8_len);
|
||||
rz_vector_free(map);
|
||||
return utf8_buf;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Decode a UTF-8 encoded string as a vector of RzCodePoint into \p out.
|
||||
*
|
||||
* \param buf The UTF-8 encoded string.
|
||||
* \param buflen Length of \p buf.
|
||||
* \param out RzVector <RzCodePoint>.
|
||||
*
|
||||
* \return false on failure.
|
||||
*/
|
||||
static bool utf8_decode_buf(const ut8 *buf, size_t buflen, RZ_NONNULL RzCodePoints *out) {
|
||||
rz_return_val_if_fail(out, false);
|
||||
for (size_t i = 0; i < buflen;) {
|
||||
RzCodePoint cp;
|
||||
const size_t len = rz_utf8_decode(buf + i, buflen - i, &cp, false);
|
||||
void *elem = rz_vector_push(out, &cp);
|
||||
if (!elem) {
|
||||
return false;
|
||||
}
|
||||
i += len ? len : 1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool emacs_mode_modify_one(RzEmacsModeModifyOp op, RZ_NONNULL RzLine *line, RZ_NONNULL const RzCodePoints *cpbuffer, ssize_t start, size_t end) {
|
||||
rz_return_val_if_fail(line && cpbuffer && start >= 0 && end >= 0, false);
|
||||
const ssize_t start_byte = emacs_mode_find_byte_index(cpbuffer, start);
|
||||
const ssize_t end_byte = emacs_mode_find_byte_index(cpbuffer, end);
|
||||
if (start_byte == -1 || end_byte == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
char *mword = NULL; ///< The modified word
|
||||
switch (op) {
|
||||
default:
|
||||
rz_return_val_if_reached(false);
|
||||
case EMACS_MODIFY_CAPITALIZE:
|
||||
mword = emacs_mode_capitalize(cpbuffer, start, end, end_byte - start_byte);
|
||||
break;
|
||||
case EMACS_MODIFY_TOLOWER:
|
||||
mword = emacs_mode_tolower(cpbuffer, start, end, end_byte - start_byte);
|
||||
break;
|
||||
case EMACS_MODIFY_TOUPPER:
|
||||
mword = emacs_mode_toupper(cpbuffer, start, end, end_byte - start_byte);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!mword) {
|
||||
return false;
|
||||
}
|
||||
char *p = line->buffer.data + start_byte;
|
||||
memcpy(p, mword, end_byte - start_byte);
|
||||
RZ_FREE(mword);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Capitalizes/lowercases/uppercases the current or next word(s).
|
||||
* Note: Optionally sets the cursor position to the end of the last word.
|
||||
*
|
||||
* \param opts The modify options
|
||||
* \param line RzLine
|
||||
*
|
||||
* \return false on failure
|
||||
*/
|
||||
RZ_IPI bool rz_emacs_mode_modify(RZ_NONNULL RzEmacsModeModifyOpts *opts, RZ_NONNULL RzLine *line) {
|
||||
rz_return_val_if_fail(line && opts, false);
|
||||
if (line->buffer.length < 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
RzCodePoints *cpbuffer = rz_vector_new(sizeof(RzCodePoint), NULL, NULL);
|
||||
const ut8 *line_buf = (ut8 *)line->buffer.data;
|
||||
const bool decoded = utf8_decode_buf(line_buf, line->buffer.length, cpbuffer);
|
||||
if (!decoded || rz_vector_len(cpbuffer) < 1) {
|
||||
goto cleanup_and_exit;
|
||||
}
|
||||
|
||||
const size_t word_count = opts->word_count_provided ? opts->word_count : 1;
|
||||
const size_t utf8_index = rz_utf8_strnlen(line_buf, line->buffer.index);
|
||||
|
||||
ssize_t start = 0, end = 0; ///< index of first/last unicode code point in the current word
|
||||
start = emacs_mode_find_word_start(cpbuffer, utf8_index);
|
||||
end = emacs_mode_find_word_end(cpbuffer, start);
|
||||
for (size_t wi = 0; wi < word_count && start < end; ++wi) {
|
||||
if (start == -1 || end == -1) {
|
||||
goto cleanup_and_exit;
|
||||
}
|
||||
const bool failed = !emacs_mode_modify_one(opts->op, line, cpbuffer, start, end);
|
||||
if (failed) {
|
||||
goto cleanup_and_exit;
|
||||
}
|
||||
if (opts->move_cursor) {
|
||||
line->buffer.index = emacs_mode_find_byte_index(cpbuffer, end);
|
||||
}
|
||||
start = emacs_mode_find_word_start(cpbuffer, end);
|
||||
end = emacs_mode_find_word_end(cpbuffer, start);
|
||||
}
|
||||
rz_vector_free(cpbuffer);
|
||||
return true;
|
||||
|
||||
cleanup_and_exit:
|
||||
rz_vector_free(cpbuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Reset \p opts to their default state.
|
||||
*
|
||||
* \param opts The modify options.
|
||||
*/
|
||||
RZ_IPI void rz_emacs_mode_modify_opts_reset(RZ_NONNULL RzEmacsModeModifyOpts *opts) {
|
||||
rz_return_if_fail(opts);
|
||||
opts->move_cursor = true;
|
||||
opts->word_count_provided = false;
|
||||
opts->word_count = 0;
|
||||
}
|
||||
30
librz/cons/i/private.h
Normal file
30
librz/cons/i/private.h
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// SPDX-FileCopyrightText: 2026 Ehab-24 <ehabs1775@gmail.com>
|
||||
// SPDX-License-Identifier: LGPL-3.0-only
|
||||
|
||||
#ifndef RZ_CONS_PRIVATE_H_
|
||||
#define RZ_CONS_PRIVATE_H_
|
||||
|
||||
#include <rz_util.h>
|
||||
#include <rz_types.h>
|
||||
#include <rz_cons.h>
|
||||
|
||||
typedef enum {
|
||||
EMACS_MODIFY_CAPITALIZE,
|
||||
EMACS_MODIFY_TOLOWER,
|
||||
EMACS_MODIFY_TOUPPER
|
||||
} RzEmacsModeModifyOp;
|
||||
|
||||
typedef struct {
|
||||
RzEmacsModeModifyOp op;
|
||||
bool move_cursor; ///< if true, cursor is moved to the end of the last word.
|
||||
bool word_count_provided; ///< if true \p word_count is used, else a single word is modified.
|
||||
size_t word_count; ///< number of words to modify.
|
||||
} RzEmacsModeModifyOpts;
|
||||
|
||||
typedef RzVector /*<RzCodePoint>*/ RzCodePoints;
|
||||
typedef RzVector /*<RzUnicodeCaseMapping>*/ RzUnicodeCaseMappings;
|
||||
|
||||
RZ_IPI void rz_emacs_mode_modify_opts_reset(RZ_NONNULL RzEmacsModeModifyOpts *opts);
|
||||
RZ_IPI bool rz_emacs_mode_modify(RZ_NONNULL RzEmacsModeModifyOpts *opts, RZ_NONNULL RzLine *line);
|
||||
|
||||
#endif
|
||||
|
|
@ -17,7 +17,8 @@ rz_cons_sources = [
|
|||
'prompt.c',
|
||||
'cpipe.c',
|
||||
'rgb.c',
|
||||
'cutf8.c'
|
||||
'cutf8.c',
|
||||
'emacs_mode.c'
|
||||
]
|
||||
|
||||
rz_cons = library('rz_cons', rz_cons_sources,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
#include <rz_project.h>
|
||||
#include <rz_flirt.h>
|
||||
#include <rz_socket.h>
|
||||
#include <locale.h>
|
||||
|
||||
static bool is_valid_gdb_file(RzCoreFile *fh) {
|
||||
RzIODesc *d = fh && fh->core ? rz_io_desc_get(fh->core->io, fh->fd) : NULL;
|
||||
|
|
@ -444,6 +445,8 @@ RZ_API int rz_main_rizin(int argc, const char **argv) {
|
|||
RzList *prefiles = rz_list_new();
|
||||
RzCmdStateOutput state = { 0 };
|
||||
|
||||
setlocale(LC_CTYPE, "");
|
||||
|
||||
#define LISTS_FREE() \
|
||||
{ \
|
||||
rz_list_free(cmds); \
|
||||
|
|
|
|||
Loading…
Reference in a new issue