From 103e5519bcb6798974f4acb9ba47b409b8e05dfc Mon Sep 17 00:00:00 2001 From: eidheim Date: Tue, 10 May 2022 10:30:12 +0200 Subject: [PATCH] Added natural sort to Find File --- src/directories.cpp | 119 +++-------------------------------------- src/utility.cpp | 107 ++++++++++++++++++++++++++++++++++++ src/utility.hpp | 12 +++++ src/window.cpp | 21 +++++--- tests/utility_test.cpp | 20 ++++++- 5 files changed, 159 insertions(+), 120 deletions(-) diff --git a/src/directories.cpp b/src/directories.cpp index 6bbc615..9e8e05f 100644 --- a/src/directories.cpp +++ b/src/directories.cpp @@ -119,111 +119,6 @@ Directories::Directories() : Gtk::ListViewText(1) { tree_store->set_sort_column(column_record.name, Gtk::SortType::SORT_ASCENDING); tree_store->set_sort_func(column_record.name, [this](const Gtk::TreeModel::iterator &it1, const Gtk::TreeModel::iterator &it2) { - /// Natural comparison supporting UTF-8 and locale - struct Natural { - static bool is_digit(char chr) { - return chr >= '0' && chr <= '9'; - } - - static int compare_characters(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2) { - ScopeGuard scope_guard{[&i1, &i2] { - ++i1; - ++i2; - }}; - auto c1 = static_cast(s1[i1]); - auto c2 = static_cast(s2[i2]); - if(c1 < 0b10000000 && c2 < 0b10000000) { // Both characters are ascii - auto at = std::tolower(s1[i1]); - auto bt = std::tolower(s2[i2]); - if(at < bt) - return -1; - else if(at == bt) - return 0; - else - return 1; - } - - Glib::ustring u1; - if(c1 >= 0b11110000) - u1 = s1.substr(i1, 4); - else if(c1 >= 0b11100000) - u1 = s1.substr(i1, 3); - else if(c1 >= 0b11000000) - u1 = s1.substr(i1, 2); - else - u1 = s1[i1]; - - Glib::ustring u2; - if(c2 >= 0b11110000) - u2 = s2.substr(i2, 4); - else if(c2 >= 0b11100000) - u2 = s2.substr(i2, 3); - else if(c2 >= 0b11000000) - u2 = s2.substr(i2, 2); - else - u2 = s2[i2]; - - i1 += u1.bytes() - 1; - i2 += u2.bytes() - 1; - - u1 = u1.lowercase(); - u2 = u2.lowercase(); - - if(u1 < u2) - return -1; - else if(u1 == u2) - return 0; - else - return 1; - } - - static int compare_numbers(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2) { - int result = 0; - while(true) { - if(i1 >= s1.size() || !is_digit(s1[i1])) { - if(i2 >= s2.size() || !is_digit(s2[i2])) // a and b has equal number of digits - return result; - return -1; // a has fewer digits - } - if(i2 >= s2.size() || !is_digit(s2[i2])) - return 1; // b has fewer digits - - if(result == 0) { - if(s1[i1] < s2[i2]) - result = -1; - if(s1[i1] > s2[i2]) - result = 1; - } - ++i1; - ++i2; - } - } - - static int compare(const std::string &s1, const std::string &s2) { - size_t i1 = 0; - size_t i2 = 0; - while(i1 < s1.size() && i2 < s2.size()) { - if(is_digit(s1[i1]) && !is_digit(s2[i2])) - return -1; - if(!is_digit(s1[i1]) && is_digit(s2[i2])) - return 1; - if(!is_digit(s1[i1]) && !is_digit(s2[i2])) { - auto result = compare_characters(i1, i2, s1, s2); - if(result != 0) - return result; - } - else { - auto result = compare_numbers(i1, i2, s1, s2); - if(result != 0) - return result; - } - } - if(i1 >= s1.size()) - return -1; - return 1; - } - }; - auto name1 = it1->get_value(column_record.name); auto name2 = it2->get_value(column_record.name); if(name1.empty()) @@ -240,7 +135,7 @@ Directories::Directories() : Gtk::ListViewText(1) { return Natural::compare(prefix1 + name1, prefix2 + name2); }); - set_enable_search(true); //TODO: why does this not work in OS X? + set_enable_search(true); // TODO: why does this not work in OS X? set_search_column(column_record.name); signal_row_activated().connect([this](const Gtk::TreeModel::Path &path, Gtk::TreeViewColumn *column) { @@ -522,7 +417,7 @@ void Directories::open(const boost::filesystem::path &dir_path) { path = filesystem::get_normal_path(dir_path); - //TODO: report that set_title does not handle '_' correctly? + // TODO: report that set_title does not handle '_' correctly? auto title = path.filename().string(); size_t pos = 0; while((pos = title.find('_', pos)) != std::string::npos) { @@ -582,7 +477,7 @@ void Directories::select(const boost::filesystem::path &select_path) { if(!filesystem::file_in_path(select_path, path)) return; - //return if the select_path is already selected + // return if the select_path is already selected auto iter = get_selection()->get_selected(); if(iter) { if(iter->get_value(column_record.path) == select_path) @@ -597,9 +492,9 @@ void Directories::select(const boost::filesystem::path &select_path) { else parent_path = select_path.parent_path(); - //check if select_path is already expanded + // check if select_path is already expanded if(directories.find(parent_path.string()) != directories.end()) { - //set cursor at select_path and return + // set cursor at select_path and return tree_store->foreach_iter([this, &select_path](const Gtk::TreeModel::iterator &iter) { if(iter->get_value(column_record.path) == select_path) { auto tree_path = Gtk::TreePath(iter); @@ -618,7 +513,7 @@ void Directories::select(const boost::filesystem::path &select_path) { paths.emplace_front(parent_path); } - //expand to select_path + // expand to select_path for(auto &a_path : paths) { tree_store->foreach_iter([this, &a_path](const Gtk::TreeModel::iterator &iter) { if(iter->get_value(column_record.path) == a_path) { @@ -629,7 +524,7 @@ void Directories::select(const boost::filesystem::path &select_path) { }); } - //set cursor at select_path + // set cursor at select_path tree_store->foreach_iter([this, &select_path](const Gtk::TreeModel::iterator &iter) { if(iter->get_value(column_record.path) == select_path) { auto tree_path = Gtk::TreePath(iter); diff --git a/src/utility.cpp b/src/utility.cpp index 4987d28..992c329 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -1,6 +1,7 @@ #include "utility.hpp" #include #include +#include #include ScopeGuard::~ScopeGuard() { @@ -232,3 +233,109 @@ int version_compare(const std::string &lhs, const std::string &rhs) { return -1; return 1; } + +bool Natural::is_digit(char chr) { + return chr >= '0' && chr <= '9'; +} + +int Natural::compare_characters(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2) { + ScopeGuard scope_guard{[&i1, &i2] { + ++i1; + ++i2; + }}; + auto c1 = static_cast(s1[i1]); + auto c2 = static_cast(s2[i2]); + if(c1 < 0b10000000 && c2 < 0b10000000) { // Both characters are ascii + auto at = std::tolower(s1[i1]); + auto bt = std::tolower(s2[i2]); + if(at < bt) + return -1; + else if(at == bt) + return 0; + else + return 1; + } + + Glib::ustring u1; + if(c1 >= 0b11110000) + u1 = s1.substr(i1, 4); + else if(c1 >= 0b11100000) + u1 = s1.substr(i1, 3); + else if(c1 >= 0b11000000) + u1 = s1.substr(i1, 2); + else + u1 = s1[i1]; + + Glib::ustring u2; + if(c2 >= 0b11110000) + u2 = s2.substr(i2, 4); + else if(c2 >= 0b11100000) + u2 = s2.substr(i2, 3); + else if(c2 >= 0b11000000) + u2 = s2.substr(i2, 2); + else + u2 = s2[i2]; + + i1 += u1.bytes() - 1; + i2 += u2.bytes() - 1; + + u1 = u1.lowercase(); + u2 = u2.lowercase(); + + if(u1 < u2) + return -1; + else if(u1 == u2) + return 0; + else + return 1; +} + +int Natural::compare_numbers(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2) { + int result = 0; + while(true) { + if(i1 >= s1.size() || !is_digit(s1[i1])) { + if(i2 >= s2.size() || !is_digit(s2[i2])) // a and b has equal number of digits + return result; + return -1; // a has fewer digits + } + if(i2 >= s2.size() || !is_digit(s2[i2])) + return 1; // b has fewer digits + + if(result == 0) { + if(s1[i1] < s2[i2]) + result = -1; + if(s1[i1] > s2[i2]) + result = 1; + } + ++i1; + ++i2; + } +} + +int Natural::compare(const std::string &s1, const std::string &s2) { + size_t i1 = 0; + size_t i2 = 0; + while(i1 < s1.size() && i2 < s2.size()) { + if(is_digit(s1[i1]) && !is_digit(s2[i2])) + return -1; + if(!is_digit(s1[i1]) && is_digit(s2[i2])) + return 1; + if(!is_digit(s1[i1]) && !is_digit(s2[i2])) { + auto result = compare_characters(i1, i2, s1, s2); + if(result != 0) + return result; + } + else { + auto result = compare_numbers(i1, i2, s1, s2); + if(result != 0) + return result; + } + } + + if(i1 == s1.size()) { + if(i2 == s2.size()) + return 0; + return -1; + } + return 1; +} diff --git a/src/utility.hpp b/src/utility.hpp index bc4d84c..1f4e1d9 100644 --- a/src/utility.hpp +++ b/src/utility.hpp @@ -35,3 +35,15 @@ std::string to_hex_string(const std::string &input); /// Returns -1 if lhs is smaller than rhs, 0 if equal, and 1 if lhs is larger than rhs int version_compare(const std::string &lhs, const std::string &rhs); + +class Natural { + static bool is_digit(char chr); + + static int compare_characters(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2); + + static int compare_numbers(size_t &i1, size_t &i2, const std::string &s1, const std::string &s2); + +public: + /// Natural comparison supporting UTF-8 and locale + static int compare(const std::string &s1, const std::string &s2); +}; diff --git a/src/window.cpp b/src/window.cpp index 1db4eef..080e762 100644 --- a/src/window.cpp +++ b/src/window.cpp @@ -17,6 +17,7 @@ #include "project.hpp" #include "selection_dialog.hpp" #include "terminal.hpp" +#include "utility.hpp" #include Window::Window() { @@ -186,7 +187,7 @@ void Window::configure() { } else css_provider_theme = Gtk::CssProvider::get_named(Config::get().theme.name, Config::get().theme.variant); - //TODO: add check if theme exists, or else write error to terminal + // TODO: add check if theme exists, or else write error to terminal Gtk::StyleContext::add_provider_for_screen(screen, css_provider_theme, GTK_STYLE_PROVIDER_PRIORITY_SETTINGS); static Glib::RefPtr css_provider_fonts; @@ -992,12 +993,18 @@ void Window::set_menu_actions() { it.no_push(); continue; } - - auto row = filesystem::get_relative_path(path, view_folder).string(); - SelectionDialog::get()->add_row(open_files.count(path.string()) ? "" + row + "" : row); files.emplace_back(path); } + std::sort(files.begin(), files.end(), [](const boost::filesystem::path &path1, const boost::filesystem::path &path2) { + return Natural::compare(path1.string(), path2.string()) < 0; + }); + + for(auto &file : files) { + auto row = filesystem::get_relative_path(file, view_folder).string(); + SelectionDialog::get()->add_row(open_files.count(file.string()) ? "" + row + "" : row); + } + if(files.empty()) { Info::get().print("No files found in current project"); return; @@ -1997,7 +2004,7 @@ bool Window::on_key_press_event(GdkEventKey *event) { if(event->keyval == GDK_KEY_Escape) EntryBox::get().hide(); -#ifdef __APPLE__ //For Apple's Command-left, right, up, down keys +#ifdef __APPLE__ // For Apple's Command-left, right, up, down keys else if((event->state & GDK_META_MASK) > 0 && (event->state & GDK_MOD1_MASK) == 0) { if(event->keyval == GDK_KEY_Left || event->keyval == GDK_KEY_KP_Left) { event->keyval = GDK_KEY_Home; @@ -2291,8 +2298,8 @@ void Window::rename_token_entry() { auto spelling = std::make_shared(view->get_token_spelling()); if(!spelling->empty()) { EntryBox::get().entries.emplace_back(*spelling, [view, spelling, iter = view->get_buffer()->get_insert()->get_iter()](const std::string &content) { - //TODO: gtk needs a way to check if iter is valid without dumping g_error message - //iter->get_buffer() will print such a message, but no segfault will occur + // TODO: gtk needs a way to check if iter is valid without dumping g_error message + // iter->get_buffer() will print such a message, but no segfault will occur if(Notebook::get().get_current_view() == view && content != *spelling && iter.get_buffer() && view->get_buffer()->get_insert()->get_iter() == iter) view->rename_similar_tokens(content); else diff --git a/tests/utility_test.cpp b/tests/utility_test.cpp index 8305d4b..c86fcd9 100644 --- a/tests/utility_test.cpp +++ b/tests/utility_test.cpp @@ -180,4 +180,22 @@ int main() { g_assert(version_compare("1.2.3x", "1.2.3z") == -1); g_assert(version_compare("1.2.3a", "1.2.3z") == -1); } -} \ No newline at end of file + { + g_assert(Natural::compare("0", "0") == 0); + g_assert(Natural::compare("0", "1") == -1); + g_assert(Natural::compare("1", "0") == 1); + g_assert(Natural::compare("10", "9") == 1); + g_assert(Natural::compare("9", "10") == -1); + g_assert(Natural::compare("99", "99") == 0); + g_assert(Natural::compare("a", "a") == 0); + g_assert(Natural::compare("a", "b") == -1); + g_assert(Natural::compare("b", "a") == 1); + g_assert(Natural::compare("", "") == 0); + g_assert(Natural::compare("z", "") == 1); + g_assert(Natural::compare("", "z") == -1); + g_assert(Natural::compare("zz", "z") == 1); + g_assert(Natural::compare("z", "zz") == -1); + g_assert(Natural::compare("z2", "z11") == -1); + g_assert(Natural::compare("z11", "z2") == 1); + } +}