From ab1777f6734695f79765bea946fe1667ae7fd37a Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Mon, 26 Oct 2015 23:25:33 +1100 Subject: [PATCH 01/13] Add Non-blocking mode checkbox and Filter Delay to Settings --- FuzzyAutocomplete/FASettings.h | 5 ++ FuzzyAutocomplete/FASettings.m | 8 +++ FuzzyAutocomplete/FASettingsWindow.xib | 74 +++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/FuzzyAutocomplete/FASettings.h b/FuzzyAutocomplete/FASettings.h index 7fb6e04..a667b28 100644 --- a/FuzzyAutocomplete/FASettings.h +++ b/FuzzyAutocomplete/FASettings.h @@ -87,5 +87,10 @@ extern NSString * FASettingsPluginEnabledDidChangeNotification; /// After how many letters should attempt to correct word order. @property (nonatomic, readonly) NSInteger correctWordOrderAfter; +/// Should the plugin use non-blocking mode. +@property (nonatomic, readonly) BOOL nonblockingMode; + +/// Autocompleting delay after stopping typing in non-blocking mode +@property (nonatomic, readonly) double filterDelay; @end diff --git a/FuzzyAutocomplete/FASettings.m b/FuzzyAutocomplete/FASettings.m index 4386a64..2a59af8 100644 --- a/FuzzyAutocomplete/FASettings.m +++ b/FuzzyAutocomplete/FASettings.m @@ -124,6 +124,8 @@ - (void) windowWillClose: (NSNotification *) notification { static const BOOL kDefaultCorrectLetterCaseBestMatchOnly = NO; static const BOOL kDefaultCorrectWordOrder = NO; static const NSInteger kDefaultCorrectWordOrderAfter = 2; +static const BOOL kDefaultNonblockingMode = NO; +static const double kDefaultFilterDelay = 0.2; - (IBAction)resetDefaults:(id)sender { self.pluginEnabled = kDefaultPluginEnabled; @@ -150,6 +152,8 @@ - (IBAction)resetDefaults:(id)sender { self.correctLetterCaseBestMatchOnly = kDefaultCorrectLetterCaseBestMatchOnly; self.correctWordOrder = kDefaultCorrectWordOrder; self.correctWordOrderAfter = kDefaultCorrectWordOrderAfter; + self.nonblockingMode = kDefaultNonblockingMode; + self.filterDelay = kDefaultFilterDelay; NSUInteger processors = [[NSProcessInfo processInfo] activeProcessorCount]; self.parallelScoring = processors > 1; @@ -193,6 +197,8 @@ - (void) loadFromDefaults { loadNumber(correctLetterCaseBestMatchOnly, CorrectLetterCaseBestMatchOnly); loadNumber(correctWordOrder, CorrectWordOrder); loadNumber(correctWordOrderAfter, CorrectWordOrderAfter); + loadNumber(nonblockingMode, NonblockingMode); + loadNumber(filterDelay, FilterDelay); #undef loadNumber @@ -267,6 +273,7 @@ - (void) set ## Name: (type) name { \ BOOL_SETTINGS_SETTER(correctLetterCase, CorrectLetterCase); BOOL_SETTINGS_SETTER(correctLetterCaseBestMatchOnly, CorrectLetterCaseBestMatchOnly); BOOL_SETTINGS_SETTER(correctWordOrder, CorrectWordOrder); +BOOL_SETTINGS_SETTER(nonblockingMode, NonblockingMode) INTEGER_SETTINGS_SETTER(maximumWorkers, MaximumWorkers) INTEGER_SETTINGS_SETTER(prefixAnchor, PrefixAnchor) @@ -277,6 +284,7 @@ - (void) set ## Name: (type) name { \ DOUBLE_SETTINGS_SETTER(priorityPower, PriorityPower) DOUBLE_SETTINGS_SETTER(priorityFactorPower, PriorityFactorPower) DOUBLE_SETTINGS_SETTER(maxPrefixBonus, MaxPrefixBonus) +DOUBLE_SETTINGS_SETTER(filterDelay, FilterDelay) STRING_SETTINGS_SETTER(scoreFormat, ScoreFormat) diff --git a/FuzzyAutocomplete/FASettingsWindow.xib b/FuzzyAutocomplete/FASettingsWindow.xib index 5c4fd38..b08b911 100644 --- a/FuzzyAutocomplete/FASettingsWindow.xib +++ b/FuzzyAutocomplete/FASettingsWindow.xib @@ -1,9 +1,9 @@ - + - + @@ -641,6 +641,17 @@ Prefix - maximum bonus factor for prefix matches + @@ -691,6 +702,43 @@ Prefix - maximum bonus factor for prefix matches + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + + + + + + + + + + + + + NSNegateBoolean + + + + @@ -701,6 +749,28 @@ Prefix - maximum bonus factor for prefix matches + + + + + + + + + + + + + + + + + + NSNegateBoolean + + + + From 936770d35568c3245860cefe975996f4a43d858c Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 01:22:48 +1100 Subject: [PATCH 02/13] First cut of non-blocking using dispatch timers --- ...TTextCompletionSession+FuzzyAutocomplete.m | 50 +++++++++++++++---- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index cda09cc..0ed90e6 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -353,19 +353,32 @@ - (void) _fa_hideCompletionsWithReason: (int) reason { [self _fa_hideCompletionsWithReason: reason]; } +// Start the delay timer +- (void)_fa_kickFilterTimer:(NSString *)prefix forceFilter: (BOOL) forceFilter +{ + // Ideally, this would be an associated object on self, but I can't seem to do it with NSValue + static dispatch_source_t timer = NULL; + + if (timer != NULL) { + dispatch_source_cancel(timer); + } + + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, [FASettings currentSettings].filterDelay * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.05 * NSEC_PER_SEC); + + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(timer, ^{ + [weakSelf _fa_performFuzzyFiltering: prefix forceFilter: forceFilter]; + }); + dispatch_resume(timer); +} + // Sets the current filtering prefix and calculates completion list. // We override here to use fuzzy matching. -- (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFilter { +- (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; // Let the original handler deal with the zero letter case @@ -390,7 +403,26 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil if (self.fa_insertingCompletion) { return; } + + if ([FASettings currentSettings].nonblockingMode) { + [self _fa_kickFilterTimer:prefix forceFilter:forceFilter]; + } else { + [self _fa_performFuzzyFiltering:prefix forceFilter:forceFilter]; + } +} +- (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceFilter +{ + + // NOTE: Maybe need to move this section into the actual performFilter part + // 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]; + } + @try { NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; From bf9894ac0820b9036c9c1c81a5a9ea2bdb166ed7 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 01:49:44 +1100 Subject: [PATCH 03/13] Prevent crash when table is being redrawn after a delayed filter --- ...DVTTextCompletionListWindowController+FuzzyAutocomplete.m | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m index d4d0350..fc98a95 100644 --- a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m @@ -90,7 +90,10 @@ - (id) _fa_tableView: (NSTableView *) aTableView valueForColumn: (NSTableColumn *) aTableColumn row: (NSInteger) rowIndex { - if ([aTableColumn.identifier isEqualToString:@"score"]) { + // This can happen if the user backspaces as the table is being redrawn + if ((rowIndex + 1) > [self.session.filteredCompletionsAlpha count]) { + return nil; + } else if ([aTableColumn.identifier isEqualToString:@"score"]) { id item = self.session.filteredCompletionsAlpha[rowIndex]; return [self.session fa_scoreForItem: item]; } else { From 3d91aa814b4fb50a2932ffabe282c272eea329ed Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 01:51:29 +1100 Subject: [PATCH 04/13] Bail out of a match if the query changes during a match --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 0ed90e6..2abd6e8 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -728,7 +728,7 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { id bestMatch; FAItemScoringMethod * method = self._fa_currentScoringMethod; - + double normalization = [method normalizationFactorForSearchString: query]; id item; @@ -742,6 +742,10 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { MULTI_TIMER_INIT(Matching); MULTI_TIMER_INIT(Scoring); MULTI_TIMER_INIT(Writing); for (NSUInteger i = lower_bound; i < upper_bound; ++i) { + // If the query changes, bail out. Can be optimised + if ( (i % 50 == 0) && ![query isEqualToString:[self fa_filteringQuery]]) { + break; + } item = array[i]; NSArray * rangesArray; NSArray * secondPassArray; From 2c6bcd89dee78f038f132a65d581da5d4cb5ecf3 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 02:16:24 +1100 Subject: [PATCH 05/13] Move actual filtering into a user interactive dispatch queue to prevent blocking on main thread --- ...TTextCompletionSession+FuzzyAutocomplete.m | 85 ++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 2abd6e8..4b54bfb 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -22,6 +22,8 @@ #import "FAItemScoringMethod.h" #import +#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 @@ -363,7 +365,7 @@ - (void)_fa_kickFilterTimer:(NSString *)prefix forceFilter: (BOOL) forceFilter dispatch_source_cancel(timer); } - timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)); dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, [FASettings currentSettings].filterDelay * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.05 * NSEC_PER_SEC); __weak typeof(self) weakSelf = self; @@ -445,7 +447,12 @@ - (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceF results = [self _fa_calculateResultsForQuery: prefix]; [resultsStack addObject: results]; } - + nsaccesswind + // If the query changes, bail out. Can be optimised + if (![prefix isEqualToString:[self fa_filteringQuery]]) { + return; + } + NSUInteger selection = [self _fa_getSelectionForFilteringResults: results previousSelection: previousSelection ranges: previousSelectionRanges @@ -455,45 +462,47 @@ - (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceF selectedIndex: selection filteringPrefix: prefix]; - self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; - - if (![self _gotUsefulCompletionsToShowInList: results.filteredItems]) { - BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; - if ([self.listWindowController showingWindow] && !shownExplicitly) { - [self.listWindowController hideWindowWithReason: 8]; + dispatch_on_main(^{ + self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; + + if (![self _gotUsefulCompletionsToShowInList: results.filteredItems]) { + BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; + if ([self.listWindowController showingWindow] && !shownExplicitly) { + [self.listWindowController hideWindowWithReason: 8]; + } + if ([self._inlinePreviewController isShowingInlinePreview]) { + [self._inlinePreviewController hideInlinePreviewWithReason: 8]; + } } - if ([self._inlinePreviewController isShowingInlinePreview]) { - [self._inlinePreviewController hideInlinePreviewWithReason: 8]; + + 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 ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember: [prefix characterAtIndex:0]]) { + BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; + if (!shownExplicitly) { + [self._inlinePreviewController hideInlinePreviewWithReason: 2]; + [self.listWindowController hideWindowWithReason: 2]; + } } - } - - 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 ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember: [prefix characterAtIndex:0]]) { - BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; - if (!shownExplicitly) { - [self._inlinePreviewController hideInlinePreviewWithReason: 2]; - [self.listWindowController hideWindowWithReason: 2]; + + if (![FASettings currentSettings].showInlinePreview) { + [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; } - } - - if (![FASettings currentSettings].showInlinePreview) { - [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; - } + }); } @catch (NSException *exception) { RLog(@"Caught an Exception %@", exception); From ec440b0bc4c94dbac9f0b0cc6e28085378827eef Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 02:19:13 +1100 Subject: [PATCH 06/13] 100ms feels a lot better --- FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m | 2 +- FuzzyAutocomplete/FASettings.m | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 4b54bfb..47cd002 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -447,7 +447,7 @@ - (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceF results = [self _fa_calculateResultsForQuery: prefix]; [resultsStack addObject: results]; } - nsaccesswind + // If the query changes, bail out. Can be optimised if (![prefix isEqualToString:[self fa_filteringQuery]]) { return; diff --git a/FuzzyAutocomplete/FASettings.m b/FuzzyAutocomplete/FASettings.m index 2a59af8..63e5182 100644 --- a/FuzzyAutocomplete/FASettings.m +++ b/FuzzyAutocomplete/FASettings.m @@ -125,7 +125,7 @@ - (void) windowWillClose: (NSNotification *) notification { static const BOOL kDefaultCorrectWordOrder = NO; static const NSInteger kDefaultCorrectWordOrderAfter = 2; static const BOOL kDefaultNonblockingMode = NO; -static const double kDefaultFilterDelay = 0.2; +static const double kDefaultFilterDelay = 0.1; - (IBAction)resetDefaults:(id)sender { self.pluginEnabled = kDefaultPluginEnabled; From b6070e2482dc12e6581a35cd548a9a8a70e22f85 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 10:08:55 +1100 Subject: [PATCH 07/13] Wrap the list completion stuff in a try catch --- ...TTextCompletionSession+FuzzyAutocomplete.m | 77 ++++++++++--------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 47cd002..3094f14 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -332,7 +332,7 @@ - (void) _fa_hideCompletionsWithReason: (int) reason { NSUInteger start_location = [[self valueForKey: @"_wordStartLocation"] unsignedIntegerValue]; NSUInteger end_location = [[self valueForKey: @"_cursorLocation"] unsignedIntegerValue]; - + DVTCompletingTextView * textView = self.textView; DVTTextStorage * storage = (DVTTextStorage *) textView.textStorage; @@ -463,44 +463,49 @@ - (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceF filteringPrefix: prefix]; dispatch_on_main(^{ - self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; - - if (![self _gotUsefulCompletionsToShowInList: results.filteredItems]) { - BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; - if ([self.listWindowController showingWindow] && !shownExplicitly) { - [self.listWindowController hideWindowWithReason: 8]; + @try { + self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; + + if (![self _gotUsefulCompletionsToShowInList: results.filteredItems]) { + BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; + if ([self.listWindowController showingWindow] && !shownExplicitly) { + [self.listWindowController hideWindowWithReason: 8]; + } + if ([self._inlinePreviewController isShowingInlinePreview]) { + [self._inlinePreviewController hideInlinePreviewWithReason: 8]; + } } - if ([self._inlinePreviewController isShowingInlinePreview]) { - [self._inlinePreviewController hideInlinePreviewWithReason: 8]; + + 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 ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember: [prefix characterAtIndex:0]]) { + BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; + if (!shownExplicitly) { + [self._inlinePreviewController hideInlinePreviewWithReason: 2]; + [self.listWindowController hideWindowWithReason: 2]; + } } - } - - 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 ([[NSCharacterSet decimalDigitCharacterSet] characterIsMember: [prefix characterAtIndex:0]]) { - BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue]; - if (!shownExplicitly) { - [self._inlinePreviewController hideInlinePreviewWithReason: 2]; - [self.listWindowController hideWindowWithReason: 2]; + + if (![FASettings currentSettings].showInlinePreview) { + [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; } - } - - if (![FASettings currentSettings].showInlinePreview) { - [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; + } @catch (NSException *exception) { + RLog(@"Caught an Exception when showing completions: %@", exception); } }); From 34961fa236d6c16a221f35ea58cd653c9bf521dc Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Tue, 27 Oct 2015 10:09:28 +1100 Subject: [PATCH 08/13] Bail out if textView is nil --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 3094f14..4dc61eb 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -464,6 +464,11 @@ - (void)_fa_performFuzzyFiltering:(NSString *) prefix forceFilter: (BOOL) forceF dispatch_on_main(^{ @try { + // This sometimes happens, not sure why. + if (self.textView == nil) { + return; + } + self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; if (![self _gotUsefulCompletionsToShowInList: results.filteredItems]) { From 9fc8cefdc262ac98e2335730bfd4ee066fcc3563 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Fri, 6 Nov 2015 20:04:29 +1100 Subject: [PATCH 09/13] Wrap filtering stage in a try catch --- ...TTextCompletionSession+FuzzyAutocomplete.m | 192 +++++++++--------- 1 file changed, 98 insertions(+), 94 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 4dc61eb..0707d45 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -737,108 +737,112 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { scores: (NSMutableDictionary **) scores secondPassRanges: (NSMutableDictionary **) second { - FAMatchPattern *pattern = [[FAMatchPattern alloc] initWithPattern:query]; - NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredSecond = second ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - - double highScore = 0.0f; - id bestMatch; - - FAItemScoringMethod * method = self._fa_currentScoringMethod; - - double normalization = [method normalizationFactorForSearchString: query]; - - id item; - NSUInteger lower_bound = offset * (array.count / total); - NSUInteger upper_bound = offset == total - 1 ? array.count : (offset + 1) * (array.count / total); - - DLog(@"Process elements %lu %lu (%lu)", lower_bound, upper_bound, array.count); - - NSCharacterSet * identStartSet = [self.textView.class identifierChars]; - - MULTI_TIMER_INIT(Matching); MULTI_TIMER_INIT(Scoring); MULTI_TIMER_INIT(Writing); - - for (NSUInteger i = lower_bound; i < upper_bound; ++i) { - // If the query changes, bail out. Can be optimised - if ( (i % 50 == 0) && ![query isEqualToString:[self fa_filteringQuery]]) { - break; - } - item = array[i]; - NSArray * rangesArray; - NSArray * secondPassArray; - double matchScore; - - NSInteger nameOffset = [item.name rangeOfCharacterFromSet: identStartSet].location; - if (nameOffset == NSNotFound) { - nameOffset = 0; - } - NSString * nameToMatch = !nameOffset ? item.name : [item.name substringFromIndex: nameOffset]; + @try { + FAMatchPattern *pattern = [[FAMatchPattern alloc] initWithPattern:query]; + NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredSecond = second ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + + double highScore = 0.0f; + id bestMatch; + + FAItemScoringMethod * method = self._fa_currentScoringMethod; + + double normalization = [method normalizationFactorForSearchString: query]; + + id item; + NSUInteger lower_bound = offset * (array.count / total); + NSUInteger upper_bound = offset == total - 1 ? array.count : (offset + 1) * (array.count / total); + + DLog(@"Process elements %lu %lu (%lu)", lower_bound, upper_bound, array.count); - MULTI_TIMER_START(Matching); - if (query.length == 1) { - NSRange range = [nameToMatch rangeOfString: query options: NSCaseInsensitiveSearch]; - if (range.location != NSNotFound) { - rangesArray = @[ [NSValue valueWithRange:range] ]; - matchScore = MAX(0.001, [pattern scoreCandidate:nameToMatch matchedRanges:&rangesArray]); + NSCharacterSet * identStartSet = [self.textView.class identifierChars]; + + MULTI_TIMER_INIT(Matching); MULTI_TIMER_INIT(Scoring); MULTI_TIMER_INIT(Writing); + + for (NSUInteger i = lower_bound; i < upper_bound; ++i) { + // If the query changes, bail out. Can be optimised + if ( (i % 50 == 0) && ![query isEqualToString:[self fa_filteringQuery]]) { + break; + } + item = array[i]; + NSArray * rangesArray; + NSArray * secondPassArray; + double matchScore; + + NSInteger nameOffset = [item.name rangeOfCharacterFromSet: identStartSet].location; + if (nameOffset == NSNotFound) { + nameOffset = 0; + } + NSString * nameToMatch = !nameOffset ? item.name : [item.name substringFromIndex: nameOffset]; + + MULTI_TIMER_START(Matching); + if (query.length == 1) { + NSRange range = [nameToMatch rangeOfString: query options: NSCaseInsensitiveSearch]; + if (range.location != NSNotFound) { + rangesArray = @[ [NSValue valueWithRange:range] ]; + matchScore = MAX(0.001, [pattern scoreCandidate:nameToMatch matchedRanges:&rangesArray]); + } else { + matchScore = 0; + } } else { - matchScore = 0; + matchScore = [pattern scoreCandidate:nameToMatch matchedRanges:&rangesArray secondPassRanges: &secondPassArray]; } - } else { - matchScore = [pattern scoreCandidate:nameToMatch matchedRanges:&rangesArray secondPassRanges: &secondPassArray]; - } - MULTI_TIMER_STOP(Matching); - - if (matchScore > 0) { - MULTI_TIMER_START(Scoring); - double factor = [self _priorityFactorForItem:item]; - double score = normalization * [method scoreItem: item - searchString: query - matchedName: nameToMatch - matchScore: matchScore - matchedRanges: rangesArray - priorityFactor: factor]; - MULTI_TIMER_STOP(Scoring); - MULTI_TIMER_START(Writing); - if (score > 0) { - if (nameOffset) { - NSMutableArray * realRanges = [NSMutableArray array]; - for (NSValue * v in rangesArray) { - NSRange r = v.rangeValue; - r.location += nameOffset; - [realRanges addObject: [NSValue valueWithRange: r]]; + MULTI_TIMER_STOP(Matching); + + if (matchScore > 0) { + MULTI_TIMER_START(Scoring); + double factor = [self _priorityFactorForItem:item]; + double score = normalization * [method scoreItem: item + searchString: query + matchedName: nameToMatch + matchScore: matchScore + matchedRanges: rangesArray + priorityFactor: factor]; + MULTI_TIMER_STOP(Scoring); + MULTI_TIMER_START(Writing); + if (score > 0) { + if (nameOffset) { + NSMutableArray * realRanges = [NSMutableArray array]; + for (NSValue * v in rangesArray) { + NSRange r = v.rangeValue; + r.location += nameOffset; + [realRanges addObject: [NSValue valueWithRange: r]]; + } + rangesArray = realRanges; } - rangesArray = realRanges; + [filteredList addObject:item]; + filteredRanges[item.name] = rangesArray ?: @[]; + filteredSecond[item.name] = secondPassArray ?: @[]; + filteredScores[item.name] = @(score); } - [filteredList addObject:item]; - filteredRanges[item.name] = rangesArray ?: @[]; - filteredSecond[item.name] = secondPassArray ?: @[]; - filteredScores[item.name] = @(score); - } - if (score > highScore) { - bestMatch = item; - highScore = score; + if (score > highScore) { + bestMatch = item; + highScore = score; + } + MULTI_TIMER_STOP(Writing); } - MULTI_TIMER_STOP(Writing); } + + DLog(@"Matching %f | Scoring %f | Writing %f", MULTI_TIMER_GET(Matching), MULTI_TIMER_GET(Scoring), MULTI_TIMER_GET(Writing)); + + if (filtered) { + *filtered = filteredList; + } + if (ranges) { + *ranges = filteredRanges; + } + if (second) { + *second = filteredSecond; + } + if (scores) { + *scores = filteredScores; + } + return bestMatch; + } @catch (NSException *exception) { + RLog(@"Caught an Exception when filtering: %@", exception); } - - DLog(@"Matching %f | Scoring %f | Writing %f", MULTI_TIMER_GET(Matching), MULTI_TIMER_GET(Scoring), MULTI_TIMER_GET(Writing)); - - if (filtered) { - *filtered = filteredList; - } - if (ranges) { - *ranges = filteredRanges; - } - if (second) { - *second = filteredSecond; - } - if (scores) { - *scores = filteredScores; - } - return bestMatch; } - (NSMutableArray *) _fa_filterResults: (NSMutableArray *) filteredList From 8927f915cc754f7d30f4adfc1e1ae14bd9605e00 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Fri, 6 Nov 2015 20:12:44 +1100 Subject: [PATCH 10/13] Serialise non-blocking matching to prevent double-free due to multiple threads modifying priority factor cache --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 0707d45..af4c699 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -364,8 +364,14 @@ - (void)_fa_kickFilterTimer:(NSString *)prefix forceFilter: (BOOL) forceFilter if (timer != NULL) { dispatch_source_cancel(timer); } + + static dispatch_once_t onceToken; + static dispatch_queue_t timerQueue; + dispatch_once(&onceToken, ^{ + timerQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.processing-queue", DISPATCH_QUEUE_SERIAL); + }); - timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0)); + timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, timerQueue); dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, [FASettings currentSettings].filterDelay * NSEC_PER_SEC), DISPATCH_TIME_FOREVER, 0.05 * NSEC_PER_SEC); __weak typeof(self) weakSelf = self; From ea27cfea7880216dbd1bf204f04884e1fceb4c35 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Fri, 6 Nov 2015 21:29:14 +1100 Subject: [PATCH 11/13] Hide inline preview before kicking off a nonblocking query to prevent weirdness --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index af4c699..88880f5 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -364,7 +364,7 @@ - (void)_fa_kickFilterTimer:(NSString *)prefix forceFilter: (BOOL) forceFilter if (timer != NULL) { dispatch_source_cancel(timer); } - + static dispatch_once_t onceToken; static dispatch_queue_t timerQueue; dispatch_once(&onceToken, ^{ @@ -405,14 +405,17 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil } return; } + // do not filter if we are inserting a completion // checking for _insertingFullCompletion is not sufficient if (self.fa_insertingCompletion) { return; } - + if ([FASettings currentSettings].nonblockingMode) { + // inline preview does weird things to input when nonblocking is on + [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; [self _fa_kickFilterTimer:prefix forceFilter:forceFilter]; } else { [self _fa_performFuzzyFiltering:prefix forceFilter:forceFilter]; From c69e3b5470a46bcd57682e019c63151d7ca6aef0 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Sat, 7 Nov 2015 18:21:20 +1100 Subject: [PATCH 12/13] It's possible for bestMatchForQuery to not complete propely and return nil objects --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 88880f5..9ae3fa6 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -658,6 +658,11 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { scores: &scoresMap secondPassRanges: &secondMap]; NAMED_TIMER_STOP(Processing); + + // If above method fails, then any of the returned structures could be nil + if (list == nil || rangesMap == nil || scoresMap == nil || secondMap == nil) { + return; + } dispatch_async(reduceQueue, ^{ NAMED_TIMER_START(Reduce); sortedItemArrays[i] = list; From 07b3fc5c4d629c16d3fad56af1b8eca5f708d2d6 Mon Sep 17 00:00:00 2001 From: "Jack Chen (chendo)" Date: Sat, 7 Nov 2015 18:24:40 +1100 Subject: [PATCH 13/13] This should ensure that the return values are populated --- ...TTextCompletionSession+FuzzyAutocomplete.m | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 9ae3fa6..40bd704 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -659,10 +659,6 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { secondPassRanges: &secondMap]; NAMED_TIMER_STOP(Processing); - // If above method fails, then any of the returned structures could be nil - if (list == nil || rangesMap == nil || scoresMap == nil || secondMap == nil) { - return; - } dispatch_async(reduceQueue, ^{ NAMED_TIMER_START(Reduce); sortedItemArrays[i] = list; @@ -751,16 +747,16 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { scores: (NSMutableDictionary **) scores secondPassRanges: (NSMutableDictionary **) second { + FAMatchPattern *pattern = [[FAMatchPattern alloc] initWithPattern:query]; + NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredSecond = second ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + + double highScore = 0.0f; + id bestMatch; + @try { - FAMatchPattern *pattern = [[FAMatchPattern alloc] initWithPattern:query]; - NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredSecond = second ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; - - double highScore = 0.0f; - id bestMatch; - FAItemScoringMethod * method = self._fa_currentScoringMethod; double normalization = [method normalizationFactorForSearchString: query]; @@ -840,7 +836,10 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { } DLog(@"Matching %f | Scoring %f | Writing %f", MULTI_TIMER_GET(Matching), MULTI_TIMER_GET(Scoring), MULTI_TIMER_GET(Writing)); - + + } @catch (NSException *exception) { + RLog(@"Caught an Exception when filtering: %@", exception); + } @finally { if (filtered) { *filtered = filteredList; } @@ -854,8 +853,6 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { *scores = filteredScores; } return bestMatch; - } @catch (NSException *exception) { - RLog(@"Caught an Exception when filtering: %@", exception); } }