From bf376c8ba718f7b613a6919e3ca5239c944d36f1 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Tue, 14 Dec 2021 13:35:07 +0100 Subject: [PATCH 1/8] Added a fuzzy search modal --- data/Application.css | 15 ++ plugins/fuzzy-search/file-item.vala | 46 ++++ plugins/fuzzy-search/fuzzy-finder.vala | 203 ++++++++++++++++++ plugins/fuzzy-search/fuzzy-search-dialog.vala | 128 +++++++++++ plugins/fuzzy-search/fuzzy-search.plugin | 9 + plugins/fuzzy-search/fuzzy-search.vala | 114 ++++++++++ plugins/fuzzy-search/meson.build | 36 ++++ plugins/fuzzy-search/search-result.vala | 13 ++ plugins/meson.build | 1 + 9 files changed, 565 insertions(+) create mode 100644 plugins/fuzzy-search/file-item.vala create mode 100644 plugins/fuzzy-search/fuzzy-finder.vala create mode 100644 plugins/fuzzy-search/fuzzy-search-dialog.vala create mode 100644 plugins/fuzzy-search/fuzzy-search.plugin create mode 100644 plugins/fuzzy-search/fuzzy-search.vala create mode 100644 plugins/fuzzy-search/meson.build create mode 100644 plugins/fuzzy-search/search-result.vala diff --git a/data/Application.css b/data/Application.css index 4020d41d60..cd24caed51 100644 --- a/data/Application.css +++ b/data/Application.css @@ -20,3 +20,18 @@ textview.scrubber { border: 0; } +.fuzzy-item { + padding: 5px; + margin-left: 10px; + margin-right: 10px; +} + +.fuzzy-item .fuzzy-file-icon { + margin-right: 5px; +} + +.fuzzy-item.preselect-fuzzy { + background-color: #64baff; + border-radius: 5px; + color: #fafafa; +} \ No newline at end of file diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala new file mode 100644 index 0000000000..13ad2a9cee --- /dev/null +++ b/plugins/fuzzy-search/file-item.vala @@ -0,0 +1,46 @@ +public class FileItem : Gtk.Box { + private SearchResult result; + + public string filepath { + get { + return result.full_path; + } + } + public FileItem (SearchResult res) { + result = res; + Icon icon; + var path_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 1); + path_box.valign = Gtk.Align.CENTER; + + var path_label = new Gtk.Label (result.relative_path); + path_label.halign = Gtk.Align.START; + + var filename_label = new Gtk.Label (Path.get_basename (result.relative_path)); + filename_label.halign = Gtk.Align.START; + var attrs = new Pango.AttrList (); + attrs.insert (Pango.attr_weight_new (Pango.Weight.BOLD)); + filename_label.attributes = attrs; + + try { + var fi = File.new_for_path (result.full_path); + var info = fi.query_info ("standard::*", 0); + icon = ContentType.get_icon (info.get_content_type ()); + } catch (Error e) { + icon = ContentType.get_icon ("text/plain"); + } + + var image = new Gtk.Image.from_gicon (icon, Gtk.IconSize.DND); + image.get_style_context ().add_class ("fuzzy-file-icon"); + + path_box.add (filename_label); + path_box.add (path_label); + + add (image); + add (path_box); + } + + construct { + orientation = Gtk.Orientation.HORIZONTAL; + valign = Gtk.Align.CENTER; + } +} diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala new file mode 100644 index 0000000000..6e13ee83b6 --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -0,0 +1,203 @@ +const int SEQUENTIAL_BONUS = 15; // bonus for adjacent matches +const int SEPARATOR_BONUS = 30; // bonus if match occurs after a separator +const int CAMEL_BONUS = 30; // bonus if match is uppercase and prev is lower +const int FIRST_LETTER_BONUS = 15; // bonus if the first letter is matched +const int LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in str before the first match +const int MAX_LEADING_LETTER_PENALTY = -15; // maximum penalty for leading letters +const int UNMATCHED_LETTER_PENALTY = -1; + + +public class Scratch.Services.FuzzyFinder { + private class RecursiveFinder { + int recursion_limit; + int max_matches; + int recursion_count; + + public RecursiveFinder (int limit = 10, int mx_mtchs = 40) { + recursion_limit = limit; + max_matches = mx_mtchs; + recursion_count = 0; + } + + private bool limit_reached () { + return recursion_count >= recursion_limit; + } + + public SearchResult fuzzy_match_recursive (string pattern, string str) { + var matches = new Gee.ArrayList (); + return fuzzy_match_recursive_internal (pattern,str, 0, 0, 0, matches); + } + + private SearchResult fuzzy_match_recursive_internal (string pattern, string str, int pattern_current_index, int str_current_index, int next_match, + Gee.ArrayList matches, Gee.ArrayList? src_matches = null) { + var out_score = 0; + // Recursion params + bool recursive_match = false; + var best_recursive_matches = new Gee.ArrayList (); + var best_recursive_score = 0; + // Loop through pattern and str looking for a match. + bool firstMatch = true; + + recursion_count++; + if (limit_reached ()) { + return new SearchResult(false, out_score); + } + + // Return if we reached ends of strings. + if (pattern_current_index == pattern.length || str_current_index == str.length) { + return new SearchResult(false, out_score); + } + + while (pattern_current_index < pattern.length && str_current_index < str.length) { + var lowerCaseChar = pattern.get_char (pattern_current_index).tolower (); + var lowerCaseStrChar = str.get_char (str_current_index).tolower (); + + // Match found. + if (lowerCaseChar == lowerCaseStrChar) { + if (next_match >= max_matches) { + return new SearchResult (false, out_score); + } + + if (firstMatch && src_matches != null) { + matches.clear (); + matches.insert_all (0, src_matches); + firstMatch = false; + } + + var recursive_matches = new Gee.ArrayList (); + var recursive_result_search = fuzzy_match_recursive_internal ( + pattern, + str, + pattern_current_index, + str_current_index + 1, + next_match, + recursive_matches, + matches + ); + + if (recursive_result_search.found) { + // Pick best recursive score. + if (!recursive_match || recursive_result_search.score > best_recursive_score) { + best_recursive_matches.clear (); + best_recursive_matches.insert_all (0, recursive_matches); + best_recursive_score = recursive_result_search.score; + } + recursive_match = true; + } + + if (matches.size <= next_match) { + matches.add (str_current_index); + } else { + matches[next_match++] = str_current_index; + } + ++pattern_current_index; + } + ++str_current_index; + } + + var matched = pattern_current_index == pattern.length; + if (matched) { + out_score = 100; + + // Apply leading letter penalty + var penalty = LEADING_LETTER_PENALTY * matches[0]; + penalty = + penalty < MAX_LEADING_LETTER_PENALTY + ? MAX_LEADING_LETTER_PENALTY + : penalty; + out_score += penalty; + + //Apply unmatched penalty + var unmatched = str.length - next_match; + out_score += UNMATCHED_LETTER_PENALTY * unmatched; + + // Apply ordering bonuses + for (var i = 0; i < next_match; i++) { + var current_index = matches[i]; + + if (i > 0) { + var previous_index = matches[i - 1]; + + if (current_index == previous_index + 1) { + out_score += SEQUENTIAL_BONUS; + } + } + + // Check for bonuses based on neighbor character value. + if (current_index > 0) { + // Camel case + var neighbor = str[current_index - 1]; + var curr = str[current_index]; + if (neighbor != neighbor.toupper () && curr != curr.tolower ()) { + out_score += CAMEL_BONUS; + } + var is_neighbour_separator = neighbor == '_' || neighbor == ' '; + if (is_neighbour_separator) { + out_score += SEPARATOR_BONUS; + } + } else { + // First letter + out_score += FIRST_LETTER_BONUS; + } + } + + // Return best result + if (out_score <= 0) { + return new SearchResult (false, out_score); + } else if (recursive_match && (!matched || best_recursive_score > out_score)) { + // Recursive score is better than "this" + matches.insert_all (0, best_recursive_matches); + out_score = best_recursive_score; + return new SearchResult (true, out_score); + } else if (matched) { + // "this" score is better than recursive + return new SearchResult (true, out_score); + } else { + return new SearchResult (false, out_score); + } + } + return new SearchResult (false, out_score); + } + } + + int recursion_limit; + int max_matches; + Gee.HashMap project_paths; + + public FuzzyFinder(Gee.HashMap pps, int limit = 10, int mx_mtchs = 256) { + max_matches = mx_mtchs; + recursion_limit = limit; + project_paths = pps; + } + + public Gee.ArrayList fuzzy_find (string search_str) { + var results = new Gee.ArrayList (); + + foreach (var project in project_paths.values) { + foreach (var path in project.relative_file_paths) { + var search_result = fuzzy_match (search_str, path); + if (search_result.found) { + var root_path = project.root_path; + search_result.relative_path = path; + search_result.full_path = @"$root_path/$path"; + results.add (search_result); + } + } + } + + results.sort ((a, b) => { + return b.score - a.score; + }); + + if (results.size <= 20) { + return results; + } + + return (Gee.ArrayList) results.slice (0, 20); + } + + private SearchResult fuzzy_match (string pattern, string str) { + var finder = new RecursiveFinder (recursion_limit, max_matches); + return finder.fuzzy_match_recursive (pattern,str); + } + } diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala new file mode 100644 index 0000000000..6d2afe69c1 --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -0,0 +1,128 @@ +public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { + private Gtk.Entry search_term_entry; + private Services.FuzzyFinder fuzzy_finder; + private Gtk.Box search_result_container; + private int preselected_index; + Gee.HashMap project_paths; + Gee.ArrayList items; + + public signal void open_file (string filepath); + public signal void close_search (); + + public FuzzySearchDialog (Gee.HashMap pps) { + Object ( + transient_for: ((Gtk.Application) GLib.Application.get_default ()).active_window, + deletable: false, + modal: true, + title: _("Search project files…"), + resizable: false, + width_request: 600 + ); + fuzzy_finder = new Services.FuzzyFinder (pps); + project_paths = pps; + items = new Gee.ArrayList (); + } + + construct { + search_term_entry = new Gtk.Entry (); + search_term_entry.halign = Gtk.Align.CENTER; + search_term_entry.expand = true; + search_term_entry.width_request = 575; + + var box = get_content_area (); + box.orientation = Gtk.Orientation.VERTICAL; + + var layout = new Gtk.Grid () { + column_spacing = 12, + row_spacing = 6 + }; + layout.attach (search_term_entry, 0, 0, 2); + layout.show_all (); + + search_result_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 1); + var scrolled = new Gtk.ScrolledWindow (null, null); + scrolled.add (search_result_container); + scrolled.margin_top = 10; + + search_term_entry.key_press_event.connect ((e) => { + // Handle key up/down to select other files found by fuzzy search + if (e.keyval == Gdk.Key.Down) { + var item = items.get (preselected_index++); + if (preselected_index >= items.size) { + preselected_index = 0; + } + var next_item = items.get (preselected_index); + preselect_new_item (item, next_item); + + return true; + } else if (e.keyval == Gdk.Key.Up) { + var item = items.get (preselected_index--); + if (preselected_index < 0) { + preselected_index = items.size -1; + } + var next_item = items.get (preselected_index); + preselect_new_item (item, next_item); + return true; + } else if (e.keyval == Gdk.Key.Escape) { + // Handle seperatly, otherwise it takes 2 escape hits to close the + // modal + close_search (); + return true; + } + return false; + }); + + search_term_entry.activate.connect (() => { + if (items.size > 0) { + var item = items.get (preselected_index); + open_file (item.filepath.strip ()); + } + }); + + search_term_entry.changed.connect ((e) => { + if (search_term_entry.text.length >= 1) { + foreach (var c in search_result_container.get_children ()) { + search_result_container.remove (c); + } + var results = fuzzy_finder.fuzzy_find (search_term_entry.text); + + bool first = true; + items.clear (); + + foreach (var result in results) { + var s = new FileItem (result); + int window_height; + int window_width; + + if (first) { + first = false; + s.get_style_context ().add_class ("preselect-fuzzy"); + preselected_index = 0; + } + + s.get_style_context ().add_class ("fuzzy-item"); + + search_result_container.add (s); + items.add (s); + } + + scrolled.show_all (); + } else if (search_term_entry.text.length == 0) { + foreach (var c in search_result_container.get_children ()) { + search_result_container.remove (c); + } + } + }); + + scrolled.height_request = 42 * 5; + + box.add (layout); + box.add (scrolled); + } + + private void preselect_new_item (FileItem old_item, FileItem new_item) { + var class_name = "preselect-fuzzy"; + old_item.get_style_context ().remove_class (class_name); + new_item.get_style_context ().add_class (class_name); + } + } diff --git a/plugins/fuzzy-search/fuzzy-search.plugin b/plugins/fuzzy-search/fuzzy-search.plugin new file mode 100644 index 0000000000..1bb55ef5be --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-search.plugin @@ -0,0 +1,9 @@ +[Plugin] +Module=fuzzy-search +Loader=C +IAge=1 +Name=Fuzzy Search +Description=Fuzzy search all project files +Icon=system-search +Authors=Marvin Ahlgrimm +Copyright=Copyright © 2021 Marvin Ahlgrimm diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala new file mode 100644 index 0000000000..721f8f19ee --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -0,0 +1,114 @@ + + +public class Scratch.Services.SearchProject { + public string root_path { get; private set; } + public Gee.ArrayList relative_file_paths { get; private set; } + + public SearchProject (string root) { + root_path = root; + relative_file_paths = new Gee.ArrayList (); + parse (root_path); + } + + private void parse (string path) { + try { + if (path.contains ("node_modules")) { + return; + } + var dir = Dir.open (path); + var name = dir.read_name (); + while (name != null) + { + var new_search_path = ""; + if (path.has_suffix ("/")) { + new_search_path = path.substring (0, path.length - 1); + } else { + new_search_path = path; + } + parse (new_search_path + "/" + name); + name = dir.read_name (); + } + } catch (FileError e) { + var subpath = path.replace (root_path, ""); + relative_file_paths.add (subpath.substring (1, subpath.length-1)); + } + } +} + + + +public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { + MainWindow window = null; + private Gee.ArrayList paths; + private Gee.HashMap project_paths; + + Scratch.Services.Interface plugins; + public Object object {owned get; construct;} + + public void update_state () { + } + + public void activate () { + plugins = (Scratch.Services.Interface) object; + paths = new Gee.ArrayList (); + project_paths = new Gee.HashMap (); + + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var project = new Services.SearchProject(path); + project_paths[path] = project; + } + + plugins.hook_window.connect ((w) => { + if (window != null) + return; + + window = w; + window.key_press_event.connect (on_window_key_press_event); + }); + } + + bool on_window_key_press_event (Gdk.EventKey event) { + /* p shows fuzzy search dialog */ + if (event.keyval == Gdk.Key.p + && Gdk.ModifierType.CONTROL_MASK in event.state) { + var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths); + + dialog.open_file.connect ((filepath) => { + // Open the file + var file = File.new_for_uri (filepath); + plugins.open_file (file); + dialog.destroy (); + }); + + dialog.close_search.connect (() => dialog.destroy ()); + + int diag_x; + int diag_y; + int window_x; + int window_y; + window.get_position (out window_x, out window_y); + dialog.get_position(out diag_x, out diag_y); + // Move the dialog a bit under the top of the application window + dialog.move(diag_x, window_y + 50); + + dialog.run (); + + return true; + } + + return false; + } + + public void deactivate () {} + +} + +[ModuleInit] +public void peas_register_types (GLib.TypeModule module) { + var objmodule = module as Peas.ObjectModule; + objmodule.register_extension_type ( + typeof (Peas.Activatable), + typeof (Scratch.Plugins.FuzzySearch) + ); +} diff --git a/plugins/fuzzy-search/meson.build b/plugins/fuzzy-search/meson.build new file mode 100644 index 0000000000..9a44cafb19 --- /dev/null +++ b/plugins/fuzzy-search/meson.build @@ -0,0 +1,36 @@ +module_name = 'fuzzy-search' + +module_files = [ + 'fuzzy-search.vala', + 'fuzzy-finder.vala', + 'file-item.vala', + 'fuzzy-search-dialog.vala', + 'search-result.vala', +] + +module_deps = [ + codecore_dep, +] + +shared_module( + module_name, + module_files, + dependencies: module_deps, + install: true, + install_dir: join_paths(pluginsdir, module_name), +) + +custom_target(module_name + '.plugin_merge', + input: module_name + '.plugin', + output: module_name + '.plugin', + command : [msgfmt, + '--desktop', + '--keyword=Description', + '--keyword=Name', + '-d' + join_paths(meson.source_root (), 'po', 'plugins'), + '--template=@INPUT@', + '-o@OUTPUT@', + ], + install : true, + install_dir: join_paths(pluginsdir, module_name), +) diff --git a/plugins/fuzzy-search/search-result.vala b/plugins/fuzzy-search/search-result.vala new file mode 100644 index 0000000000..88b1fc86b5 --- /dev/null +++ b/plugins/fuzzy-search/search-result.vala @@ -0,0 +1,13 @@ +public class SearchResult { + public string full_path; + public string relative_path; + public bool found; + public int score; + + public SearchResult (bool fo, int sc) { + full_path = ""; + relative_path = ""; + found = fo; + score = sc; + } +} \ No newline at end of file diff --git a/plugins/meson.build b/plugins/meson.build index 2e8855ee1d..48709f4d38 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -13,3 +13,4 @@ subdir('strip-trailing-save') subdir('terminal') subdir('vim-emulation') subdir('word-completion') +subdir('fuzzy-search') From e673bbf066b84edcfbdbcac228b56d261a334fb1 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Tue, 14 Dec 2021 18:34:29 +0100 Subject: [PATCH 2/8] Fixed indent --- plugins/fuzzy-search/fuzzy-search.vala | 184 ++++++++++++------------- 1 file changed, 91 insertions(+), 93 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 721f8f19ee..845aa75c23 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -1,107 +1,105 @@ public class Scratch.Services.SearchProject { - public string root_path { get; private set; } - public Gee.ArrayList relative_file_paths { get; private set; } - - public SearchProject (string root) { - root_path = root; - relative_file_paths = new Gee.ArrayList (); - parse (root_path); - } - - private void parse (string path) { - try { - if (path.contains ("node_modules")) { - return; - } - var dir = Dir.open (path); - var name = dir.read_name (); - while (name != null) - { - var new_search_path = ""; - if (path.has_suffix ("/")) { - new_search_path = path.substring (0, path.length - 1); - } else { - new_search_path = path; + public string root_path { get; private set; } + public Gee.ArrayList relative_file_paths { get; private set; } + + public SearchProject (string root) { + root_path = root; + relative_file_paths = new Gee.ArrayList (); + parse (root_path); + } + + private void parse (string path) { + try { + if (path.contains ("node_modules")) { + return; + } + var dir = Dir.open (path); + var name = dir.read_name (); + while (name != null) { + var new_search_path = ""; + if (path.has_suffix ("/")) { + new_search_path = path.substring (0, path.length - 1); + } else { + new_search_path = path; + } + parse (new_search_path + "/" + name); + name = dir.read_name (); + } + } catch (FileError e) { + var subpath = path.replace (root_path, ""); + relative_file_paths.add (subpath.substring (1, subpath.length-1)); } - parse (new_search_path + "/" + name); - name = dir.read_name (); - } - } catch (FileError e) { - var subpath = path.replace (root_path, ""); - relative_file_paths.add (subpath.substring (1, subpath.length-1)); } - } } public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { - MainWindow window = null; - private Gee.ArrayList paths; - private Gee.HashMap project_paths; - - Scratch.Services.Interface plugins; - public Object object {owned get; construct;} - - public void update_state () { - } - - public void activate () { - plugins = (Scratch.Services.Interface) object; - paths = new Gee.ArrayList (); - project_paths = new Gee.HashMap (); - - var settings = new GLib.Settings ("io.elementary.code.folder-manager"); - foreach (unowned string path in settings.get_strv ("opened-folders")) { - var project = new Services.SearchProject(path); - project_paths[path] = project; - } - - plugins.hook_window.connect ((w) => { - if (window != null) - return; - - window = w; - window.key_press_event.connect (on_window_key_press_event); - }); - } - - bool on_window_key_press_event (Gdk.EventKey event) { - /* p shows fuzzy search dialog */ - if (event.keyval == Gdk.Key.p - && Gdk.ModifierType.CONTROL_MASK in event.state) { - var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths); - - dialog.open_file.connect ((filepath) => { - // Open the file - var file = File.new_for_uri (filepath); - plugins.open_file (file); - dialog.destroy (); - }); - - dialog.close_search.connect (() => dialog.destroy ()); - - int diag_x; - int diag_y; - int window_x; - int window_y; - window.get_position (out window_x, out window_y); - dialog.get_position(out diag_x, out diag_y); - // Move the dialog a bit under the top of the application window - dialog.move(diag_x, window_y + 50); - - dialog.run (); - - return true; - } - - return false; - } - - public void deactivate () {} + MainWindow window = null; + private Gee.ArrayList paths; + private Gee.HashMap project_paths; + + Scratch.Services.Interface plugins; + public Object object {owned get; construct;} + + public void update_state () { + } + + public void activate () { + plugins = (Scratch.Services.Interface) object; + paths = new Gee.ArrayList (); + project_paths = new Gee.HashMap (); + + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var project = new Services.SearchProject(path); + project_paths[path] = project; + } + + plugins.hook_window.connect ((w) => { + if (window != null) + return; + + window = w; + window.key_press_event.connect (on_window_key_press_event); + }); + } + + bool on_window_key_press_event (Gdk.EventKey event) { + /* p shows fuzzy search dialog */ + if (event.keyval == Gdk.Key.p + && Gdk.ModifierType.CONTROL_MASK in event.state) { + var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths); + + dialog.open_file.connect ((filepath) => { + // Open the file + var file = File.new_for_uri (filepath); + plugins.open_file (file); + dialog.destroy (); + }); + + dialog.close_search.connect (() => dialog.destroy ()); + + int diag_x; + int diag_y; + int window_x; + int window_y; + window.get_position (out window_x, out window_y); + dialog.get_position(out diag_x, out diag_y); + // Move the dialog a bit under the top of the application window + dialog.move(diag_x, window_y + 50); + + dialog.run (); + + return true; + } + + return false; + } + public void deactivate () {} } [ModuleInit] From ee498aaf7f7bb28ee864675e09405ce0390e208e Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Wed, 15 Dec 2021 07:30:01 +0100 Subject: [PATCH 3/8] Use accent color for selection --- data/Application.css | 4 ++-- src/Services/PluginManager.vala | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/data/Application.css b/data/Application.css index cd24caed51..cc3ae67177 100644 --- a/data/Application.css +++ b/data/Application.css @@ -31,7 +31,7 @@ textview.scrubber { } .fuzzy-item.preselect-fuzzy { - background-color: #64baff; + background-color: @selected_bg_color; border-radius: 5px; - color: #fafafa; + color: alpha (@text_color, 0.7); } \ No newline at end of file diff --git a/src/Services/PluginManager.vala b/src/Services/PluginManager.vala index c8036d5db5..dbe1c08843 100644 --- a/src/Services/PluginManager.vala +++ b/src/Services/PluginManager.vala @@ -1,7 +1,7 @@ // -*- Mode: vala; indent-tabs-mode: nil; tab-width: 4 -*- /*** BEGIN LICENSE - + Copyright (C) 2013 Mario Guerriero This program is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3, as published From 7e324a60a6ce1a642e7f3ce394c5ed3fd45081d7 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Wed, 15 Dec 2021 11:28:04 +0100 Subject: [PATCH 4/8] Listen to git change --- plugins/fuzzy-search/fuzzy-search.vala | 13 ++++++++++++- src/Services/GitManager.vala | 5 +++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 845aa75c23..ae78d182f0 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -12,7 +12,8 @@ public class Scratch.Services.SearchProject { private void parse (string path) { try { - if (path.contains ("node_modules")) { + // TODO: Replace with ignore of .gitignore + if (path.contains ("node_modules") || path.contains ("dist") || path.contains ("build") || path.contains (".git")) { return; } var dir = Dir.open (path); @@ -64,6 +65,16 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { window = w; window.key_press_event.connect (on_window_key_press_event); + var git_manager = Services.GitManager.get_instance (); + git_manager.opened_project.connect ((root_path) => { + var project = new Services.SearchProject(root_path); + project_paths[root_path] = project; + }); + + git_manager.removed_project.connect ((root_path) => { + var project = project_paths[root_path]; + project_paths.unset (root_path, out project); + }); }); } diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 99a28b72e4..d50ea9e87e 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -23,6 +23,9 @@ namespace Scratch.Services { public ListStore project_liststore { get; private set; } public string active_project_path { get; set; default = "";} + public signal void opened_project (string root_path); + public signal void removed_project (string root_path); + static Gee.HashMap project_gitrepo_map; static GitManager? instance; @@ -56,6 +59,7 @@ namespace Scratch.Services { var monitored_repo = new MonitoredRepository (git_repo); project_gitrepo_map.@set (root_path, monitored_repo); + opened_project (root_path); return project_gitrepo_map.@get (root_path); } catch (Error e) { debug ("Error opening git repo for %s, means this probably isn't one: %s", root_path, e.message); @@ -76,6 +80,7 @@ namespace Scratch.Services { uint position; if (project_liststore.find (root_folder, out position)) { project_liststore.remove (position); + removed_project (root_path); } else { critical ("Can't remove: %s", root_path); } From 96c8ee990dff99363fc4e215439109d56dab1a13 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 10:55:09 +0100 Subject: [PATCH 5/8] Use async fuzzy finder and ignore paths from .gitignore --- plugins/fuzzy-search/fuzzy-finder.vala | 64 +++++++++++++------ plugins/fuzzy-search/fuzzy-search-dialog.vala | 51 +++++++++------ plugins/fuzzy-search/fuzzy-search.vala | 42 +++++++----- src/Services/GitManager.vala | 4 ++ 4 files changed, 105 insertions(+), 56 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 6e13ee83b6..52683be94c 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -170,30 +170,56 @@ public class Scratch.Services.FuzzyFinder { project_paths = pps; } + public async Gee.ArrayList fuzzy_find_async (string search_str) { + var results = new Gee.ArrayList (); + + SourceFunc callback = fuzzy_find_async.callback; + new Thread("fuzzy-find", () => { + results = fuzzy_find(search_str); + Idle.add((owned) callback); + }); + + yield; + return results; + } + public Gee.ArrayList fuzzy_find (string search_str) { - var results = new Gee.ArrayList (); - - foreach (var project in project_paths.values) { - foreach (var path in project.relative_file_paths) { - var search_result = fuzzy_match (search_str, path); - if (search_result.found) { - var root_path = project.root_path; - search_result.relative_path = path; - search_result.full_path = @"$root_path/$path"; - results.add (search_result); + var results = new Gee.ArrayList (); + + foreach (var project in project_paths.values) { + foreach (var path in project.relative_file_paths) { + SearchResult search_result; + + // If there is more than one project prepend the project name + // to the front of the path + // This helps to search for specific files only in one project, e.g. + // "code/fuzfind" will probably only return fuzzy_finder.vala from this project + // even if their is a "fuzzy_finder" file in another project + if (project_paths.size > 1) { + var project_name= Path.get_basename (project.root_path); + search_result = fuzzy_match (search_str, @"$project_name/$path"); + } else { + search_result = fuzzy_match (search_str, path); + } + + if (search_result.found) { + var root_path = project.root_path; + search_result.relative_path = path; + search_result.full_path = @"$root_path/$path"; + results.add (search_result); + } } - } - } + } - results.sort ((a, b) => { - return b.score - a.score; - }); + results.sort ((a, b) => { + return b.score - a.score; + }); - if (results.size <= 20) { - return results; - } + if (results.size <= 20) { + return results; + } - return (Gee.ArrayList) results.slice (0, 20); + return (Gee.ArrayList) results.slice (0, 20); } private SearchResult fuzzy_match (string pattern, string str) { diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 6d2afe69c1..fd6c2572ee 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -81,36 +81,45 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_term_entry.changed.connect ((e) => { if (search_term_entry.text.length >= 1) { - foreach (var c in search_result_container.get_children ()) { - search_result_container.remove (c); - } - var results = fuzzy_finder.fuzzy_find (search_term_entry.text); - - bool first = true; - items.clear (); + var previous_text = search_term_entry.text; + fuzzy_finder.fuzzy_find_async.begin (search_term_entry.text, (obj, res) =>{ + var results = fuzzy_finder.fuzzy_find_async.end(res); + bool first = true; + + // If the entry is empty or the text has changed + // since searching, do nothing + if (previous_text.length == 0 || previous_text != search_term_entry.text) { + return; + } - foreach (var result in results) { - var s = new FileItem (result); - int window_height; - int window_width; - if (first) { - first = false; - s.get_style_context ().add_class ("preselect-fuzzy"); - preselected_index = 0; + foreach (var c in search_result_container.get_children ()) { + search_result_container.remove (c); } + items.clear (); - s.get_style_context ().add_class ("fuzzy-item"); + foreach (var result in results) { + var file_item = new FileItem (result); - search_result_container.add (s); - items.add (s); - } + if (first) { + first = false; + file_item.get_style_context ().add_class ("preselect-fuzzy"); + preselected_index = 0; + } + + file_item.get_style_context ().add_class ("fuzzy-item"); - scrolled.show_all (); - } else if (search_term_entry.text.length == 0) { + search_result_container.add (file_item); + items.add (file_item); + } + + scrolled.show_all (); + }); + } else { foreach (var c in search_result_container.get_children ()) { search_result_container.remove (c); } + items.clear (); } }); diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index ae78d182f0..d3d8fbc151 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -1,21 +1,27 @@ - - public class Scratch.Services.SearchProject { public string root_path { get; private set; } public Gee.ArrayList relative_file_paths { get; private set; } + private MonitoredRepository? monitored_repo; - public SearchProject (string root) { + public SearchProject (string root, MonitoredRepository? repo) { root_path = root; + monitored_repo = repo; relative_file_paths = new Gee.ArrayList (); + parse (root_path); } private void parse (string path) { try { - // TODO: Replace with ignore of .gitignore - if (path.contains ("node_modules") || path.contains ("dist") || path.contains ("build") || path.contains (".git")) { - return; + try { + // Don't use paths which are ignored from .gitignore + if (monitored_repo != null && monitored_repo.path_is_ignored (path)) { + return; + } + } catch (Error e) { + warning ("An error occurred while checking if item '%s' is git-ignored: %s", path, e.message); } + var dir = Dir.open (path); var name = dir.read_name (); while (name != null) { @@ -29,14 +35,16 @@ public class Scratch.Services.SearchProject { name = dir.read_name (); } } catch (FileError e) { + // This adds branch is reached when a non-directory was reached, i.e. is a file + // If a file was reached, add it's relative path (starting after the project root path) + // to the list. + // Relativ paths are used because the longer the path is the less accurate are the results var subpath = path.replace (root_path, ""); relative_file_paths.add (subpath.substring (1, subpath.length-1)); } } } - - public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { MainWindow window = null; private Gee.ArrayList paths; @@ -53,24 +61,26 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { paths = new Gee.ArrayList (); project_paths = new Gee.HashMap (); - var settings = new GLib.Settings ("io.elementary.code.folder-manager"); - foreach (unowned string path in settings.get_strv ("opened-folders")) { - var project = new Services.SearchProject(path); - project_paths[path] = project; - } - plugins.hook_window.connect ((w) => { if (window != null) return; + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); window = w; window.key_press_event.connect (on_window_key_press_event); + + foreach (unowned string path in settings.get_strv ("opened-folders")) { + project_paths[path] = new Services.SearchProject(path, Services.GitManager.get_monitored_repository (path)); + } + var git_manager = Services.GitManager.get_instance (); + + //Todo: also listen for non-git projects git_manager.opened_project.connect ((root_path) => { - var project = new Services.SearchProject(root_path); - project_paths[root_path] = project; + project_paths[root_path] = new Services.SearchProject(root_path, Services.GitManager.get_monitored_repository (root_path)); }); + //Todo: also listen for non-git projects git_manager.removed_project.connect ((root_path) => { var project = project_paths[root_path]; project_paths.unset (root_path, out project); diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index d50ea9e87e..d51f73182b 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -35,6 +35,10 @@ namespace Scratch.Services { project_gitrepo_map = new Gee.HashMap (); } + public static MonitoredRepository? get_monitored_repository (string root_path) { + return project_gitrepo_map[root_path]; + } + public static GitManager get_instance () { if (instance == null) { instance = new GitManager (); From 67371366a75eba9ed693aa44a04cffef3016a4a9 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 11:53:50 +0100 Subject: [PATCH 6/8] Resize dialog --- plugins/fuzzy-search/fuzzy-search-dialog.vala | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index fd6c2572ee..cebc8455a3 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -113,6 +113,14 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { items.add (file_item); } + if (items.size > 5) { + scrolled.height_request = 42 * 5; + } else { + scrolled.height_request = 42 * items.size; + } + + + scrolled.hide (); scrolled.show_all (); }); } else { @@ -120,11 +128,10 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_result_container.remove (c); } items.clear (); + scrolled.hide (); } }); - scrolled.height_request = 42 * 5; - box.add (layout); box.add (scrolled); } From 5f40e7b5f99adaed5b4d576eb072416dea4558e4 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 14:32:43 +0100 Subject: [PATCH 7/8] Added scrolling by up/down arrows --- data/Application.css | 5 +- plugins/fuzzy-search/fuzzy-search-dialog.vala | 88 ++++++++++++++----- plugins/fuzzy-search/fuzzy-search.vala | 18 ++-- 3 files changed, 82 insertions(+), 29 deletions(-) diff --git a/data/Application.css b/data/Application.css index cc3ae67177..710081404b 100644 --- a/data/Application.css +++ b/data/Application.css @@ -33,5 +33,8 @@ textview.scrubber { .fuzzy-item.preselect-fuzzy { background-color: @selected_bg_color; border-radius: 5px; - color: alpha (@text_color, 0.7); +} + +.fuzzy-item.preselect-fuzzy label { + opacity: 0.7; } \ No newline at end of file diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index cebc8455a3..33628d1f0c 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -3,13 +3,16 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { private Services.FuzzyFinder fuzzy_finder; private Gtk.Box search_result_container; private int preselected_index; + private Gtk.ScrolledWindow scrolled; Gee.HashMap project_paths; Gee.ArrayList items; + private int window_height; + private int max_items; public signal void open_file (string filepath); public signal void close_search (); - public FuzzySearchDialog (Gee.HashMap pps) { + public FuzzySearchDialog (Gee.HashMap pps, int height) { Object ( transient_for: ((Gtk.Application) GLib.Application.get_default ()).active_window, deletable: false, @@ -18,9 +21,47 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { resizable: false, width_request: 600 ); + window_height = height; fuzzy_finder = new Services.FuzzyFinder (pps); project_paths = pps; items = new Gee.ArrayList (); + + // Limit the shown results if the window height is too small + if (window_height > 400) { + max_items = 5; + } else { + max_items = 3; + } + + scrolled.set_max_content_height (43 /* height */ * max_items); + } + + private void calculate_scroll_offset (int old_position, int new_position) { + // Shortcut if jumping from first to last or the other way round + if (new_position == 0 && old_position > new_position) { + scrolled.vadjustment.value = 0; + return; + } else if (old_position == 0 && new_position == items.size - 1) { + scrolled.vadjustment.value = scrolled.vadjustment.get_upper (); + return; + } + + var size_box = scrolled.vadjustment.get_upper () / items.size; + var current_top = scrolled.vadjustment.value; + var current_bottom = current_top + size_box * (max_items - 2); + if (old_position < new_position) { + // Down movement + var new_adjust = size_box * (preselected_index); + if (new_adjust >= current_bottom) { + scrolled.vadjustment.value = size_box * (preselected_index - (max_items - 1)); + } + } else if (old_position > new_position) { + // Up movement + var new_adjust = size_box * (preselected_index); + if (new_adjust < current_top) { + scrolled.vadjustment.value = new_adjust; + } + } } construct { @@ -40,32 +81,41 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { layout.show_all (); search_result_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 1); - var scrolled = new Gtk.ScrolledWindow (null, null); + scrolled = new Gtk.ScrolledWindow (null, null); scrolled.add (search_result_container); scrolled.margin_top = 10; search_term_entry.key_press_event.connect ((e) => { // Handle key up/down to select other files found by fuzzy search if (e.keyval == Gdk.Key.Down) { - var item = items.get (preselected_index++); - if (preselected_index >= items.size) { - preselected_index = 0; + if (items.size > 0) { + var old_index = preselected_index; + var item = items.get (preselected_index++); + if (preselected_index >= items.size) { + preselected_index = 0; + } + var next_item = items.get (preselected_index); + preselect_new_item (item, next_item); + + calculate_scroll_offset (old_index, preselected_index); } - var next_item = items.get (preselected_index); - preselect_new_item (item, next_item); return true; } else if (e.keyval == Gdk.Key.Up) { - var item = items.get (preselected_index--); - if (preselected_index < 0) { - preselected_index = items.size -1; + if (items.size > 0) { + var old_index = preselected_index; + var item = items.get (preselected_index--); + if (preselected_index < 0) { + preselected_index = items.size -1; + } + var next_item = items.get (preselected_index); + preselect_new_item (item, next_item); + + calculate_scroll_offset (old_index, preselected_index); } - var next_item = items.get (preselected_index); - preselect_new_item (item, next_item); return true; } else if (e.keyval == Gdk.Key.Escape) { - // Handle seperatly, otherwise it takes 2 escape hits to close the - // modal + // Handle seperatly, otherwise it takes 2 escape hits to close the modal close_search (); return true; } @@ -113,15 +163,11 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { items.add (file_item); } - if (items.size > 5) { - scrolled.height_request = 42 * 5; - } else { - scrolled.height_request = 42 * items.size; - } - scrolled.hide (); scrolled.show_all (); + // Reset scrolling + scrolled.vadjustment.value = 0; }); } else { foreach (var c in search_result_container.get_children ()) { @@ -132,6 +178,8 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { } }); + scrolled.propagate_natural_height = true; + box.add (layout); box.add (scrolled); } diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index d3d8fbc151..3d37c24023 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -92,7 +92,16 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { /* p shows fuzzy search dialog */ if (event.keyval == Gdk.Key.p && Gdk.ModifierType.CONTROL_MASK in event.state) { - var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths); + int diag_x; + int diag_y; + int window_x; + int window_y; + int window_height; + int window_width; + window.get_position (out window_x, out window_y); + window.get_size (out window_width, out window_height); + var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); + dialog.get_position(out diag_x, out diag_y); dialog.open_file.connect ((filepath) => { // Open the file @@ -102,13 +111,6 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { }); dialog.close_search.connect (() => dialog.destroy ()); - - int diag_x; - int diag_y; - int window_x; - int window_y; - window.get_position (out window_x, out window_y); - dialog.get_position(out diag_x, out diag_y); // Move the dialog a bit under the top of the application window dialog.move(diag_x, window_y + 50); From 8963310c96c8fed7348437de6475aa79f0b72ef9 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Sat, 18 Dec 2021 14:31:01 +0100 Subject: [PATCH 8/8] Check project content on each search to get all changes --- plugins/fuzzy-search/fuzzy-search.vala | 37 +++++++++----------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 3d37c24023..2ea9c0def9 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -47,8 +47,6 @@ public class Scratch.Services.SearchProject { public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { MainWindow window = null; - private Gee.ArrayList paths; - private Gee.HashMap project_paths; Scratch.Services.Interface plugins; public Object object {owned get; construct;} @@ -58,33 +56,13 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { public void activate () { plugins = (Scratch.Services.Interface) object; - paths = new Gee.ArrayList (); - project_paths = new Gee.HashMap (); plugins.hook_window.connect ((w) => { if (window != null) return; - var settings = new GLib.Settings ("io.elementary.code.folder-manager"); window = w; window.key_press_event.connect (on_window_key_press_event); - - foreach (unowned string path in settings.get_strv ("opened-folders")) { - project_paths[path] = new Services.SearchProject(path, Services.GitManager.get_monitored_repository (path)); - } - - var git_manager = Services.GitManager.get_instance (); - - //Todo: also listen for non-git projects - git_manager.opened_project.connect ((root_path) => { - project_paths[root_path] = new Services.SearchProject(root_path, Services.GitManager.get_monitored_repository (root_path)); - }); - - //Todo: also listen for non-git projects - git_manager.removed_project.connect ((root_path) => { - var project = project_paths[root_path]; - project_paths.unset (root_path, out project); - }); }); } @@ -92,6 +70,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { /* p shows fuzzy search dialog */ if (event.keyval == Gdk.Key.p && Gdk.ModifierType.CONTROL_MASK in event.state) { + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); int diag_x; int diag_y; int window_x; @@ -100,13 +79,21 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { int window_width; window.get_position (out window_x, out window_y); window.get_size (out window_width, out window_height); + + var project_paths = new Gee.HashMap (); + + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var monitor = Services.GitManager.get_monitored_repository (path); + project_paths[path] = new Services.SearchProject(path, monitor); + } var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); dialog.get_position(out diag_x, out diag_y); dialog.open_file.connect ((filepath) => { - // Open the file - var file = File.new_for_uri (filepath); - plugins.open_file (file); + var file = new Scratch.FolderManager.File (filepath); + var doc = new Scratch.Services.Document (window.actions, file.file); + + window.open_document (doc); dialog.destroy (); });