From 112efd45a12a6c03201be5c32521cf63488975c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Thu, 17 Apr 2014 22:37:54 +0200 Subject: [PATCH 01/12] Make previous/next completion shortcuts use fuzzy scoring (fix #36) - sort items before calling the original _selectNextPreviousByPriority - also make the sort preserve alphabetic order for equal scores --- ...TTextCompletionSession+FuzzyAutocomplete.m | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index a6692f0..3653a37 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -51,7 +51,11 @@ + (void) load { [self jr_swizzleMethod: @selector(insertCurrentCompletion) withMethod: @selector(_fa_insertCurrentCompletion) - error: nil]; + error: nil]; + + [self jr_swizzleMethod: @selector(_selectNextPreviousByPriority:) + withMethod: @selector(_fa_selectNextPreviousByPriority:) + error: nil]; } #pragma mark - public methods @@ -140,6 +144,28 @@ - (NSRange) _fa_rangeOfFirstWordInString: (NSString *) string { return NSMakeRange(0, string.length); } +// We override to calculate _filteredCompletionsAlpha before calling the original +// This way the hotkeys for prev/next by score use our scoring, not Xcode's +- (void) _fa_selectNextPreviousByPriority: (BOOL) next { + if (![self valueForKey: @"_filteredCompletionsPriority"]) { + NSArray * sorted = nil; + NSDictionary * filteredScores = self.fa_scoresForFilteredCompletions; + if ([FASettings currentSettings].sortByScore) { + sorted = self.filteredCompletionsAlpha.reverseObjectEnumerator.allObjects; + } else if (filteredScores) { + sorted = [self.filteredCompletionsAlpha sortedArrayWithOptions: NSSortConcurrent + usingComparator: ^(id obj1, id obj2) + { + NSComparisonResult result = [filteredScores[obj1.name] compare: filteredScores[obj2.name]]; + return result == NSOrderedSame ? [obj2.name caseInsensitiveCompare: obj1.name] : result; + }]; + } + [self setValue: sorted forKey: @"_filteredCompletionsPriority"]; + } + + [self _fa_selectNextPreviousByPriority: next]; +} + // We override to add formatting to the inline preview. // The ghostCompletionRange is also overriden to be after the last matched letter. - (NSDictionary *) _fa_attributesForCompletionAtCharacterIndex: (NSUInteger) index @@ -350,7 +376,8 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil if ([FASettings currentSettings].sortByScore) { [filteredList sortWithOptions: NSSortConcurrent usingComparator:^(id obj1, id obj2) { - return [filteredScores[obj2.name] compare: filteredScores[obj1.name]]; + NSComparisonResult result = [filteredScores[obj2.name] compare: filteredScores[obj1.name]]; + return result == NSOrderedSame ? [obj1.name caseInsensitiveCompare: obj2.name] : result; }]; } @@ -381,7 +408,8 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil [self setValue: filteredList 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"]; From 1693bfde67e490dee4d7c6522be4655ed946ce9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Fri, 18 Apr 2014 01:16:15 +0200 Subject: [PATCH 02/12] Faster filtering with prefix anchor also added forgotten line to actually cache the results --- ...TTextCompletionSession+FuzzyAutocomplete.m | 89 ++++++++++--------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 3653a37..a2cdd03 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -539,44 +539,54 @@ - (void) _fa_setAllCompletions: (NSArray *) allCompletions { return bestMatch; } -// Performs a simple binary search to find rirst item with given prefix. -- (NSInteger) _fa_indexOfFirstItemWithPrefix: (NSString *) prefix inSortedArray: (NSArray *) array { - const NSUInteger N = array.count; - - if (N == 0) return NSNotFound; - - id item; - - if ([(item = array[0]).name compare: prefix options: NSCaseInsensitiveSearch] == NSOrderedDescending) { - if ([[item.name lowercaseString] hasPrefix: prefix]) { - return 0; - } else { - return NSNotFound; +// Returns index of first element passing test, or NSNotFound, assumes sorted range wrt test +- (NSUInteger) _fa_indexOfFirstElementInSortedRange: (NSRange) range + inArray: (NSArray *) array + passingTest: (BOOL(^)(id)) test +{ + if (range.length == 0) return NSNotFound; + NSUInteger a = range.location, b = range.location + range.length - 1; + if (test(array[a])) { + return a; + } else if (!test(array[b])) { + return NSNotFound; + } else { + while (b > a + 1) { + NSUInteger c = (a + b) / 2; + if (test(array[c])) { + b = c; + } else { + a = c; + } } + return b; } +} - if ([(item = array[N-1]).name compare: prefix options: NSCaseInsensitiveSearch] == NSOrderedAscending) { - return NSNotFound; - } +// Performs binary searches to find items with given prefix. +- (NSRange) _fa_rangeOfItemsWithPrefix: (NSString *) prefix + inSortedRange: (NSRange) range + inArray: (NSArray *) array +{ + NSUInteger lowerBound = [self _fa_indexOfFirstElementInSortedRange: range inArray: array passingTest: ^BOOL(id item) { + return [item.name caseInsensitiveCompare: prefix] != NSOrderedAscending; + }]; - NSUInteger a = 0, b = N-1; - while (b > a+1) { - NSUInteger c = (a + b) / 2; - if ([(item = array[c]).name compare: prefix options: NSCaseInsensitiveSearch] == NSOrderedAscending) { - a = c; - } else { - b = c; - } + if (lowerBound == NSNotFound) { + return NSMakeRange(0, 0); } - if ([[(item = array[a]).name lowercaseString] hasPrefix: prefix]) { - return a; - } - if ([[(item = array[b]).name lowercaseString] hasPrefix: prefix]) { - return b; + range.location += lowerBound; range.length -= lowerBound; + + NSUInteger upperBound = [self _fa_indexOfFirstElementInSortedRange: range inArray: array passingTest: ^BOOL(id item) { + return ![item.name.lowercaseString hasPrefix: prefix]; + }]; + + if (upperBound != NSNotFound) { + range.length = upperBound - lowerBound; } - return NSNotFound; + return range; } // gets a subset of allCompletions for given prefix @@ -590,26 +600,17 @@ - (NSArray *) _fa_filteredCompletionsForPrefix: (NSString *) prefix { NSArray *completionsForPrefix = filteredCompletionCache[prefix]; if (!completionsForPrefix) { NSArray * searchSet = self.allCompletions; - for (int i = 1; i < prefix.length; ++i) { + for (NSUInteger i = prefix.length - 1; i > 0; --i) { NSArray * cached = filteredCompletionCache[[prefix substringToIndex: i]]; if (cached) { searchSet = cached; + break; } } // searchSet is sorted so we can do a binary search - NSUInteger idx = [self _fa_indexOfFirstItemWithPrefix: prefix inSortedArray: searchSet]; - if (idx == NSNotFound) { - completionsForPrefix = @[]; - } else { - NSMutableArray * array = [NSMutableArray array]; - const NSUInteger N = searchSet.count; - id item; - while (idx < N && [(item = searchSet[idx]).name.lowercaseString hasPrefix: prefix]) { - [array addObject: item]; - ++idx; - } - completionsForPrefix = array; - } + NSRange range = [self _fa_rangeOfItemsWithPrefix: prefix inSortedRange: NSMakeRange(0, searchSet.count) inArray: searchSet]; + completionsForPrefix = [searchSet subarrayWithRange: range]; + filteredCompletionCache[prefix] = completionsForPrefix; } return completionsForPrefix; } From 6646a53733b054b8efea6bcf0d19b5d3455cf0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Fri, 18 Apr 2014 14:40:57 +0200 Subject: [PATCH 03/12] Use a stack with cached results for faster backspacing (fix #29) --- ...TTextCompletionSession+FuzzyAutocomplete.m | 354 ++++++++++-------- 1 file changed, 192 insertions(+), 162 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index a2cdd03..8fb9513 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -21,6 +21,21 @@ #import #define MIN_CHUNK_LENGTH 100 +/// A simple helper class to avoid using a dictionary in resultsStack +@interface FAFilteringResults : NSObject + +@property (nonatomic, retain) NSString * query; +@property (nonatomic, retain) NSArray * allItems; +@property (nonatomic, retain) NSArray * filteredItems; +@property (nonatomic, retain) NSDictionary * scores; +@property (nonatomic, retain) NSDictionary * ranges; +@property (nonatomic, assign) NSUInteger selection; + +@end + +@implementation FAFilteringResults + +@end @implementation DVTTextCompletionSession (FuzzyAutocomplete) @@ -133,6 +148,8 @@ - (instancetype) _fa_initWithTextView: (NSTextView *) textView method.maxPrefixBonus = settings.maxPrefixBonus; session._fa_currentScoringMethod = method; + + session._fa_resultsStack = [NSMutableArray array]; } return session; } @@ -220,16 +237,22 @@ - (NSString *) _fa_usefulPartialCompletionPrefixForItems: (NSArray *) items - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFilter { DLog(@"filteringPrefix = @\"%@\"", prefix); - self.fa_filteringTime = 0; + // 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]; + } - NSString *lastPrefix = [self valueForKey: @"_filteringPrefix"]; + self.fa_filteringTime = 0; // Let the original handler deal with the zero letter case if (prefix.length == 0) { NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; - self.fa_matchedRangesForFilteredCompletions = nil; - self.fa_scoresForFilteredCompletions = nil; + [self._fa_resultsStack removeAllObjects]; + [self _fa_setFilteringPrefix:prefix forceFilter:forceFilter]; if (![FASettings currentSettings].showInlinePreview) { [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; @@ -254,147 +277,22 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil NSTimeInterval start = [NSDate timeIntervalSinceReferenceDate]; - NSArray *searchSet = nil; - [self setValue: prefix forKey: @"_filteringPrefix"]; - const NSInteger anchor = [FASettings currentSettings].prefixAnchor; - - NAMED_TIMER_START(ObtainSearchSet); - - if (lastPrefix && [[prefix lowercaseString] hasPrefix: [lastPrefix lowercaseString]]) { - if (lastPrefix.length >= anchor) { - searchSet = self.fa_nonZeroMatches; - } else { - searchSet = [self _fa_filteredCompletionsForPrefix: [prefix substringToIndex: MIN(prefix.length, anchor)]]; - } - } else { - if (anchor > 0) { - searchSet = [self _fa_filteredCompletionsForPrefix: [prefix substringToIndex: MIN(prefix.length, anchor)]]; - } else { - searchSet = [self _fa_filteredCompletionsForLetter: [prefix substringToIndex:1]]; - } - } - - NAMED_TIMER_STOP(ObtainSearchSet); + FAFilteringResults * results; - NSMutableArray * filteredList; - NSMutableDictionary *filteredRanges; - NSMutableDictionary *filteredScores; - - __block id bestMatch = nil; - - NSUInteger workerCount = [FASettings currentSettings].parallelScoring ? [FASettings currentSettings].maximumWorkers : 1; - workerCount = MIN(MAX(searchSet.count / MIN_CHUNK_LENGTH, 1), workerCount); - - NAMED_TIMER_START(CalculateScores); - - if (workerCount < 2) { - bestMatch = [self _fa_bestMatchForQuery: prefix - inArray: searchSet - filteredList: &filteredList - rangesMap: &filteredRanges - scores: &filteredScores]; + if (resultsStack.count && [prefix isEqualToString: [[resultsStack lastObject] query]]) { + results = [resultsStack lastObject]; } else { - dispatch_queue_t processingQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.processing-queue", DISPATCH_QUEUE_CONCURRENT); - dispatch_queue_t reduceQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.reduce-queue", DISPATCH_QUEUE_SERIAL); - dispatch_group_t group = dispatch_group_create(); - - NSMutableArray *bestMatches = [NSMutableArray array]; - filteredList = [NSMutableArray array]; - filteredRanges = [NSMutableDictionary dictionary]; - filteredScores = [NSMutableDictionary dictionary]; - - for (NSInteger i = 0; i < workerCount; ++i) { - dispatch_group_async(group, processingQueue, ^{ - NSArray *list; - NSDictionary *rangesMap; - NSDictionary *scoresMap; - NAMED_TIMER_START(Processing); - id bestMatch = [self _fa_bestMatchForQuery: prefix - inArray: searchSet - offset: i - total: workerCount - filteredList: &list - rangesMap: &rangesMap - scores: &scoresMap]; - NAMED_TIMER_STOP(Processing); - dispatch_async(reduceQueue, ^{ - NAMED_TIMER_START(Reduce); - if (bestMatch) { - [bestMatches addObject:bestMatch]; - } - [filteredList addObjectsFromArray:list]; - [filteredRanges addEntriesFromDictionary:rangesMap]; - [filteredScores addEntriesFromDictionary:scoresMap]; - NAMED_TIMER_STOP(Reduce); - }); - }); - } - - dispatch_group_wait(group, DISPATCH_TIME_FOREVER); - dispatch_sync(reduceQueue, ^{}); - - bestMatch = [self _fa_bestMatchForQuery: prefix - inArray: bestMatches - filteredList: nil - rangesMap: nil - scores: nil]; - - } - - NAMED_TIMER_STOP(CalculateScores); - - if ([FASettings currentSettings].showInlinePreview) { - if ([self._inlinePreviewController isShowingInlinePreview]) { - [self._inlinePreviewController hideInlinePreviewWithReason:0x8]; - } - } - - // setter copies the array - self.fa_nonZeroMatches = filteredList; - - NAMED_TIMER_START(FilterByScore); - - double threshold = [FASettings currentSettings].minimumScoreThreshold; - if ([FASettings currentSettings].filterByScore && threshold != 0) { - if ([FASettings currentSettings].normalizeScores) { - threshold *= [filteredScores[bestMatch.name] doubleValue]; - } - NSMutableArray * newArray = [NSMutableArray array]; - for (id item in filteredList) { - if ([filteredScores[item.name] doubleValue] >= threshold) { - [newArray addObject: item]; - } - } - filteredList = newArray; - } - - NAMED_TIMER_STOP(FilterByScore); - - NAMED_TIMER_START(SortByScore); - - if ([FASettings currentSettings].sortByScore) { - [filteredList sortWithOptions: NSSortConcurrent usingComparator:^(id obj1, id obj2) { - NSComparisonResult result = [filteredScores[obj2.name] compare: filteredScores[obj1.name]]; - return result == NSOrderedSame ? [obj1.name caseInsensitiveCompare: obj2.name] : result; - }]; + results = [self _fa_calculateResultsForQuery: prefix]; + [resultsStack addObject: results]; } - NAMED_TIMER_STOP(SortByScore); - - NAMED_TIMER_START(FindSelection); - - NSUInteger selection = filteredList.count && bestMatch ? [filteredList indexOfObject:bestMatch] : NSNotFound; - - NAMED_TIMER_STOP(FindSelection); - [self setPendingRequestState: 0]; - NSString * partial = [self _usefulPartialCompletionPrefixForItems: filteredList selectedIndex: selection filteringPrefix: prefix]; - - self.fa_matchedRangesForFilteredCompletions = filteredRanges; - self.fa_scoresForFilteredCompletions = filteredScores; + NSString * partial = [self _usefulPartialCompletionPrefixForItems: results.filteredItems + selectedIndex: results.selection + filteringPrefix: prefix]; self.fa_filteringTime = [NSDate timeIntervalSinceReferenceDate] - start; @@ -405,9 +303,9 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil [self willChangeValueForKey:@"usefulPrefix"]; [self willChangeValueForKey:@"selectedCompletionIndex"]; - [self setValue: filteredList forKey: @"_filteredCompletionsAlpha"]; + [self setValue: results.filteredItems forKey: @"_filteredCompletionsAlpha"]; [self setValue: partial forKey: @"_usefulPrefix"]; - [self setValue: @(selection) forKey: @"_selectedCompletionIndex"]; + [self setValue: @(results.selection) forKey: @"_selectedCompletionIndex"]; [self setValue: nil forKey: @"_filteredCompletionsPriority"]; [self didChangeValueForKey:@"filteredCompletionsAlpha"]; @@ -418,6 +316,10 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil if (![FASettings currentSettings].showInlinePreview) { [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; + } else { + if ([self._inlinePreviewController isShowingInlinePreview]) { + [self._inlinePreviewController hideInlinePreviewWithReason:0x8]; + } } } @@ -436,14 +338,153 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil // We nullify the caches when completions change. - (void) _fa_setAllCompletions: (NSArray *) allCompletions { [self _fa_setAllCompletions:allCompletions]; - self.fa_matchedRangesForFilteredCompletions = nil; - self.fa_scoresForFilteredCompletions = nil; + [self._fa_resultsStack removeAllObjects]; [objc_getAssociatedObject(self, &letterFilteredCompletionCacheKey) removeAllObjects]; [objc_getAssociatedObject(self, &prefixFilteredCompletionCacheKey) removeAllObjects]; } #pragma mark - helpers +// Calculate all the results needed by setFilteringPrefix +- (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { + + FAFilteringResults * results = [[FAFilteringResults alloc] init]; + results.query = query; + + NSArray *searchSet = nil; + NSMutableArray * filteredList; + NSMutableDictionary *filteredRanges; + NSMutableDictionary *filteredScores; + + const NSInteger anchor = [FASettings currentSettings].prefixAnchor; + + FAFilteringResults * lastResults = self._fa_resultsStack.count ? self._fa_resultsStack.lastObject : nil; + + NAMED_TIMER_START(ObtainSearchSet); + + if (lastResults.query.length && [[query lowercaseString] hasPrefix: [lastResults.query lowercaseString]]) { + if (lastResults.query.length >= anchor) { + searchSet = lastResults.allItems; + } else { + searchSet = [self _fa_filteredCompletionsForPrefix: [query substringToIndex: MIN(query.length, anchor)]]; + } + } else { + if (anchor > 0) { + searchSet = [self _fa_filteredCompletionsForPrefix: [query substringToIndex: MIN(query.length, anchor)]]; + } else { + searchSet = [self _fa_filteredCompletionsForLetter: [query substringToIndex:1]]; + } + } + + NAMED_TIMER_STOP(ObtainSearchSet); + + __block id bestMatch = nil; + + NSUInteger workerCount = [FASettings currentSettings].parallelScoring ? [FASettings currentSettings].maximumWorkers : 1; + workerCount = MIN(MAX(searchSet.count / MIN_CHUNK_LENGTH, 1), workerCount); + + NAMED_TIMER_START(CalculateScores); + + if (workerCount < 2) { + bestMatch = [self _fa_bestMatchForQuery: query + inArray: searchSet + filteredList: &filteredList + rangesMap: &filteredRanges + scores: &filteredScores]; + } else { + dispatch_queue_t processingQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.processing-queue", DISPATCH_QUEUE_CONCURRENT); + dispatch_queue_t reduceQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.reduce-queue", DISPATCH_QUEUE_SERIAL); + dispatch_group_t group = dispatch_group_create(); + + NSMutableArray *bestMatches = [NSMutableArray array]; + filteredList = [NSMutableArray array]; + filteredRanges = [NSMutableDictionary dictionary]; + filteredScores = [NSMutableDictionary dictionary]; + + for (NSInteger i = 0; i < workerCount; ++i) { + dispatch_group_async(group, processingQueue, ^{ + NSArray *list; + NSDictionary *rangesMap; + NSDictionary *scoresMap; + NAMED_TIMER_START(Processing); + id bestMatch = [self _fa_bestMatchForQuery: query + inArray: [NSArray arrayWithArray: searchSet] + offset: i + total: workerCount + filteredList: &list + rangesMap: &rangesMap + scores: &scoresMap]; + NAMED_TIMER_STOP(Processing); + dispatch_async(reduceQueue, ^{ + NAMED_TIMER_START(Reduce); + if (bestMatch) { + [bestMatches addObject:bestMatch]; + } + [filteredList addObjectsFromArray:list]; + [filteredRanges addEntriesFromDictionary:rangesMap]; + [filteredScores addEntriesFromDictionary:scoresMap]; + NAMED_TIMER_STOP(Reduce); + }); + }); + } + + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); + dispatch_sync(reduceQueue, ^{}); + + bestMatch = [self _fa_bestMatchForQuery: query + inArray: bestMatches + filteredList: nil + rangesMap: nil + scores: nil]; + + } + + NAMED_TIMER_STOP(CalculateScores); + + results.allItems = [NSArray arrayWithArray: filteredList]; + + NAMED_TIMER_START(FilterByScore); + + double threshold = [FASettings currentSettings].minimumScoreThreshold; + if ([FASettings currentSettings].filterByScore && threshold != 0) { + if ([FASettings currentSettings].normalizeScores) { + threshold *= [filteredScores[bestMatch.name] doubleValue]; + } + NSMutableArray * newArray = [NSMutableArray array]; + for (id item in filteredList) { + if ([filteredScores[item.name] doubleValue] >= threshold) { + [newArray addObject: item]; + } + } + filteredList = newArray; + } + + NAMED_TIMER_STOP(FilterByScore); + + NAMED_TIMER_START(SortByScore); + + if ([FASettings currentSettings].sortByScore) { + [filteredList sortWithOptions: NSSortConcurrent usingComparator:^(id obj1, id obj2) { + NSComparisonResult result = [filteredScores[obj2.name] compare: filteredScores[obj1.name]]; + return result == NSOrderedSame ? [obj1.name caseInsensitiveCompare: obj2.name] : result; + }]; + } + + NAMED_TIMER_STOP(SortByScore); + + NAMED_TIMER_START(FindSelection); + + results.selection = filteredList.count && bestMatch ? [filteredList indexOfObject:bestMatch] : NSNotFound; + + NAMED_TIMER_STOP(FindSelection); + + results.filteredItems = filteredList; + results.ranges = filteredRanges; + results.scores = filteredScores; + + return results; +} + // Score the items, store filtered list, matched ranges, scores and the best match. - (id) _fa_bestMatchForQuery: (NSString *) query inArray: (NSArray *) array @@ -704,34 +745,23 @@ - (void) setFa_insertingCompletion: (BOOL) value { objc_setAssociatedObject(self, &insertingCompletionKey, @(value), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -static char matchedRangesKey; - - (NSDictionary *) fa_matchedRangesForFilteredCompletions { - return objc_getAssociatedObject(self, &matchedRangesKey); -} - -- (void) setFa_matchedRangesForFilteredCompletions: (NSDictionary *) dict { - objc_setAssociatedObject(self, &matchedRangesKey, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC); -} - -static char scoresKey; - -- (void) setFa_scoresForFilteredCompletions: (NSDictionary *) dict { - objc_setAssociatedObject(self, &scoresKey, dict, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + NSArray * stack = [self _fa_resultsStack]; + return stack.count ? [stack.lastObject ranges] : nil; } - (NSDictionary *) fa_scoresForFilteredCompletions { - return objc_getAssociatedObject(self, &scoresKey); + NSArray * stack = [self _fa_resultsStack]; + return stack.count ? [stack.lastObject scores] : nil; } -static char kNonZeroMatchesKey; - -- (NSArray *) fa_nonZeroMatches { - return objc_getAssociatedObject(self, &kNonZeroMatchesKey); +static char kResultsStackKey; +- (NSMutableArray *) _fa_resultsStack { + return objc_getAssociatedObject(self, &kResultsStackKey); } -- (void) setFa_nonZeroMatches: (NSArray *) array { - objc_setAssociatedObject(self, &kNonZeroMatchesKey, array, OBJC_ASSOCIATION_COPY_NONATOMIC); +- (void) set_fa_resultsStack: (NSMutableArray *) stack { + objc_setAssociatedObject(self, &kResultsStackKey, stack, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -@end +@end \ No newline at end of file From 406ddc056a415f678bcad568dd5f02b2b3001f76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Fri, 18 Apr 2014 23:38:02 +0200 Subject: [PATCH 04/12] Remove (now redundant) prefix- and letter- caches. --- ...TTextCompletionSession+FuzzyAutocomplete.m | 64 +++++++------------ 1 file changed, 22 insertions(+), 42 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 8fb9513..c4f9ade 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -332,15 +332,10 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil } -static char letterFilteredCompletionCacheKey; -static char prefixFilteredCompletionCacheKey; - // We nullify the caches when completions change. - (void) _fa_setAllCompletions: (NSArray *) allCompletions { [self _fa_setAllCompletions:allCompletions]; [self._fa_resultsStack removeAllObjects]; - [objc_getAssociatedObject(self, &letterFilteredCompletionCacheKey) removeAllObjects]; - [objc_getAssociatedObject(self, &prefixFilteredCompletionCacheKey) removeAllObjects]; } #pragma mark - helpers @@ -358,7 +353,7 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { const NSInteger anchor = [FASettings currentSettings].prefixAnchor; - FAFilteringResults * lastResults = self._fa_resultsStack.count ? self._fa_resultsStack.lastObject : nil; + FAFilteringResults * lastResults = [self _fa_lastFilteringResults]; NAMED_TIMER_START(ObtainSearchSet); @@ -633,52 +628,33 @@ - (NSRange) _fa_rangeOfItemsWithPrefix: (NSString *) prefix // gets a subset of allCompletions for given prefix - (NSArray *) _fa_filteredCompletionsForPrefix: (NSString *) prefix { prefix = [prefix lowercaseString]; - NSMutableDictionary *filteredCompletionCache = objc_getAssociatedObject(self, &prefixFilteredCompletionCacheKey); - if (!filteredCompletionCache) { - filteredCompletionCache = [NSMutableDictionary dictionary]; - objc_setAssociatedObject(self, &prefixFilteredCompletionCacheKey, filteredCompletionCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - NSArray *completionsForPrefix = filteredCompletionCache[prefix]; - if (!completionsForPrefix) { - NSArray * searchSet = self.allCompletions; - for (NSUInteger i = prefix.length - 1; i > 0; --i) { - NSArray * cached = filteredCompletionCache[[prefix substringToIndex: i]]; - if (cached) { - searchSet = cached; - break; - } - } + FAFilteringResults * lastResults = [self _fa_lastFilteringResults]; + NSArray * array; + if ([lastResults.query.lowercaseString hasPrefix: prefix]) { + array = lastResults.allItems; + } else { + NSArray * searchSet = lastResults.allItems ?: self.allCompletions; // searchSet is sorted so we can do a binary search NSRange range = [self _fa_rangeOfItemsWithPrefix: prefix inSortedRange: NSMakeRange(0, searchSet.count) inArray: searchSet]; - completionsForPrefix = [searchSet subarrayWithRange: range]; - filteredCompletionCache[prefix] = completionsForPrefix; + array = [searchSet subarrayWithRange: range]; } - return completionsForPrefix; + return array; } // gets a subset of allCompletions for given letter - (NSArray *) _fa_filteredCompletionsForLetter: (NSString *) letter { letter = [letter lowercaseString]; - NSMutableDictionary *filteredCompletionCache = objc_getAssociatedObject(self, &letterFilteredCompletionCacheKey); - if (!filteredCompletionCache) { - filteredCompletionCache = [NSMutableDictionary dictionary]; - objc_setAssociatedObject(self, &letterFilteredCompletionCacheKey, filteredCompletionCache, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } - NSArray *completionsForLetter = [filteredCompletionCache objectForKey:letter]; - if (!completionsForLetter) { - NSString * lowerAndUpper = [letter stringByAppendingString: letter.uppercaseString]; - NSCharacterSet * set = [NSCharacterSet characterSetWithCharactersInString: lowerAndUpper]; - NSMutableArray * array = [NSMutableArray array]; - for (id item in self.allCompletions) { - NSRange range = [item.name rangeOfCharacterFromSet: set]; - if (range.location != NSNotFound) { - [array addObject: item]; - } + + NSString * lowerAndUpper = [letter stringByAppendingString: letter.uppercaseString]; + NSCharacterSet * set = [NSCharacterSet characterSetWithCharactersInString: lowerAndUpper]; + NSMutableArray * array = [NSMutableArray array]; + for (id item in self.allCompletions) { + NSRange range = [item.name rangeOfCharacterFromSet: set]; + if (range.location != NSNotFound) { + [array addObject: item]; } - completionsForLetter = array; - filteredCompletionCache[letter] = completionsForLetter; } - return completionsForLetter; + return array; } - (void)_fa_debugCompletionsByScore:(NSArray *)completions withQuery:(NSString *)query { @@ -764,4 +740,8 @@ - (void) set_fa_resultsStack: (NSMutableArray *) stack { objc_setAssociatedObject(self, &kResultsStackKey, stack, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (FAFilteringResults *) _fa_lastFilteringResults { + return self._fa_resultsStack.count ? self._fa_resultsStack.lastObject : nil; +} + @end \ No newline at end of file From 75be018a582da4bbead142f71fb0459dc05589d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Sat, 19 Apr 2014 01:01:54 +0200 Subject: [PATCH 05/12] Bugfix: inline preview sometimes shows despite being disabled - when bringing back completions another time --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index c4f9ade..8502d6d 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -71,6 +71,10 @@ + (void) load { [self jr_swizzleMethod: @selector(_selectNextPreviousByPriority:) withMethod: @selector(_fa_selectNextPreviousByPriority:) error: nil]; + + [self jr_swizzleMethod: @selector(showCompletionsExplicitly:) + withMethod: @selector(_fa_showCompletionsExplicitly:) + error: nil]; } #pragma mark - public methods @@ -130,7 +134,15 @@ - (BOOL) _fa_insertCurrentCompletion { return ret; } -// We additionally refresh the theme upon session creation. +// We override here to hide inline preview if disabled +- (void) _fa_showCompletionsExplicitly: (BOOL) explicitly { + [self _fa_showCompletionsExplicitly: explicitly]; + if (![FASettings currentSettings].showInlinePreview) { + [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; + } +} + +// We additionally load the settings and refresh the theme upon session creation. - (instancetype) _fa_initWithTextView: (NSTextView *) textView atLocation: (NSInteger) location cursorLocation: (NSInteger) cursorLocation From 2ebb09a700b19cce6513e912cb6a81eb79ee7409 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Sat, 19 Apr 2014 01:05:27 +0200 Subject: [PATCH 06/12] Bugfix: completions not showing for just one letter (fix #37) - removed some not needed and wrong legacy code --- .../DVTTextCompletionSession+FuzzyAutocomplete.m | 6 ------ 1 file changed, 6 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 8502d6d..06eceb2 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -300,8 +300,6 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil [resultsStack addObject: results]; } - [self setPendingRequestState: 0]; - NSString * partial = [self _usefulPartialCompletionPrefixForItems: results.filteredItems selectedIndex: results.selection filteringPrefix: prefix]; @@ -328,10 +326,6 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil if (![FASettings currentSettings].showInlinePreview) { [self._inlinePreviewController hideInlinePreviewWithReason: 0x0]; - } else { - if ([self._inlinePreviewController isShowingInlinePreview]) { - [self._inlinePreviewController hideInlinePreviewWithReason:0x8]; - } } } From fcd384fdf0164748567430d4c38e9fbbcb0dc073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Sun, 20 Apr 2014 00:45:59 +0200 Subject: [PATCH 07/12] Move FuzzyAutocomplete menu item to Editor submenu --- FuzzyAutocomplete/FASettings.m | 4 +- FuzzyAutocomplete/FuzzyAutocomplete.m | 70 ++++++++++++++++----------- 2 files changed, 43 insertions(+), 31 deletions(-) diff --git a/FuzzyAutocomplete/FASettings.m b/FuzzyAutocomplete/FASettings.m index ad1911c..05fc69b 100644 --- a/FuzzyAutocomplete/FASettings.m +++ b/FuzzyAutocomplete/FASettings.m @@ -11,7 +11,7 @@ #import "FATheme.h" // increment to show settings screen to the user -static const NSUInteger kSettingsVersion = 1; +static const NSUInteger kSettingsVersion = 2; @interface FASettings () @@ -163,7 +163,7 @@ - (void) loadFromDefaults { defaultButton: @"View" alternateButton: @"Skip" otherButton: nil - informativeTextWithFormat: @"New settings are available for %@ plugin. Do you want to review them now? You can always access the settings later from the Menu: Xcode > %@ > Plugin Settings...", pluginName, pluginName]; + informativeTextWithFormat: @"New settings are available for %@ plugin. Do you want to review them now? You can always access the settings later from the Menu: Editor > %@ > Plugin Settings...", pluginName, pluginName]; if ([alert runModal] == NSAlertDefaultReturn) { [self showSettingsWindow]; } diff --git a/FuzzyAutocomplete/FuzzyAutocomplete.m b/FuzzyAutocomplete/FuzzyAutocomplete.m index 57986c9..edc164f 100644 --- a/FuzzyAutocomplete/FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/FuzzyAutocomplete.m @@ -20,41 +20,53 @@ + (void)pluginDidLoad:(NSBundle *)plugin { if ([currentApplicationName isEqual:@"Xcode"]) { dispatch_once(&onceToken, ^{ - [self createMenuItem: plugin]; + [self createMenuItem]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(menuDidChange:) + name: NSMenuDidChangeItemNotification + object: nil]; }); } } -+ (void)createMenuItem: (NSBundle *) pluginBundle { ++ (void) menuDidChange: (NSNotification *) notification { + [self createMenuItem]; +} + ++ (void)createMenuItem { + NSBundle * pluginBundle = [NSBundle bundleForClass: self]; NSString * name = pluginBundle.lsl_bundleName; - NSMenuItem * xcodeMenuItem = [[NSApp mainMenu] itemAtIndex: 0]; - NSMenuItem * fuzzyItem = [[NSMenuItem alloc] initWithTitle: name - action: NULL - keyEquivalent: @""]; - - NSString * version = [@"Plugin Version: " stringByAppendingString: pluginBundle.lsl_bundleVersion]; - NSMenuItem * versionItem = [[NSMenuItem alloc] initWithTitle: version - action: NULL - keyEquivalent: @""]; - - NSMenuItem * settingsItem = [[NSMenuItem alloc] initWithTitle: @"Plugin Settings..." - action: @selector(showSettingsWindow) - keyEquivalent: @""]; - - settingsItem.target = [FASettings currentSettings]; - - fuzzyItem.submenu = [[NSMenu alloc] initWithTitle: name]; - [fuzzyItem.submenu addItem: versionItem]; - [fuzzyItem.submenu addItem: settingsItem]; - - NSInteger menuIndex = [xcodeMenuItem.submenu indexOfItemWithTitle: @"Behaviors"]; - if (menuIndex == -1) { - menuIndex = 3; - } else { - ++menuIndex; - } + NSMenuItem * editorMenuItem = [[NSApp mainMenu] itemWithTitle: @"Editor"]; + + if (editorMenuItem && ![editorMenuItem.submenu itemWithTitle: name]) { + NSMenuItem * fuzzyItem = [[NSMenuItem alloc] initWithTitle: name + action: NULL + keyEquivalent: @""]; + + NSString * version = [@"Plugin Version: " stringByAppendingString: pluginBundle.lsl_bundleVersion]; + NSMenuItem * versionItem = [[NSMenuItem alloc] initWithTitle: version + action: NULL + keyEquivalent: @""]; - [xcodeMenuItem.submenu insertItem: fuzzyItem atIndex: menuIndex]; + NSMenuItem * settingsItem = [[NSMenuItem alloc] initWithTitle: @"Plugin Settings..." + action: @selector(showSettingsWindow) + keyEquivalent: @""]; + + settingsItem.target = [FASettings currentSettings]; + + fuzzyItem.submenu = [[NSMenu alloc] initWithTitle: name]; + [fuzzyItem.submenu addItem: versionItem]; + [fuzzyItem.submenu addItem: settingsItem]; + + NSInteger menuIndex = [editorMenuItem.submenu indexOfItemWithTitle: @"Show Completions"]; + if (menuIndex == -1) { + [editorMenuItem.submenu addItem: [NSMenuItem separatorItem]]; + [editorMenuItem.submenu addItem: fuzzyItem]; + } else { + [editorMenuItem.submenu insertItem: fuzzyItem atIndex: menuIndex]; + } + + } } @end From 55388157713bae5209a0d52de817b030bbe46d9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Tue, 22 Apr 2014 22:58:55 +0200 Subject: [PATCH 08/12] Add an option to enable/disable the plugin in the settings. --- ...nlinePreviewController+FuzzyAutocomplete.h | 3 ++ ...nlinePreviewController+FuzzyAutocomplete.m | 2 +- ...onListWindowController+FuzzyAutocomplete.h | 3 ++ ...onListWindowController+FuzzyAutocomplete.m | 2 +- ...TTextCompletionSession+FuzzyAutocomplete.h | 3 ++ ...TTextCompletionSession+FuzzyAutocomplete.m | 2 +- FuzzyAutocomplete/FASettings.h | 5 +++ FuzzyAutocomplete/FASettings.m | 33 +++++++++++++++++++ FuzzyAutocomplete/FASettingsWindow.xib | 11 +++++++ FuzzyAutocomplete/FuzzyAutocomplete.m | 32 ++++++++++++++++++ 10 files changed, 93 insertions(+), 3 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.h b/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.h index 91d9e64..75593b8 100644 --- a/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.h +++ b/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.h @@ -10,6 +10,9 @@ @interface DVTTextCompletionInlinePreviewController (FuzzyAutocomplete) +/// Swizzles methods to enable/disable the plugin ++ (void) fa_swizzleMethods; + /// Matched ranges mapped to preview space. @property (nonatomic, retain) NSArray * fa_matchedRanges; diff --git a/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.m index d40d4ce..2cf8bf3 100644 --- a/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.m @@ -15,7 +15,7 @@ @implementation DVTTextCompletionInlinePreviewController (FuzzyAutocomplete) -+ (void) load { ++ (void) fa_swizzleMethods { [self jr_swizzleMethod: @selector(ghostComplementRange) withMethod: @selector(_fa_ghostComplementRange) error: NULL]; diff --git a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.h b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.h index 4dbe9de..64cba00 100644 --- a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.h +++ b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.h @@ -10,4 +10,7 @@ @interface DVTTextCompletionListWindowController (FuzzyAutocomplete) +/// Swizzles methods to enable/disable the plugin ++ (void) fa_swizzleMethods; + @end diff --git a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m index 881d682..3f19150 100644 --- a/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionListWindowController+FuzzyAutocomplete.m @@ -20,7 +20,7 @@ @implementation DVTTextCompletionListWindowController (FuzzyAutocomplete) -+ (void) load { ++ (void) fa_swizzleMethods { [self jr_swizzleMethod: @selector(tableView:willDisplayCell:forTableColumn:row:) withMethod: @selector(_fa_tableView:willDisplayCell:forTableColumn:row:) error: NULL]; diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.h b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.h index afd0773..d9da2b7 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.h +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.h @@ -15,6 +15,9 @@ @interface DVTTextCompletionSession (FuzzyAutocomplete) +/// Swizzles methods to enable/disable the plugin ++ (void) fa_swizzleMethods; + /// Current filtering query. - (NSString *) fa_filteringQuery; diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index 06eceb2..b6f47d1 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -39,7 +39,7 @@ @implementation FAFilteringResults @implementation DVTTextCompletionSession (FuzzyAutocomplete) -+ (void) load { ++ (void) fa_swizzleMethods { [self jr_swizzleMethod: @selector(_setFilteringPrefix:forceFilter:) withMethod: @selector(_fa_setFilteringPrefix:forceFilter:) error: NULL]; diff --git a/FuzzyAutocomplete/FASettings.h b/FuzzyAutocomplete/FASettings.h index 29ed27a..1f44617 100644 --- a/FuzzyAutocomplete/FASettings.h +++ b/FuzzyAutocomplete/FASettings.h @@ -8,6 +8,8 @@ #import +extern NSString * FASettingsPluginEnabledDidChangeNotification; + @interface FASettings : NSObject /// Gets the singleton. Note that the settings are not loaded automatically. @@ -22,6 +24,9 @@ /// Reset to the default values. - (IBAction) resetDefaults: (id) sender; +/// Is the plugin enabled. +@property (nonatomic, readonly) BOOL pluginEnabled; + /// How many workers should work in parallel. @property (nonatomic, readonly) NSInteger prefixAnchor; diff --git a/FuzzyAutocomplete/FASettings.m b/FuzzyAutocomplete/FASettings.m index 05fc69b..dcc1191 100644 --- a/FuzzyAutocomplete/FASettings.m +++ b/FuzzyAutocomplete/FASettings.m @@ -10,11 +10,15 @@ #import "FASettings.h" #import "FATheme.h" +NSString * FASettingsPluginEnabledDidChangeNotification = @"io.github.FuzzyAutocomplete.PluginEnabledDidChange"; + // increment to show settings screen to the user static const NSUInteger kSettingsVersion = 2; @interface FASettings () +@property (nonatomic, readwrite) BOOL pluginEnabled; + @property (nonatomic, readwrite) double minimumScoreThreshold; @property (nonatomic, readwrite) BOOL filterByScore; @property (nonatomic, readwrite) BOOL sortByScore; @@ -70,7 +74,15 @@ - (void) showSettingsWindow { style.alignment = NSCenterTextAlignment; [attributed addAttribute: NSParagraphStyleAttributeName value: style range: NSMakeRange(0, attributed.length)]; label.attributedStringValue = attributed; + + BOOL enabled = self.pluginEnabled; + [NSApp runModalForWindow: window]; + + if (self.pluginEnabled != enabled) { + [[NSNotificationCenter defaultCenter] postNotificationName: FASettingsPluginEnabledDidChangeNotification object: self]; + } + [[NSUserDefaults standardUserDefaults] synchronize]; } } @@ -84,6 +96,8 @@ - (void) windowWillClose: (NSNotification *) notification { #pragma mark - defaults +static const BOOL kDefaultPluginEnabled = YES; + static const double kDefaultMinimumScoreThreshold = 0.01; static const NSInteger kDefaultPrefixAnchor = 0; static const BOOL kDefaultSortByScore = YES; @@ -101,6 +115,8 @@ - (void) windowWillClose: (NSNotification *) notification { static const double kDefaultMaxPrefixBonus = 0.5; - (IBAction)resetDefaults:(id)sender { + self.pluginEnabled = kDefaultPluginEnabled; + self.minimumScoreThreshold = kDefaultMinimumScoreThreshold; self.filterByScore = kDefaultFilterByScore; self.sortByScore = kDefaultSortByScore; @@ -134,6 +150,8 @@ - (void) loadFromDefaults { number = [defaults objectForKey: k ## Name ## Key]; \ [self setValue: number ?: @(kDefault ## Name) forKey: @#name] + loadNumber(pluginEnabled, PluginEnabled); + loadNumber(minimumScoreThreshold, MinimumScoreThreshold); loadNumber(sortByScore, SortByScore); loadNumber(filterByScore, FilterByScore); @@ -156,7 +174,9 @@ - (void) loadFromDefaults { self.scoreFormat = [defaults stringForKey: kScoreFormatKey] ?: kDefaultScoreFormat; number = [defaults objectForKey: kSettingsVersionKey]; + if (!number || [number unsignedIntegerValue] < kSettingsVersion) { + [self migrateSettingsFromVersion: [number unsignedIntegerValue]]; NSString * pluginName = [NSBundle bundleForClass: self.class].lsl_bundleName; [defaults setObject: @(kSettingsVersion) forKey: kSettingsVersionKey]; NSAlert * alert = [NSAlert alertWithMessageText: [NSString stringWithFormat: @"New settings for %@.", pluginName] @@ -173,6 +193,17 @@ - (void) loadFromDefaults { } +# pragma mark - migrate + +- (void) migrateSettingsFromVersion:(NSUInteger)version { + switch (version) { + case 0: // just break, dont migrate for 0 + break; + case 1: // dont break, fall through to higher cases + ; + } +} + # pragma mark - boilerplate // use macros to avoid some copy-paste errors @@ -194,6 +225,8 @@ - (void) set ## Name: (type) name { \ SETTINGS_KEY(SettingsVersion); +BOOL_SETTINGS_SETTER(pluginEnabled, PluginEnabled) + BOOL_SETTINGS_SETTER(showScores, ShowScores) BOOL_SETTINGS_SETTER(filterByScore, FilterByScore) BOOL_SETTINGS_SETTER(sortByScore, SortByScore) diff --git a/FuzzyAutocomplete/FASettingsWindow.xib b/FuzzyAutocomplete/FASettingsWindow.xib index 0138f21..fbcbe88 100644 --- a/FuzzyAutocomplete/FASettingsWindow.xib +++ b/FuzzyAutocomplete/FASettingsWindow.xib @@ -600,6 +600,17 @@ Prefix - maximum bonus factor for prefix matches + diff --git a/FuzzyAutocomplete/FuzzyAutocomplete.m b/FuzzyAutocomplete/FuzzyAutocomplete.m index edc164f..f98cf89 100644 --- a/FuzzyAutocomplete/FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/FuzzyAutocomplete.m @@ -12,6 +12,10 @@ #import "FuzzyAutocomplete.h" #import "FASettings.h" +#import "DVTTextCompletionSession+FuzzyAutocomplete.h" +#import "DVTTextCompletionListWindowController+FuzzyAutocomplete.h" +#import "DVTTextCompletionInlinePreviewController+FuzzyAutocomplete.h" + @implementation FuzzyAutocomplete + (void)pluginDidLoad:(NSBundle *)plugin { @@ -21,6 +25,10 @@ + (void)pluginDidLoad:(NSBundle *)plugin { if ([currentApplicationName isEqual:@"Xcode"]) { dispatch_once(&onceToken, ^{ [self createMenuItem]; + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(applicationDidFinishLaunching:) + name: NSApplicationDidFinishLaunchingNotification + object: nil]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(menuDidChange:) name: NSMenuDidChangeItemNotification @@ -29,6 +37,24 @@ + (void)pluginDidLoad:(NSBundle *)plugin { } } ++ (void) pluginEnabledOrDisabled: (NSNotification *) notification { + if (notification.object == [FASettings currentSettings]) { + [self swizzleMethods]; + } +} + ++ (void) applicationDidFinishLaunching: (NSNotification *) notification { + [[NSNotificationCenter defaultCenter] removeObserver: self name: NSApplicationDidFinishLaunchingNotification object: nil]; + [[FASettings currentSettings] loadFromDefaults]; + if ([FASettings currentSettings].pluginEnabled) { + [self swizzleMethods]; + } + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(pluginEnabledOrDisabled:) + name: FASettingsPluginEnabledDidChangeNotification + object: nil]; +} + + (void) menuDidChange: (NSNotification *) notification { [self createMenuItem]; } @@ -69,4 +95,10 @@ + (void)createMenuItem { } } ++ (void) swizzleMethods { + [DVTTextCompletionSession fa_swizzleMethods]; + [DVTTextCompletionListWindowController fa_swizzleMethods]; + [DVTTextCompletionInlinePreviewController fa_swizzleMethods]; +} + @end From 1e6007a4e95609e5f80790be89077f2e27f17383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Wed, 23 Apr 2014 17:03:56 +0200 Subject: [PATCH 09/12] Bugfix: alphabetical list was not really sorted if using multiple workers ... each worker segment was, but segments order was arbitrary also tweaked reduce phase and finding selection --- ...TTextCompletionSession+FuzzyAutocomplete.m | 84 +++++++++++-------- 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m index b6f47d1..1c92403 100644 --- a/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m +++ b/FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m @@ -353,9 +353,9 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { results.query = query; NSArray *searchSet = nil; - NSMutableArray * filteredList; - NSMutableDictionary *filteredRanges; - NSMutableDictionary *filteredScores; + NSMutableArray * filteredList = nil; + __block NSMutableDictionary * filteredRanges = nil; + __block NSMutableDictionary * filteredScores = nil; const NSInteger anchor = [FASettings currentSettings].prefixAnchor; @@ -397,19 +397,19 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { dispatch_queue_t reduceQueue = dispatch_queue_create("io.github.FuzzyAutocomplete.reduce-queue", DISPATCH_QUEUE_SERIAL); dispatch_group_t group = dispatch_group_create(); - NSMutableArray *bestMatches = [NSMutableArray array]; - filteredList = [NSMutableArray array]; - filteredRanges = [NSMutableDictionary dictionary]; - filteredScores = [NSMutableDictionary dictionary]; - + NSMutableArray * sortedItemArrays = [NSMutableArray array]; + for (NSInteger i = 0; i < workerCount; ++i) { + [sortedItemArrays addObject: @[]]; + } + for (NSInteger i = 0; i < workerCount; ++i) { dispatch_group_async(group, processingQueue, ^{ - NSArray *list; - NSDictionary *rangesMap; - NSDictionary *scoresMap; + NSMutableArray *list; + NSMutableDictionary *rangesMap; + NSMutableDictionary *scoresMap; NAMED_TIMER_START(Processing); - id bestMatch = [self _fa_bestMatchForQuery: query - inArray: [NSArray arrayWithArray: searchSet] + id goodMatch = [self _fa_bestMatchForQuery: query + inArray: searchSet offset: i total: workerCount filteredList: &list @@ -418,12 +418,18 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { NAMED_TIMER_STOP(Processing); dispatch_async(reduceQueue, ^{ NAMED_TIMER_START(Reduce); - if (bestMatch) { - [bestMatches addObject:bestMatch]; + sortedItemArrays[i] = list; + if (!filteredRanges) { + filteredRanges = rangesMap; + filteredScores = scoresMap; + bestMatch = goodMatch; + } else { + [filteredRanges addEntriesFromDictionary: rangesMap]; + [filteredScores addEntriesFromDictionary: scoresMap]; + if ([filteredScores[goodMatch.name] doubleValue] > [filteredScores[bestMatch.name] doubleValue]) { + bestMatch = goodMatch; + } } - [filteredList addObjectsFromArray:list]; - [filteredRanges addEntriesFromDictionary:rangesMap]; - [filteredScores addEntriesFromDictionary:scoresMap]; NAMED_TIMER_STOP(Reduce); }); }); @@ -432,12 +438,10 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { dispatch_group_wait(group, DISPATCH_TIME_FOREVER); dispatch_sync(reduceQueue, ^{}); - bestMatch = [self _fa_bestMatchForQuery: query - inArray: bestMatches - filteredList: nil - rangesMap: nil - scores: nil]; - + filteredList = sortedItemArrays[0]; + for (NSInteger i = 1; i < workerCount; ++i) { + [filteredList addObjectsFromArray: sortedItemArrays[i]]; + } } NAMED_TIMER_STOP(CalculateScores); @@ -474,8 +478,18 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { NAMED_TIMER_STOP(SortByScore); NAMED_TIMER_START(FindSelection); - - results.selection = filteredList.count && bestMatch ? [filteredList indexOfObject:bestMatch] : NSNotFound; + + if (!filteredList.count || !bestMatch) { + results.selection = NSNotFound; + } else { + if ([FASettings currentSettings].sortByScore) { + results.selection = 0; + } else { + results.selection = [self _fa_indexOfFirstElementInSortedRange: NSMakeRange(0, filteredList.count) inArray: filteredList passingTest: ^BOOL(id item) { + return [item.name caseInsensitiveCompare: bestMatch.name] != NSOrderedAscending; + }]; + } + } NAMED_TIMER_STOP(FindSelection); @@ -489,9 +503,9 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { // Score the items, store filtered list, matched ranges, scores and the best match. - (id) _fa_bestMatchForQuery: (NSString *) query inArray: (NSArray *) array - filteredList: (NSArray **) filtered - rangesMap: (NSDictionary **) ranges - scores: (NSDictionary **) scores + filteredList: (NSMutableArray **) filtered + rangesMap: (NSMutableDictionary **) ranges + scores: (NSMutableDictionary **) scores { return [self _fa_bestMatchForQuery:query inArray:array offset:0 total:1 filteredList:filtered rangesMap:ranges scores:scores]; } @@ -501,14 +515,14 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query { inArray: (NSArray *) array offset: (NSUInteger) offset total: (NSUInteger) total - filteredList: (NSArray **) filtered - rangesMap: (NSDictionary **) ranges - scores: (NSDictionary **) scores + filteredList: (NSMutableArray **) filtered + rangesMap: (NSMutableDictionary **) ranges + scores: (NSMutableDictionary **) scores { IDEOpenQuicklyPattern *pattern = [[IDEOpenQuicklyPattern alloc] initWithPattern:query]; - NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count] : nil; - NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count] : nil; - NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count] : nil; + NSMutableArray *filteredList = filtered ? [NSMutableArray arrayWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredRanges = ranges ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; + NSMutableDictionary *filteredScores = scores ? [NSMutableDictionary dictionaryWithCapacity: array.count / total] : nil; double highScore = 0.0f; id bestMatch; From 7fda923b522306111ec0adae63b63672f2dfad4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leszek=20S=CC=81laz=CC=87yn=CC=81ski?= Date: Wed, 23 Apr 2014 17:04:40 +0200 Subject: [PATCH 10/12] Don't show all the options when plugin is disabled. also fix some misaligned text --- FuzzyAutocomplete/FASettingsWindow.xib | 27 ++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/FuzzyAutocomplete/FASettingsWindow.xib b/FuzzyAutocomplete/FASettingsWindow.xib index fbcbe88..447b118 100644 --- a/FuzzyAutocomplete/FASettingsWindow.xib +++ b/FuzzyAutocomplete/FASettingsWindow.xib @@ -1,8 +1,8 @@ - + - + @@ -432,11 +432,11 @@ Especially useful for adjusting the threshold and debugging. - + - If enabled the completion list header will contain time it took to score the completion items. Useful mostly for profiling and debugging. + If enabled, the completion list header will contain time it took to score the completion items. Useful mostly for profiling and debugging. @@ -577,7 +577,26 @@ Prefix - maximum bonus factor for prefix matches + + + + NSNegateBoolean + + + + + + + + + + + + + + +