diff --git a/config/config.c b/config/config.c index c899d0f9..2d2347a5 100644 --- a/config/config.c +++ b/config/config.c @@ -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 */ diff --git a/doc/rofi.1.markdown b/doc/rofi.1.markdown index 79f342b8..1e705501 100644 --- a/doc/rofi.1.markdown +++ b/doc/rofi.1.markdown @@ -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'. diff --git a/include/helper.h b/include/helper.h index d85e4737..937bb30d 100644 --- a/include/helper.h +++ b/include/helper.h @@ -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 @@ -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 @@ -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); /*@}*/ /** @@ -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. diff --git a/include/settings.h b/include/settings.h index d5be2dfe..d0eb4074 100644 --- a/include/settings.h +++ b/include/settings.h @@ -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 */ diff --git a/include/view-internal.h b/include/view-internal.h index da6bd09a..bd48a68a 100644 --- a/include/view-internal.h +++ b/include/view-internal.h @@ -144,6 +144,8 @@ struct RofiViewState { /** Regexs used for matching */ rofi_int_matcher **tokens; + /** For case-sensitivity */ + gboolean case_sensitive; }; /** @} */ #endif diff --git a/source/helper.c b/source/helper.c index 60a9556b..28a9eba1 100644 --- a/source/helper.c +++ b/source/helper.c @@ -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; @@ -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]; @@ -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; } @@ -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 @@ -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++) { diff --git a/source/modes/dmenu.c b/source/modes/dmenu.c index 026244f6..1482de9d 100644 --- a/source/modes/dmenu.c +++ b/source/modes/dmenu.c @@ -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)) { @@ -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)) { diff --git a/source/view.c b/source/view.c index e1054bee..c21d7d12 100644 --- a/source/view.c +++ b/source/view.c @@ -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 "±"; } @@ -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); @@ -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. @@ -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: @@ -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. @@ -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 diff --git a/source/xrmoptions.c b/source/xrmoptions.c index 077bf0ec..e4cbe4af 100644 --- a/source/xrmoptions.c +++ b/source/xrmoptions.c @@ -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}, diff --git a/test/helper-test.c b/test/helper-test.c index 1aca07e7..9e6aa5c2 100644 --- a/test/helper-test.c +++ b/test/helper-test.c @@ -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. @@ -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}",