From 50e3501c596c2a179f67ddc79fad076bc2c7fa45 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Tue, 14 Dec 2021 13:35:07 +0100 Subject: [PATCH 01/45] 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 aa8891eb76..906dcd0780 100644 --- a/plugins/meson.build +++ b/plugins/meson.build @@ -10,3 +10,4 @@ subdir('preserve-indent') subdir('spell') subdir('vim-emulation') subdir('word-completion') +subdir('fuzzy-search') From dbd8692b557c9af1e42d5a9f2fa10f564f51f65c Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Tue, 14 Dec 2021 18:34:29 +0100 Subject: [PATCH 02/45] 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 021ba912514e7696ea1cdffcfe302108e1b63315 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Wed, 15 Dec 2021 07:30:01 +0100 Subject: [PATCH 03/45] Use accent color for selection --- data/Application.css | 4 ++-- 1 file changed, 2 insertions(+), 2 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 From b51d4565ebaa78ab7c87d4ac4a705ba2a04d932a Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Wed, 15 Dec 2021 11:28:04 +0100 Subject: [PATCH 04/45] Listen to git change --- plugins/fuzzy-search/fuzzy-search.vala | 13 ++++++++++++- src/Services/GitManager.vala | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) 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 891fdc1a3f..41f4b172f8 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; @@ -51,9 +54,10 @@ namespace Scratch.Services { try { var git_repo = Ggit.Repository.open (root_folder.file.file); if (!project_gitrepo_map.has_key (root_path)) { - 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 ( @@ -86,6 +90,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 eb3827d2908f53c46e98983dd4988251ef1ffb01 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 10:55:09 +0100 Subject: [PATCH 05/45] 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 41f4b172f8..2bef2c5a09 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 830c45d7a7bfb38937c9749989a8fdcdcce631cc Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 11:53:50 +0100 Subject: [PATCH 06/45] 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 88adb7221d788ed44268113e53f48178cce9c0c3 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Thu, 16 Dec 2021 14:32:43 +0100 Subject: [PATCH 07/45] 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 b1be136ca4d1615856c9c3295aafcea728de82b6 Mon Sep 17 00:00:00 2001 From: Marvin Ahlgrimm Date: Sat, 18 Dec 2021 14:31:01 +0100 Subject: [PATCH 08/45] 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 (); }); From 4db8e571c716d60025ba47591e6d4df8b890d0ca Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 14:01:12 +0000 Subject: [PATCH 09/45] Add basic debouncing and cancellation for each search query --- plugins/fuzzy-search/fuzzy-finder.vala | 28 +++++-- plugins/fuzzy-search/fuzzy-search-dialog.vala | 77 ++++++++++++------- 2 files changed, 72 insertions(+), 33 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 52683be94c..4756c8cd9b 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -170,12 +170,12 @@ public class Scratch.Services.FuzzyFinder { project_paths = pps; } - public async Gee.ArrayList fuzzy_find_async (string search_str) { + public async Gee.ArrayList fuzzy_find_async (string search_str, GLib.Cancellable cancellable) { var results = new Gee.ArrayList (); SourceFunc callback = fuzzy_find_async.callback; new Thread("fuzzy-find", () => { - results = fuzzy_find(search_str); + results = fuzzy_find(search_str, cancellable); Idle.add((owned) callback); }); @@ -183,11 +183,27 @@ public class Scratch.Services.FuzzyFinder { return results; } - public Gee.ArrayList fuzzy_find (string search_str) { + public Gee.ArrayList fuzzy_find (string search_str, GLib.Cancellable cancellable) { var results = new Gee.ArrayList (); + var projects = project_paths.values.to_array (); - foreach (var project in project_paths.values) { - foreach (var path in project.relative_file_paths) { + for (int i = 0; i < projects.length; i++) { + if (cancellable.is_cancelled ()) { + if (results.size <= 20) { + return results; + } + + return (Gee.ArrayList) results.slice (0, 20); + } + + var project = projects[i]; + + for (int j = 0; j < project.relative_file_paths.size; j++) { + if (cancellable.is_cancelled ()) { + return results; + } + + var path = project.relative_file_paths[j]; SearchResult search_result; // If there is more than one project prepend the project name @@ -226,4 +242,4 @@ public class Scratch.Services.FuzzyFinder { 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 index 33628d1f0c..106f2e1555 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -8,6 +8,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { Gee.ArrayList items; private int window_height; private int max_items; + private Gee.LinkedList cancellables; public signal void open_file (string filepath); public signal void close_search (); @@ -25,6 +26,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { fuzzy_finder = new Services.FuzzyFinder (pps); project_paths = pps; items = new Gee.ArrayList (); + cancellables = new Gee.LinkedList (); // Limit the shown results if the window height is too small if (window_height > 400) { @@ -132,42 +134,63 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_term_entry.changed.connect ((e) => { if (search_term_entry.text.length >= 1) { 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; - } + if (cancellables.size > 0) { + var last_cancellable = cancellables.last (); + last_cancellable.cancel (); + } + Timeout.add (100, () => { + var next_cancellable = new GLib.Cancellable (); + cancellables.add (next_cancellable); + fuzzy_finder.fuzzy_find_async.begin (search_term_entry.text, next_cancellable, (obj, res) =>{ + if (next_cancellable.is_cancelled ()) { + cancellables.remove (next_cancellable); + return; + } - foreach (var c in search_result_container.get_children ()) { - search_result_container.remove (c); - } - items.clear (); + var results = fuzzy_finder.fuzzy_find_async.end (res); + if (results == null) { + return; + } + + print ("Results Size: %d\n", results.size); - foreach (var result in results) { - var file_item = new FileItem (result); + bool first = true; - if (first) { - first = false; - file_item.get_style_context ().add_class ("preselect-fuzzy"); - preselected_index = 0; + // 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; } - file_item.get_style_context ().add_class ("fuzzy-item"); + foreach (var c in search_result_container.get_children ()) { + search_result_container.remove (c); + } + items.clear (); + + foreach (var result in results) { + var file_item = new FileItem (result); + + 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"); + + search_result_container.add (file_item); + items.add (file_item); + } - search_result_container.add (file_item); - items.add (file_item); - } + scrolled.hide (); + scrolled.show_all (); + // Reset scrolling + scrolled.vadjustment.value = 0; + }); - scrolled.hide (); - scrolled.show_all (); - // Reset scrolling - scrolled.vadjustment.value = 0; + return Source.REMOVE; }); } else { foreach (var c in search_result_container.get_children ()) { From c717a35e42675cd91ddeeedac4c7f2f6de1d2406 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 14:14:08 +0000 Subject: [PATCH 10/45] Add cancellable checks to all loops during fuzzy search --- plugins/fuzzy-search/fuzzy-finder.vala | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 4756c8cd9b..3c2342e4fd 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -23,13 +23,13 @@ public class Scratch.Services.FuzzyFinder { return recursion_count >= recursion_limit; } - public SearchResult fuzzy_match_recursive (string pattern, string str) { + public SearchResult fuzzy_match_recursive (string pattern, string str, GLib.Cancellable cancellable) { var matches = new Gee.ArrayList (); - return fuzzy_match_recursive_internal (pattern,str, 0, 0, 0, matches); + return fuzzy_match_recursive_internal (pattern,str, 0, 0, 0, cancellable, 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) { + GLib.Cancellable cancellable, Gee.ArrayList matches, Gee.ArrayList? src_matches = null) { var out_score = 0; // Recursion params bool recursive_match = false; @@ -39,7 +39,7 @@ public class Scratch.Services.FuzzyFinder { bool firstMatch = true; recursion_count++; - if (limit_reached ()) { + if (cancellable.is_cancelled () || limit_reached ()) { return new SearchResult(false, out_score); } @@ -49,6 +49,10 @@ public class Scratch.Services.FuzzyFinder { } while (pattern_current_index < pattern.length && str_current_index < str.length) { + if (cancellable.is_cancelled ()) { + return new SearchResult(false, out_score); + } + var lowerCaseChar = pattern.get_char (pattern_current_index).tolower (); var lowerCaseStrChar = str.get_char (str_current_index).tolower (); @@ -71,6 +75,7 @@ public class Scratch.Services.FuzzyFinder { pattern_current_index, str_current_index + 1, next_match, + cancellable, recursive_matches, matches ); @@ -113,6 +118,10 @@ public class Scratch.Services.FuzzyFinder { // Apply ordering bonuses for (var i = 0; i < next_match; i++) { + if (cancellable.is_cancelled ()) { + return new SearchResult (false, out_score); + } + var current_index = matches[i]; if (i > 0) { @@ -213,9 +222,9 @@ public class Scratch.Services.FuzzyFinder { // 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"); + search_result = fuzzy_match (search_str, @"$project_name/$path", cancellable); } else { - search_result = fuzzy_match (search_str, path); + search_result = fuzzy_match (search_str, path, cancellable); } if (search_result.found) { @@ -238,8 +247,8 @@ public class Scratch.Services.FuzzyFinder { return (Gee.ArrayList) results.slice (0, 20); } - private SearchResult fuzzy_match (string pattern, string str) { + private SearchResult fuzzy_match (string pattern, string str, GLib.Cancellable cancellable) { var finder = new RecursiveFinder (recursion_limit, max_matches); - return finder.fuzzy_match_recursive (pattern,str); + return finder.fuzzy_match_recursive (pattern,str, cancellable); } } From b9e419e656da3ff67374b8f666d81c00ad208a1d Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 14:18:23 +0000 Subject: [PATCH 11/45] Increase fuzzy search results update speed --- plugins/fuzzy-search/fuzzy-search-dialog.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 106f2e1555..0bb5e70b15 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -139,7 +139,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { last_cancellable.cancel (); } - Timeout.add (100, () => { + Timeout.add (1, () => { var next_cancellable = new GLib.Cancellable (); cancellables.add (next_cancellable); fuzzy_finder.fuzzy_find_async.begin (search_term_entry.text, next_cancellable, (obj, res) =>{ From b65057c8550cfd787511f0dfdded85c160ce1481 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 15:56:12 +0000 Subject: [PATCH 12/45] Fix long delay before fuzzy finder dialog opens - Fuzzy finder paths indexed when plugin is activated - Fuzy finder dialog now opens instantly --- plugins/fuzzy-search/fuzzy-finder.vala | 4 +-- plugins/fuzzy-search/fuzzy-search.vala | 44 +++++++++++++++++++------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 3c2342e4fd..d71e7fc656 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -183,9 +183,9 @@ public class Scratch.Services.FuzzyFinder { var results = new Gee.ArrayList (); SourceFunc callback = fuzzy_find_async.callback; - new Thread("fuzzy-find", () => { + new Thread("fuzzy-find", () => { results = fuzzy_find(search_str, cancellable); - Idle.add((owned) callback); + Idle.add ((owned) callback); }); yield; diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 2ea9c0def9..becb72f66c 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -7,11 +7,21 @@ public class Scratch.Services.SearchProject { root_path = root; monitored_repo = repo; relative_file_paths = new Gee.ArrayList (); + } + + public async void parse_async (string path) { + new Thread (null, () => { + parse_async_internal.begin (path, (obj, res) => { + parse_async_internal.end (res); + }); + + Idle.add (parse_async.callback); + }); - parse (root_path); + yield; } - private void parse (string path) { + private async void parse_async_internal (string path) { try { try { // Don't use paths which are ignored from .gitignore @@ -25,20 +35,25 @@ public class Scratch.Services.SearchProject { var dir = Dir.open (path); var name = dir.read_name (); while (name != null) { + print ("Parsed name: %s\n", name); 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); + + parse_async_internal.begin (new_search_path + "/" + name, (obj, res) => { + parse_async_internal.end (res); + }); + 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 + // Relative 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)); } @@ -46,6 +61,8 @@ public class Scratch.Services.SearchProject { } public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { + Gee.HashMap project_paths; + MainWindow window = null; Scratch.Services.Interface plugins; @@ -61,8 +78,20 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { if (window != null) return; + 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 monitor = Services.GitManager.get_monitored_repository (path); + var project_path = new Services.SearchProject (path, monitor); + project_path.parse_async.begin (path, (obj, res) => {}); + + project_paths[path] = project_path; + } + window = w; window.key_press_event.connect (on_window_key_press_event); + }); } @@ -70,7 +99,6 @@ 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; @@ -80,12 +108,6 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { 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); From aa9f61a2fe6e299f8287488f53b32cd334cff936 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 20:38:26 +0000 Subject: [PATCH 13/45] Fix vala-lint errors in fuzzy-search plugin directory --- plugins/fuzzy-search/fuzzy-finder.vala | 32 +++++++++---------- plugins/fuzzy-search/fuzzy-search-dialog.vala | 4 +-- plugins/fuzzy-search/fuzzy-search.vala | 6 ++-- plugins/fuzzy-search/search-result.vala | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index d71e7fc656..5869261ae3 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -25,7 +25,7 @@ public class Scratch.Services.FuzzyFinder { public SearchResult fuzzy_match_recursive (string pattern, string str, GLib.Cancellable cancellable) { var matches = new Gee.ArrayList (); - return fuzzy_match_recursive_internal (pattern,str, 0, 0, 0, cancellable, matches); + return fuzzy_match_recursive_internal (pattern, str, 0, 0, 0, cancellable, matches); } private SearchResult fuzzy_match_recursive_internal (string pattern, string str, int pattern_current_index, int str_current_index, int next_match, @@ -36,36 +36,36 @@ public class Scratch.Services.FuzzyFinder { var best_recursive_matches = new Gee.ArrayList (); var best_recursive_score = 0; // Loop through pattern and str looking for a match. - bool firstMatch = true; + bool first_match = true; recursion_count++; if (cancellable.is_cancelled () || limit_reached ()) { - return new SearchResult(false, out_score); + 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); + return new SearchResult (false, out_score); } while (pattern_current_index < pattern.length && str_current_index < str.length) { if (cancellable.is_cancelled ()) { - return new SearchResult(false, out_score); + return new SearchResult (false, out_score); } - var lowerCaseChar = pattern.get_char (pattern_current_index).tolower (); - var lowerCaseStrChar = str.get_char (str_current_index).tolower (); + var lower_case_char = pattern.get_char (pattern_current_index).tolower (); + var lower_case_str_char = str.get_char (str_current_index).tolower (); // Match found. - if (lowerCaseChar == lowerCaseStrChar) { + if (lower_case_char == lower_case_str_char) { if (next_match >= max_matches) { return new SearchResult (false, out_score); } - if (firstMatch && src_matches != null) { + if (first_match && src_matches != null) { matches.clear (); matches.insert_all (0, src_matches); - firstMatch = false; + first_match = false; } var recursive_matches = new Gee.ArrayList (); @@ -173,7 +173,7 @@ public class Scratch.Services.FuzzyFinder { int max_matches; Gee.HashMap project_paths; - public FuzzyFinder(Gee.HashMap pps, int limit = 10, int mx_mtchs = 256) { + public FuzzyFinder (Gee.HashMap pps, int limit = 10, int mx_mtchs = 256) { max_matches = mx_mtchs; recursion_limit = limit; project_paths = pps; @@ -183,8 +183,8 @@ public class Scratch.Services.FuzzyFinder { var results = new Gee.ArrayList (); SourceFunc callback = fuzzy_find_async.callback; - new Thread("fuzzy-find", () => { - results = fuzzy_find(search_str, cancellable); + new Thread ("fuzzy-find", () => { + results = fuzzy_find (search_str, cancellable); Idle.add ((owned) callback); }); @@ -211,7 +211,7 @@ public class Scratch.Services.FuzzyFinder { if (cancellable.is_cancelled ()) { return results; } - + var path = project.relative_file_paths[j]; SearchResult search_result; @@ -221,7 +221,7 @@ public class Scratch.Services.FuzzyFinder { // "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); + var project_name = Path.get_basename (project.root_path); search_result = fuzzy_match (search_str, @"$project_name/$path", cancellable); } else { search_result = fuzzy_match (search_str, path, cancellable); @@ -249,6 +249,6 @@ public class Scratch.Services.FuzzyFinder { private SearchResult fuzzy_match (string pattern, string str, GLib.Cancellable cancellable) { var finder = new RecursiveFinder (recursion_limit, max_matches); - return finder.fuzzy_match_recursive (pattern,str, cancellable); + return finder.fuzzy_match_recursive (pattern, str, cancellable); } } diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 0bb5e70b15..b1e8a1ee91 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -18,7 +18,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { transient_for: ((Gtk.Application) GLib.Application.get_default ()).active_window, deletable: false, modal: true, - title: _("Search project files…"), + title: _("Search project files…"), resizable: false, width_request: 600 ); @@ -108,7 +108,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { var old_index = preselected_index; var item = items.get (preselected_index--); if (preselected_index < 0) { - preselected_index = items.size -1; + preselected_index = items.size - 1; } var next_item = items.get (preselected_index); preselect_new_item (item, next_item); diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index becb72f66c..7c22560939 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -55,7 +55,7 @@ public class Scratch.Services.SearchProject { // to the list. // Relative 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)); + relative_file_paths.add (subpath.substring (1, subpath.length - 1)); } } } @@ -109,7 +109,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { 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.get_position (out diag_x, out diag_y); dialog.open_file.connect ((filepath) => { var file = new Scratch.FolderManager.File (filepath); @@ -121,7 +121,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { dialog.close_search.connect (() => dialog.destroy ()); // Move the dialog a bit under the top of the application window - dialog.move(diag_x, window_y + 50); + dialog.move (diag_x, window_y + 50); dialog.run (); diff --git a/plugins/fuzzy-search/search-result.vala b/plugins/fuzzy-search/search-result.vala index 88b1fc86b5..d546866eb3 100644 --- a/plugins/fuzzy-search/search-result.vala +++ b/plugins/fuzzy-search/search-result.vala @@ -10,4 +10,4 @@ public class SearchResult { found = fo; score = sc; } -} \ No newline at end of file +} From 6fbf1c6004b3075ff0b0c3c9faeb03a902a9687f Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 21:04:40 +0000 Subject: [PATCH 14/45] Prevent fuzzy search dialog from opening if there are no projects opened --- plugins/fuzzy-search/fuzzy-search.vala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 7c22560939..e2df0e1371 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -99,6 +99,13 @@ 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"); + + string[] opened_folders = settings.get_strv ("opened-folders"); + if (opened_folders == null || opened_folders.length < 1) { + return false; + } + int diag_x; int diag_y; int window_x; From 6b89011b7f3efc65badaeb4a8396eafd2a8492ec Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 21:47:16 +0000 Subject: [PATCH 15/45] Clicking on fuzzy search results loads the clicked result --- plugins/fuzzy-search/file-item.vala | 18 +++++++++++------- plugins/fuzzy-search/fuzzy-search-dialog.vala | 11 +++++++---- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala index 13ad2a9cee..93d402a364 100644 --- a/plugins/fuzzy-search/file-item.vala +++ b/plugins/fuzzy-search/file-item.vala @@ -1,4 +1,4 @@ -public class FileItem : Gtk.Box { +public class FileItem : Gtk.Button { private SearchResult result; public string filepath { @@ -7,6 +7,9 @@ public class FileItem : Gtk.Box { } } public FileItem (SearchResult res) { + this.get_style_context ().add_class ("fuzzy-item"); + this.get_style_context ().add_class ("flat"); + result = res; Icon icon; var path_box = new Gtk.Box (Gtk.Orientation.VERTICAL, 1); @@ -35,12 +38,13 @@ public class FileItem : Gtk.Box { path_box.add (filename_label); path_box.add (path_label); - add (image); - add (path_box); - } + var container_box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 1) { + valign = Gtk.Align.CENTER + }; + + container_box.add (image); + container_box.add (path_box); - construct { - orientation = Gtk.Orientation.HORIZONTAL; - valign = Gtk.Align.CENTER; + this.child = container_box; } } diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index b1e8a1ee91..6e1b186baa 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -126,8 +126,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_term_entry.activate.connect (() => { if (items.size > 0) { - var item = items.get (preselected_index); - open_file (item.filepath.strip ()); + handle_item_selection (preselected_index); } }); @@ -177,10 +176,9 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { preselected_index = 0; } - file_item.get_style_context ().add_class ("fuzzy-item"); - search_result_container.add (file_item); items.add (file_item); + file_item.clicked.connect ((e) => handle_item_selection (items.index_of (file_item))); } @@ -207,6 +205,11 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { box.add (scrolled); } + private void handle_item_selection (int index) { + var item = items.get (index); + open_file (item.filepath.strip ()); + } + 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); From 05a3214d478069e813ce346a109f1f9db14d5c03 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 22:47:47 +0000 Subject: [PATCH 16/45] Fuzzy search results across multiple projects are disambiguated using the project name before file path --- plugins/fuzzy-search/file-item.vala | 7 +++++-- plugins/fuzzy-search/fuzzy-finder.vala | 4 +++- plugins/fuzzy-search/fuzzy-search-dialog.vala | 5 ++++- plugins/fuzzy-search/search-result.vala | 2 ++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala index 93d402a364..36e5c0bce0 100644 --- a/plugins/fuzzy-search/file-item.vala +++ b/plugins/fuzzy-search/file-item.vala @@ -6,7 +6,7 @@ public class FileItem : Gtk.Button { return result.full_path; } } - public FileItem (SearchResult res) { + public FileItem (SearchResult res, bool should_distinguish_project = false) { this.get_style_context ().add_class ("fuzzy-item"); this.get_style_context ().add_class ("flat"); @@ -15,7 +15,10 @@ public class FileItem : Gtk.Button { 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); + var path_label = new Gtk.Label ( + @"$(should_distinguish_project ? result.project + " • " : "")$(result.relative_path)" + ); + path_label.halign = Gtk.Align.START; var filename_label = new Gtk.Label (Path.get_basename (result.relative_path)); diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 5869261ae3..98157e4fe5 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -220,8 +220,9 @@ public class Scratch.Services.FuzzyFinder { // 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 + string project_name = ""; if (project_paths.size > 1) { - var project_name = Path.get_basename (project.root_path); + project_name = Path.get_basename (project.root_path); search_result = fuzzy_match (search_str, @"$project_name/$path", cancellable); } else { search_result = fuzzy_match (search_str, path, cancellable); @@ -231,6 +232,7 @@ public class Scratch.Services.FuzzyFinder { var root_path = project.root_path; search_result.relative_path = path; search_result.full_path = @"$root_path/$path"; + search_result.project = project_name; results.add (search_result); } } diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 6e1b186baa..2644690903 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -9,6 +9,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { private int window_height; private int max_items; private Gee.LinkedList cancellables; + private bool should_distinguish_projects; public signal void open_file (string filepath); public signal void close_search (); @@ -28,6 +29,8 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { items = new Gee.ArrayList (); cancellables = new Gee.LinkedList (); + should_distinguish_projects = project_paths.size > 1; + // Limit the shown results if the window height is too small if (window_height > 400) { max_items = 5; @@ -168,7 +171,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { items.clear (); foreach (var result in results) { - var file_item = new FileItem (result); + var file_item = new FileItem (result, should_distinguish_projects); if (first) { first = false; diff --git a/plugins/fuzzy-search/search-result.vala b/plugins/fuzzy-search/search-result.vala index d546866eb3..ac33b1a4a8 100644 --- a/plugins/fuzzy-search/search-result.vala +++ b/plugins/fuzzy-search/search-result.vala @@ -1,12 +1,14 @@ public class SearchResult { public string full_path; public string relative_path; + public string project; public bool found; public int score; public SearchResult (bool fo, int sc) { full_path = ""; relative_path = ""; + project = ""; found = fo; score = sc; } From 03686c45c5dee693a3d292ba32bb3d97d0deb18a Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Tue, 19 Dec 2023 21:11:11 +0000 Subject: [PATCH 17/45] Update fuzzy search debug logs --- plugins/fuzzy-search/fuzzy-search-dialog.vala | 2 -- plugins/fuzzy-search/fuzzy-search.vala | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 2644690903..dff3b4c209 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -155,8 +155,6 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { return; } - print ("Results Size: %d\n", results.size); - bool first = true; // If the entry is empty or the text has changed diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index e2df0e1371..16a2a1f33c 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -35,7 +35,7 @@ public class Scratch.Services.SearchProject { var dir = Dir.open (path); var name = dir.read_name (); while (name != null) { - print ("Parsed name: %s\n", name); + debug ("Fuzzy Search - Parsed fuzzy search path: %s\n", name); var new_search_path = ""; if (path.has_suffix ("/")) { new_search_path = path.substring (0, path.length - 1); From a8c119a456fc8318056798f908191f8b360aa1f0 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 05:00:21 +0000 Subject: [PATCH 18/45] Improve Fuzzy Search Accuracy - Combine file name and file paths in fuzzy finder --- plugins/fuzzy-search/fuzzy-finder.vala | 36 ++++++++++++++++++-------- plugins/fuzzy-search/fuzzy-search.vala | 4 +-- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 98157e4fe5..6b80d43e4a 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -6,7 +6,6 @@ const int LEADING_LETTER_PENALTY = -5; // penalty applied for every letter in st 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; @@ -140,7 +139,7 @@ public class Scratch.Services.FuzzyFinder { if (neighbor != neighbor.toupper () && curr != curr.tolower ()) { out_score += CAMEL_BONUS; } - var is_neighbour_separator = neighbor == '_' || neighbor == ' '; + var is_neighbour_separator = neighbor == '_' || neighbor == ' ' || neighbor == GLib.Path.DIR_SEPARATOR; if (is_neighbour_separator) { out_score += SEPARATOR_BONUS; } @@ -213,7 +212,8 @@ public class Scratch.Services.FuzzyFinder { } var path = project.relative_file_paths[j]; - SearchResult search_result; + SearchResult path_search_result; + SearchResult filename_search_result; // If there is more than one project prepend the project name // to the front of the path @@ -221,19 +221,33 @@ public class Scratch.Services.FuzzyFinder { // "code/fuzfind" will probably only return fuzzy_finder.vala from this project // even if their is a "fuzzy_finder" file in another project string project_name = ""; + if (project_paths.size > 1) { project_name = Path.get_basename (project.root_path); - search_result = fuzzy_match (search_str, @"$project_name/$path", cancellable); + path_search_result = fuzzy_match (search_str, @"$project_name/$path", cancellable); } else { - search_result = fuzzy_match (search_str, path, cancellable); + path_search_result = fuzzy_match (search_str, path, cancellable); + } + + string filename = Path.get_basename (path); + filename_search_result = fuzzy_match (search_str, filename, cancellable); + + var root_path = project.root_path; + if (filename_search_result.found && path_search_result.found) { + int combined_score = path_search_result.score + filename_search_result.score; + filename_search_result.score = combined_score; } - if (search_result.found) { - var root_path = project.root_path; - search_result.relative_path = path; - search_result.full_path = @"$root_path/$path"; - search_result.project = project_name; - results.add (search_result); + if (filename_search_result.found) { + filename_search_result.relative_path = path; + filename_search_result.full_path = @"$root_path/$path"; + filename_search_result.project = project_name; + results.add (filename_search_result); + } else if (path_search_result.found) { + path_search_result.relative_path = path; + path_search_result.full_path = @"$root_path/$path"; + path_search_result.project = project_name; + results.add (path_search_result); } } } diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 16a2a1f33c..f959441939 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -37,13 +37,13 @@ public class Scratch.Services.SearchProject { while (name != null) { debug ("Fuzzy Search - Parsed fuzzy search path: %s\n", name); var new_search_path = ""; - if (path.has_suffix ("/")) { + if (path.has_suffix (GLib.Path.DIR_SEPARATOR_S)) { new_search_path = path.substring (0, path.length - 1); } else { new_search_path = path; } - parse_async_internal.begin (new_search_path + "/" + name, (obj, res) => { + parse_async_internal.begin (new_search_path + GLib.Path.DIR_SEPARATOR_S + name, (obj, res) => { parse_async_internal.end (res); }); From a514b940a67749aaac5a0e70cfec8cc51a7fc331 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 06:06:49 +0000 Subject: [PATCH 19/45] Ignore dot-prefixed directories in fuzzy search --- plugins/fuzzy-search/fuzzy-search.vala | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index f959441939..bd0920b15d 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -23,6 +23,12 @@ public class Scratch.Services.SearchProject { private async void parse_async_internal (string path) { try { + // Ignore dot-prefixed directories + string path_basename = Path.get_basename (path); + if (FileUtils.test (path, GLib.FileTest.IS_DIR) && path_basename.has_prefix (".")) { + return; + } + try { // Don't use paths which are ignored from .gitignore if (monitored_repo != null && monitored_repo.path_is_ignored (path)) { @@ -34,6 +40,7 @@ public class Scratch.Services.SearchProject { var dir = Dir.open (path); var name = dir.read_name (); + while (name != null) { debug ("Fuzzy Search - Parsed fuzzy search path: %s\n", name); var new_search_path = ""; From ea29d638922a4f10018cec739f82e46cc79be547 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 21:22:20 +0000 Subject: [PATCH 20/45] Update debug messaging for fuzzy search --- plugins/fuzzy-search/fuzzy-search.vala | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index bd0920b15d..e345a6cc44 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -38,11 +38,12 @@ public class Scratch.Services.SearchProject { warning ("An error occurred while checking if item '%s' is git-ignored: %s", path, e.message); } + debug ("Fuzzy Search - Indexing from directory: %s\n", path); + var dir = Dir.open (path); var name = dir.read_name (); while (name != null) { - debug ("Fuzzy Search - Parsed fuzzy search path: %s\n", name); var new_search_path = ""; if (path.has_suffix (GLib.Path.DIR_SEPARATOR_S)) { new_search_path = path.substring (0, path.length - 1); @@ -85,20 +86,8 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { if (window != null) return; - 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 monitor = Services.GitManager.get_monitored_repository (path); - var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, (obj, res) => {}); - - project_paths[path] = project_path; - } - window = w; window.key_press_event.connect (on_window_key_press_event); - }); } @@ -122,6 +111,16 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { window.get_position (out window_x, out window_y); window.get_size (out window_width, out window_height); + project_paths = new Gee.HashMap (); + + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var monitor = Services.GitManager.get_monitored_repository (path); + var project_path = new Services.SearchProject (path, monitor); + project_path.parse_async.begin (path, (obj, res) => {}); + + project_paths[path] = project_path; + } + var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); dialog.get_position (out diag_x, out diag_y); From 949c927001675d8959815ec73ba24b5f978fd06b Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 21:22:52 +0000 Subject: [PATCH 21/45] Fix code style warnings in fuzzy search plugin --- plugins/fuzzy-search/fuzzy-finder.vala | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 6b80d43e4a..55cf17d308 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -27,8 +27,14 @@ public class Scratch.Services.FuzzyFinder { return fuzzy_match_recursive_internal (pattern, str, 0, 0, 0, cancellable, matches); } - private SearchResult fuzzy_match_recursive_internal (string pattern, string str, int pattern_current_index, int str_current_index, int next_match, - GLib.Cancellable cancellable, Gee.ArrayList matches, Gee.ArrayList? src_matches = null) { + private SearchResult fuzzy_match_recursive_internal (string pattern, + string str, + int pattern_current_index, + int str_current_index, + int next_match, + GLib.Cancellable cancellable, + Gee.ArrayList matches, + Gee.ArrayList? src_matches = null) { var out_score = 0; // Recursion params bool recursive_match = false; @@ -139,7 +145,10 @@ public class Scratch.Services.FuzzyFinder { if (neighbor != neighbor.toupper () && curr != curr.tolower ()) { out_score += CAMEL_BONUS; } - var is_neighbour_separator = neighbor == '_' || neighbor == ' ' || neighbor == GLib.Path.DIR_SEPARATOR; + + bool is_neighbour_separator = neighbor == '_' || neighbor == ' ' + || neighbor == GLib.Path.DIR_SEPARATOR; + if (is_neighbour_separator) { out_score += SEPARATOR_BONUS; } From 33be958e257c8abcd4a6a8d91153368775cb0f4c Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 21:41:21 +0000 Subject: [PATCH 22/45] Update license and copyright info for Fuzzy Finder plugin --- plugins/fuzzy-search/file-item.vala | 7 +++++++ plugins/fuzzy-search/fuzzy-finder.vala | 8 ++++++++ plugins/fuzzy-search/fuzzy-search-dialog.vala | 6 ++++++ plugins/fuzzy-search/fuzzy-search.plugin | 2 +- plugins/fuzzy-search/fuzzy-search.vala | 6 ++++++ plugins/fuzzy-search/search-result.vala | 6 ++++++ 6 files changed, 34 insertions(+), 1 deletion(-) diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala index 36e5c0bce0..474293e2de 100644 --- a/plugins/fuzzy-search/file-item.vala +++ b/plugins/fuzzy-search/file-item.vala @@ -1,3 +1,10 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + * Colin Kiama + */ public class FileItem : Gtk.Button { private SearchResult result; diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 55cf17d308..96d8ad7e12 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -1,3 +1,11 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + * Colin Kiama + */ + 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 diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index dff3b4c209..02b99f999f 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -1,3 +1,9 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + */ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { private Gtk.Entry search_term_entry; private Services.FuzzyFinder fuzzy_finder; diff --git a/plugins/fuzzy-search/fuzzy-search.plugin b/plugins/fuzzy-search/fuzzy-search.plugin index 1bb55ef5be..846cb3bc31 100644 --- a/plugins/fuzzy-search/fuzzy-search.plugin +++ b/plugins/fuzzy-search/fuzzy-search.plugin @@ -5,5 +5,5 @@ IAge=1 Name=Fuzzy Search Description=Fuzzy search all project files Icon=system-search -Authors=Marvin Ahlgrimm +Authors=Marvin Ahlgrimm;Colin Kiama Copyright=Copyright © 2021 Marvin Ahlgrimm diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index e345a6cc44..4763bebf26 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -1,3 +1,9 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + */ public class Scratch.Services.SearchProject { public string root_path { get; private set; } public Gee.ArrayList relative_file_paths { get; private set; } diff --git a/plugins/fuzzy-search/search-result.vala b/plugins/fuzzy-search/search-result.vala index ac33b1a4a8..7383c4fdd9 100644 --- a/plugins/fuzzy-search/search-result.vala +++ b/plugins/fuzzy-search/search-result.vala @@ -1,3 +1,9 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + */ public class SearchResult { public string full_path; public string relative_path; From b579d3a353b61cb187e389a233fbd0f161f8731f Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 21:49:50 +0000 Subject: [PATCH 23/45] Add end method to project path parsing async call --- plugins/fuzzy-search/fuzzy-search.vala | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 4763bebf26..20945272e9 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -122,7 +122,9 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { foreach (unowned string path in settings.get_strv ("opened-folders")) { var monitor = Services.GitManager.get_monitored_repository (path); var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, (obj, res) => {}); + project_path.parse_async.begin (path, (obj, res) => { + project_path.parse_async.end (res); + }); project_paths[path] = project_path; } From e1989caaa5f6abf7f1a5ea5dae0913b1121ef8d0 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 20 Dec 2023 21:54:37 +0000 Subject: [PATCH 24/45] Fix vala-lint errors --- plugins/fuzzy-search/fuzzy-finder.vala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 96d8ad7e12..23e869172e 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -5,7 +5,7 @@ * Authored by: Marvin Ahlgrimm * Colin Kiama */ - + 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 From 73a9b3e0b0079ae1f08c8f48782e4f3c3d15bd1a Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Mon, 25 Dec 2023 16:13:06 +0000 Subject: [PATCH 25/45] Improve fuzzy matching - Fix issue how matches are added to list of matches - Stop using a combined score. Prefer filename based matches when available - Stop including directory separator in separator bonus --- plugins/fuzzy-search/fuzzy-finder.vala | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 23e869172e..982a1fc620 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -105,11 +105,12 @@ public class Scratch.Services.FuzzyFinder { if (matches.size <= next_match) { matches.add (str_current_index); - } else { - matches[next_match++] = str_current_index; } + + ++next_match; ++pattern_current_index; } + ++str_current_index; } @@ -154,9 +155,7 @@ public class Scratch.Services.FuzzyFinder { out_score += CAMEL_BONUS; } - bool is_neighbour_separator = neighbor == '_' || neighbor == ' ' - || neighbor == GLib.Path.DIR_SEPARATOR; - + bool is_neighbour_separator = neighbor == '_' || neighbor == ' '; if (is_neighbour_separator) { out_score += SEPARATOR_BONUS; } @@ -250,10 +249,6 @@ public class Scratch.Services.FuzzyFinder { filename_search_result = fuzzy_match (search_str, filename, cancellable); var root_path = project.root_path; - if (filename_search_result.found && path_search_result.found) { - int combined_score = path_search_result.score + filename_search_result.score; - filename_search_result.score = combined_score; - } if (filename_search_result.found) { filename_search_result.relative_path = path; @@ -264,6 +259,7 @@ public class Scratch.Services.FuzzyFinder { path_search_result.relative_path = path; path_search_result.full_path = @"$root_path/$path"; path_search_result.project = project_name; + path_search_result.score = (int) (path_search_result.score * 0.5); results.add (path_search_result); } } From 903505b73cccd473debd02b6f75a33f4ff570910 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 00:49:17 +0000 Subject: [PATCH 26/45] Change fuzzy search keyboard shortcut to 'ALT + F' --- plugins/fuzzy-search/fuzzy-search.plugin | 2 +- plugins/fuzzy-search/fuzzy-search.vala | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.plugin b/plugins/fuzzy-search/fuzzy-search.plugin index 846cb3bc31..3c6ec9c228 100644 --- a/plugins/fuzzy-search/fuzzy-search.plugin +++ b/plugins/fuzzy-search/fuzzy-search.plugin @@ -3,7 +3,7 @@ Module=fuzzy-search Loader=C IAge=1 Name=Fuzzy Search -Description=Fuzzy search all project files +Description=Fuzzy search all project files (Show using ALT + F) Icon=system-search Authors=Marvin Ahlgrimm;Colin Kiama Copyright=Copyright © 2021 Marvin Ahlgrimm diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 20945272e9..2acdec2616 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -1,8 +1,8 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2023 elementary, Inc. +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. * - * Authored by: Marvin Ahlgrimm + * Authored by: Marvin Ahlgrimm */ public class Scratch.Services.SearchProject { public string root_path { get; private set; } @@ -99,8 +99,8 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { 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) { + if (event.keyval == Gdk.Key.f + && Gdk.ModifierType.MOD1_MASK in event.state) { var settings = new GLib.Settings ("io.elementary.code.folder-manager"); string[] opened_folders = settings.get_strv ("opened-folders"); From 8610573d0b7bd3629e75826c4e01d806862ddd39 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 01:22:09 +0000 Subject: [PATCH 27/45] Remove unused GitManager signals --- src/Services/GitManager.vala | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Services/GitManager.vala b/src/Services/GitManager.vala index 2bef2c5a09..679e0e0c14 100644 --- a/src/Services/GitManager.vala +++ b/src/Services/GitManager.vala @@ -23,9 +23,6 @@ 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; @@ -60,7 +57,6 @@ namespace Scratch.Services { if (!project_gitrepo_map.has_key (root_path)) { 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) { @@ -94,7 +90,6 @@ 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 31936a012d8bd17049b5ad15cf08a6d69e91d32b Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 02:06:03 +0000 Subject: [PATCH 28/45] Files that cannot be opened in Code are filtered out of fuzzy search --- plugins/fuzzy-search/fuzzy-search.vala | 17 +++++++++++++++-- src/FolderManager/File.vala | 15 +-------------- src/Utils.vala | 17 +++++++++++++++++ 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 2acdec2616..b5a4c25979 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -67,9 +67,22 @@ public class Scratch.Services.SearchProject { // 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. + // Relative 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)); + if (check_if_valid_path_to_add (path)) { + var subpath = path.replace (root_path, ""); + relative_file_paths.add (subpath.substring (1, subpath.length - 1)); + } + } + } + + private bool check_if_valid_path_to_add (string path) { + try { + File file = File.new_for_path (path); + var file_info = file.query_info ("standard::*", 0); + return Utils.check_if_valid_text_file (path, file_info); + } catch (Error e) { + return false; } } } diff --git a/src/FolderManager/File.vala b/src/FolderManager/File.vala index 8b23a7ad8c..1e3c36d047 100644 --- a/src/FolderManager/File.vala +++ b/src/FolderManager/File.vala @@ -102,20 +102,7 @@ namespace Scratch.FolderManager { // checks if we're dealing with a textfile public bool is_valid_textfile { get { - if (path.has_prefix (".goutputstream")) { - return false; - } - - if (info.get_is_backup ()) { - return false; - } - - if (info.get_file_type () == FileType.REGULAR && - ContentType.is_a (info.get_content_type (), "text/*")) { - return true; - } - - return false; + return Utils.check_if_valid_text_file (path, info); } } diff --git a/src/Utils.vala b/src/Utils.vala index bf3b8f8247..3b09bfc740 100644 --- a/src/Utils.vala +++ b/src/Utils.vala @@ -163,4 +163,21 @@ namespace Scratch.Utils { return true; } + public bool check_if_valid_text_file (string path, FileInfo info) { + if (path.has_prefix (".goutputstream")) { + return false; + } + + if (info.get_is_backup ()) { + return false; + } + + if (info.get_file_type () == FileType.REGULAR && + ContentType.is_a (info.get_content_type (), "text/*")) { + return true; + } + + return false; + } + } From 09df39265e2e6b4889490098105fa6116eb8f35e Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 11:27:02 +0000 Subject: [PATCH 29/45] Render fuzzy search items with list boxes --- plugins/fuzzy-search/file-item.vala | 13 +++----- plugins/fuzzy-search/fuzzy-search-dialog.vala | 33 +++++++++++++------ 2 files changed, 28 insertions(+), 18 deletions(-) diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala index 474293e2de..35ee97f1ee 100644 --- a/plugins/fuzzy-search/file-item.vala +++ b/plugins/fuzzy-search/file-item.vala @@ -1,11 +1,11 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2023 elementary, Inc. +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. * - * Authored by: Marvin Ahlgrimm + * Authored by: Marvin Ahlgrimm * Colin Kiama */ -public class FileItem : Gtk.Button { +public class FileItem : Gtk.ListBoxRow { private SearchResult result; public string filepath { @@ -30,9 +30,6 @@ public class FileItem : Gtk.Button { 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); diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 02b99f999f..1641df8391 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -1,13 +1,13 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2023 elementary, Inc. +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. * - * Authored by: Marvin Ahlgrimm + * Authored by: Marvin Ahlgrimm */ 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 Gtk.ListBox search_result_container; private int preselected_index; private Gtk.ScrolledWindow scrolled; Gee.HashMap project_paths; @@ -91,11 +91,24 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { layout.attach (search_term_entry, 0, 0, 2); layout.show_all (); - search_result_container = new Gtk.Box (Gtk.Orientation.VERTICAL, 1); + search_result_container = new Gtk.ListBox () { + selection_mode = Gtk.SelectionMode.NONE, + activate_on_single_click = true + }; + scrolled = new Gtk.ScrolledWindow (null, null); scrolled.add (search_result_container); scrolled.margin_top = 10; + search_result_container.row_activated.connect ((row) => { + var file_item = row as FileItem; + if (file_item == null) { + return; + } + + handle_item_selection (items.index_of (file_item)); + }); + 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) { @@ -105,9 +118,9 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { 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); } @@ -119,9 +132,9 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { 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); } return true; @@ -130,6 +143,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { close_search (); return true; } + return false; }); @@ -185,10 +199,8 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_result_container.add (file_item); items.add (file_item); - file_item.clicked.connect ((e) => handle_item_selection (items.index_of (file_item))); } - scrolled.hide (); scrolled.show_all (); // Reset scrolling @@ -201,6 +213,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { foreach (var c in search_result_container.get_children ()) { search_result_container.remove (c); } + items.clear (); scrolled.hide (); } From 7f82dc175f2f95b0993d93d4e4a63674a67e1cb9 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 11:28:20 +0000 Subject: [PATCH 30/45] Add mouser hover styling to fuzzy search items --- data/Application.css | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/data/Application.css b/data/Application.css index 710081404b..c902701f49 100644 --- a/data/Application.css +++ b/data/Application.css @@ -26,15 +26,28 @@ textview.scrubber { margin-right: 10px; } -.fuzzy-item .fuzzy-file-icon { - margin-right: 5px; + +.fuzzy-item.preselect-fuzzy, +.fuzzy-item:hover { + border-radius: 5px; } .fuzzy-item.preselect-fuzzy { background-color: @selected_bg_color; - border-radius: 5px; +} + +.fuzzy-item:hover { + background-color: @theme_unfocused_selected_bg_color; +} + +.fuzzy-item .fuzzy-file-icon { + margin-right: 5px; +} + +.fuzzy-item label:nth-child(1) { + font-weight: 700; } .fuzzy-item.preselect-fuzzy label { opacity: 0.7; -} \ No newline at end of file +} From 848e4615a175b4d5b7ace41c09c08087426d97c1 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 16:55:59 +0000 Subject: [PATCH 31/45] Handle Fuzzy Search key presses with EventControllerKey --- data/Application.css | 6 +- plugins/fuzzy-search/fuzzy-search-dialog.vala | 67 +++++----- plugins/fuzzy-search/fuzzy-search.vala | 116 ++++++++++-------- 3 files changed, 105 insertions(+), 84 deletions(-) diff --git a/data/Application.css b/data/Application.css index c902701f49..996cf009de 100644 --- a/data/Application.css +++ b/data/Application.css @@ -20,13 +20,17 @@ textview.scrubber { border: 0; } +.fuzzy-list { + background-color: transparent; +} + .fuzzy-item { padding: 5px; margin-left: 10px; margin-right: 10px; + background-color: transparent; } - .fuzzy-item.preselect-fuzzy, .fuzzy-item:hover { border-radius: 5px; diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-dialog.vala index 1641df8391..8e22a5a905 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-dialog.vala @@ -10,12 +10,13 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { private Gtk.ListBox search_result_container; private int preselected_index; private Gtk.ScrolledWindow scrolled; - Gee.HashMap project_paths; - Gee.ArrayList items; + private Gee.HashMap project_paths; + private Gee.ArrayList items; private int window_height; private int max_items; private Gee.LinkedList cancellables; private bool should_distinguish_projects; + private Gtk.EventControllerKey search_term_entry_key_controller; public signal void open_file (string filepath); public signal void close_search (); @@ -96,6 +97,8 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { activate_on_single_click = true }; + search_result_container.get_style_context ().add_class ("fuzzy-list"); + scrolled = new Gtk.ScrolledWindow (null, null); scrolled.add (search_result_container); scrolled.margin_top = 10; @@ -109,39 +112,43 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { handle_item_selection (items.index_of (file_item)); }); - search_term_entry.key_press_event.connect ((e) => { + search_term_entry_key_controller = new Gtk.EventControllerKey (search_term_entry); + search_term_entry_key_controller.key_pressed.connect ((keyval, keycode, state) => { // Handle key up/down to select other files found by fuzzy search - if (e.keyval == Gdk.Key.Down) { - if (items.size > 0) { - var old_index = preselected_index; - var item = items.get (preselected_index++); - if (preselected_index >= items.size) { - preselected_index = 0; + switch (keyval) { + case Gdk.Key.Down: + 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); - calculate_scroll_offset (old_index, preselected_index); - } + return true; + case Gdk.Key.Up: + if (items.size > 0) { + var old_index = preselected_index; + var item = items.get (preselected_index--); + if (preselected_index < 0) { + preselected_index = items.size - 1; + } - return true; - } else if (e.keyval == Gdk.Key.Up) { - 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); - calculate_scroll_offset (old_index, preselected_index); - } - 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 true; + case Gdk.Key.Escape: + // Handle seperately, otherwise it takes 2 escape hits to close the modal + close_search (); + return true; + default: + break; } return false; diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index b5a4c25979..f4434a5351 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -44,7 +44,7 @@ public class Scratch.Services.SearchProject { warning ("An error occurred while checking if item '%s' is git-ignored: %s", path, e.message); } - debug ("Fuzzy Search - Indexing from directory: %s\n", path); + // debug ("Fuzzy Search - Indexing from directory: %s\n", path); var dir = Dir.open (path); var name = dir.read_name (); @@ -88,14 +88,15 @@ public class Scratch.Services.SearchProject { } public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { - Gee.HashMap project_paths; + public Object object { owned get; construct; } - MainWindow window = null; - - Scratch.Services.Interface plugins; - public Object object {owned get; construct;} + private Gee.HashMap project_paths; + private MainWindow window = null; + private Scratch.Services.Interface plugins; + private Gtk.EventControllerKey key_controller; public void update_state () { + } public void activate () { @@ -106,66 +107,75 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { return; window = w; - window.key_press_event.connect (on_window_key_press_event); + key_controller = new Gtk.EventControllerKey (window); + key_controller.key_pressed.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.f - && Gdk.ModifierType.MOD1_MASK in event.state) { - var settings = new GLib.Settings ("io.elementary.code.folder-manager"); - - string[] opened_folders = settings.get_strv ("opened-folders"); - if (opened_folders == null || opened_folders.length < 1) { - return false; - } - - 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); - - project_paths = new Gee.HashMap (); - - foreach (unowned string path in settings.get_strv ("opened-folders")) { - var monitor = Services.GitManager.get_monitored_repository (path); - var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, (obj, res) => { - project_path.parse_async.end (res); + bool on_window_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { + /* f shows fuzzy search dialog */ + switch (Gdk.keyval_to_upper (keyval)) { + case Gdk.Key.F: + if (state == Gdk.ModifierType.MOD1_MASK) { + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); + + string[] opened_folders = settings.get_strv ("opened-folders"); + if (opened_folders == null || opened_folders.length < 1) { + return false; + } + + 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); + + project_paths = new Gee.HashMap (); + + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var monitor = Services.GitManager.get_monitored_repository (path); + var project_path = new Services.SearchProject (path, monitor); + project_path.parse_async.begin (path, (obj, res) => { + project_path.parse_async.end (res); + }); + + project_paths[path] = project_path; + } + + var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); + dialog.get_position (out diag_x, out diag_y); + + dialog.open_file.connect ((filepath) => { + var file = new Scratch.FolderManager.File (filepath); + var doc = new Scratch.Services.Document (window.actions, file.file); + + window.open_document (doc); + dialog.destroy (); }); - project_paths[path] = project_path; - } - - var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); - dialog.get_position (out diag_x, out diag_y); - - dialog.open_file.connect ((filepath) => { - var file = new Scratch.FolderManager.File (filepath); - var doc = new Scratch.Services.Document (window.actions, file.file); - - window.open_document (doc); - dialog.destroy (); - }); + dialog.close_search.connect (() => dialog.destroy ()); + // Move the dialog a bit under the top of the application window + dialog.move (diag_x, window_y + 50); - dialog.close_search.connect (() => dialog.destroy ()); - // Move the dialog a bit under the top of the application window - dialog.move (diag_x, window_y + 50); + dialog.run (); - dialog.run (); + return true; + } - return true; + break; + default: + return false; } return false; } - public void deactivate () {} + public void deactivate () { + key_controller.key_pressed.disconnect (on_window_key_press_event); + } } [ModuleInit] From 243a3e54a509a8f0075c46ba10785f61ba63c5cf Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 19:49:39 +0000 Subject: [PATCH 32/45] Now using popover to show fuzzy search --- data/Application.css | 27 ++++++-- plugins/fuzzy-search/file-item.vala | 1 + ...-dialog.vala => fuzzy-search-popover.vala} | 64 +++++++++++-------- plugins/fuzzy-search/fuzzy-search.vala | 18 ++---- plugins/fuzzy-search/meson.build | 2 +- plugins/fuzzy-search/search-result.vala | 9 +-- 6 files changed, 69 insertions(+), 52 deletions(-) rename plugins/fuzzy-search/{fuzzy-search-dialog.vala => fuzzy-search-popover.vala} (87%) diff --git a/data/Application.css b/data/Application.css index 996cf009de..a49f389256 100644 --- a/data/Application.css +++ b/data/Application.css @@ -20,12 +20,25 @@ textview.scrubber { border: 0; } +.fuzzy-popover { + padding: 1rem 0; +} + +.fuzzy-popover entry { + margin-left: 1rem; + margin-right: 1rem; +} + +.fuzzy-popover scrolledwindow { + margin-top: 1rem; +} + .fuzzy-list { background-color: transparent; } .fuzzy-item { - padding: 5px; + padding: 0.5rem; margin-left: 10px; margin-right: 10px; background-color: transparent; @@ -33,19 +46,19 @@ textview.scrubber { .fuzzy-item.preselect-fuzzy, .fuzzy-item:hover { - border-radius: 5px; -} - -.fuzzy-item.preselect-fuzzy { - background-color: @selected_bg_color; + border-radius: 0.5rem; } .fuzzy-item:hover { background-color: @theme_unfocused_selected_bg_color; } +.fuzzy-item.preselect-fuzzy { + background-color: @selected_bg_color; +} + .fuzzy-item .fuzzy-file-icon { - margin-right: 5px; + margin-right: 0.5rem; } .fuzzy-item label:nth-child(1) { diff --git a/plugins/fuzzy-search/file-item.vala b/plugins/fuzzy-search/file-item.vala index 35ee97f1ee..62d5a6e466 100644 --- a/plugins/fuzzy-search/file-item.vala +++ b/plugins/fuzzy-search/file-item.vala @@ -5,6 +5,7 @@ * Authored by: Marvin Ahlgrimm * Colin Kiama */ + public class FileItem : Gtk.ListBoxRow { private SearchResult result; diff --git a/plugins/fuzzy-search/fuzzy-search-dialog.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala similarity index 87% rename from plugins/fuzzy-search/fuzzy-search-dialog.vala rename to plugins/fuzzy-search/fuzzy-search-popover.vala index 8e22a5a905..c43ce79eaa 100644 --- a/plugins/fuzzy-search/fuzzy-search-dialog.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -3,8 +3,10 @@ * SPDX-FileCopyrightText: 2023 elementary, Inc. * * Authored by: Marvin Ahlgrimm + * Colin Kiama */ -public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { + +public class Scratch.FuzzySearchPopover : Gtk.Popover { private Gtk.Entry search_term_entry; private Services.FuzzyFinder fuzzy_finder; private Gtk.ListBox search_result_container; @@ -21,15 +23,14 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { public signal void open_file (string filepath); public signal void close_search (); - public FuzzySearchDialog (Gee.HashMap pps, int height) { + public FuzzySearchPopover (Gee.HashMap pps, Gtk.Widget? relative_to, int height) { Object ( - transient_for: ((Gtk.Application) GLib.Application.get_default ()).active_window, - deletable: false, modal: true, - title: _("Search project files…"), - resizable: false, - width_request: 600 + relative_to: relative_to, + constrain_to: Gtk.PopoverConstraint.WINDOW, + width_request: 500 ); + window_height = height; fuzzy_finder = new Services.FuzzyFinder (pps); project_paths = pps; @@ -45,7 +46,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { max_items = 3; } - scrolled.set_max_content_height (43 /* height */ * max_items); + scrolled.set_max_content_height (45 /* height */ * max_items); } private void calculate_scroll_offset (int old_position, int new_position) { @@ -77,20 +78,12 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { } 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; + this.get_style_context ().add_class ("fuzzy-popover"); + this.get_style_context ().add_class ("flat"); - 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_term_entry = new Gtk.Entry (); + search_term_entry.halign = Gtk.Align.FILL; + search_term_entry.hexpand = true; search_result_container = new Gtk.ListBox () { selection_mode = Gtk.SelectionMode.NONE, @@ -99,10 +92,6 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { search_result_container.get_style_context ().add_class ("fuzzy-list"); - scrolled = new Gtk.ScrolledWindow (null, null); - scrolled.add (search_result_container); - scrolled.margin_top = 10; - search_result_container.row_activated.connect ((row) => { var file_item = row as FileItem; if (file_item == null) { @@ -193,6 +182,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { foreach (var c in search_result_container.get_children ()) { search_result_container.remove (c); } + items.clear (); foreach (var result in results) { @@ -210,6 +200,7 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { scrolled.hide (); scrolled.show_all (); + // Reset scrolling scrolled.vadjustment.value = 0; }); @@ -226,10 +217,27 @@ public class Scratch.Dialogs.FuzzySearchDialog : Gtk.Dialog { } }); - scrolled.propagate_natural_height = true; - box.add (layout); - box.add (scrolled); + var entry_layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + entry_layout.valign = Gtk.Align.START; + + entry_layout.add (search_term_entry); + search_term_entry.valign = Gtk.Align.START; + + scrolled = new Gtk.ScrolledWindow (null, null) { + propagate_natural_height = true, + hexpand = true, + }; + + scrolled.add (search_result_container); + + var box = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); + box.pack_start (entry_layout, false, false); + box.pack_end (scrolled, true, true); + box.show_all (); + + scrolled.hide (); + this.add (box); } private void handle_item_selection (int index) { diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index f4434a5351..1a74c56dfc 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -4,6 +4,7 @@ * * Authored by: Marvin Ahlgrimm */ + public class Scratch.Services.SearchProject { public string root_path { get; private set; } public Gee.ArrayList relative_file_paths { get; private set; } @@ -44,8 +45,6 @@ public class Scratch.Services.SearchProject { warning ("An error occurred while checking if item '%s' is git-ignored: %s", path, e.message); } - // debug ("Fuzzy Search - Indexing from directory: %s\n", path); - var dir = Dir.open (path); var name = dir.read_name (); @@ -145,22 +144,17 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { project_paths[path] = project_path; } - var dialog = new Scratch.Dialogs.FuzzySearchDialog (project_paths, window_height); - dialog.get_position (out diag_x, out diag_y); - - dialog.open_file.connect ((filepath) => { + var popover = new Scratch.FuzzySearchPopover (project_paths, window.toolbar, window_height); + popover.open_file.connect ((filepath) => { var file = new Scratch.FolderManager.File (filepath); var doc = new Scratch.Services.Document (window.actions, file.file); window.open_document (doc); - dialog.destroy (); + popover.popdown (); }); - dialog.close_search.connect (() => dialog.destroy ()); - // Move the dialog a bit under the top of the application window - dialog.move (diag_x, window_y + 50); - - dialog.run (); + popover.close_search.connect (() => popover.popdown ()); + popover.popup (); return true; } diff --git a/plugins/fuzzy-search/meson.build b/plugins/fuzzy-search/meson.build index 9a44cafb19..155ce60c7f 100644 --- a/plugins/fuzzy-search/meson.build +++ b/plugins/fuzzy-search/meson.build @@ -4,7 +4,7 @@ module_files = [ 'fuzzy-search.vala', 'fuzzy-finder.vala', 'file-item.vala', - 'fuzzy-search-dialog.vala', + 'fuzzy-search-popover.vala', 'search-result.vala', ] diff --git a/plugins/fuzzy-search/search-result.vala b/plugins/fuzzy-search/search-result.vala index 7383c4fdd9..62105ad71a 100644 --- a/plugins/fuzzy-search/search-result.vala +++ b/plugins/fuzzy-search/search-result.vala @@ -1,9 +1,10 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2023 elementary, Inc. +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. * - * Authored by: Marvin Ahlgrimm + * Authored by: Marvin Ahlgrimm */ + public class SearchResult { public string full_path; public string relative_path; From ad97f5522b2e051354e97cd88a3d463de4b65d6e Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 20:39:36 +0000 Subject: [PATCH 33/45] Add title label to fuzzy search popover --- data/Application.css | 3 ++- plugins/fuzzy-search/fuzzy-search-popover.vala | 13 +++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/data/Application.css b/data/Application.css index a49f389256..f607b3d3ea 100644 --- a/data/Application.css +++ b/data/Application.css @@ -21,7 +21,8 @@ textview.scrubber { } .fuzzy-popover { - padding: 1rem 0; + padding-top: 0.5rem; + padding-bottom: 1rem; } .fuzzy-popover entry { diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index c43ce79eaa..ecf89f867d 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -7,7 +7,7 @@ */ public class Scratch.FuzzySearchPopover : Gtk.Popover { - private Gtk.Entry search_term_entry; + private Gtk.SearchEntry search_term_entry; private Services.FuzzyFinder fuzzy_finder; private Gtk.ListBox search_result_container; private int preselected_index; @@ -19,6 +19,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { private Gee.LinkedList cancellables; private bool should_distinguish_projects; private Gtk.EventControllerKey search_term_entry_key_controller; + private Gtk.Label title_label; public signal void open_file (string filepath); public signal void close_search (); @@ -79,9 +80,12 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { construct { this.get_style_context ().add_class ("fuzzy-popover"); - this.get_style_context ().add_class ("flat"); - search_term_entry = new Gtk.Entry (); + title_label = new Gtk.Label (_("Search for project files")); + title_label.halign = Gtk.Align.START; + title_label.get_style_context ().add_class ("h4"); + + search_term_entry = new Gtk.SearchEntry (); search_term_entry.halign = Gtk.Align.FILL; search_term_entry.hexpand = true; @@ -218,9 +222,10 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { }); - var entry_layout = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + var entry_layout = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); entry_layout.valign = Gtk.Align.START; + entry_layout.add (title_label); entry_layout.add (search_term_entry); search_term_entry.valign = Gtk.Align.START; From bfd7a92e6313530da088448ac71710269777d424 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 20:46:12 +0000 Subject: [PATCH 34/45] Remove unused window size and positining code --- plugins/fuzzy-search/fuzzy-search.vala | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 1a74c56dfc..f6d2984cd8 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -123,15 +123,10 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { return false; } - 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); + window.get_size (out window_width, out window_height); project_paths = new Gee.HashMap (); foreach (unowned string path in settings.get_strv ("opened-folders")) { From dbfa10ea5dd1b24be8d11e38a3489ad016609fd8 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 22:22:24 +0000 Subject: [PATCH 35/45] Prevent focus on fuzzy item results --- plugins/fuzzy-search/fuzzy-search-popover.vala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index ecf89f867d..137de2e75f 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -91,7 +91,8 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { search_result_container = new Gtk.ListBox () { selection_mode = Gtk.SelectionMode.NONE, - activate_on_single_click = true + activate_on_single_click = true, + can_focus = false }; search_result_container.get_style_context ().add_class ("fuzzy-list"); @@ -191,7 +192,8 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { foreach (var result in results) { var file_item = new FileItem (result, should_distinguish_projects); - + file_item.can_focus = false; +1 if (first) { first = false; file_item.get_style_context ().add_class ("preselect-fuzzy"); From c836cbbb53e43abaa8e007bc1705b11ec92f4a8b Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 27 Dec 2023 23:48:25 +0000 Subject: [PATCH 36/45] Fuzzy search results for current project are prioritised --- plugins/fuzzy-search/fuzzy-finder.vala | 29 +++++++++---- .../fuzzy-search/fuzzy-search-popover.vala | 41 ++++++++++++++++--- plugins/fuzzy-search/fuzzy-search.vala | 6 +-- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-finder.vala b/plugins/fuzzy-search/fuzzy-finder.vala index 982a1fc620..9bf5d1829c 100644 --- a/plugins/fuzzy-search/fuzzy-finder.vala +++ b/plugins/fuzzy-search/fuzzy-finder.vala @@ -1,8 +1,8 @@ -/* - * SPDX-License-Identifier: GPL-3.0-or-later - * SPDX-FileCopyrightText: 2023 elementary, Inc. +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. * - * Authored by: Marvin Ahlgrimm + * Authored by: Marvin Ahlgrimm * Colin Kiama */ @@ -10,6 +10,7 @@ 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 CURRENT_PROJECT_PRIORITY_BONUS = 20; // Bonus if search result is for current project 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; @@ -194,12 +195,14 @@ public class Scratch.Services.FuzzyFinder { project_paths = pps; } - public async Gee.ArrayList fuzzy_find_async (string search_str, GLib.Cancellable cancellable) { + public async Gee.ArrayList fuzzy_find_async (string search_str, + string current_project, + GLib.Cancellable cancellable) { var results = new Gee.ArrayList (); SourceFunc callback = fuzzy_find_async.callback; new Thread ("fuzzy-find", () => { - results = fuzzy_find (search_str, cancellable); + results = fuzzy_find (search_str, current_project, cancellable); Idle.add ((owned) callback); }); @@ -207,7 +210,9 @@ public class Scratch.Services.FuzzyFinder { return results; } - public Gee.ArrayList fuzzy_find (string search_str, GLib.Cancellable cancellable) { + public Gee.ArrayList fuzzy_find (string search_str, + string current_project, + GLib.Cancellable cancellable) { var results = new Gee.ArrayList (); var projects = project_paths.values.to_array (); @@ -254,12 +259,20 @@ public class Scratch.Services.FuzzyFinder { filename_search_result.relative_path = path; filename_search_result.full_path = @"$root_path/$path"; filename_search_result.project = project_name; + filename_search_result.score += project.root_path == current_project + ? CURRENT_PROJECT_PRIORITY_BONUS + : 0; + results.add (filename_search_result); } else if (path_search_result.found) { path_search_result.relative_path = path; path_search_result.full_path = @"$root_path/$path"; path_search_result.project = project_name; - path_search_result.score = (int) (path_search_result.score * 0.5); + path_search_result.score = (int) (path_search_result.score * 0.5) + + (project.root_path == current_project + ? CURRENT_PROJECT_PRIORITY_BONUS + : 0); + results.add (path_search_result); } } diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index 137de2e75f..742e1fcec9 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -20,19 +20,25 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { private bool should_distinguish_projects; private Gtk.EventControllerKey search_term_entry_key_controller; private Gtk.Label title_label; + private Scratch.MainWindow current_window; public signal void open_file (string filepath); public signal void close_search (); - public FuzzySearchPopover (Gee.HashMap pps, Gtk.Widget? relative_to, int height) { + public FuzzySearchPopover (Gee.HashMap pps, Scratch.MainWindow window) { Object ( modal: true, - relative_to: relative_to, + relative_to: window.toolbar, constrain_to: Gtk.PopoverConstraint.WINDOW, width_request: 500 ); + current_window = window; + + int height; + current_window.get_size (null, out height); window_height = height; + fuzzy_finder = new Services.FuzzyFinder (pps); project_paths = pps; items = new Gee.ArrayList (); @@ -165,7 +171,10 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { Timeout.add (1, () => { var next_cancellable = new GLib.Cancellable (); cancellables.add (next_cancellable); - fuzzy_finder.fuzzy_find_async.begin (search_term_entry.text, next_cancellable, (obj, res) =>{ + fuzzy_finder.fuzzy_find_async.begin (search_term_entry.text, + get_current_project (), + next_cancellable, + (obj, res) => { if (next_cancellable.is_cancelled ()) { cancellables.remove (next_cancellable); return; @@ -193,7 +202,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { foreach (var result in results) { var file_item = new FileItem (result, should_distinguish_projects); file_item.can_focus = false; -1 + if (first) { first = false; file_item.get_style_context ().add_class ("preselect-fuzzy"); @@ -223,7 +232,6 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { } }); - var entry_layout = new Gtk.Box (Gtk.Orientation.VERTICAL, 0); entry_layout.valign = Gtk.Align.START; @@ -257,4 +265,27 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { old_item.get_style_context ().remove_class (class_name); new_item.get_style_context ().add_class (class_name); } + + private string get_current_project () { + Scratch.Services.Document current_document = current_window.document_view.current_document; + if (current_document == null) { + return ""; + } + + if (current_document.is_file_temporary) { + return ""; + } + + string file_path = current_document.file.get_path (); + + var iter = project_paths.keys.iterator (); + while (iter.next ()) { + string project_path = iter.get (); + if (file_path.has_prefix (project_path)) { + return project_path; + } + } + + return ""; + } } diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index f6d2984cd8..5ded14329f 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -123,10 +123,6 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { return false; } - int window_height; - int window_width; - - window.get_size (out window_width, out window_height); project_paths = new Gee.HashMap (); foreach (unowned string path in settings.get_strv ("opened-folders")) { @@ -139,7 +135,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { project_paths[path] = project_path; } - var popover = new Scratch.FuzzySearchPopover (project_paths, window.toolbar, window_height); + var popover = new Scratch.FuzzySearchPopover (project_paths, window); popover.open_file.connect ((filepath) => { var file = new Scratch.FolderManager.File (filepath); var doc = new Scratch.Services.Document (window.actions, file.file); From 0c1fba84c86b709149b450c64f51c32f63141761 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Thu, 4 Jan 2024 18:33:59 +0000 Subject: [PATCH 37/45] Expose fuzzy find function and accelerator in sidebar action menu (#1395) * Add Fuzzy Find menuitem to sidebar * Consistently use "Find Project Files" * Move SearchProject to separate file * Fix lint * Remove menuitem on deactivation of "Find Project Files" plugin --- .../fuzzy-search/fuzzy-search-popover.vala | 2 +- .../fuzzy-search/fuzzy-search-project.vala | 87 +++++++++ plugins/fuzzy-search/fuzzy-search.plugin | 4 +- plugins/fuzzy-search/fuzzy-search.vala | 169 ++++++------------ plugins/fuzzy-search/meson.build | 1 + src/Widgets/Sidebar.vala | 3 +- 6 files changed, 148 insertions(+), 118 deletions(-) create mode 100644 plugins/fuzzy-search/fuzzy-search-project.vala diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index 742e1fcec9..98273e17f0 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -87,7 +87,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { construct { this.get_style_context ().add_class ("fuzzy-popover"); - title_label = new Gtk.Label (_("Search for project files")); + title_label = new Gtk.Label (_("Find project files")); title_label.halign = Gtk.Align.START; title_label.get_style_context ().add_class ("h4"); diff --git a/plugins/fuzzy-search/fuzzy-search-project.vala b/plugins/fuzzy-search/fuzzy-search-project.vala new file mode 100644 index 0000000000..24559449bc --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-search-project.vala @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2023 elementary, Inc. + * + * Authored by: Marvin Ahlgrimm + */ + +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, MonitoredRepository? repo) { + root_path = root; + monitored_repo = repo; + relative_file_paths = new Gee.ArrayList (); + } + + public async void parse_async (string path) { + new Thread (null, () => { + parse_async_internal.begin (path, (obj, res) => { + parse_async_internal.end (res); + }); + + Idle.add (parse_async.callback); + }); + + yield; + } + + private async void parse_async_internal (string path) { + try { + // Ignore dot-prefixed directories + string path_basename = Path.get_basename (path); + if (FileUtils.test (path, GLib.FileTest.IS_DIR) && path_basename.has_prefix (".")) { + 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) { + var new_search_path = ""; + if (path.has_suffix (GLib.Path.DIR_SEPARATOR_S)) { + new_search_path = path.substring (0, path.length - 1); + } else { + new_search_path = path; + } + + parse_async_internal.begin (new_search_path + GLib.Path.DIR_SEPARATOR_S + name, (obj, res) => { + parse_async_internal.end (res); + }); + + 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. + + // Relative paths are used because the longer the path is the less accurate are the results + if (check_if_valid_path_to_add (path)) { + var subpath = path.replace (root_path, ""); + relative_file_paths.add (subpath.substring (1, subpath.length - 1)); + } + } + } + + private bool check_if_valid_path_to_add (string path) { + try { + File file = File.new_for_path (path); + var file_info = file.query_info ("standard::*", 0); + return Utils.check_if_valid_text_file (path, file_info); + } catch (Error e) { + return false; + } + } +} diff --git a/plugins/fuzzy-search/fuzzy-search.plugin b/plugins/fuzzy-search/fuzzy-search.plugin index 3c6ec9c228..57b3e4f42e 100644 --- a/plugins/fuzzy-search/fuzzy-search.plugin +++ b/plugins/fuzzy-search/fuzzy-search.plugin @@ -2,8 +2,8 @@ Module=fuzzy-search Loader=C IAge=1 -Name=Fuzzy Search -Description=Fuzzy search all project files (Show using ALT + F) +Name=Find Project Files +Description=Fuzzy search for files in all open projects (Show using ALT + F) Icon=system-search Authors=Marvin Ahlgrimm;Colin Kiama Copyright=Copyright © 2021 Marvin Ahlgrimm diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index 5ded14329f..caea6e9b40 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -5,94 +5,18 @@ * Authored by: Marvin Ahlgrimm */ -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, MonitoredRepository? repo) { - root_path = root; - monitored_repo = repo; - relative_file_paths = new Gee.ArrayList (); - } - - public async void parse_async (string path) { - new Thread (null, () => { - parse_async_internal.begin (path, (obj, res) => { - parse_async_internal.end (res); - }); - - Idle.add (parse_async.callback); - }); - - yield; - } - - private async void parse_async_internal (string path) { - try { - // Ignore dot-prefixed directories - string path_basename = Path.get_basename (path); - if (FileUtils.test (path, GLib.FileTest.IS_DIR) && path_basename.has_prefix (".")) { - 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) { - var new_search_path = ""; - if (path.has_suffix (GLib.Path.DIR_SEPARATOR_S)) { - new_search_path = path.substring (0, path.length - 1); - } else { - new_search_path = path; - } - - parse_async_internal.begin (new_search_path + GLib.Path.DIR_SEPARATOR_S + name, (obj, res) => { - parse_async_internal.end (res); - }); - - 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. - - // Relative paths are used because the longer the path is the less accurate are the results - if (check_if_valid_path_to_add (path)) { - var subpath = path.replace (root_path, ""); - relative_file_paths.add (subpath.substring (1, subpath.length - 1)); - } - } - } - - private bool check_if_valid_path_to_add (string path) { - try { - File file = File.new_for_path (path); - var file_info = file.query_info ("standard::*", 0); - return Utils.check_if_valid_text_file (path, file_info); - } catch (Error e) { - return false; - } - } -} public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { public Object object { owned get; construct; } + private const uint ACCEL_KEY = Gdk.Key.F; + private const Gdk.ModifierType ACCEL_MODTYPE = Gdk.ModifierType.MOD1_MASK; + private Gee.HashMap project_paths; private MainWindow window = null; private Scratch.Services.Interface plugins; private Gtk.EventControllerKey key_controller; + private Gtk.MenuItem fuzzy_menuitem; public void update_state () { @@ -102,64 +26,81 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { plugins = (Scratch.Services.Interface) object; plugins.hook_window.connect ((w) => { - if (window != null) + if (window != null) { return; + } window = w; - key_controller = new Gtk.EventControllerKey (window); + key_controller = new Gtk.EventControllerKey (window) { + propagation_phase = BUBBLE + }; key_controller.key_pressed.connect (on_window_key_press_event); + + fuzzy_menuitem = new Gtk.MenuItem.with_label (_("Find Project Files")); + var child = ((Gtk.Bin)fuzzy_menuitem).get_child (); + if (child is Gtk.AccelLabel) { + ((Gtk.AccelLabel)child).set_accel (ACCEL_KEY, ACCEL_MODTYPE); + } + + fuzzy_menuitem.activate.connect (fuzzy_find); + fuzzy_menuitem.show (); + window.sidebar.project_menu.append (fuzzy_menuitem); }); } bool on_window_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { /* f shows fuzzy search dialog */ switch (Gdk.keyval_to_upper (keyval)) { - case Gdk.Key.F: - if (state == Gdk.ModifierType.MOD1_MASK) { - var settings = new GLib.Settings ("io.elementary.code.folder-manager"); + case ACCEL_KEY: + if (state == ACCEL_MODTYPE) { + fuzzy_find (); + return true; + } - string[] opened_folders = settings.get_strv ("opened-folders"); - if (opened_folders == null || opened_folders.length < 1) { - return false; - } + break; + default: + return false; + } - project_paths = new Gee.HashMap (); + return false; + } - foreach (unowned string path in settings.get_strv ("opened-folders")) { - var monitor = Services.GitManager.get_monitored_repository (path); - var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, (obj, res) => { - project_path.parse_async.end (res); - }); + private void fuzzy_find () { + var settings = new GLib.Settings ("io.elementary.code.folder-manager"); - project_paths[path] = project_path; - } + string[] opened_folders = settings.get_strv ("opened-folders"); + if (opened_folders == null || opened_folders.length < 1) { + return; + } - var popover = new Scratch.FuzzySearchPopover (project_paths, window); - popover.open_file.connect ((filepath) => { - var file = new Scratch.FolderManager.File (filepath); - var doc = new Scratch.Services.Document (window.actions, file.file); + project_paths = new Gee.HashMap (); - window.open_document (doc); - popover.popdown (); - }); + foreach (unowned string path in settings.get_strv ("opened-folders")) { + var monitor = Services.GitManager.get_monitored_repository (path); + var project_path = new Services.SearchProject (path, monitor); + project_path.parse_async.begin (path, (obj, res) => { + project_path.parse_async.end (res); + }); - popover.close_search.connect (() => popover.popdown ()); - popover.popup (); + project_paths[path] = project_path; + } - return true; - } + var popover = new Scratch.FuzzySearchPopover (project_paths, window); + popover.open_file.connect ((filepath) => { + var file = new Scratch.FolderManager.File (filepath); + var doc = new Scratch.Services.Document (window.actions, file.file); - break; - default: - return false; - } + window.open_document (doc); + popover.popdown (); + }); - return false; + popover.close_search.connect (() => popover.popdown ()); + popover.popup (); } public void deactivate () { key_controller.key_pressed.disconnect (on_window_key_press_event); + window.sidebar.project_menu.remove (fuzzy_menuitem); } } diff --git a/plugins/fuzzy-search/meson.build b/plugins/fuzzy-search/meson.build index 155ce60c7f..a4168ca408 100644 --- a/plugins/fuzzy-search/meson.build +++ b/plugins/fuzzy-search/meson.build @@ -6,6 +6,7 @@ module_files = [ 'file-item.vala', 'fuzzy-search-popover.vala', 'search-result.vala', + 'fuzzy-search-project.vala' ] module_deps = [ diff --git a/src/Widgets/Sidebar.vala b/src/Widgets/Sidebar.vala index 578acef4fb..4b4102a035 100644 --- a/src/Widgets/Sidebar.vala +++ b/src/Widgets/Sidebar.vala @@ -25,6 +25,7 @@ public class Code.Sidebar : Gtk.Grid { public Gtk.Stack stack { get; private set; } public Code.ChooseProjectButton choose_project_button { get; private set; } public Hdy.HeaderBar headerbar { get; private set; } + public Gtk.Menu project_menu { get; construct; } private Gtk.StackSwitcher stack_switcher; @@ -70,7 +71,7 @@ public class Code.Sidebar : Gtk.Grid { order_projects_menu_item.action_name = Scratch.MainWindow.ACTION_PREFIX + Scratch.MainWindow.ACTION_ORDER_FOLDERS; - var project_menu = new Gtk.Menu (); + project_menu = new Gtk.Menu (); project_menu.append (collapse_all_menu_item); project_menu.append (order_projects_menu_item); project_menu.show_all (); From 8c550d62c2147de2750c5a5763bd4b0d4f689c25 Mon Sep 17 00:00:00 2001 From: Jeremy Wootten Date: Sat, 6 Jan 2024 17:51:46 +0000 Subject: [PATCH 38/45] Associate popover with foldermanager (#1396) * Associate popover with foldermanager * Remove deprecated PopoverConstraint --- plugins/fuzzy-search/fuzzy-search-popover.vala | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index 98273e17f0..07d58589f7 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -20,7 +20,8 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { private bool should_distinguish_projects; private Gtk.EventControllerKey search_term_entry_key_controller; private Gtk.Label title_label; - private Scratch.MainWindow current_window; + public Scratch.MainWindow current_window { get; construct; } + public bool sidebar_is_visible { get; set; } public signal void open_file (string filepath); public signal void close_search (); @@ -28,13 +29,11 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { public FuzzySearchPopover (Gee.HashMap pps, Scratch.MainWindow window) { Object ( modal: true, - relative_to: window.toolbar, - constrain_to: Gtk.PopoverConstraint.WINDOW, - width_request: 500 + relative_to: window.document_view, + width_request: 500, + current_window: window ); - current_window = window; - int height; current_window.get_size (null, out height); window_height = height; @@ -85,6 +84,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { } construct { + pointing_to = { 0, 32, 1, 1 }; this.get_style_context ().add_class ("fuzzy-popover"); title_label = new Gtk.Label (_("Find project files")); From e973920106728296fb35da369fd83a5ab29b0007 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 11:46:15 +0000 Subject: [PATCH 39/45] Add folder_item_change plugin manager hook in preparation for background indexing --- src/FolderManager/FileView.vala | 8 ++++++++ src/FolderManager/FolderItem.vala | 2 ++ src/MainWindow.vala | 2 +- src/Services/PluginManager.vala | 10 ++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/FolderManager/FileView.vala b/src/FolderManager/FileView.vala index 5b65ac2669..6de7f1ec61 100644 --- a/src/FolderManager/FileView.vala +++ b/src/FolderManager/FileView.vala @@ -25,6 +25,7 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P private GLib.Settings settings; private Scratch.Services.GitManager git_manager; private ActionGroup? toplevel_action_group = null; + private Scratch.Services.PluginsManager plugins; public signal void select (string file); @@ -37,6 +38,10 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P } } + public FileView (Scratch.Services.PluginsManager plugins_manager) { + plugins = plugins_manager; + } + construct { icon_name = "folder-symbolic"; title = _("Folders"); @@ -231,6 +236,9 @@ public class Scratch.FolderManager.FileView : Granite.Widgets.SourceList, Code.P } } + public void folder_item_update_hook (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { + plugins.hook_folder_item_change (source, dest, event); + } private void rename_items_with_same_name (Item item) { string item_name = item.file.name; diff --git a/src/FolderManager/FolderItem.vala b/src/FolderManager/FolderItem.vala index de02500802..73d0e56f80 100644 --- a/src/FolderManager/FolderItem.vala +++ b/src/FolderManager/FolderItem.vala @@ -258,6 +258,8 @@ namespace Scratch.FolderManager { return; // Ignore changes due to temp files and streams } + view.folder_item_update_hook (source, dest, event); + if (!children_loaded) { // No child items except dummy, child never expanded /* Empty folder with dummy item will come here even if expanded */ switch (event) { diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 151e857c69..e5ad5ac2e1 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -439,7 +439,7 @@ namespace Scratch { sidebar = new Code.Sidebar (); - folder_manager_view = new FolderManager.FileView (); + folder_manager_view = new FolderManager.FileView (plugins); sidebar.add_tab (folder_manager_view); folder_manager_view.show_all (); diff --git a/src/Services/PluginManager.vala b/src/Services/PluginManager.vala index 906c988aac..479c6a2a9e 100644 --- a/src/Services/PluginManager.vala +++ b/src/Services/PluginManager.vala @@ -29,6 +29,7 @@ namespace Scratch.Services { public signal void hook_toolbar (Scratch.HeaderBar toolbar); public signal void hook_document (Scratch.Services.Document doc); public signal void hook_preferences_dialog (Scratch.Dialogs.Preferences dialog); + public signal void hook_folder_item_change (File file, File? other_file, FileMonitorEvent event_type); public Scratch.TemplateManager template_manager { private set; get; } @@ -66,6 +67,7 @@ namespace Scratch.Services { public signal void hook_toolbar (Scratch.HeaderBar toolbar); public signal void hook_document (Scratch.Services.Document doc); public signal void hook_preferences_dialog (Scratch.Dialogs.Preferences dialog); + public signal void hook_folder_item_change (File file, File? other_file, FileMonitorEvent event_type); public signal void extension_added (Peas.PluginInfo info); public signal void extension_removed (Peas.PluginInfo info); @@ -90,10 +92,12 @@ namespace Scratch.Services { ((Peas.Activatable)ext).activate (); extension_added (info); }); + exts.extension_removed.connect ((info, ext) => { ((Peas.Activatable)ext).deactivate (); extension_removed (info); }); + exts.foreach (on_extension_foreach); // Connect managers signals to interface's signals @@ -104,6 +108,7 @@ namespace Scratch.Services { this.hook_share_menu.connect ((m) => { plugin_iface.hook_share_menu (m); }); + this.hook_toolbar.connect ((t) => { plugin_iface.hook_toolbar (t); }); @@ -111,9 +116,14 @@ namespace Scratch.Services { this.hook_document.connect ((d) => { plugin_iface.hook_document (d); }); + this.hook_preferences_dialog.connect ((d) => { plugin_iface.hook_preferences_dialog (d); }); + + this.hook_folder_item_change.connect ((source, dest, event) => { + plugin_iface.hook_folder_item_change (source, dest, event); + }); } void on_extension_foreach (Peas.ExtensionSet set, Peas.PluginInfo info, Peas.Extension extension) { From 868d188e2009594a854a2431a7eb8c8ae3d38308 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 11:56:37 +0000 Subject: [PATCH 40/45] Allow search project parsing to be cancellable --- .../fuzzy-search/fuzzy-search-project.vala | 23 ++++++++++++++----- plugins/fuzzy-search/fuzzy-search.vala | 9 ++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-project.vala b/plugins/fuzzy-search/fuzzy-search-project.vala index 24559449bc..968052cb4d 100644 --- a/plugins/fuzzy-search/fuzzy-search-project.vala +++ b/plugins/fuzzy-search/fuzzy-search-project.vala @@ -16,9 +16,9 @@ public class Scratch.Services.SearchProject { relative_file_paths = new Gee.ArrayList (); } - public async void parse_async (string path) { + public async void parse_async (string path, GLib.Cancellable cancellable) { new Thread (null, () => { - parse_async_internal.begin (path, (obj, res) => { + parse_async_internal.begin (path, cancellable, (obj, res) => { parse_async_internal.end (res); }); @@ -28,7 +28,11 @@ public class Scratch.Services.SearchProject { yield; } - private async void parse_async_internal (string path) { + private async void parse_async_internal (string path, GLib.Cancellable cancellable) { + if (cancellable.is_cancelled ()) { + return; + } + try { // Ignore dot-prefixed directories string path_basename = Path.get_basename (path); @@ -49,6 +53,10 @@ public class Scratch.Services.SearchProject { var name = dir.read_name (); while (name != null) { + if (cancellable.is_cancelled ()) { + return; + } + var new_search_path = ""; if (path.has_suffix (GLib.Path.DIR_SEPARATOR_S)) { new_search_path = path.substring (0, path.length - 1); @@ -56,8 +64,11 @@ public class Scratch.Services.SearchProject { new_search_path = path; } - parse_async_internal.begin (new_search_path + GLib.Path.DIR_SEPARATOR_S + name, (obj, res) => { - parse_async_internal.end (res); + parse_async_internal.begin ( + new_search_path + GLib.Path.DIR_SEPARATOR_S + name, + cancellable, + (obj, res) => { + parse_async_internal.end (res); }); name = dir.read_name (); @@ -69,7 +80,7 @@ public class Scratch.Services.SearchProject { // Relative paths are used because the longer the path is the less accurate are the results if (check_if_valid_path_to_add (path)) { - var subpath = path.replace (root_path, ""); + string subpath = path.replace (root_path, ""); relative_file_paths.add (subpath.substring (1, subpath.length - 1)); } } diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index caea6e9b40..b5f1049136 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -8,7 +8,6 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { public Object object { owned get; construct; } - private const uint ACCEL_KEY = Gdk.Key.F; private const Gdk.ModifierType ACCEL_MODTYPE = Gdk.ModifierType.MOD1_MASK; @@ -17,6 +16,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { private Scratch.Services.Interface plugins; private Gtk.EventControllerKey key_controller; private Gtk.MenuItem fuzzy_menuitem; + private GLib.Cancellable cancellable; public void update_state () { @@ -30,6 +30,8 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { return; } + cancellable = new GLib.Cancellable (); + window = w; key_controller = new Gtk.EventControllerKey (window) { propagation_phase = BUBBLE @@ -78,7 +80,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { foreach (unowned string path in settings.get_strv ("opened-folders")) { var monitor = Services.GitManager.get_monitored_repository (path); var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, (obj, res) => { + project_path.parse_async.begin (path, cancellable, (obj, res) => { project_path.parse_async.end (res); }); @@ -101,6 +103,9 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { public void deactivate () { key_controller.key_pressed.disconnect (on_window_key_press_event); window.sidebar.project_menu.remove (fuzzy_menuitem); + if (cancellable != null) { + cancellable.cancel (); + } } } From 0fdcd7744f21c354df9527a81b486c63bfbccbdb Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 12:06:12 +0000 Subject: [PATCH 41/45] Create indexer and add methods for adding and removing files and directories to search project class --- .../fuzzy-search/fuzzy-search-indexer.vala | 332 ++++++++++++++++++ .../fuzzy-search/fuzzy-search-project.vala | 49 +++ plugins/fuzzy-search/meson.build | 5 +- 3 files changed, 384 insertions(+), 2 deletions(-) create mode 100644 plugins/fuzzy-search/fuzzy-search-indexer.vala diff --git a/plugins/fuzzy-search/fuzzy-search-indexer.vala b/plugins/fuzzy-search/fuzzy-search-indexer.vala new file mode 100644 index 0000000000..9ea2430f8f --- /dev/null +++ b/plugins/fuzzy-search/fuzzy-search-indexer.vala @@ -0,0 +1,332 @@ +/* +* SPDX-License-Identifier: GPL-3.0-or-later +* SPDX-FileCopyrightText: 2023 elementary, Inc. +* +* Authored by: Colin Kiama +*/ +const long SECONDS_IN_MICROSECONDS = 1000000; // 1 Million microseconds = 1 second; + +public enum IndexerMessageType { + INITIAL, + PROJECT_UPDATE +} + +public enum IndexerStatus { + INITIALISING, + INITIAL_PROCESSING, + IDLE, + PROCESSING +} + +public enum ProjectUpdateType { + ADDED, + REMOVED, + FILE_CREATED, + DIRECTORY_CREATED, + FILE_DELETED +} + +public interface IndexerMessage : GLib.Object { + public abstract IndexerMessageType message_type { get; construct; } +} + +public class InitialIndexRequest : GLib.Object, IndexerMessage { + public IndexerMessageType message_type { get; construct; } + public string project_path { get; construct; } + + public InitialIndexRequest (string project_path) { + Object ( + message_type: IndexerMessageType.INITIAL, + project_path: project_path + ); + } +} + +public class ProjectUpdate : GLib.Object, IndexerMessage { + public IndexerMessageType message_type { get; construct; } + public ProjectUpdateType update_type { get; construct; } + public string source_path { get; construct; } + public string? destination_path { get; construct; } + public string? project_path { get; construct; } + + public ProjectUpdate (ProjectUpdateType update_type, string source_path, string? destination_path = null) { + Object ( + message_type: IndexerMessageType.PROJECT_UPDATE, + update_type: update_type, + source_path: source_path, + destination_path: destination_path + ); + } +} + +public class Scratch.Services.FuzzySearchIndexer : GLib.Object { + public Gee.HashMap project_paths { get; private set; } + + private Gee.ArrayList initial_indexing_queue; + private GLib.Settings folder_settings; + private GLib.Cancellable cancellable; + private Gee.ConcurrentList processing_queue; + private IndexerStatus status; + + public FuzzySearchIndexer (GLib.Cancellable cancellable) { + this.cancellable = cancellable; + status = IndexerStatus.INITIALISING; + initial_indexing_queue = new Gee.ArrayList (); + processing_queue = new Gee.ConcurrentList (); + project_paths = new Gee.HashMap (); + + folder_settings = new GLib.Settings ("io.elementary.code.folder-manager"); + folder_settings.changed["opened-folders"].connect (handle_opened_projects_change); + } + + public void handle_folder_item_change (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { + debug ("Folder Item Change:\nSource File: %s\ndDestination File: %s\nEvent Type: %s", + source.get_path (), + dest != null + ? dest.get_path () + : "(None)", + FuzzySearchIndexer.enum_to_nick (event, typeof (GLib.FileMonitorEvent)) + ); + + + switch (event) { + case GLib.FileMonitorEvent.CREATED: + string path = source.get_path (); + bool is_directory = FileUtils.test (path, GLib.FileTest.IS_DIR); + + var project_update = new ProjectUpdate ( + is_directory ? ProjectUpdateType.DIRECTORY_CREATED : ProjectUpdateType.FILE_CREATED, + path + ); + + processing_queue.add (project_update); + break; + case GLib.FileMonitorEvent.DELETED: + string path = source.get_path (); + + var project_update = new ProjectUpdate (ProjectUpdateType.FILE_DELETED, path); + processing_queue.add (project_update); + break; + default: + break; + } + } + + public async void start_async () { + string[] initial_projects = folder_settings.get_strv ("opened-folders"); + if (initial_projects != null) { + foreach (unowned string path in initial_projects) { + initial_indexing_queue.add (new InitialIndexRequest (path)); + } + } + + new Thread (null, () => { + while (cancellable.is_cancelled () == false) { + switch (status) { + case IndexerStatus.INITIALISING: + if (initial_indexing_queue.size < 1 ) { + status = IndexerStatus.IDLE; + debug ("Fuzzy Search - Indexer is now idle!\n"); + break; + } + + if (initial_indexing_queue.size > 0) { + process_initial_indexing_requests_async + .begin (initial_indexing_queue, + project_paths, (obj, res) => { + process_initial_indexing_requests_async.end (res); + status = IndexerStatus.IDLE; + } + ); + status = IndexerStatus.INITIAL_PROCESSING; + } + + break; + // Indexer initialization is complete, now waiting for incoming messages to process. + case IndexerStatus.IDLE: + if (processing_queue.size > 0) { + var first_item = processing_queue.get (0); + process_next_message_async.begin (first_item, (obj, res) => { + process_next_message_async.end (res); + processing_queue.remove (first_item); + status = IndexerStatus.IDLE; + debug ("Fuzzy Search - Indexer is now idle!"); + }); + + status = IndexerStatus.PROCESSING; + debug ("Fuzzy Search - Indexer now processing!"); + } + break; + case IndexerStatus.INITIAL_PROCESSING: + case IndexerStatus.PROCESSING: + break; + default: + break; + } + + Thread.usleep (1 * SECONDS_IN_MICROSECONDS); + } + + folder_settings.changed["opened-folders"].disconnect (handle_opened_projects_change); + Idle.add (start_async.callback); + }); + + yield; + } + + private async void process_next_message_async (IndexerMessage message) { + switch (message.message_type) { + case IndexerMessageType.PROJECT_UPDATE: + process_project_update_async.begin ((ProjectUpdate) message, (obj, res) => { + process_project_update_async.end (res); + }); + + break; + default: + break; + } + } + + private async void process_project_update_async (ProjectUpdate message) { + switch (message.update_type) { + case ProjectUpdateType.ADDED: + add_project_async.begin (message, (obj, res) => { + add_project_async.end (res); + }); + + break; + case ProjectUpdateType.REMOVED: + remove_project (message); + break; + case ProjectUpdateType.FILE_CREATED: + add_file (message); + break; + case ProjectUpdateType.DIRECTORY_CREATED: + add_directory_async.begin (message, (obj, res) => { + add_directory_async.end (res); + }); + + break; + case ProjectUpdateType.FILE_DELETED: + remove_file (message); + break; + } + } + + private void remove_file (ProjectUpdate message) { + string path = message.source_path; + string project_key = get_project_path_of_file (path); + if (project_key == null) { + return; + } + + Services.SearchProject project_search = project_paths[project_key]; + project_search.remove_file (path, this.cancellable); + processing_queue.remove (message); + } + + private void add_file (ProjectUpdate message) { + string path = message.source_path; + string project_key = get_project_path_of_file (path); + if (project_key == null) { + return; + } + + Services.SearchProject project_search = project_paths[project_key]; + project_search.add_file (path, this.cancellable); + processing_queue.remove (message); + } + + private async void add_directory_async (ProjectUpdate message) { + string path = message.source_path; + string project_key = get_project_path_of_file (path); + if (project_key == null) { + return; + } + + Services.SearchProject project_search = project_paths[project_key]; + project_search.add_directory_async.begin (path, this.cancellable, (obj, res) => { + project_search.add_directory_async.end (res); + processing_queue.remove (message); + }); + } + + private async void add_project_async (ProjectUpdate message) { + string path = message.source_path; + var monitor = Services.GitManager.get_monitored_repository (path); + var project_search = new Services.SearchProject (path, monitor); + project_paths[path] = project_search; + + project_search.parse_async.begin (path, this.cancellable, (obj, res) => { + project_search.parse_async.end (res); + processing_queue.remove (message); + }); + } + + private void remove_project (ProjectUpdate message) { + string path = message.source_path; + project_paths.unset (path); + } + + private void handle_opened_projects_change () { + string[] opened_projects_array = folder_settings.get_strv ("opened-folders"); + var opened_projects = new Gee.ArrayList.wrap (opened_projects_array); + // Handle project additions + foreach (string project in opened_projects) { + if (project_paths.keys.contains (project) == false) { + processing_queue.add (new ProjectUpdate (ProjectUpdateType.ADDED, project)); + } + } + + // Handle project removals + foreach (string project in project_paths.keys) { + if (opened_projects.contains (project) == false) { + processing_queue.add ( new ProjectUpdate (ProjectUpdateType.REMOVED, project)); + } + } + } + + private async void process_initial_indexing_requests_async ( + Gee.ArrayList request_queue, + Gee.HashMap project_paths) { + for (int i = 0; i < request_queue.size; i++) { + var request = request_queue[i]; + var monitor = Services.GitManager.get_monitored_repository (request.project_path); + var project_search = new Services.SearchProject (request.project_path, monitor); + + project_paths[request.project_path] = project_search; + project_search.parse_async.begin (request.project_path, cancellable, (obj, res) => { + project_search.parse_async.end (res); + request_queue.remove (request); + }); + } + } + + private string? get_project_path_of_file (string file_path) { + var iter = project_paths.keys.iterator (); + while (iter.next ()) { + string project_path = iter.get (); + if (file_path.has_prefix (project_path)) { + return project_path; + } + } + + return null; + } + + private static string enum_to_nick (int @value, Type enum_type) { + var enum_class = (EnumClass) enum_type.class_ref (); + + if (enum_class == null) { + return "%i".printf (@value); + } + + unowned var enum_value = enum_class.get_value (@value); + + if (enum_value == null) { + return "%i".printf (@value); + } + + return enum_value.value_nick; + } +} diff --git a/plugins/fuzzy-search/fuzzy-search-project.vala b/plugins/fuzzy-search/fuzzy-search-project.vala index 968052cb4d..84b376f14e 100644 --- a/plugins/fuzzy-search/fuzzy-search-project.vala +++ b/plugins/fuzzy-search/fuzzy-search-project.vala @@ -28,6 +28,55 @@ public class Scratch.Services.SearchProject { yield; } + public void remove_file (string path, GLib.Cancellable cancellable) { + if (cancellable.is_cancelled ()) { + return; + } + + string subpath = path.replace (root_path, ""); + string deleted_path = subpath.substring (1, subpath.length - 1); + + if (relative_file_paths.contains (deleted_path)) { + // path deleted is for a file + relative_file_paths.remove (deleted_path); + return; + } + + // Path deleted is for a directory + int start_length = relative_file_paths.size; + for (int i = start_length - 1; i > -1; i--) { + string relative_path = relative_file_paths[i]; + if (relative_path.has_prefix (deleted_path)) { + relative_file_paths.remove (relative_path); + } + } + + } + + public void add_file (string path, GLib.Cancellable cancellable) { + if (cancellable.is_cancelled ()) { + 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); + } + + string subpath = path.replace (root_path, ""); + relative_file_paths.add (subpath.substring (1, subpath.length - 1)); + } + + public async void add_directory_async (string path, GLib.Cancellable cancellable) { + parse_async_internal.begin (path, cancellable, (obj, res) => { + parse_async_internal.end (res); + }); + } + private async void parse_async_internal (string path, GLib.Cancellable cancellable) { if (cancellable.is_cancelled ()) { return; diff --git a/plugins/fuzzy-search/meson.build b/plugins/fuzzy-search/meson.build index a4168ca408..5a717fcd1f 100644 --- a/plugins/fuzzy-search/meson.build +++ b/plugins/fuzzy-search/meson.build @@ -1,12 +1,13 @@ module_name = 'fuzzy-search' module_files = [ + 'file-item.vala', 'fuzzy-search.vala', 'fuzzy-finder.vala', - 'file-item.vala', + 'fuzzy-search-indexer.vala', 'fuzzy-search-popover.vala', + 'fuzzy-search-project.vala', 'search-result.vala', - 'fuzzy-search-project.vala' ] module_deps = [ From a9980b263b721a7de274f78a41003cb4c0ae20e8 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 12:30:13 +0000 Subject: [PATCH 42/45] Use fuzzy search indexer for fuzzy search results --- .../fuzzy-search/fuzzy-search-popover.vala | 17 +++++------ plugins/fuzzy-search/fuzzy-search.vala | 30 ++++++++++--------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-popover.vala b/plugins/fuzzy-search/fuzzy-search-popover.vala index 07d58589f7..419da2f030 100644 --- a/plugins/fuzzy-search/fuzzy-search-popover.vala +++ b/plugins/fuzzy-search/fuzzy-search-popover.vala @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2023 elementary, Inc. * * Authored by: Marvin Ahlgrimm - * Colin Kiama + * Colin Kiama */ public class Scratch.FuzzySearchPopover : Gtk.Popover { @@ -12,12 +12,11 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { private Gtk.ListBox search_result_container; private int preselected_index; private Gtk.ScrolledWindow scrolled; - private Gee.HashMap project_paths; private Gee.ArrayList items; + private Scratch.Services.FuzzySearchIndexer indexer; private int window_height; private int max_items; private Gee.LinkedList cancellables; - private bool should_distinguish_projects; private Gtk.EventControllerKey search_term_entry_key_controller; private Gtk.Label title_label; public Scratch.MainWindow current_window { get; construct; } @@ -26,7 +25,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { public signal void open_file (string filepath); public signal void close_search (); - public FuzzySearchPopover (Gee.HashMap pps, Scratch.MainWindow window) { + public FuzzySearchPopover (Scratch.Services.FuzzySearchIndexer search_indexer, Scratch.MainWindow window) { Object ( modal: true, relative_to: window.document_view, @@ -38,13 +37,11 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { current_window.get_size (null, out height); window_height = height; - fuzzy_finder = new Services.FuzzyFinder (pps); - project_paths = pps; + fuzzy_finder = new Services.FuzzyFinder (search_indexer.project_paths); + indexer = search_indexer; items = new Gee.ArrayList (); cancellables = new Gee.LinkedList (); - should_distinguish_projects = project_paths.size > 1; - // Limit the shown results if the window height is too small if (window_height > 400) { max_items = 5; @@ -200,7 +197,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { items.clear (); foreach (var result in results) { - var file_item = new FileItem (result, should_distinguish_projects); + var file_item = new FileItem (result, indexer.project_paths.size > 1); file_item.can_focus = false; if (first) { @@ -278,7 +275,7 @@ public class Scratch.FuzzySearchPopover : Gtk.Popover { string file_path = current_document.file.get_path (); - var iter = project_paths.keys.iterator (); + var iter = indexer.project_paths.keys.iterator (); while (iter.next ()) { string project_path = iter.get (); if (file_path.has_prefix (project_path)) { diff --git a/plugins/fuzzy-search/fuzzy-search.vala b/plugins/fuzzy-search/fuzzy-search.vala index b5f1049136..ea07cd4e52 100644 --- a/plugins/fuzzy-search/fuzzy-search.vala +++ b/plugins/fuzzy-search/fuzzy-search.vala @@ -11,7 +11,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { private const uint ACCEL_KEY = Gdk.Key.F; private const Gdk.ModifierType ACCEL_MODTYPE = Gdk.ModifierType.MOD1_MASK; - private Gee.HashMap project_paths; + private Scratch.Services.FuzzySearchIndexer indexer; private MainWindow window = null; private Scratch.Services.Interface plugins; private Gtk.EventControllerKey key_controller; @@ -31,11 +31,17 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { } cancellable = new GLib.Cancellable (); + indexer = new Scratch.Services.FuzzySearchIndexer (cancellable); + + indexer.start_async.begin ((obj, res) => { + indexer.start_async.end (res); + }); window = w; key_controller = new Gtk.EventControllerKey (window) { propagation_phase = BUBBLE }; + key_controller.key_pressed.connect (on_window_key_press_event); fuzzy_menuitem = new Gtk.MenuItem.with_label (_("Find Project Files")); @@ -48,6 +54,14 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { fuzzy_menuitem.show (); window.sidebar.project_menu.append (fuzzy_menuitem); }); + + plugins.hook_folder_item_change.connect ((src, dest, event) => { + if (indexer == null) { + return; + } + + indexer.handle_folder_item_change (src, dest, event); + }); } bool on_window_key_press_event (uint keyval, uint keycode, Gdk.ModifierType state) { @@ -75,19 +89,7 @@ public class Scratch.Plugins.FuzzySearch: Peas.ExtensionBase, Peas.Activatable { return; } - project_paths = new Gee.HashMap (); - - foreach (unowned string path in settings.get_strv ("opened-folders")) { - var monitor = Services.GitManager.get_monitored_repository (path); - var project_path = new Services.SearchProject (path, monitor); - project_path.parse_async.begin (path, cancellable, (obj, res) => { - project_path.parse_async.end (res); - }); - - project_paths[path] = project_path; - } - - var popover = new Scratch.FuzzySearchPopover (project_paths, window); + var popover = new Scratch.FuzzySearchPopover (indexer, window); popover.open_file.connect ((filepath) => { var file = new Scratch.FolderManager.File (filepath); var doc = new Scratch.Services.Document (window.actions, file.file); From 1671c8917c58b8736b56301999958b25d8c84544 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 12:50:35 +0000 Subject: [PATCH 43/45] Update indexer debug logs --- .../fuzzy-search/fuzzy-search-indexer.vala | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-indexer.vala b/plugins/fuzzy-search/fuzzy-search-indexer.vala index 9ea2430f8f..cb3c5cf98b 100644 --- a/plugins/fuzzy-search/fuzzy-search-indexer.vala +++ b/plugins/fuzzy-search/fuzzy-search-indexer.vala @@ -80,7 +80,7 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { } public void handle_folder_item_change (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { - debug ("Folder Item Change:\nSource File: %s\ndDestination File: %s\nEvent Type: %s", + debug ("Find Project Files: Folder Item Change:\nSource File: %s\ndDestination File: %s\nEvent Type: %s", source.get_path (), dest != null ? dest.get_path () @@ -88,7 +88,6 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { FuzzySearchIndexer.enum_to_nick (event, typeof (GLib.FileMonitorEvent)) ); - switch (event) { case GLib.FileMonitorEvent.CREATED: string path = source.get_path (); @@ -126,19 +125,21 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { case IndexerStatus.INITIALISING: if (initial_indexing_queue.size < 1 ) { status = IndexerStatus.IDLE; - debug ("Fuzzy Search - Indexer is now idle!\n"); + debug ("Find Project Files: Indexer is now idle!\n"); break; } if (initial_indexing_queue.size > 0) { - process_initial_indexing_requests_async - .begin (initial_indexing_queue, - project_paths, (obj, res) => { - process_initial_indexing_requests_async.end (res); - status = IndexerStatus.IDLE; - } - ); + process_initial_indexing_requests_async.begin ( + initial_indexing_queue, + project_paths, + (obj, res) => { + process_initial_indexing_requests_async.end (res); + status = IndexerStatus.IDLE; + }); + status = IndexerStatus.INITIAL_PROCESSING; + debug ("Find Project Files: Indexer is now doing initial processing!"); } break; @@ -150,11 +151,11 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { process_next_message_async.end (res); processing_queue.remove (first_item); status = IndexerStatus.IDLE; - debug ("Fuzzy Search - Indexer is now idle!"); + debug ("Find Project Files: Indexer is now idle!"); }); status = IndexerStatus.PROCESSING; - debug ("Fuzzy Search - Indexer now processing!"); + debug ("Find Project Files: Indexer now processing!"); } break; case IndexerStatus.INITIAL_PROCESSING: @@ -192,23 +193,29 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { case ProjectUpdateType.ADDED: add_project_async.begin (message, (obj, res) => { add_project_async.end (res); + debug ("Find Project Files: Added project: %s", message.source_path); }); break; case ProjectUpdateType.REMOVED: remove_project (message); + debug ("Find Project Files: Removed project: %s", message.source_path); break; case ProjectUpdateType.FILE_CREATED: add_file (message); + debug ("Find Project Files: Added file: %s", message.source_path); + break; case ProjectUpdateType.DIRECTORY_CREATED: add_directory_async.begin (message, (obj, res) => { add_directory_async.end (res); + debug ("Find Project Files: Added directory: %s", message.source_path); }); break; case ProjectUpdateType.FILE_DELETED: remove_file (message); + debug ("Find Project Files: Deleted directory: %s", message.source_path); break; } } From 225b3ad4fbc1c6cea25976b38e96e54dd7d8d1c1 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 12:51:45 +0000 Subject: [PATCH 44/45] Update license info and comments in Fuzzy Search Project --- plugins/fuzzy-search/fuzzy-search-project.vala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-project.vala b/plugins/fuzzy-search/fuzzy-search-project.vala index 84b376f14e..a87430683b 100644 --- a/plugins/fuzzy-search/fuzzy-search-project.vala +++ b/plugins/fuzzy-search/fuzzy-search-project.vala @@ -3,6 +3,7 @@ * SPDX-FileCopyrightText: 2023 elementary, Inc. * * Authored by: Marvin Ahlgrimm + * Authored by: Colin Kiama */ public class Scratch.Services.SearchProject { @@ -36,14 +37,14 @@ public class Scratch.Services.SearchProject { string subpath = path.replace (root_path, ""); string deleted_path = subpath.substring (1, subpath.length - 1); + // Remove File if (relative_file_paths.contains (deleted_path)) { - // path deleted is for a file relative_file_paths.remove (deleted_path); return; } - // Path deleted is for a directory int start_length = relative_file_paths.size; + // Remove directory for (int i = start_length - 1; i > -1; i--) { string relative_path = relative_file_paths[i]; if (relative_path.has_prefix (deleted_path)) { From 8b90e6436c1dca20fb9e8da84b7213ba207bd227 Mon Sep 17 00:00:00 2001 From: Colin Kiama Date: Wed, 10 Jan 2024 13:05:55 +0000 Subject: [PATCH 45/45] Remove old fuzzy search indexer testing logs --- .../fuzzy-search/fuzzy-search-indexer.vala | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/plugins/fuzzy-search/fuzzy-search-indexer.vala b/plugins/fuzzy-search/fuzzy-search-indexer.vala index cb3c5cf98b..4c02bd3443 100644 --- a/plugins/fuzzy-search/fuzzy-search-indexer.vala +++ b/plugins/fuzzy-search/fuzzy-search-indexer.vala @@ -80,14 +80,6 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { } public void handle_folder_item_change (GLib.File source, GLib.File? dest, GLib.FileMonitorEvent event) { - debug ("Find Project Files: Folder Item Change:\nSource File: %s\ndDestination File: %s\nEvent Type: %s", - source.get_path (), - dest != null - ? dest.get_path () - : "(None)", - FuzzySearchIndexer.enum_to_nick (event, typeof (GLib.FileMonitorEvent)) - ); - switch (event) { case GLib.FileMonitorEvent.CREATED: string path = source.get_path (); @@ -320,20 +312,4 @@ public class Scratch.Services.FuzzySearchIndexer : GLib.Object { return null; } - - private static string enum_to_nick (int @value, Type enum_type) { - var enum_class = (EnumClass) enum_type.class_ref (); - - if (enum_class == null) { - return "%i".printf (@value); - } - - unowned var enum_value = enum_class.get_value (@value); - - if (enum_value == null) { - return "%i".printf (@value); - } - - return enum_value.value_nick; - } }