Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<DVTTextCompletionItem> item = self.session.filteredCompletionsAlpha[rowIndex];
return [self.session fa_scoreForItem: item];
} else {
Expand Down
155 changes: 105 additions & 50 deletions FuzzyAutocomplete/DVTTextCompletionSession+FuzzyAutocomplete.m
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#import "FAItemScoringMethod.h"
#import <objc/runtime.h>

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

Choose a reason for hiding this comment

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

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't have any dispatch_sync calling _fa_performFuzzyFiltering from a background thread which is the only case where it would cause a deadlock, but I can work around it so we don't call dispatch_get_current_queue() cause it's deprecated


#define MIN_CHUNK_LENGTH 100
/// A simple helper class to avoid using a dictionary in resultsStack
@interface FAFilteringResults : NSObject
Expand Down Expand Up @@ -330,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;

Expand All @@ -353,19 +355,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_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;
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
Expand All @@ -390,7 +405,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];

Expand All @@ -413,7 +447,12 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil
results = [self _fa_calculateResultsForQuery: prefix];
[resultsStack addObject: results];
}


// 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
Expand All @@ -423,45 +462,57 @@ - (void)_fa_setFilteringPrefix: (NSString *) prefix forceFilter: (BOOL) forceFil
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];
}
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];
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]) {
BOOL shownExplicitly = [[self valueForKey:@"_shownExplicitly"] boolValue];
if ([self.listWindowController showingWindow] && !shownExplicitly) {
[self.listWindowController hideWindowWithReason: 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];
}
}

if (![FASettings currentSettings].showInlinePreview) {
[self._inlinePreviewController hideInlinePreviewWithReason: 0x0];
}
} @catch (NSException *exception) {
RLog(@"Caught an Exception when showing completions: %@", exception);
}
}

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

} @catch (NSException *exception) {
RLog(@"Caught an Exception %@", exception);
Expand Down Expand Up @@ -696,7 +747,7 @@ - (FAFilteringResults *)_fa_calculateResultsForQuery: (NSString *) query {
id<DVTTextCompletionItem> bestMatch;

FAItemScoringMethod * method = self._fa_currentScoringMethod;

double normalization = [method normalizationFactorForSearchString: query];

id<DVTTextCompletionItem> item;
Expand All @@ -710,6 +761,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;
Expand Down
5 changes: 5 additions & 0 deletions FuzzyAutocomplete/FASettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions FuzzyAutocomplete/FASettings.m
Original file line number Diff line number Diff line change
Expand Up @@ -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.1;

- (IBAction)resetDefaults:(id)sender {
self.pluginEnabled = kDefaultPluginEnabled;
Expand All @@ -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;
Expand Down Expand Up @@ -193,6 +197,8 @@ - (void) loadFromDefaults {
loadNumber(correctLetterCaseBestMatchOnly, CorrectLetterCaseBestMatchOnly);
loadNumber(correctWordOrder, CorrectWordOrder);
loadNumber(correctWordOrderAfter, CorrectWordOrderAfter);
loadNumber(nonblockingMode, NonblockingMode);
loadNumber(filterDelay, FilterDelay);

#undef loadNumber

Expand Down Expand Up @@ -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)
Expand All @@ -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)

Expand Down
Loading