Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fuzzy finder #1393

Merged
merged 45 commits into from
Jan 10, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
50e3501
Added a fuzzy search modal
treagod Dec 14, 2021
dbd8692
Fixed indent
treagod Dec 14, 2021
021ba91
Use accent color for selection
treagod Dec 15, 2021
b51d456
Listen to git change
treagod Dec 15, 2021
eb3827d
Use async fuzzy finder and ignore paths from .gitignore
treagod Dec 16, 2021
830c45d
Resize dialog
treagod Dec 16, 2021
88adb72
Added scrolling by up/down arrows
treagod Dec 16, 2021
b1be136
Check project content on each search to get all changes
treagod Dec 18, 2021
4db8e57
Add basic debouncing and cancellation for each search query
colinkiama Dec 19, 2023
c717a35
Add cancellable checks to all loops during fuzzy search
colinkiama Dec 19, 2023
b9e419e
Increase fuzzy search results update speed
colinkiama Dec 19, 2023
b65057c
Fix long delay before fuzzy finder dialog opens
colinkiama Dec 19, 2023
aa9f61a
Fix vala-lint errors in fuzzy-search plugin directory
colinkiama Dec 19, 2023
6fbf1c6
Prevent fuzzy search dialog from opening if there are no projects opened
colinkiama Dec 19, 2023
6b89011
Clicking on fuzzy search results loads the clicked result
colinkiama Dec 19, 2023
05a3214
Fuzzy search results across multiple projects are disambiguated using…
colinkiama Dec 19, 2023
03686c4
Update fuzzy search debug logs
colinkiama Dec 19, 2023
a8c119a
Improve Fuzzy Search Accuracy
colinkiama Dec 20, 2023
a514b94
Ignore dot-prefixed directories in fuzzy search
colinkiama Dec 20, 2023
ea29d63
Update debug messaging for fuzzy search
colinkiama Dec 20, 2023
949c927
Fix code style warnings in fuzzy search plugin
colinkiama Dec 20, 2023
33be958
Update license and copyright info for Fuzzy Finder plugin
colinkiama Dec 20, 2023
b579d3a
Add end method to project path parsing async call
colinkiama Dec 20, 2023
e1989ca
Fix vala-lint errors
colinkiama Dec 20, 2023
73a9b3e
Improve fuzzy matching
colinkiama Dec 25, 2023
903505b
Change fuzzy search keyboard shortcut to 'ALT + F'
colinkiama Dec 27, 2023
8610573
Remove unused GitManager signals
colinkiama Dec 27, 2023
31936a0
Files that cannot be opened in Code are filtered out of fuzzy search
colinkiama Dec 27, 2023
09df392
Render fuzzy search items with list boxes
colinkiama Dec 27, 2023
7f82dc1
Add mouser hover styling to fuzzy search items
colinkiama Dec 27, 2023
848e461
Handle Fuzzy Search key presses with EventControllerKey
colinkiama Dec 27, 2023
243a3e5
Now using popover to show fuzzy search
colinkiama Dec 27, 2023
ad97f55
Add title label to fuzzy search popover
colinkiama Dec 27, 2023
bfd7a92
Remove unused window size and positining code
colinkiama Dec 27, 2023
dbfa10e
Prevent focus on fuzzy item results
colinkiama Dec 27, 2023
c836cbb
Fuzzy search results for current project are prioritised
colinkiama Dec 27, 2023
0c1fba8
Expose fuzzy find function and accelerator in sidebar action menu (#1…
jeremypw Jan 4, 2024
8c550d6
Associate popover with foldermanager (#1396)
jeremypw Jan 6, 2024
e973920
Add folder_item_change plugin manager hook in preparation for backgro…
colinkiama Jan 10, 2024
868d188
Allow search project parsing to be cancellable
colinkiama Jan 10, 2024
0fdcd77
Create indexer and add methods for adding and removing files and dire…
colinkiama Jan 10, 2024
a9980b2
Use fuzzy search indexer for fuzzy search results
colinkiama Jan 10, 2024
1671c89
Update indexer debug logs
colinkiama Jan 10, 2024
225b3ad
Update license info and comments in Fuzzy Search Project
colinkiama Jan 10, 2024
8b90e64
Remove old fuzzy search indexer testing logs
colinkiama Jan 10, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
332 changes: 332 additions & 0 deletions plugins/fuzzy-search/fuzzy-search-indexer.vala
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2023 elementary, Inc. <https://elementary.io>
*
* Authored by: Colin Kiama <[email protected]>
*/
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<string, Services.SearchProject> project_paths { get; private set; }

private Gee.ArrayList<InitialIndexRequest> initial_indexing_queue;
private GLib.Settings folder_settings;
private GLib.Cancellable cancellable;
private Gee.ConcurrentList<IndexerMessage> processing_queue;
private IndexerStatus status;

public FuzzySearchIndexer (GLib.Cancellable cancellable) {
this.cancellable = cancellable;
status = IndexerStatus.INITIALISING;
initial_indexing_queue = new Gee.ArrayList<InitialIndexRequest> ();
processing_queue = new Gee.ConcurrentList<IndexerMessage> ();
project_paths = new Gee.HashMap<string, Services.SearchProject> ();

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<void> (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;
}
);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra tab present

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!");
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank line after if {} missing

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<string>.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<InitialIndexRequest> request_queue,
Gee.HashMap<string, Services.SearchProject> 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;
}
}
49 changes: 49 additions & 0 deletions plugins/fuzzy-search/fuzzy-search-project.vala
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading