Skip to content

WIP: Non-blocking filtering #59

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

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
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
187 changes: 124 additions & 63 deletions FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#import "FAItemScoringMethod.h"
#import <objc/runtime.h>

#define dispatch_on_main($block) (dispatch_get_current_queue() == dispatch_get_main_queue() ? $block() : dispatch_sync(dispatch_get_main_queue(), $block))

#define MIN_CHUNK_LENGTH 100
/// A simple helper class to avoid using a dictionary in resultsStack
@interface FAFilteringResults : NSObject
Expand Down Expand Up @@ -337,55 +339,77 @@ - (void) _fa_hideCompletionsWithReason: (int) reason {
[self _fa_hideCompletionsWithReason: reason];
}

// Sets the current filtering prefix and calculates completion list.
// We override here to use fuzzy matching.
- (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFilter {
DLog(@"filteringPrefix = @\"%@\"", prefix);

// remove all cached results which are not case-insensitive prefixes of the new prefix
// only if case-sensitive exact match happens the whole cached result is used
// when case-insensitive prefix match happens we can still use allItems as a start point
NSMutableArray * resultsStack = self._fa_resultsStack;
while (resultsStack.count && ![prefix.lowercaseString hasPrefix: [[resultsStack lastObject] query].lowercaseString]) {
[resultsStack removeLastObject];
}

self.fa_filteringTime = 0;
- (void)_fa_kickFilterTimer
{
static NSOperationQueue *filterQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
filterQueue = [[NSOperationQueue alloc] init];
filterQueue.maxConcurrentOperationCount = 1;
});

[filterQueue cancelAllOperations];
NSString *queuedPrefix = [self valueForKey:@"_filteringPrefix"];
// TODO: Make the delay configurable
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.05 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSString *currentPrefix = [self valueForKey:@"_filteringPrefix"];
if ([currentPrefix isEqualToString:queuedPrefix]) {
[filterQueue addOperationWithBlock: ^{
[self _fa_performFuzzyFiltering];
}];
}
});
}

// Let the original handler deal with the zero letter case
if (prefix.length == 0) {
[self._fa_resultsStack removeAllObjects];
- (void)_fa_performFuzzyFiltering
{
@try {
NSString *prefix = [self valueForKey:@"_filteringPrefix"];
// Let the original handler deal with the zero letter case
if (!prefix || prefix.length == 0) {
dispatch_on_main(^{
[self._fa_resultsStack removeAllObjects];

NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
[self _fa_setFilteringPrefix:prefix forceFilter:self._fa_forceFilter];
if (![FASettings currentSettings].showInlinePreview) {
[self._inlinePreviewController hideInlinePreviewWithReason: 0x0];
}
self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start;

if ([FASettings currentSettings].showTiming) {
[self._listWindowController _updateCurrentDisplayState];
}

NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];
[self _fa_setFilteringPrefix:prefix forceFilter:forceFilter];
if (![FASettings currentSettings].showInlinePreview) {
[self._inlinePreviewController hideInlinePreviewWithReason: 0x0];
});
return;
}
self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start;

if ([FASettings currentSettings].showTiming) {
[self._listWindowController _updateCurrentDisplayState];

// remove all cached results which are not case-insensitive prefixes of the new prefix
// only if case-sensitive exact match happens the whole cached result is used
// when case-insensitive prefix match happens we can still use allItems as a start point
NSMutableArray * resultsStack = self._fa_resultsStack;
while (resultsStack.count && ![prefix.lowercaseString hasPrefix: [[resultsStack lastObject] query].lowercaseString]) {
[resultsStack removeLastObject];
}
return;
}

self.fa_filteringTime = 0;

// do not filter if we are inserting a completion
// checking for _insertingFullCompletion is not sufficient
if (self.fa_insertingCompletion) {
return;
}

// do not filter if we are inserting a completion
// checking for _insertingFullCompletion is not sufficient
if (self.fa_insertingCompletion) {
return;
}

@try {
NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate];

[self setValue: prefix forKey: @"_filteringPrefix"];


id<DVTTextCompletionItem> previousSelection = nil;
NSArray * previousSelectionRanges = nil;
BOOL wasBest = YES;

FAFilteringResults * results;

if (resultsStack.count && [prefix isEqualToString: [[resultsStack lastObject] query]]) {
results = [resultsStack lastObject];
} else {
Expand All @@ -397,43 +421,59 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil
results = [self _fa_calculateResultsForQuery: prefix];
[resultsStack addObject: results];
}

NSUInteger selection = [self _fa_getSelectionForFilteringResults: results
previousSelection: previousSelection
ranges: previousSelectionRanges
wasBestMatch: wasBest];

NSString * partial = [self _usefulPartialCompletionPrefixForItems: results.filteredItems
selectedIndex: selection
filteringPrefix: prefix];

self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start;

NAMED_TIMER_START(SendNotifications);
// send the notifications in the same way the original does
[self willChangeValueForKey:@"filteredCompletionsAlpha"];
[self willChangeValueForKey:@"usefulPrefix"];
[self willChangeValueForKey:@"selectedCompletionIndex"];
dispatch_on_main(^{
// Pointer comparison is okay here because if filteringPrefix changed, another one will be executed
if (prefix != [self valueForKey:@"_filteringPrefix"]) {
return;
}
self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start;

NAMED_TIMER_START(SendNotifications);
// send the notifications in the same way the original does
[self willChangeValueForKey:@"filteredCompletionsAlpha"];
[self willChangeValueForKey:@"usefulPrefix"];
[self willChangeValueForKey:@"selectedCompletionIndex"];

[self setValue: results.filteredItems forKey: @"_filteredCompletionsAlpha"];
[self setValue: partial forKey: @"_usefulPrefix"];
[self setValue: @(selection) forKey: @"_selectedCompletionIndex"];
[self setValue: nil forKey: @"_filteredCompletionsPriority"];

[self didChangeValueForKey:@"filteredCompletionsAlpha"];
[self didChangeValueForKey:@"usefulPrefix"];
[self didChangeValueForKey:@"selectedCompletionIndex"];
NAMED_TIMER_STOP(SendNotifications);

if (![FASettings currentSettings].showInlinePreview) {
[self._inlinePreviewController hideInlinePreviewWithReason: 0x0];
}
});

[self setValue: results.filteredItems forKey: @"_filteredCompletionsAlpha"];
[self setValue: partial forKey: @"_usefulPrefix"];
[self setValue: @(selection) forKey: @"_selectedCompletionIndex"];
[self setValue: nil forKey: @"_filteredCompletionsPriority"];

[self didChangeValueForKey:@"filteredCompletionsAlpha"];
[self didChangeValueForKey:@"usefulPrefix"];
[self didChangeValueForKey:@"selectedCompletionIndex"];
NAMED_TIMER_STOP(SendNotifications);

if (![FASettings currentSettings].showInlinePreview) {
[self._inlinePreviewController hideInlinePreviewWithReason: 0x0];
}

} @catch (NSException *exception) {
RLog(@"Caught an Exception %@", exception);
}
}

// Sets the current filtering prefix and calculates completion list.
// We override here to use fuzzy matching.
- (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFilter {
DLog(@"filteringPrefix = @\"%@\"", prefix);

[self setValue: prefix forKey: @"_filteringPrefix"];
self._fa_forceFilter = forceFilter;
[self _fa_kickFilterTimer];
}

// We nullify the caches when completions change.
- (void) _fa_setAllCompletions: (NSArray *) allCompletions {
[self _fa_setAllCompletions:allCompletions];
Expand Down Expand Up @@ -499,7 +539,9 @@ - (NSArray *) _fa_obtainSearchSetForQuery: (NSString *) query {

// Calculate all the results needed by setFilteringPrefix
- (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query {

if (![query isEqualToString:[self fa_filteringQuery]] || self.fa_insertingCompletion) {
return nil;
}
NSArray * searchSet = [self _fa_obtainSearchSetForQuery: query];

FAFilteringResults * results = [[FAFilteringResults alloc] init];
Expand Down Expand Up @@ -530,7 +572,7 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query {
for (NSInteger i = 0; i < workerCount; ++i) {
[sortedItemArrays addObject: @[]];
}

for (NSInteger i = 0; i < workerCount; ++i) {
dispatch_group_async(group, processingQueue, ^{
NSMutableArray *list;
Expand All @@ -544,6 +586,10 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query {
rangesMap: &rangesMap
scores: &scoresMap
secondPassRanges: &secondMap];
if (!goodMatch) {
// Search aborted
return;
}
NAMED_TIMER_STOP(Processing);
dispatch_async(reduceQueue, ^{
NAMED_TIMER_START(Reduce);
Expand Down Expand Up @@ -654,6 +700,11 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query {

NSCharacterSet * identStartSet = [self.textView.class identifierChars];

if (!identStartSet) {
// No idea why this can be nil. Executing on main thread doesn't make a difference
return nil;
}

MULTI_TIMER_INIT(Matching); MULTI_TIMER_INIT(Scoring); MULTI_TIMER_INIT(Writing);

for (NSUInteger i = lower_bound; i < upper_bound; ++i) {
Expand Down Expand Up @@ -961,4 +1012,14 @@ - (FAFilteringResults *) _fa_lastFilteringResults {
return self._fa_resultsStack.lastObject;
}

static char kForceFilterKey;
- (BOOL) _fa_forceFilter {
return [objc_getAssociatedObject(self, &kForceFilterKey) boolValue];
}

- (void) set_fa_forceFilter: (BOOL) value {
objc_setAssociatedObject(self, &kForceFilterKey, @(value), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}


@end