Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add smartcase support like vim #2060

Merged
merged 1 commit into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions config/config.c
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ Settings config = {
.sorting_method = "normal",
/** Case sensitivity of the search */
.case_sensitive = FALSE,
/** Case smart of the search */
.case_smart = FALSE,
/** Cycle through in the element list */
.cycle = TRUE,
/** Height of an element in #chars */
Expand Down
6 changes: 6 additions & 0 deletions doc/rofi.1.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ exec command. For that case, `#` can be used as a separator.
Start in case-sensitive mode. This option can be changed at run-time using the
`-kb-toggle-case-sensitivity` key binding.

`-case-smart`

Start in case-smart mode behave like vim's `smartcase`, which determines
case-sensitivity by input. When enabled, this will suppress `-case-sensitive`
config.

`-cycle`

Cycle through the result list. Default is 'true'.
Expand Down
14 changes: 12 additions & 2 deletions include/helper.h
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,15 @@ char *rofi_expand_path(const char *input);
* @param needlelen The length of the needle
* @param haystack The string to match against
* @param haystacklen The length of the haystack
* @param case_sensitive Whether case is significant.
*
* UTF-8 aware levenshtein distance calculation
*
* @returns the levenshtein distance between needle and haystack
*/
unsigned int levenshtein(const char *needle, const glong needlelen,
const char *haystack, const glong haystacklen);
const char *haystack, const glong haystacklen,
const int case_sensitive);

/**
* @param data the unvalidated character array holding possible UTF-8 data
Expand Down Expand Up @@ -234,6 +236,7 @@ char *rofi_latin_to_utf8_strdup(const char *input, gssize length);
* @param plen Pattern length.
* @param str The input to match against pattern.
* @param slen Length of str.
* @param case_sensitive Whether case is significant.
*
* rofi_scorer_fuzzy_evaluate implements a global sequence alignment algorithm
* to find the maximum accumulated score by aligning `pattern` to `str`. It
Expand Down Expand Up @@ -263,7 +266,7 @@ char *rofi_latin_to_utf8_strdup(const char *input, gssize length);
* @returns the sorting weight.
*/
int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str,
glong slen);
glong slen, const int case_sensitive);
/*@}*/

/**
Expand Down Expand Up @@ -353,6 +356,13 @@ cairo_surface_t *cairo_image_surface_create_from_svg(const gchar *file,
*/
void parse_ranges(char *input, rofi_range_pair **list, unsigned int *length);

/**
* @param input String to parse
*
* @returns String matching should be case sensitive or insensitive
*/
int parse_case_sensitivity(const char *input);

/**
* @param format The format string used. See below for possible syntax.
* @param string The selected entry.
Expand Down
2 changes: 2 additions & 0 deletions include/settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ typedef struct {

/** Search case sensitivity */
unsigned int case_sensitive;
/** Smart case sensitivity like vim */
unsigned int case_smart;
/** Cycle through in the element list */
unsigned int cycle;
/** Height of an element in number of rows */
Expand Down
2 changes: 2 additions & 0 deletions include/view-internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,8 @@ struct RofiViewState {

/** Regexs used for matching */
rofi_int_matcher **tokens;
/** For case-sensitivity */
gboolean case_sensitive;
};
/** @} */
#endif
36 changes: 29 additions & 7 deletions source/helper.c
Original file line number Diff line number Diff line change
Expand Up @@ -768,7 +768,8 @@ char *rofi_expand_path(const char *input) {
((a) < (b) ? ((a) < (c) ? (a) : (c)) : ((b) < (c) ? (b) : (c)))

unsigned int levenshtein(const char *needle, const glong needlelen,
const char *haystack, const glong haystacklen) {
const char *haystack, const glong haystacklen,
int case_sensitive) {
if (needlelen == G_MAXLONG) {
// String to long, we cannot handle this.
return UINT_MAX;
Expand All @@ -784,12 +785,12 @@ unsigned int levenshtein(const char *needle, const glong needlelen,
const char *needles = needle;
column[0] = x;
gunichar haystackc = g_utf8_get_char(haystack);
if (!config.case_sensitive) {
if (!case_sensitive) {
haystackc = g_unichar_tolower(haystackc);
}
for (glong y = 1, lastdiag = x - 1; y <= needlelen; y++) {
gunichar needlec = g_utf8_get_char(needles);
if (!config.case_sensitive) {
if (!case_sensitive) {
needlec = g_unichar_tolower(needlec);
}
unsigned int olddiag = column[y];
Expand Down Expand Up @@ -916,7 +917,7 @@ static int rofi_scorer_get_score_for(enum CharClass prev, enum CharClass curr) {
}

int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str,
glong slen) {
glong slen, int case_sensitive) {
if (slen > FUZZY_SCORER_MAX_LENGTH) {
return -MIN_SCORE;
}
Expand Down Expand Up @@ -951,9 +952,8 @@ int rofi_scorer_fuzzy_evaluate(const char *pattern, glong plen, const char *str,
left = dp[si];
lefts = MAX(lefts + GAP_SCORE, left);
sc = g_utf8_get_char(sit);
if (config.case_sensitive
? pc == sc
: g_unichar_tolower(pc) == g_unichar_tolower(sc)) {
if (case_sensitive ? pc == sc
: g_unichar_tolower(pc) == g_unichar_tolower(sc)) {
int t = score[si] * (pstart ? PATTERN_START_MULTIPLIER
: PATTERN_NON_START_MULTIPLIER);
dp[si] = pfirst ? LEADING_GAP_SCORE * si + t
Expand Down Expand Up @@ -1247,6 +1247,28 @@ void parse_ranges(char *input, rofi_range_pair **list, unsigned int *length) {
}
}
}

int parse_case_sensitivity(const char *input) {
int case_sensitive = config.case_sensitive;
if (config.case_smart) {
// By default case is false, unless the search query has a
// uppercase in it?
case_sensitive = FALSE;
const char *end;
if (g_utf8_validate(input, -1, &end)) {
for (const char *c = (input); !case_sensitive && c != NULL && *c;
c = g_utf8_next_char(c)) {
gunichar uc = g_utf8_get_char(c);
if (g_unichar_isupper(uc)) {
case_sensitive = TRUE;
}
}
}
}

return case_sensitive;
}

void rofi_output_formatted_line(const char *format, const char *string,
int selected_line, const char *filter) {
for (int i = 0; format && format[i]; i++) {
Expand Down
8 changes: 5 additions & 3 deletions source/modes/dmenu.c
Original file line number Diff line number Diff line change
Expand Up @@ -954,7 +954,8 @@ int dmenu_mode_dialog(void) {
char *select = NULL;
find_arg_str("-select", &select);
if (select != NULL) {
rofi_int_matcher **tokens = helper_tokenize(select, config.case_sensitive);
rofi_int_matcher **tokens =
helper_tokenize(select, parse_case_sensitivity(select));
unsigned int i = 0;
for (i = 0; i < cmd_list_length; i++) {
if (helper_token_match(tokens, cmd_list[i].entry)) {
Expand All @@ -965,8 +966,9 @@ int dmenu_mode_dialog(void) {
helper_tokenize_free(tokens);
}
if (find_arg("-dump") >= 0) {
rofi_int_matcher **tokens = helper_tokenize(
config.filter ? config.filter : "", config.case_sensitive);
char *filter = config.filter ? config.filter : "";
rofi_int_matcher **tokens =
helper_tokenize(filter, parse_case_sensitivity(filter));
unsigned int i = 0;
for (i = 0; i < cmd_list_length; i++) {
if (tokens == NULL || helper_token_match(tokens, cmd_list[i].entry)) {
Expand Down
24 changes: 15 additions & 9 deletions source/view.c
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ void rofi_view_get_current_monitor(int *width, int *height) {
*height = CacheState.mon.h;
}
}
static char *get_matching_state(void) {
if (config.case_sensitive) {
static char *get_matching_state(RofiViewState *state) {
if (state->case_sensitive) {
if (config.sort) {
return "±";
}
Expand Down Expand Up @@ -775,12 +775,13 @@ static void filter_elements(thread_state *ts,
glong slen = g_utf8_strlen(str, -1);
switch (config.sorting_method_enum) {
case SORT_FZF:
t->state->distance[i] =
rofi_scorer_fuzzy_evaluate(t->pattern, t->plen, str, slen);
t->state->distance[i] = rofi_scorer_fuzzy_evaluate(
t->pattern, t->plen, str, slen, t->state->case_sensitive);
break;
case SORT_NORMAL:
default:
t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen);
t->state->distance[i] = levenshtein(t->pattern, t->plen, str, slen,
t->state->case_sensitive);
break;
}
g_free(str);
Expand Down Expand Up @@ -1482,7 +1483,12 @@ static gboolean rofi_view_refilter_real(RofiViewState *state) {
unsigned int j = 0;
gchar *pattern = mode_preprocess_input(state->sw, state->text->text);
glong plen = pattern ? g_utf8_strlen(pattern, -1) : 0;
state->tokens = helper_tokenize(pattern, config.case_sensitive);
state->case_sensitive = parse_case_sensitivity(state->text->text);
state->tokens = helper_tokenize(pattern, state->case_sensitive);

if ( config.case_smart && state->case_indicator ) {
textbox_text(state->case_indicator, get_matching_state(state));
}
/**
* On long lists it can be beneficial to parallelize.
* If number of threads is 1, no thread is spawn.
Expand Down Expand Up @@ -1709,7 +1715,7 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) {
if (state->case_indicator != NULL) {
config.sort = !config.sort;
state->refilter = TRUE;
textbox_text(state->case_indicator, get_matching_state());
textbox_text(state->case_indicator, get_matching_state(state));
}
break;
case MODE_PREVIOUS:
Expand Down Expand Up @@ -1739,7 +1745,7 @@ static void rofi_view_trigger_global_action(KeyBindingAction action) {
config.case_sensitive = !config.case_sensitive;
(state->selected_line) = 0;
state->refilter = TRUE;
textbox_text(state->case_indicator, get_matching_state());
textbox_text(state->case_indicator, get_matching_state(state));
}
break;
// Special delete entry command.
Expand Down Expand Up @@ -2397,7 +2403,7 @@ static void rofi_view_add_widget(RofiViewState *state, widget *parent_widget,
TB_AUTOWIDTH | TB_AUTOHEIGHT, NORMAL, "*", 0, 0);
// Add small separator between case indicator and text box.
box_add((box *)parent_widget, WIDGET(state->case_indicator), FALSE);
textbox_text(state->case_indicator, get_matching_state());
textbox_text(state->case_indicator, get_matching_state(state));
}
/**
* ENTRY BOX
Expand Down
6 changes: 6 additions & 0 deletions source/xrmoptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ static XrmOption xrmOptions[] = {
NULL,
"Set case-sensitivity",
CONFIG_DEFAULT},
{xrm_Boolean,
"case-smart",
{.num = &config.case_smart},
NULL,
"Set smartcase like vim (determine case-sensitivity by input)",
CONFIG_DEFAULT},
{xrm_Boolean,
"cycle",
{.num = &config.cycle},
Expand Down
60 changes: 44 additions & 16 deletions test/helper-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -142,25 +142,25 @@ int main(int argc, char **argv) {
*/

TASSERT(levenshtein("aap", g_utf8_strlen("aap", -1), "aap",
g_utf8_strlen("aap", -1)) == 0);
g_utf8_strlen("aap", -1), 0) == 0);
TASSERT(levenshtein("aap", g_utf8_strlen("aap", -1), "aap ",
g_utf8_strlen("aap ", -1)) == 1);
g_utf8_strlen("aap ", -1), 0) == 1);
TASSERT(levenshtein("aap ", g_utf8_strlen("aap ", -1), "aap",
g_utf8_strlen("aap", -1)) == 1);
g_utf8_strlen("aap", -1), 0) == 1);
TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "aap noot",
g_utf8_strlen("aap noot", -1)),
g_utf8_strlen("aap noot", -1), 0),
5u);
TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "noot aap",
g_utf8_strlen("noot aap", -1)),
g_utf8_strlen("noot aap", -1), 0),
5u);
TASSERTE(levenshtein("aap", g_utf8_strlen("aap", -1), "noot aap mies",
g_utf8_strlen("noot aap mies", -1)),
g_utf8_strlen("noot aap mies", -1), 0),
10u);
TASSERTE(levenshtein("noot aap mies", g_utf8_strlen("noot aap mies", -1),
"aap", g_utf8_strlen("aap", -1)),
"aap", g_utf8_strlen("aap", -1), 0),
10u);
TASSERTE(levenshtein("otp", g_utf8_strlen("otp", -1), "noot aap",
g_utf8_strlen("noot aap", -1)),
g_utf8_strlen("noot aap", -1), 0),
5u);
/**
* Quick converision check.
Expand Down Expand Up @@ -192,20 +192,48 @@ int main(int argc, char **argv) {
}
{
TASSERTL(
rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "aap noot mies", 12),
rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "aap noot mies", 12, 0),
-605);
TASSERTL(rofi_scorer_fuzzy_evaluate("anm", 3, "aap noot mies", 12), -155);
TASSERTL(rofi_scorer_fuzzy_evaluate("blu", 3, "aap noot mies", 12),
TASSERTL(rofi_scorer_fuzzy_evaluate("anm", 3, "aap noot mies", 12, 0),
-155);
TASSERTL(rofi_scorer_fuzzy_evaluate("blu", 3, "aap noot mies", 12, 0),
1073741824);
config.case_sensitive = TRUE;
TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12),
TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12, 1),
1073741754);
config.case_sensitive = FALSE;
TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12), -155);
TASSERTL(rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "Anm", 3),
TASSERTL(rofi_scorer_fuzzy_evaluate("Anm", 3, "aap noot mies", 12, 0),
-155);
TASSERTL(rofi_scorer_fuzzy_evaluate("aap noot mies", 12, "Anm", 3, 0),
1073741824);
}

/**
* Case sensitivity
*/
{
int case_smart = config.case_smart;
int case_sensitive = config.case_sensitive;
{
config.case_smart = FALSE;
config.case_sensitive = FALSE;
TASSERT(parse_case_sensitivity("all lower case 你好") == 0);
TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 0);
config.case_sensitive = TRUE;
TASSERT(parse_case_sensitivity("all lower case 你好") == 1);
TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 1);
}
{
config.case_smart = TRUE;
config.case_sensitive = TRUE;
TASSERT(parse_case_sensitivity("all lower case") == 0);
TASSERT(parse_case_sensitivity("AAAAAAAAAAAA") == 1);
config.case_sensitive = FALSE;
TASSERT(parse_case_sensitivity("all lower case 你好") == 0);
TASSERT(parse_case_sensitivity("not All lowEr Case 你好") == 1);
}
config.case_smart = case_smart;
config.case_sensitive = case_sensitive;
}

char *a;
a = helper_string_replace_if_exists(
"{terminal} [-t {title} blub ]-e {cmd}", "{cmd}", "aap", "{title}",
Expand Down
Loading