diff options
author | Bibiko <bibiko@eva.mpg.de> | 2010-01-19 21:33:40 +0000 |
---|---|---|
committer | Bibiko <bibiko@eva.mpg.de> | 2010-01-19 21:33:40 +0000 |
commit | 4be3b84999e8ea31d47f3ec6b6926d1f13ec2ccf (patch) | |
tree | 15f6bf1c855761c5cbf4b3c5c8454e30838553ef | |
parent | 9e3181e71ebcb13c87843ed90f1df6798a562b94 (diff) | |
download | sequelpro-4be3b84999e8ea31d47f3ec6b6926d1f13ec2ccf.tar.gz sequelpro-4be3b84999e8ea31d47f3ec6b6926d1f13ec2ccf.tar.bz2 sequelpro-4be3b84999e8ea31d47f3ec6b6926d1f13ec2ccf.zip |
• improved completion for Query Editor bound to ESC
- up to now for MySQL >4 available
- implemented by narrow-down, i.e. write/delete to narrow-down/expand the suggestion list
- ↩ inserts the suggestion, if suggestion is db/table/field/proc/func name inserts backtick quoted
- for a db/table/field/proc/func name press ⇧↩ to insert à la `db`.`table`.`field` relatively to the current selected db
- context-sensitive to leading db. or table. or db.table. or .table if uniquely identifiable
- mysql and information_schema if available and not selected appear at the end
- `|` | := caret shows the current table's fields if any or the current selected db if selected at the top; suggestions are hierarchically arranged by db, table; easiest way to insert a table/field name - e.g. type `max_c` and press ⇧↩ to insert `mysql`.`user`.`max_connections`
- for table/field name suggestions press at the right column to get the path info
• F5 invokes completion based on spell checker and selected language
- since it is not possible to auto-detect if the user wants to complete MySQL statements or prose text
Note: GUI needs improvements; completion should be tested exhaustively
-rw-r--r-- | Source/CMTextView.h | 4 | ||||
-rw-r--r-- | Source/CMTextView.m | 301 | ||||
-rw-r--r-- | Source/SPNarrowDownCompletion.h | 5 | ||||
-rw-r--r-- | Source/SPNarrowDownCompletion.m | 284 |
4 files changed, 336 insertions, 258 deletions
diff --git a/Source/CMTextView.h b/Source/CMTextView.h index cfabd3ef..80515928 100644 --- a/Source/CMTextView.h +++ b/Source/CMTextView.h @@ -69,7 +69,7 @@ static inline void NSMutableAttributedStringAddAttributeValueRange (NSMutableAtt - (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix; - (BOOL) shiftSelectionRight; - (BOOL) shiftSelectionLeft; -- (NSArray *) completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index; +// - (NSArray *) completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index; - (NSArray *) keywords; - (NSArray *) functions; - (void) setAutoindent:(BOOL)enableAutoindent; @@ -87,7 +87,7 @@ static inline void NSMutableAttributedStringAddAttributeValueRange (NSMutableAtt - (void) autoHelp; - (void) doSyntaxHighlighting; - (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion; -- (void) doCompletion; +- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode; - (NSArray *)suggestionsForSQLCompletionWith:(NSString *)currentWord dictMode:(BOOL)isDictMode browseMode:(BOOL)dbBrowseMode withTableName:(NSString*)aTableName withDbName:(NSString*)aDbName; - (void) selectCurrentQuery; diff --git a/Source/CMTextView.m b/Source/CMTextView.m index c3b27987..3b0cab17 100644 --- a/Source/CMTextView.m +++ b/Source/CMTextView.m @@ -166,18 +166,13 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) NSMutableArray *possibleCompletions = [[NSMutableArray alloc] initWithCapacity:32]; - if(isDictMode) { - for (id w in [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,[currentWord length]) inString:currentWord language:nil inSpellDocumentWithTag:0]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; - } - // If caret is not inside backticks add keywords and all words coming from the view. - if([[self string] length] && !dbBrowseMode) + if(!dbBrowseMode) { // Only parse for words if text size is less than 6MB - if([[self string] length]<6000000) + if([[self string] length] && [[self string] length]<6000000) { - NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()[]{}\"'`-!;=+|?:~@"]; + NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()[]{}\"'`-!;=+|?:~@."]; NSMutableArray *uniqueArray = [NSMutableArray array]; [uniqueArray addObjectsFromArray:[[NSSet setWithArray:[[self string] componentsSeparatedByCharactersInSet:separators]] allObjects]]; @@ -222,28 +217,41 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"tableName"] != nil) currentTable = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"tableName"]; + // Put current selected db at the top if(aTableName == nil && aDbName == nil && [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]) { - // Put current selected db at the top currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]; [sortedDbs removeObject:currentDb]; [sortedDbs insertObject:currentDb atIndex:0]; } - // Put information_schema and mysql db at the end if not chosen - if(currentDb && ![currentDb isEqualToString:@"mysql"]) { + + // Put information_schema and/or mysql db at the end if not selected + if(currentDb && ![currentDb isEqualToString:@"mysql"] && [sortedDbs containsObject:@"mysql"]) { [sortedDbs removeObject:@"mysql"]; [sortedDbs addObject:@"mysql"]; } - if(currentDb && ![currentDb isEqualToString:@"information_schema"]) { + if(currentDb && ![currentDb isEqualToString:@"information_schema"] && [sortedDbs containsObject:@"information_schema"]) { [sortedDbs removeObject:@"information_schema"]; [sortedDbs addObject:@"information_schema"]; } BOOL aTableNameExists = NO; if(!aDbName) { - if(aTableName && [aTableName length] && [dbs objectForKey:currentDb] && [[dbs objectForKey:currentDb] objectForKey:aTableName]) { + // Try to suggest only items which are uniquely valid for the parsed string + + NSInteger uniqueSchemaKind = [mySQLConnection getUniqueDbIndentifierFor:[aTableName lowercaseString]]; + + // If no db name but table name check if table name is a valid name in the current selected db + if(aTableName && [aTableName length] && [dbs objectForKey:currentDb] && [[dbs objectForKey:currentDb] objectForKey:aTableName] && uniqueSchemaKind == 2) { aTableNameExists = YES; aDbName = [NSString stringWithString:currentDb]; } + + // If no db name but table name check if table name is a valid db name + if(!aTableNameExists && aTableName && [aTableName length] && uniqueSchemaKind == 1) { + aDbName = [NSString stringWithString:aTableName]; + aTableNameExists = NO; + } + } else if (aDbName && [aDbName length]) { if(aTableName && [aTableName length] && [dbs objectForKey:aDbName] && [[dbs objectForKey:aDbName] objectForKey:aTableName]) { aTableNameExists = YES; @@ -264,12 +272,10 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } else { [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:db, @"display", @"database-small", @"image", @"", @"isRef", nil]]; [sortedTables addObjectsFromArray:[allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; - // if(aDbName == nil && aTableName) { - if([sortedTables count] > 1 && [sortedTables containsObject:currentTable]) { - [sortedTables removeObject:currentTable]; - [sortedTables insertObject:currentTable atIndex:0]; - } - // } + if([sortedTables count] > 1 && [sortedTables containsObject:currentTable]) { + [sortedTables removeObject:currentTable]; + [sortedTables insertObject:currentTable atIndex:0]; + } } for(id table in sortedTables) { NSDictionary * theTable = [[dbs objectForKey:db] objectForKey:table]; @@ -285,11 +291,11 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-view-small-square", @"image", db, @"path", @"", @"isRef", nil]]; break; case 2: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"proc-small", @"image", db, @"path", @"", @"isRef", nil]]; + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"proc-small", @"image", db, @"path", nil]]; breakFlag = YES; break; case 3: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"func-small", @"image", db, @"path", @"", @"isRef", nil]]; + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"func-small", @"image", db, @"path", nil]]; breakFlag = YES; break; } @@ -306,7 +312,7 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } else { // Fallback for MySQL < 5 and if the data gathering is in progress if(mySQLmajorVersion > 4) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"fetching table data…", @"fetching table data for completion in progress message"), @"path", nil]]; + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"fetching table data…", @"fetching table data for completion in progress message"), @"path", @"", @"noCompletion", nil]]; // Add all database names to completions list for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"]) @@ -354,7 +360,7 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } -- (void)doCompletion +- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode { // No completion for a selection (yet?) and if caret positiopn == 0 @@ -367,26 +373,28 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) NSRange parseRange = completionRange; NSString* currentWord = [[self string] substringWithRange:completionRange]; NSString* prefix = @""; - NSString* allow = @"_. "; // additional chars which not close the popup NSString *currentDb = nil; + NSString* allow; // additional chars which not close the popup + if(isDictMode) + allow= @"_"; + else + allow= @"_. "; + + BOOL dbBrowseMode = NO; NSInteger backtickMode = 0; // 0 none, 1 rigth only, 2 left only, 3 both BOOL caseInsensitive = YES; - currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]; - if(!currentDb) currentDb = @""; - - // Check if the caret is inside quotes "" or ''; if so - // return the normal word suggestion due to the spelling's settings - // plus all unique words used in the textView - BOOL isDictMode = NO; - if(completionRange.length) - isDictMode = ([[[self textStorage] attribute:kQuote atIndex:completionRange.location effectiveRange:nil] isEqualToString:kQuoteValue] ); - - if(!isDictMode) { + // Parse for leading db.table.field infos + + if([[[self window] delegate] isKindOfClass:[TableDocument class]] && [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]) + currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]; + else + currentDb = @""; + NSInteger caretPos = [self selectedRange].location; BOOL caretIsInsideBackticks = NO; @@ -401,11 +409,15 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) NSCharacterSet *whiteSpaceCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; NSInteger start = caretPos; NSInteger backticksCounter = (caretIsInsideBackticks) ? 1 : 0; - NSInteger pointCounter = 0; - NSInteger firstPoint = 0; - NSInteger secondPoint = 0; - BOOL doParsing = YES; + NSInteger pointCounter = 0; + NSInteger firstPoint = 0; + NSInteger secondPoint = 0; + BOOL rightBacktick = NO; + BOOL leftBacktick = NO; + BOOL doParsing = YES; + unichar currentCharacter; + while(start > 0 && doParsing) { currentCharacter = [[self string] characterAtIndex:--start]; if(!(backticksCounter%2) && [whiteSpaceCharSet characterIsMember:currentCharacter]) { @@ -446,11 +458,15 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) NSString *parsedString = [[self string] substringWithRange:parseRange]; // Check if parsed string is wrapped by `` - if([parsedString hasPrefix:@"`"]) backtickMode+=1; + if([parsedString hasPrefix:@"`"]) { + backtickMode+=1; + leftBacktick = YES; + } if([[self string] length] > parseRange.location+parseRange.length) { if([[self string] characterAtIndex:parseRange.location+parseRange.length] == '`') { backtickMode+=2; parseRange.length++; // adjust parse string for right ` + rightBacktick = YES; } } @@ -468,8 +484,27 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) } else { filter = [[parsedString stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; } - if(![filter length]) - completionRange = NSMakeRange(parseRange.location+parseRange.length,0); + + // Adjust completion range + if(firstPoint>0) { + completionRange = NSMakeRange(firstPoint+1+start,[parsedString length]-firstPoint-1); + } + else if([filter length] && leftBacktick) { + completionRange = NSMakeRange(completionRange.location-1,completionRange.length+1); + } + if(rightBacktick) + completionRange.length++; + + // Check leading . since .tableName == <currentDB>.tableName etc. + if([filter hasPrefix:@".`"]) { + filter = [filter substringFromIndex:2]; + completionRange = NSMakeRange(completionRange.location-1,completionRange.length+1); + } else if([filter hasPrefix:@"."]) { + filter = [filter substringFromIndex:1]; + } else if([tableName hasPrefix:@".`"]) { + tableName = [tableName substringFromIndex:2]; + } + } else { filter = [NSString stringWithString:currentWord]; } @@ -493,12 +528,11 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) selectedDb:currentDb]; //Get the NSPoint of the first character of the current word - NSRange range = NSMakeRange(completionRange.location,0); - NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:range actualCharacterRange:NULL]; + NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(completionRange.location,1) actualCharacterRange:NULL]; NSRect boundingRect = [[self layoutManager] boundingRectForGlyphRange:glyphRange inTextContainer:[self textContainer]]; boundingRect = [self convertRect: boundingRect toView: NULL]; NSPoint pos = [[self window] convertBaseToScreen: NSMakePoint(boundingRect.origin.x + boundingRect.size.width,boundingRect.origin.y + boundingRect.size.height)]; - + // TODO: check if needed // if(filter) // pos.x -= [filter sizeWithAttributes:[NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]].width; @@ -737,14 +771,14 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) long curFlags = ([theEvent modifierFlags] & allFlags); if ([theEvent keyCode] == 53){ // ESC key for internal completion - [super keyDown: theEvent]; + [self doCompletionByUsingSpellChecker:NO]; // Remove that attribute to suppress auto-uppercasing of certain keyword combinations if(![self selectedRange].length && [self selectedRange].location) - [[self textStorage] removeAttribute:kSQLkeyword range:NSMakeRange([self selectedRange].location-1,1)]; + [[self textStorage] removeAttribute:kSQLkeyword range:[self getRangeForCurrentWord]]; return; } - if (insertedCharacter == NSF5FunctionKey){ // F5 for cocoa completion - [self doCompletion]; + if (insertedCharacter == NSF5FunctionKey){ // F5 for completion based on spell checker + [self doCompletionByUsingSpellChecker:YES]; // Remove that attribute to suppress auto-uppercasing of certain keyword combinations if(![self selectedRange].length && [self selectedRange].location) [[self textStorage] removeAttribute:kSQLkeyword range:[self getRangeForCurrentWord]]; @@ -1111,90 +1145,90 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) /* * Handle autocompletion, returning a list of suggested completions for the supplied character range. */ -- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index -{ - - if (!charRange.length) return nil; - - // Refresh quote attributes - [[self textStorage] removeAttribute:kQuote range:NSMakeRange(0,[[self string] length])]; - [self insertText:@""]; - - - // Check if the caret is inside quotes "" or ''; if so - // return the normal word suggestion due to the spelling's settings - if([[[self textStorage] attribute:kQuote atIndex:charRange.location effectiveRange:nil] isEqualToString:kQuoteValue] ) - return [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,charRange.length) inString:[[self string] substringWithRange:charRange] language:nil inSpellDocumentWithTag:0]; - - - NSMutableArray *compl = [[NSMutableArray alloc] initWithCapacity:32]; - NSMutableArray *possibleCompletions = [[NSMutableArray alloc] initWithCapacity:32]; - - NSString *partialString = [[self string] substringWithRange:charRange]; - NSUInteger partialLength = [partialString length]; - - NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@ AND length > %lu", partialString, (unsigned long)partialLength]; - NSArray *matchingCompletions; - - NSUInteger i, insindex; - insindex = 0; - - - if([mySQLConnection isConnected]) - { - - // Add all database names to completions list - [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"]]; - - // Add table names to completions list - [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allTableAndViewNames"]]; - - // Add field names to completions list for currently selected table - if ([[[self window] delegate] table] != nil) - [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"]]; - - // Add proc/func only for MySQL version 5 or higher - if(mySQLmajorVersion > 4) { - [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allProcedureNames"]]; - [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allFunctionNames"]]; - } - - } - // If caret is not inside backticks add keywords and all words coming from the view. - if(![[[self textStorage] attribute:kBTQuote atIndex:charRange.location effectiveRange:nil] isEqualToString:kBTQuoteValue] ) - { - // Only parse for words if text size is less than 6MB - if([[self string] length]<6000000) - { - NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()[]{}\"'`-!;=+|?:~@"]; - NSMutableArray *uniqueArray = [NSMutableArray array]; - [uniqueArray addObjectsFromArray:[[NSSet setWithArray:[[self string] componentsSeparatedByCharactersInSet:separators]] allObjects]]; - [possibleCompletions addObjectsFromArray:uniqueArray]; - } - - [possibleCompletions addObjectsFromArray:[self keywords]]; - [possibleCompletions addObjectsFromArray:[self functions]]; - } - - // Check for possible completions - matchingCompletions = [[possibleCompletions filteredArrayUsingPredicate:predicate] sortedArrayUsingSelector:@selector(compare:)]; - - for (i = 0; i < [matchingCompletions count]; i++) - { - NSString* obj = NSArrayObjectAtIndex(matchingCompletions, i); - if(![compl containsObject:obj]) - if ([partialString isEqualToString:[obj substringToIndex:partialLength]]) - // Matches case --> Insert at beginning of completion list - [compl insertObject:obj atIndex:insindex++]; - else - // Not matching case --> Insert at end of completion list - [compl addObject:obj]; - } - - [possibleCompletions release]; - - return [compl autorelease]; -} +// - (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(NSInteger *)index +// { +// +// if (!charRange.length) return nil; +// +// // Refresh quote attributes +// [[self textStorage] removeAttribute:kQuote range:NSMakeRange(0,[[self string] length])]; +// [self insertText:@""]; +// +// +// // Check if the caret is inside quotes "" or ''; if so +// // return the normal word suggestion due to the spelling's settings +// if([[[self textStorage] attribute:kQuote atIndex:charRange.location effectiveRange:nil] isEqualToString:kQuoteValue] ) +// return [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,charRange.length) inString:[[self string] substringWithRange:charRange] language:nil inSpellDocumentWithTag:0]; +// +// +// NSMutableArray *compl = [[NSMutableArray alloc] initWithCapacity:32]; +// NSMutableArray *possibleCompletions = [[NSMutableArray alloc] initWithCapacity:32]; +// +// NSString *partialString = [[self string] substringWithRange:charRange]; +// NSUInteger partialLength = [partialString length]; +// +// NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@ AND length > %lu", partialString, (unsigned long)partialLength]; +// NSArray *matchingCompletions; +// +// NSUInteger i, insindex; +// insindex = 0; +// +// +// if([mySQLConnection isConnected]) +// { +// +// // Add all database names to completions list +// [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"]]; +// +// // Add table names to completions list +// [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allTableAndViewNames"]]; +// +// // Add field names to completions list for currently selected table +// if ([[[self window] delegate] table] != nil) +// [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"]]; +// +// // Add proc/func only for MySQL version 5 or higher +// if(mySQLmajorVersion > 4) { +// [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allProcedureNames"]]; +// [possibleCompletions addObjectsFromArray:[[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allFunctionNames"]]; +// } +// +// } +// // If caret is not inside backticks add keywords and all words coming from the view. +// if(![[[self textStorage] attribute:kBTQuote atIndex:charRange.location effectiveRange:nil] isEqualToString:kBTQuoteValue] ) +// { +// // Only parse for words if text size is less than 6MB +// if([[self string] length]<6000000) +// { +// NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()[]{}\"'`-!;=+|?:~@"]; +// NSMutableArray *uniqueArray = [NSMutableArray array]; +// [uniqueArray addObjectsFromArray:[[NSSet setWithArray:[[self string] componentsSeparatedByCharactersInSet:separators]] allObjects]]; +// [possibleCompletions addObjectsFromArray:uniqueArray]; +// } +// +// [possibleCompletions addObjectsFromArray:[self keywords]]; +// [possibleCompletions addObjectsFromArray:[self functions]]; +// } +// +// // Check for possible completions +// matchingCompletions = [[possibleCompletions filteredArrayUsingPredicate:predicate] sortedArrayUsingSelector:@selector(compare:)]; +// +// for (i = 0; i < [matchingCompletions count]; i++) +// { +// NSString* obj = NSArrayObjectAtIndex(matchingCompletions, i); +// if(![compl containsObject:obj]) +// if ([partialString isEqualToString:[obj substringToIndex:partialLength]]) +// // Matches case --> Insert at beginning of completion list +// [compl insertObject:obj atIndex:insindex++]; +// else +// // Not matching case --> Insert at end of completion list +// [compl addObject:obj]; +// } +// +// [possibleCompletions release]; +// +// return [compl autorelease]; +// } /* @@ -1579,6 +1613,7 @@ NSInteger alphabeticSort(id string1, id string2, void *reverse) @"OPTIONS", @"OR", @"ORDER", + @"ORDER BY", @"OUT", @"OUTER", @"OUTFILE", diff --git a/Source/SPNarrowDownCompletion.h b/Source/SPNarrowDownCompletion.h index 88e76567..d442b00f 100644 --- a/Source/SPNarrowDownCompletion.h +++ b/Source/SPNarrowDownCompletion.h @@ -44,13 +44,16 @@ BOOL caseSensitive; BOOL dictMode; BOOL dbStructureMode; + BOOL noFilterString; NSInteger backtickMode; NSFont *tableFont; NSRange theCharRange; NSRange theParseRange; - NSArray *words; + NSString *theDbName; id theView; + NSInteger maxWindowWidth; + NSMutableCharacterSet* textualInputCharacters; } diff --git a/Source/SPNarrowDownCompletion.m b/Source/SPNarrowDownCompletion.m index 82564567..dbc03b38 100644 --- a/Source/SPNarrowDownCompletion.m +++ b/Source/SPNarrowDownCompletion.m @@ -99,13 +99,16 @@ // ============================= - (id)init { - if(self = [super initWithContentRect:NSMakeRect(0,0,450,0) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]) + + maxWindowWidth = 450; + + if(self = [super initWithContentRect:NSMakeRect(0,0,maxWindowWidth,0) styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO]) { mutablePrefix = [NSMutableString new]; textualInputCharacters = [[NSMutableCharacterSet alphanumericCharacterSet] retain]; caseSensitive = YES; filtered = nil; - + tableFont = [NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] dataForKey:SPCustomQueryEditorFont]]; [self setupInterface]; } @@ -134,10 +137,8 @@ if(self = [self init]) { - BOOL filterStringIsBacktick = ([aUserString isEqualToString:@"`"]) ? YES : NO; - - // Set filter string - if aUserString == ` user invoked it via `|` ie show all db/tables/fields etc. - if(aUserString && !filterStringIsBacktick) + // Set filter string + if(aUserString) [mutablePrefix appendString:aUserString]; dbStructureMode = theDbMode; @@ -150,24 +151,22 @@ caseSensitive = isCaseSensitive; theCharRange = initRange; - - if(filterStringIsBacktick) { - theCharRange.length = 0; - theCharRange.location++; - } + noFilterString = ([aUserString length]) ? NO : YES; theParseRange = parseRange; theView = aView; dictMode = mode; - if(!dictMode) { - suggestions = [someSuggestions retain]; - words = nil; - } + suggestions = [someSuggestions retain]; + + [[theTableView tableColumnWithIdentifier:@"image"] setWidth:((dictMode) ? 0 : 20)]; + [[theTableView tableColumnWithIdentifier:@"name"] setWidth:((dictMode) ? 440 : 180)]; currentDb = selectedDb; + theDbName = dbName; + if(someAdditionalWordCharacters) [textualInputCharacters addCharactersInString:someAdditionalWordCharacters]; @@ -178,24 +177,25 @@ - (void)setCaretPos:(NSPoint)aPos { caretPos = aPos; - isAbove = NO; - + NSRect mainScreen = [self rectOfMainScreen]; - + NSInteger offx = (caretPos.x/mainScreen.size.width) + 1; + if((caretPos.x + [self frame].size.width) > (mainScreen.size.width*offx)) - caretPos.x = caretPos.x - [self frame].size.width; - - if(caretPos.y>=0 && caretPos.y<[self frame].size.height) + caretPos.x = (mainScreen.size.width*offx) - [self frame].size.width - 5; + + if(caretPos.y >= 0 && caretPos.y < [self frame].size.height) { - caretPos.y = caretPos.y + ([self frame].size.height + [tableFont pointSize]*1.5); + caretPos.y += [self frame].size.height + ([tableFont pointSize]*1.5); isAbove = YES; } - if(caretPos.y<0 && (mainScreen.size.height-[self frame].size.height)<(caretPos.y*-1)) + if(caretPos.y < 0 && (mainScreen.size.height-[self frame].size.height) < (caretPos.y*-1)) { - caretPos.y = caretPos.y + ([self frame].size.height + [tableFont pointSize]*1.5); + caretPos.y += [self frame].size.height + ([tableFont pointSize]*1.5); isAbove = YES; } + [self setFrameTopLeftPoint:caretPos]; } @@ -208,7 +208,7 @@ [self setAlphaValue:0.9]; NSScrollView* scrollView = [[[NSScrollView alloc] initWithFrame:NSZeroRect] autorelease]; - // [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; [scrollView setAutohidesScrollers:YES]; [scrollView setHasVerticalScroller:YES]; [scrollView setHasHorizontalScroller:NO]; @@ -219,30 +219,31 @@ [theTableView setFocusRingType:NSFocusRingTypeNone]; [theTableView setAllowsEmptySelection:NO]; [theTableView setHeaderView:nil]; - // [theTableView setSelectionHighlightStyle:NSTableViewSelectionHighlightStyleSourceList]; + [theTableView setDelegate:self]; NSTableColumn *column0 = [[[NSTableColumn alloc] initWithIdentifier:@"image"] autorelease]; [column0 setDataCell:[[ImageAndTextCell new] autorelease]]; [column0 setEditable:NO]; [theTableView addTableColumn:column0]; + [column0 setMinWidth:0]; [column0 setWidth:20]; + NSTableColumn *column1 = [[[NSTableColumn alloc] initWithIdentifier:@"name"] autorelease]; [column1 setEditable:NO]; - // [[column1 dataCell] setFont:[NSFont systemFontOfSize:12]]; [theTableView addTableColumn:column1]; - [column1 setWidth:180]; + [column1 setWidth:170]; + NSTableColumn *column2 = [[[NSTableColumn alloc] initWithIdentifier:@"type"] autorelease]; [column2 setEditable:NO]; - // [[column2 dataCell] setFont:[NSFont systemFontOfSize:11]]; [[column2 dataCell] setTextColor:[NSColor darkGrayColor]]; [theTableView addTableColumn:column2]; - [column2 setWidth:120]; + [column2 setWidth:145]; + NSTableColumn *column3 = [[[NSTableColumn alloc] initWithIdentifier:@"path"] autorelease]; [column3 setEditable:NO]; - // [[column3 dataCell] setFont:[NSFont systemFontOfSize:11]]; [[column3 dataCell] setTextColor:[NSColor darkGrayColor]]; [theTableView addTableColumn:column3]; - [column3 setWidth:130]; + [column3 setWidth:95]; [theTableView setDataSource:self]; [scrollView setDocumentView:theTableView]; @@ -258,10 +259,43 @@ return [filtered count]; } +// ------- nstokenfield delegates -- does not work for menus due to the click event does not reach it -- why??? +// - (NSMenu *)tokenFieldCell:(NSTokenFieldCell *)tokenFieldCell menuForRepresentedObject:(id)representedObject +// { +// NSMenu *tokenMenu = [[[NSMenu alloc] init] autorelease]; +// +// if (!representedObject) +// return nil; +// +// NSMenuItem *artistItem = [[[NSMenuItem alloc] init] autorelease]; +// [artistItem setTitle:@"aaa"]; +// [tokenMenu addItem:artistItem]; +// +// NSMenuItem *albumItem = [[[NSMenuItem alloc] init] autorelease]; +// [albumItem setTitle:@"ksajdhkjas"]; +// [tokenMenu addItem:albumItem]; +// +// +// // NSMenuItem *mItem = [[[NSMenuItem alloc] initWithTitle:@"Show Album Art" action:@selector(showAlbumArt:) keyEquivalent:@""] autorelease]; +// // [mItem setTarget:self]; +// // [mItem setRepresentedObject:representedObject]; +// // [tokenMenu addItem:mItem]; +// +// return tokenMenu; +// } +// - (BOOL)tokenFieldCell:(NSTokenFieldCell *)tokenFieldCell hasMenuForRepresentedObject:(id)representedObject +// { +// return YES; +// } +// - (NSString *)tokenFieldCell:(NSTokenFieldCell *)tokenFieldCell displayStringForRepresentedObject:(id)representedObject +// { +// return representedObject; +// } - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex { NSImage* image = nil; NSString* imageName = nil; + if([[aTableColumn identifier] isEqualToString:@"image"]) { if(!dictMode) { imageName = [[filtered objectAtIndex:rowIndex] objectForKey:@"image"]; @@ -270,21 +304,48 @@ [[aTableColumn dataCell] setImage:image]; } return @""; + } else if([[aTableColumn identifier] isEqualToString:@"name"]) { - return (dictMode) ? [filtered objectAtIndex:rowIndex] : [[filtered objectAtIndex:rowIndex] objectForKey:@"display"]; + return [[filtered objectAtIndex:rowIndex] objectForKey:@"display"]; + } else if([[aTableColumn identifier] isEqualToString:@"type"]) { if(dictMode) { return @""; } else { - [[aTableColumn dataCell] setTextColor:([aTableView selectedRow] == rowIndex)?[NSColor whiteColor]:[NSColor darkGrayColor]]; - return ([[filtered objectAtIndex:rowIndex] objectForKey:@"type"]) ? [[filtered objectAtIndex:rowIndex] objectForKey:@"type"] : @""; + // [[aTableColumn dataCell] setTextColor:([aTableView selectedRow] == rowIndex)?[NSColor whiteColor]:[NSColor darkGrayColor]]; + // return ([[filtered objectAtIndex:rowIndex] objectForKey:@"type"]) ? [[filtered objectAtIndex:rowIndex] objectForKey:@"type"] : @""; + NSTokenFieldCell *b = [[[NSTokenFieldCell alloc] initTextCell:([[filtered objectAtIndex:rowIndex] objectForKey:@"type"]) ? [[filtered objectAtIndex:rowIndex] objectForKey:@"type"] : @""] autorelease]; + [b setEditable:NO]; + [b setFont:[NSFont systemFontOfSize:11]]; + [b setDelegate:self]; + return b; } + } else if ([[aTableColumn identifier] isEqualToString:@"path"]) { if(dictMode) { return @""; } else { - [[aTableColumn dataCell] setTextColor:([aTableView selectedRow] == rowIndex)?[NSColor whiteColor]:[NSColor darkGrayColor]]; - return ([[filtered objectAtIndex:rowIndex] objectForKey:@"path"]) ? [[filtered objectAtIndex:rowIndex] objectForKey:@"path"] : @""; + // [[aTableColumn dataCell] setTextColor:([aTableView selectedRow] == rowIndex)?[NSColor whiteColor]:[NSColor darkGrayColor]]; + // return ([[filtered objectAtIndex:rowIndex] objectForKey:@"path"]) ? [[filtered objectAtIndex:rowIndex] objectForKey:@"path"] : @""; + if([[filtered objectAtIndex:rowIndex] objectForKey:@"path"]) { + NSPopUpButtonCell *b = [[NSPopUpButtonCell new] autorelease]; + [b setPullsDown:NO]; + [b setAltersStateOfSelectedItem:NO]; + [b setControlSize:NSMiniControlSize]; + NSMenu *m = [[NSMenu alloc] init]; + for(id p in [[[filtered objectAtIndex:rowIndex] objectForKey:@"path"] componentsSeparatedByString:@"⇠"]) + [m addItemWithTitle:p action:NULL keyEquivalent:@""]; + [b setMenu:m]; + [m release]; + [b setPreferredEdge:NSMinXEdge]; + [b setArrowPosition:([m numberOfItems]>1) ? NSPopUpArrowAtCenter : NSPopUpNoArrow]; + [b setFont:[NSFont systemFontOfSize:11]]; + [b setBordered:NO]; + [aTableColumn setDataCell:b]; + } else { + [aTableColumn setDataCell:[[NSTextFieldCell new] autorelease]]; + } + return @""; } } return [filtered objectAtIndex:rowIndex]; @@ -295,77 +356,49 @@ // ==================== - (void)filter { - // NSRect mainScreen = [self rectOfMainScreen]; - NSArray* newFiltered; + NSMutableArray* newFiltered = [[NSMutableArray alloc] initWithCapacity:5]; if([mutablePrefix length] > 0) { - if(dictMode) { - newFiltered = [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,[[self filterString] length]) inString:[self filterString] language:nil inSpellDocumentWithTag:0]; - } else { - NSPredicate* predicate; - if(caseSensitive) - predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH %@ OR (match == NULL AND display BEGINSWITH %@)", [self filterString], [self filterString]]; - else - predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH[c] %@ OR (match == NULL AND display BEGINSWITH[c] %@)", [self filterString], [self filterString]]; - newFiltered = [suggestions filteredArrayUsingPredicate:predicate]; - } + NSPredicate* predicate; + if(caseSensitive) + predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH %@ OR (match == NULL AND display BEGINSWITH %@)", [self filterString], [self filterString]]; + else + predicate = [NSPredicate predicateWithFormat:@"match BEGINSWITH[c] %@ OR (match == NULL AND display BEGINSWITH[c] %@)", [self filterString], [self filterString]]; + [newFiltered addObjectsFromArray:[suggestions filteredArrayUsingPredicate:predicate]]; + if(dictMode) + for(id w in [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,[[self filterString] length]) inString:[self filterString] language:nil inSpellDocumentWithTag:0]) + [newFiltered addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", nil]]; } else { - if(dictMode) - newFiltered = nil; - else - newFiltered = suggestions; + if(!dictMode) + [newFiltered addObjectsFromArray:suggestions]; } + + if(![newFiltered count]) + [newFiltered addObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"No completions found", @"no completions found message"), @"display", @"", @"noCompletion", nil]]; + NSPoint old = NSMakePoint([self frame].origin.x, [self frame].origin.y + [self frame].size.height); - + NSInteger displayedRows = [newFiltered count] < SP_NARROWDOWNLIST_MAX_ROWS ? [newFiltered count] : SP_NARROWDOWNLIST_MAX_ROWS; - CGFloat newHeight = ([theTableView rowHeight] + [theTableView intercellSpacing].height) * displayedRows; - - // CGFloat maxLen = 1; - // NSString* item; - // NSInteger i; - // BOOL spaceInSuggestion = NO; - // [textualInputCharacters removeCharactersInString:@" "]; - // CGFloat maxWidth = [self frame].size.width; - // if([newFiltered count]>0) - // { - // for(i=0; i<[newFiltered count]; i++) - // { - // if(dictMode) - // item = NSArrayObjectAtIndex(newFiltered, i); - // else - // item = [NSArrayObjectAtIndex(newFiltered, i) objectForKey:@"display"]; - // // If space in suggestion add space to allowed input chars - // if(!spaceInSuggestion && [item rangeOfString:@" "].length) { - // [textualInputCharacters addCharactersInString:@" "]; - // spaceInSuggestion = YES; - // } - // - // if([item length]>maxLen) - // maxLen = [item length]; - // } - // maxWidth = maxLen*16; - // maxWidth = (maxWidth>340) ? 340 : maxWidth; - // maxWidth = (maxWidth<20) ? 20 : maxWidth; - // } - // if(caretPos.y>=0 && (isAbove || caretPos.y<newHeight)) - // { - // isAbove = YES; - // old.y = caretPos.y + (newHeight + [tableFont pointSize]*1.5); - // } - // if(caretPos.y<0 && (isAbove || (mainScreen.size.height-newHeight)<(caretPos.y*-1))) - // { - // old.y = caretPos.y + (newHeight + [tableFont pointSize]*1.5); - // } - + CGFloat newHeight = ([theTableView rowHeight] + [theTableView intercellSpacing].height) * ((displayedRows) ? displayedRows : 1); + + if(caretPos.y >= 0 && (isAbove || caretPos.y < newHeight)) + { + isAbove = YES; + old.y = caretPos.y + newHeight + ([tableFont pointSize]*1.5); + } + if(caretPos.y < 0 && (isAbove || ([self rectOfMainScreen].size.height-newHeight) < (caretPos.y*-1))) + old.y = caretPos.y + newHeight + ([tableFont pointSize]*1.5); + // newHeight is currently the new height for theTableView, but we need to resize the whole window // so here we use the difference in height to find the new height for the window - // newHeight = [[self contentView] frame].size.height + (newHeight - [theTableView frame].size.height); - [self setFrame:NSMakeRect(old.x, old.y-newHeight, 450, newHeight) display:YES]; + [self setFrame:NSMakeRect(old.x, old.y-newHeight, maxWindowWidth, newHeight) display:YES]; + if (filtered) [filtered release]; filtered = [newFiltered retain]; + [newFiltered release]; [theTableView reloadData]; } @@ -498,6 +531,7 @@ } } [self close]; + usleep(70); // tiny delay to suppress while continously pressing of ESC overlapping } // ================== @@ -511,10 +545,7 @@ id cur = [filtered objectAtIndex:row]; NSString* curMatch; - if(dictMode) - curMatch = [NSString stringWithString:cur]; - else - curMatch = [cur objectForKey:@"match"] ?: [cur objectForKey:@"display"]; + curMatch = [cur objectForKey:@"match"] ?: [cur objectForKey:@"display"]; if([[self filterString] length] + 1 < [curMatch length]) { NSString* prefix = [curMatch substringToIndex:[[self filterString] length] + 1]; @@ -523,10 +554,7 @@ { id candidate = [filtered objectAtIndex:i]; NSString* candidateMatch; - if(dictMode) - candidateMatch = [filtered objectAtIndex:i]; - else - candidateMatch = [candidate objectForKey:@"match"] ?: [candidate objectForKey:@"display"]; + candidateMatch = [candidate objectForKey:@"match"] ?: [candidate objectForKey:@"display"]; if([candidateMatch hasPrefix:prefix]) [candidates addObject:candidateMatch]; } @@ -557,7 +585,7 @@ { [theView setSelectedRange:theCharRange]; [theView insertText:aString]; - // If completion was invoked inside backticks move caret out of the backticks + // If completion string contains backticks move caret out of the backticks if(backtickMode) [theView performSelector:@selector(moveRight:)]; } @@ -567,30 +595,42 @@ if([theTableView selectedRow] == -1) return; + NSDictionary* selectedItem = [filtered objectAtIndex:[theTableView selectedRow]]; + + if([selectedItem objectForKey:@"noCompletion"]) { + return; + } + if(dictMode){ - [self insert_text:[[[filtered objectAtIndex:[theTableView selectedRow]] mutableCopy] autorelease]]; + [self insert_text:[selectedItem objectForKey:@"match"] ?: [selectedItem objectForKey:@"display"]]; } else { - NSMutableDictionary* selectedItem = [[[filtered objectAtIndex:[theTableView selectedRow]] mutableCopy] autorelease]; NSString* candidateMatch = [selectedItem objectForKey:@"match"] ?: [selectedItem objectForKey:@"display"]; - if( [[NSApp currentEvent] modifierFlags] & (NSShiftKeyMask)) { - if([[selectedItem objectForKey:@"path"] length]) { - NSString *path = [NSString stringWithFormat:@"%@.%@", - [[[[[selectedItem objectForKey:@"path"] componentsSeparatedByString:@"⇠"] reverseObjectEnumerator] allObjects] componentsJoinedByPeriodAndBacktickQuoted], - [candidateMatch backtickQuotedString]]; - - // Check if path's db name is the current selected db name - NSRange r = [path rangeOfString:[currentDb backtickQuotedString] options:NSCaseInsensitiveSearch range:NSMakeRange(0, [[currentDb backtickQuotedString] length])]; - theCharRange = theParseRange; - backtickMode = 0; // suppress move the caret one step rightwards - if(path && [path length] && r.length) { - [self insert_text:[path substringFromIndex:r.length+1]]; + if([selectedItem objectForKey:@"isRef"] + && ([[NSApp currentEvent] modifierFlags] & (NSShiftKeyMask)) + && [[selectedItem objectForKey:@"path"] length]) { + NSString *path = [NSString stringWithFormat:@"%@.%@", + [[[[[selectedItem objectForKey:@"path"] componentsSeparatedByString:@"⇠"] reverseObjectEnumerator] allObjects] componentsJoinedByPeriodAndBacktickQuoted], + [candidateMatch backtickQuotedString]]; + + // Check if path's db name is the current selected db name + NSRange r = [path rangeOfString:[currentDb backtickQuotedString] options:NSCaseInsensitiveSearch range:NSMakeRange(0, [[currentDb backtickQuotedString] length])]; + theCharRange = theParseRange; + backtickMode = 0; // suppress move the caret one step rightwards + if(path && [path length] && r.length) { + [self insert_text:[path substringFromIndex:r.length+1]]; + } else { + [self insert_text:path]; + } + } else { + if([[self filterString] length] < [candidateMatch length]) { + // Is completion string a schema name for current connection + if([selectedItem objectForKey:@"isRef"]) { + backtickMode = 0; // suppress move the caret one step rightwards + [self insert_text:[candidateMatch backtickQuotedString]]; } else { - [self insert_text:path]; + [self insert_text:candidateMatch]; } } - } else { - if([[self filterString] length] < [candidateMatch length]) - [self insert_text:candidateMatch]; } } closeMe = YES; |