// // $Id$ // // CMTextView.m // sequel-pro // // Created by Carsten Blüm. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation; either version 2 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA // // More info at #import "CMTextView.h" #import "CustomQuery.h" #import "TableDocument.h" #import "SPStringAdditions.h" #import "SPArrayAdditions.h" #import "SPTextViewAdditions.h" #import "SPNarrowDownCompletion.h" #import "SPConstants.h" #import "SPQueryController.h" #pragma mark - #pragma mark lex init /* * Include all the extern variables and prototypes required for flex (used for syntax highlighting) */ #import "SPEditorTokens.h" extern NSInteger yylex(); extern NSInteger yyuoffset, yyuleng; typedef struct yy_buffer_state *YY_BUFFER_STATE; void yy_switch_to_buffer(YY_BUFFER_STATE); YY_BUFFER_STATE yy_scan_string (const char *); #pragma mark - #pragma mark attribute definition #define kAPlinked @"Linked" // attribute for a via auto-pair inserted char #define kAPval @"linked" #define kLEXToken @"Quoted" // set via lex to indicate a quoted string #define kLEXTokenValue @"isMarked" #define kSQLkeyword @"SQLkw" // attribute for found SQL keywords #define kQuote @"Quote" #define kQuoteValue @"isQuoted" #define kValue @"dummy" #define kBTQuote @"BTQuote" #define kBTQuoteValue @"isBTQuoted" #pragma mark - #pragma mark constant definitions #define SP_CQ_SEARCH_IN_MYSQL_HELP_MENU_ITEM_TAG 1000 #define SP_CQ_COPY_AS_RTF_MENU_ITEM_TAG 1001 #define SP_CQ_SELECT_CURRENT_QUERY_MENU_ITEM_TAG 1002 #define SP_SYNTAX_HILITE_BIAS 2000 #define MYSQL_DOC_SEARCH_URL @"http://dev.mysql.com/doc/refman/%@/en/%@.html" #pragma mark - @implementation CMTextView /* * Sort function (mainly used to sort the words in the textView) */ NSInteger alphabeticSort(id string1, id string2, void *reverse) { return [string1 localizedCaseInsensitiveCompare:string2]; } - (void) awakeFromNib { // Set self as delegate for the textView's textStorage to enable syntax highlighting, [[self textStorage] setDelegate:self]; // Set defaults for general usage autoindentEnabled = YES; autopairEnabled = YES; autoindentIgnoresEnter = NO; autouppercaseKeywordsEnabled = YES; autohelpEnabled = NO; delBackwardsWasPressed = NO; startListeningToBoundChanges = NO; snippetControlCounter = -1; lineNumberView = [[NoodleLineNumberView alloc] initWithScrollView:scrollView]; [scrollView setVerticalRulerView:lineNumberView]; [scrollView setHasHorizontalRuler:NO]; [scrollView setHasVerticalRuler:YES]; [scrollView setRulersVisible:YES]; // Re-define 64 tab stops for a better editing NSFont *tvFont = [self font]; float firstColumnInch = 0.5, otherColumnInch = 0.5, pntPerInch = 72.0; NSInteger i; NSTextTab *aTab; NSMutableArray *myArrayOfTabs; NSMutableParagraphStyle *paragraphStyle; myArrayOfTabs = [NSMutableArray arrayWithCapacity:64]; aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:firstColumnInch*pntPerInch]; [myArrayOfTabs addObject:aTab]; [aTab release]; for(i=1; i<64; i++) { aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:(firstColumnInch*pntPerInch) + ((float)i * otherColumnInch * pntPerInch)]; [myArrayOfTabs addObject:aTab]; [aTab release]; } paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; [paragraphStyle setTabStops:myArrayOfTabs]; // Soft wrapped lines are indented slightly [paragraphStyle setHeadIndent:4.0]; NSMutableDictionary *textAttributes = [[[NSMutableDictionary alloc] initWithCapacity:1] autorelease]; [textAttributes setObject:paragraphStyle forKey:NSParagraphStyleAttributeName]; NSRange range = NSMakeRange(0, [[self textStorage] length]); if ([self shouldChangeTextInRange:range replacementString:nil]) { [[self textStorage] setAttributes:textAttributes range: range]; [self didChangeText]; } [self setTypingAttributes:textAttributes]; [self setDefaultParagraphStyle:paragraphStyle]; [paragraphStyle release]; [self setFont:tvFont]; // disabled to get the current text range in textView safer [[self layoutManager] setBackgroundLayoutEnabled:NO]; // add NSViewBoundsDidChangeNotification to scrollView [[scrollView contentView] setPostsBoundsChangedNotifications:YES]; NSNotificationCenter *aNotificationCenter = [NSNotificationCenter defaultCenter]; [aNotificationCenter addObserver:self selector:@selector(boundsDidChangeNotification:) name:@"NSViewBoundsDidChangeNotification" object:[scrollView contentView]]; prefs = [[NSUserDefaults standardUserDefaults] retain]; } - (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion { mySQLConnection = theConnection; mySQLmajorVersion = majorVersion; } /* * Return an array of NSDictionary containing the sorted strings representing * the set of unique words, SQL keywords, user-defined funcs/procs, tables etc. * NSDic key "display" := the displayed and to be inserted word * NSDic key "image" := an image to be shown left from "display" (optional) * * [NSDictionary dictionaryWithObjectsAndKeys:@"foo", @"display", @"`foo`", @"match", @"func-small", @"image", nil] */ - (NSArray *)suggestionsForSQLCompletionWith:(NSString *)currentWord dictMode:(BOOL)isDictMode browseMode:(BOOL)dbBrowseMode withTableName:(NSString*)aTableName withDbName:(NSString*)aDbName { NSMutableArray *possibleCompletions = [[NSMutableArray alloc] initWithCapacity:32]; // If caret is not inside backticks add keywords and all words coming from the view. if(!dbBrowseMode) { // Only parse for words if text size is less than 60kB if([[self string] length] && [[self string] length]<60000) { NSMutableArray *uniqueArray = [NSMutableArray array]; [uniqueArray addObjectsFromArray:[NSSet setWithArray:[[self string] componentsMatchedByRegex:@"\\w+"]]]; // Remove current word from list [uniqueArray removeObject:currentWord]; NSInteger reverseSort = NO; NSArray *sortedArray = [[[uniqueArray mutableCopy] autorelease] sortedArrayUsingFunction:alphabeticSort context:&reverseSort]; for(id w in sortedArray) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; } if(!isDictMode) { // Add predefined keywords for(id s in [self keywords]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"dummy-small", @"image", nil]]; // Add predefined functions for(id s in [self functions]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"func-small", @"image", nil]]; } } if(!isDictMode && [mySQLConnection isConnected]) { // Add structural db/table/field data to completions list or fallback to gathering TablesList data NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[mySQLConnection getDbStructure]]; if(dbs != nil && [dbs count]) { NSMutableArray *allDbs = [[NSMutableArray array] autorelease]; [allDbs addObjectsFromArray:[dbs allKeys]]; // Add database names having no tables since they don't appear in the information_schema if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"] != nil) for(id db in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"]) if(![allDbs containsObject:db]) [allDbs addObject:db]; NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; NSMutableArray *sortedDbs = [[NSMutableArray array] autorelease]; [sortedDbs addObjectsFromArray:[allDbs sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; NSString *currentDb = nil; NSString *currentTable = nil; if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"selectedDatabase"] != nil) currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]; 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"]) { currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]; [sortedDbs removeObject:currentDb]; [sortedDbs insertObject:currentDb atIndex:0]; } // 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"] && [sortedDbs containsObject:@"information_schema"]) { [sortedDbs removeObject:@"information_schema"]; [sortedDbs addObject:@"information_schema"]; } BOOL aTableNameExists = NO; if(!aDbName) { // 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; } } // If aDbName exist show only those table if(aDbName && [aDbName length] && [allDbs containsObject:aDbName]) { [sortedDbs removeAllObjects]; [sortedDbs addObject:aDbName]; } for(id db in sortedDbs) { NSArray *allTables = [[dbs objectForKey:db] allKeys]; NSMutableArray *sortedTables = [NSMutableArray array]; if(aTableNameExists) { [sortedTables addObject:aTableName]; } else { [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:db, @"display", @"database-small", @"image", db, @"isRef", nil]]; [sortedTables addObjectsFromArray:[allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; 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]; NSArray *allFields = [theTable allKeys]; NSInteger structtype = [[theTable objectForKey:@" struct_type "] intValue]; BOOL breakFlag = NO; if(!aTableNameExists) switch(structtype) { case 0: [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-small-square", @"image", db, @"path", [NSString stringWithFormat:@"%@.%@",db,table], @"isRef", nil]]; break; case 1: [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-view-small-square", @"image", db, @"path", [NSString stringWithFormat:@"%@.%@",db,table], @"isRef", nil]]; break; case 2: [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"proc-small", @"image", db, @"path", [NSString stringWithFormat:@"%@.%@",db,table], @"isRef", nil]]; breakFlag = YES; break; case 3: [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"func-small", @"image", db, @"path", [NSString stringWithFormat:@"%@.%@",db,table], @"isRef", nil]]; breakFlag = YES; break; } if(!breakFlag) { NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; for(id field in sortedFields) { if(![field hasPrefix:@" "]) { NSString *typ = [theTable objectForKey:field]; // Check if type definition contains a , if so replace the bracket content by … and add // the bracket content as "list" key to prevend the token field to split them by , if(typ && [typ rangeOfString:@","].length) { NSString *t = [typ stringByReplacingOccurrencesOfRegex:@"\\(.*?\\)" withString:@"(…)"]; NSString *lst = [typ stringByMatching:@"\\(([^\\)]*?)\\)" capture:1L]; [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: field, @"display", @"field-small-square", @"image", [NSString stringWithFormat:@"%@⇠%@",table,db], @"path", t, @"type", lst, @"list", [NSString stringWithFormat:@"%@.%@.%@",db,table,field], @"isRef", nil]]; } else { [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: field, @"display", @"field-small-square", @"image", [NSString stringWithFormat:@"%@⇠%@",table,db], @"path", typ, @"type", [NSString stringWithFormat:@"%@.%@.%@",db,table,field], @"isRef", nil]]; } } } } } } if(desc) [desc release]; } 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", @"", @"noCompletion", nil]]; // Add all database names to completions list for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; // Add all system database names to completions list for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allSystemDatabaseNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; // Add table names to completions list for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allTableNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-small-square", @"image", @"", @"isRef", nil]]; // Add view names to completions list for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allViewNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-view-small-square", @"image", @"", @"isRef", nil]]; // Add field names to completions list for currently selected table if ([[[self window] delegate] table] != nil) for (id obj in [[[[self window] delegate] valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"field-small-square", @"image", @"", @"isRef", nil]]; // Add proc/func only for MySQL version 5 or higher if(mySQLmajorVersion > 4) { // Add all procedures to completions list for currently selected table for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allProcedureNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"proc-small", @"image", @"", @"isRef", nil]]; // Add all function to completions list for currently selected table for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allFunctionNames"]) [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"func-small", @"image", @"", @"isRef", nil]]; } } } // Make suggestions unique NSMutableArray *compl = [[NSMutableArray alloc] initWithCapacity:32]; for(id suggestion in possibleCompletions) if(![compl containsObject:suggestion]) [compl addObject:suggestion]; [possibleCompletions release]; return [compl autorelease]; } - (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch { [self breakUndoCoalescing]; NSInteger caretPos = NSMaxRange([self selectedRange]); // [self setSelectedRange:NSMakeRange(caretPos, 0)]; BOOL caretMovedLeft = NO; // Check if caret is located after a ` - if so move caret inside if([[self string] length] && [[self string] characterAtIndex:caretPos-1] == '`') { if([[self string] length] > caretPos && [[self string] characterAtIndex:caretPos] == '`') { ; } else { caretPos--; caretMovedLeft = YES; [self setSelectedRange:NSMakeRange(caretPos, 0)]; } } NSString* filter; NSString* dbName = nil; NSString* tableName = nil; NSRange completionRange = [self getRangeForCurrentWord]; NSRange parseRange = completionRange; NSString* currentWord = [[self string] substringWithRange:completionRange]; NSString* prefix = @""; 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; // Remove that attribute to suppress auto-uppercasing of certain keyword combinations if(![self selectedRange].length && [self selectedRange].location) [[self textStorage] removeAttribute:kSQLkeyword range:completionRange]; [self setSelectedRange:NSMakeRange(caretPos, 0)]; 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 = @""; BOOL caretIsInsideBackticks = NO; // Is the caret inside backticks // Do not using attribute:atIndex: since it could return wrong results due to editing. // This approach counts the number of ` up to the beginning of the current line from caret position NSRange lineHeadRange = [[self string] lineRangeForRange:NSMakeRange(caretPos, 0)]; NSString *lineHead = [[self string] substringWithRange:NSMakeRange(lineHeadRange.location, caretPos - lineHeadRange.location)]; for(NSInteger i=0; i<[lineHead length]; i++) if([lineHead characterAtIndex:i]=='`') caretIsInsideBackticks = !caretIsInsideBackticks; NSCharacterSet *whiteSpaceCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; NSInteger start = caretPos; NSInteger backticksCounter = (caretIsInsideBackticks) ? 1 : 0; 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]) { start++; break; } if(currentCharacter == '.' && !(backticksCounter%2)) { pointCounter++; switch(pointCounter) { case 1: firstPoint = start; break; case 2: secondPoint = start; break; default: doParsing = NO; start++; } } if(doParsing && currentCharacter == '`') { backticksCounter++; if(!(backticksCounter%2) && start > 0) { currentCharacter = [[self string] characterAtIndex:start-1]; if(currentCharacter != '`' && currentCharacter != '.') break; if(currentCharacter == '`') { // ignore `` backticksCounter++; start--; } } } } dbBrowseMode = (pointCounter || backticksCounter); if(dbBrowseMode) { parseRange = NSMakeRange(start, caretPos-start); NSString *parsedString = [[self string] substringWithRange:parseRange]; // Check if parsed string is wrapped by `` 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; } } // Normalize point positions firstPoint-=start; secondPoint-=start; if(secondPoint>0) { dbName = [[[parsedString substringWithRange:NSMakeRange(0, secondPoint)] stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; tableName = [[[parsedString substringWithRange:NSMakeRange(secondPoint+1,firstPoint-secondPoint-1)] stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; filter = [[[parsedString substringWithRange:NSMakeRange(firstPoint+1,[parsedString length]-firstPoint-1)] stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; } else if(firstPoint>0) { tableName = [[[parsedString substringWithRange:NSMakeRange(0, firstPoint)] stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; filter = [[[parsedString substringWithRange:NSMakeRange(firstPoint+1,[parsedString length]-firstPoint-1)] stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; } else { filter = [[parsedString stringByReplacingOccurrencesOfString:@"``" withString:@"`"] stringByReplacingOccurrencesOfRegex:@"^`|`$" withString:@""]; } // 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 == .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]; } if(fuzzySearch) { filter = [[NSString stringWithString:[[self string] substringWithRange:parseRange]] stringByReplacingOccurrencesOfString:@"`" withString:@""]; completionRange = parseRange; } } else { filter = [NSString stringWithString:currentWord]; } } else { filter = [NSString stringWithString:currentWord]; } SPNarrowDownCompletion* completionPopUp = [[SPNarrowDownCompletion alloc] initWithItems:[self suggestionsForSQLCompletionWith:currentWord dictMode:isDictMode browseMode:dbBrowseMode withTableName:tableName withDbName:dbName] alreadyTyped:filter staticPrefix:prefix additionalWordCharacters:allow caseSensitive:!caseInsensitive charRange:completionRange parseRange:parseRange inView:self dictMode:isDictMode dbMode:dbBrowseMode tabTriggerMode:((snippetControlCounter > -1) ? YES : NO) fuzzySearch:fuzzySearch backtickMode:backtickMode withDbName:dbName withTableName:tableName selectedDb:currentDb caretMovedLeft:caretMovedLeft]; //Get the NSPoint of the first character of the current word 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; // Adjust list location to be under the current word or insertion point pos.y -= [[self font] pointSize]*1.25; [completionPopUp setCaretPos:pos]; [completionPopUp orderFront:self]; [completionPopUp insertCommonPrefix]; } /* * Returns the associated line number for a character position inside of the CMTextView */ - (NSUInteger) getLineNumberForCharacterIndex:(NSUInteger)anIndex { return [lineNumberView lineNumberForCharacterIndex:anIndex inText:[self string]]+1; } /* * Checks if the char after the current caret position/selection matches a supplied attribute */ - (BOOL) isNextCharMarkedBy:(id)attribute withValue:(id)aValue { NSUInteger caretPosition = [self selectedRange].location; // Perform bounds checking if (caretPosition >= [[self string] length]) return NO; // Perform the check if ([[[self textStorage] attribute:attribute atIndex:caretPosition effectiveRange:nil] isEqualToString:aValue]) return YES; return NO; } /* * Checks if the caret adjoins to an alphanumeric char |word or word| or wo|rd * Exception for word| and char is a “(” to allow e.g. auto-pairing () for functions */ - (BOOL) isCaretAdjacentToAlphanumCharWithInsertionOf:(unichar)aChar { NSUInteger caretPosition = [self selectedRange].location; NSCharacterSet *alphanum = [NSCharacterSet alphanumericCharacterSet]; BOOL leftIsAlphanum = NO; BOOL rightIsAlphanum = NO; BOOL charIsOpenBracket = (aChar == '('); // Check previous/next character for being alphanum // @try block for bounds checking @try { leftIsAlphanum = [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition-1]] && !charIsOpenBracket; } @catch(id ae) { } @try { rightIsAlphanum= [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition]]; } @catch(id ae) { } return (leftIsAlphanum ^ rightIsAlphanum || leftIsAlphanum && rightIsAlphanum); } /* * Checks if the caret is wrapped by auto-paired characters. * e.g. [| := caret]: "|" */ - (BOOL) areAdjacentCharsLinked { NSUInteger caretPosition = [self selectedRange].location; unichar leftChar, matchingChar; // Perform bounds checking if ([self selectedRange].length) return NO; if (caretPosition < 1) return NO; if (caretPosition >= [[self string] length]) return NO; // Check the character to the left of the cursor and set the pairing character if appropriate leftChar = [[self string] characterAtIndex:caretPosition - 1]; if (leftChar == '(') matchingChar = ')'; else if (leftChar == '"' || leftChar == '`' || leftChar == '\'') matchingChar = leftChar; else return NO; // Check that the pairing character exists after the caret, and is tagged with the link attribute if (matchingChar == [[self string] characterAtIndex:caretPosition] && [[[self textStorage] attribute:kAPlinked atIndex:caretPosition effectiveRange:nil] isEqualToString:kAPval]) { return YES; } return NO; } #pragma mark - #pragma mark user actions /* * Search for the current selection or current word in the MySQL Help */ - (IBAction) showMySQLHelpForCurrentWord:(id)sender { [[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] showHelpForCurrentWord:self]; } /* * If the textview has a selection, wrap it with the supplied prefix and suffix strings; * return whether or not any wrap was performed. */ - (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix { // Only proceed if a selection is active if ([self selectedRange].length == 0) return NO; // Replace the current selection with the selected string wrapped in prefix and suffix [self insertText: [NSString stringWithFormat:@"%@%@%@", prefix, [[self string] substringWithRange:[self selectedRange]], suffix ] ]; return YES; } /* * Copy selected text chunk as RTF to preserve syntax highlighting */ - (void) copyAsRTF { NSPasteboard *pb = [NSPasteboard generalPasteboard]; NSTextStorage *textStorage = [self textStorage]; NSData *rtf = [textStorage RTFFromRange:[self selectedRange] documentAttributes:nil]; if (rtf) { [pb declareTypes:[NSArray arrayWithObject:NSRTFPboardType] owner:self]; [pb setData:rtf forType:NSRTFPboardType]; } } - (void) selectCurrentQuery { [[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] selectCurrentQuery]; } /* * Selects the line lineNumber relatively to a selection (if given) and scrolls to it */ - (void) selectLineNumber:(NSUInteger)lineNumber ignoreLeadingNewLines:(BOOL)ignLeadingNewLines { NSRange selRange; NSArray *lineRanges; if([self selectedRange].length) lineRanges = [[[self string] substringWithRange:[self selectedRange]] lineRangesForRange:NSMakeRange(0, [self selectedRange].length)]; else lineRanges = [[self string] lineRangesForRange:NSMakeRange(0, [[self string] length])]; if(ignLeadingNewLines) // ignore leading empty lines { NSInteger arrayCount = [lineRanges count]; NSInteger i; for (i = 0; i < arrayCount; i++) { if(NSRangeFromString([lineRanges objectAtIndex:i]).length > 0) break; lineNumber++; } } // Safety-check the line number if (lineNumber > [lineRanges count]) lineNumber = [lineRanges count]; // Grab the range to select selRange = NSRangeFromString([lineRanges objectAtIndex:lineNumber-1]); // adjust selRange if a selection was given if([self selectedRange].length) selRange.location += [self selectedRange].location; [self setSelectedRange:selRange]; [self scrollRangeToVisible:selRange]; } /* * Shifts the selection, if any, rightwards by indenting any selected lines with one tab. * If the caret is within a line, the selection is not changed after the index; if the selection * has length, all lines crossed by the length are indented and fully selected. * Returns whether or not an indentation was performed. */ - (BOOL) shiftSelectionRight { NSString *textViewString = [[self textStorage] string]; NSRange currentLineRange; NSArray *lineRanges; NSString *tabString = @"\t"; NSInteger i, indentedLinesLength = 0; if ([self selectedRange].location == NSNotFound) return NO; // Indent the currently selected line if the caret is within a single line if ([self selectedRange].length == 0) { NSRange currentLineRange; // Extract the current line range based on the text caret currentLineRange = [textViewString lineRangeForRange:[self selectedRange]]; // Register the indent for undo [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 0) replacementString:tabString]; // Insert the new tab [self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 0) withString:tabString]; return YES; } // Otherwise, the selection has a length - get an array of current line ranges for the specified selection lineRanges = [textViewString lineRangesForRange:[self selectedRange]]; // Loop through the ranges, storing a count of the overall length. for (i = 0; i < [lineRanges count]; i++) { currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]); indentedLinesLength += currentLineRange.length + 1; // Register the indent for undo [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location+i, 0) replacementString:tabString]; // Insert the new tab [self replaceCharactersInRange:NSMakeRange(currentLineRange.location+i, 0) withString:tabString]; } // Select the entirety of the new range [self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, indentedLinesLength)]; return YES; } /* * Shifts the selection, if any, leftwards by un-indenting any selected lines by one tab if possible. * If the caret is within a line, the selection is not changed after the undent; if the selection has * length, all lines crossed by the length are un-indented and fully selected. * Returns whether or not an indentation was performed. */ - (BOOL) shiftSelectionLeft { NSString *textViewString = [[self textStorage] string]; NSRange currentLineRange; NSArray *lineRanges; NSInteger i, unindentedLines = 0, unindentedLinesLength = 0; if ([self selectedRange].location == NSNotFound) return NO; // Undent the currently selected line if the caret is within a single line if ([self selectedRange].length == 0) { NSRange currentLineRange; // Extract the current line range based on the text caret currentLineRange = [textViewString lineRangeForRange:[self selectedRange]]; // Ensure that the line has length and that the first character is a tab if (currentLineRange.length < 1 || [textViewString characterAtIndex:currentLineRange.location] != '\t') return NO; // Register the undent for undo [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 1) replacementString:@""]; // Remove the tab [self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 1) withString:@""]; return YES; } // Otherwise, the selection has a length - get an array of current line ranges for the specified selection lineRanges = [textViewString lineRangesForRange:[self selectedRange]]; // Loop through the ranges, storing a count of the total lines changed and the new length. for (i = 0; i < [lineRanges count]; i++) { currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]); unindentedLinesLength += currentLineRange.length; // Ensure that the line has length and that the first character is a tab if (currentLineRange.length < 1 || [textViewString characterAtIndex:currentLineRange.location-unindentedLines] != '\t') continue; // Register the undent for undo [self shouldChangeTextInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) replacementString:@""]; // Remove the tab [self replaceCharactersInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) withString:@""]; // As a line has been unindented, modify counts and lengths unindentedLines++; unindentedLinesLength--; } // If a change was made, select the entirety of the new range and return success if (unindentedLines) { [self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, unindentedLinesLength)]; return YES; } return NO; } #pragma mark - #pragma mark snippet handler /* * Reset snippet controller variables to end a snippet session */ - (void)endSnippetSession { snippetControlCounter = -1; currentSnippetIndex = -1; snippetControlMax = -1; snippetWasJustInserted = NO; } /* * Selects the current snippet defined by “currentSnippetIndex” */ - (void)selectCurrentSnippet { if( snippetControlCounter > -1 && currentSnippetIndex >= 0 && currentSnippetIndex <= snippetControlMax ) { // Place the caret at the end of the query favorite snippet // and finish snippet editing if(currentSnippetIndex == snippetControlMax) { [self setSelectedRange:NSMakeRange(snippetControlArray[snippetControlMax][0] + snippetControlArray[snippetControlMax][1], 0)]; [self endSnippetSession]; return; } if(currentSnippetIndex >= 0 && currentSnippetIndex < 20) { if(snippetControlArray[currentSnippetIndex][2] == 0) { [self setSelectedRange:NSMakeRange(snippetControlArray[currentSnippetIndex][0], snippetControlArray[currentSnippetIndex][1])]; } } else { // for safety reasons [self endSnippetSession]; } } else { // for safety reasons [self endSnippetSession]; } } /* * Inserts a chosen query favorite and initialze a snippet session if user defined any */ - (void)insertFavoriteAsSnippet:(NSString*)theSnippet atRange:(NSRange)targetRange { NSInteger i; // reset snippet array for(i=0; i<20; i++) { snippetControlArray[i][0] = -1; // snippet location snippetControlArray[i][1] = -1; // snippet length snippetControlArray[i][2] = -1; // snippet task : -1 not valid, 0 select snippet } if(theSnippet == nil || ![theSnippet length]) return; NSMutableString *snip = [[NSMutableString alloc] initWithCapacity:[theSnippet length]]; @try{ NSString *re = @"(?18 || snipCnt<0) { NSLog(@"Only snippets in the range of 0…18 allowed."); [self endSnippetSession]; break; } // Remember the maximal snippet number defined by user if(snipCnt>snippetControlMax) snippetControlMax = snipCnt; [snip replaceCharactersInRange:snipRange withString:[snip substringWithRange:hintRange]]; [snip flushCachedRegexData]; // Store found snippet range snippetControlArray[snipCnt][0] = snipRange.location + targetRange.location; snippetControlArray[snipCnt][1] = snipRange.length-4-((snipCnt>9)?2:1); snippetControlArray[snipCnt][2] = 0; // Adjust successive snippets for(i=0; i<20; i++) if(snippetControlArray[i][0] > -1 && i != snipCnt && snippetControlArray[i][0] > snippetControlArray[snipCnt][0]) snippetControlArray[i][0] -= 3+((snipCnt>9)?2:1); // 3 := length(${:) } if(snippetControlCounter > -1) { // Store the end for tab out snippetControlMax++; snippetControlArray[snippetControlMax][0] = targetRange.location + [snip length]; snippetControlArray[snippetControlMax][1] = 0; snippetControlArray[snippetControlMax][2] = 0; } // Registering for undo [self breakUndoCoalescing]; // Insert favorite query as snippet if any [self setSelectedRange:targetRange]; // Suppress snippet range calculation in [self textStorageDidProcessEditing] while initial insertion snippetWasJustInserted = YES; [self insertText:snip]; // Any snippets defined? if(snippetControlCounter > -1) { // Find and select first defined snippet currentSnippetIndex = 0; // Look for next defined snippet since snippet numbers must not serial like 1, 5, and 12 e.g. while(snippetControlArray[currentSnippetIndex][0] == -1 && currentSnippetIndex < 20) currentSnippetIndex++; [self selectCurrentSnippet]; } snippetWasJustInserted = NO; } @catch(id ae) { // For safety reasons catch exceptions NSLog(@"Snippet Error: %@", [ae description]); [self endSnippetSession]; snippetWasJustInserted = NO; } if(snip)[snip release]; } /* * Checks whether the current caret position in inside of a defined snippet range */ - (BOOL)checkForCaretInsideSnippet { if(snippetWasJustInserted) return YES; BOOL isCaretInsideASnippet = NO; if(snippetControlCounter < 0 || currentSnippetIndex == snippetControlMax) { [self endSnippetSession]; return NO; } NSInteger caretPos = [self selectedRange].location; NSInteger i, j; NSInteger foundSnippetIndices[20]; // array to hold nested snippets j = -1; // Go through all snippet ranges and check whether the caret is inside of the // current snippet range. Remember matches // in foundSnippetIndices array to test for nested snippets. for(i=0; i<=snippetControlMax; i++) { j++; foundSnippetIndices[j] = 0; if(snippetControlArray[i][0] != -1 && caretPos >= snippetControlArray[i][0] && caretPos <= snippetControlArray[i][0] + snippetControlArray[i][1]) { foundSnippetIndices[j] = 1; if(i == currentSnippetIndex) { isCaretInsideASnippet = YES; break; } } } // If caret is not inside the current snippet range check if caret is inside of // another defined snippet; if so set currentSnippetIndex to it (this allows to use the // mouse to activate another snippet). If the caret is inside of overlapped snippets (nested) // then select this snippet which has the smallest length. if(!isCaretInsideASnippet && foundSnippetIndices[currentSnippetIndex] == 1) { isCaretInsideASnippet = YES; } else { NSInteger index = -1; NSInteger smallestLength = -1; for(i=0; i snippetControlArray[i][1]) { index = i; smallestLength = snippetControlArray[i][1]; } } } } // Reset the active snippet if(index > -1 && smallestLength > -1) { currentSnippetIndex = index; isCaretInsideASnippet = YES; } } return isCaretInsideASnippet; } #pragma mark - #pragma mark event management /* * Used for autoHelp update if the user changed the caret position by using the mouse. */ - (void) mouseDown:(NSEvent *)theEvent { // Cancel autoHelp timer if([prefs boolForKey:SPCustomQueryUpdateAutoHelp]) [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(autoHelp) object:nil]; [super mouseDown:theEvent]; // Start autoHelp timer if([prefs boolForKey:SPCustomQueryUpdateAutoHelp]) [self performSelector:@selector(autoHelp) withObject:nil afterDelay:[[prefs valueForKey:SPCustomQueryAutoHelpDelay] doubleValue]]; } /* * Handle some keyDown events in order to provide autopairing functionality (if enabled). */ - (void) keyDown:(NSEvent *)theEvent { if([prefs boolForKey:SPCustomQueryUpdateAutoHelp]) {// restart autoHelp timer [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(autoHelp) object:nil]; [self performSelector:@selector(autoHelp) withObject:nil afterDelay:[[prefs valueForKey:SPCustomQueryAutoHelpDelay] doubleValue]]; } long allFlags = (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask); // Check if user pressed ⌥ to allow composing of accented characters. // e.g. for US keyboard "⌥u a" to insert ä // or for non-US keyboards to allow to enter dead keys // e.g. for German keyboard ` is a dead key, press space to enter ` if (([theEvent modifierFlags] & allFlags) == NSAlternateKeyMask || [[theEvent characters] length] == 0) { [super keyDown: theEvent]; return; } NSString *characters = [theEvent characters]; NSString *charactersIgnMod = [theEvent charactersIgnoringModifiers]; unichar insertedCharacter = [characters characterAtIndex:0]; long curFlags = ([theEvent modifierFlags] & allFlags); if ([theEvent keyCode] == 53 && [self isEditable]){ // ESC key for internal completion if(curFlags==(NSControlKeyMask)) [self doCompletionByUsingSpellChecker:NO fuzzyMode:YES]; else [self doCompletionByUsingSpellChecker:NO fuzzyMode:NO]; return; } if (insertedCharacter == NSF5FunctionKey){ // F5 for completion based on spell checker [self doCompletionByUsingSpellChecker:YES fuzzyMode:NO]; return; } // Check for {SHIFT}TAB to try to insert query favorite via TAB trigger if CMTextView belongs to CustomQuery if ([theEvent keyCode] == 48 && [self isEditable] && [[self delegate] isKindOfClass:[CustomQuery class]]){ NSRange targetRange = [self getRangeForCurrentWord]; NSString *tabTrigger = [[self string] substringWithRange:targetRange]; // Is TAB trigger active change selection according to {SHIFT}TAB if([self checkForCaretInsideSnippet] && snippetControlCounter > -1){ if(curFlags==(NSShiftKeyMask)) { // select previous snippet currentSnippetIndex--; // Look for previous defined snippet since snippet numbers must not serial like 1, 5, and 12 e.g. while(snippetControlArray[currentSnippetIndex][0] == -1 && currentSnippetIndex > -2) currentSnippetIndex--; if(currentSnippetIndex < 0) { currentSnippetIndex = 0; while(snippetControlArray[currentSnippetIndex][0] == -1 && currentSnippetIndex < 20) currentSnippetIndex++; NSBeep(); } [self selectCurrentSnippet]; return; } else { // select next snippet currentSnippetIndex++; // Look for next defined snippet since snippet numbers must not serial like 1, 5, and 12 e.g. while(snippetControlArray[currentSnippetIndex][0] == -1 && currentSnippetIndex < 20) currentSnippetIndex++; if(currentSnippetIndex > snippetControlMax) { // for safety reasons [self endSnippetSession]; } else { [self selectCurrentSnippet]; return; } } [self endSnippetSession]; } // Check if tab trigger is defined; if so insert it, otherwise pass through event if(snippetControlCounter < 0 && [[[self window] delegate] fileURL]) { NSArray *snippets = [[SPQueryController sharedQueryController] queryFavoritesForFileURL:[[[self window] delegate] fileURL] andTabTrigger:tabTrigger includeGlobals:YES]; if([snippets count] > 0 && [(NSString*)[(NSDictionary*)[snippets objectAtIndex:0] objectForKey:@"query"] length]) { [self insertFavoriteAsSnippet:[(NSDictionary*)[snippets objectAtIndex:0] objectForKey:@"query"] atRange:targetRange]; return; } } } // Note: switch(insertedCharacter) {} does not work instead use charactersIgnoringModifiers if([charactersIgnMod isEqualToString:@"c"]) // ^C copy as RTF if(curFlags==(NSControlKeyMask)) { [self copyAsRTF]; return; } if([charactersIgnMod isEqualToString:@"h"]) // ^H show MySQL Help if(curFlags==(NSControlKeyMask)) { [self showMySQLHelpForCurrentWord:self]; return; } if([charactersIgnMod isEqualToString:@"y"]) // ^Y select current query if(curFlags==(NSControlKeyMask)) { [self selectCurrentQuery]; return; } if(curFlags & NSCommandKeyMask) { if([charactersIgnMod isEqualToString:@"+"]) // increase text size by 1; ⌘+ and numpad + { [self makeTextSizeLarger]; return; } if([charactersIgnMod isEqualToString:@"-"]) // decrease text size by 1; ⌘- and numpad - { [self makeTextSizeSmaller]; return; } } // Only process for character autopairing if autopairing is enabled and a single character is being added. if ([prefs boolForKey:SPCustomQueryAutoPairCharacters] && characters && [characters length] == 1) { delBackwardsWasPressed = NO; NSString *matchingCharacter = nil; BOOL processAutopair = NO, skipTypedLinkedCharacter = NO; NSRange currentRange; // When a quote character is being inserted into a string quoted with other // quote characters, or if it's the same character but is escaped, don't // automatically match it. if( // Only for " ` or ' quote characters (insertedCharacter == '\'' || insertedCharacter == '"' || insertedCharacter == '`') // And if the next char marked as linked auto-pair && [self isNextCharMarkedBy:kAPlinked withValue:kAPval] // And we are inside a quoted string && [self isNextCharMarkedBy:kLEXToken withValue:kLEXTokenValue] // And there is no selection, just the text caret && ![self selectedRange].length && ( // And the user is inserting an escaped string [[self string] characterAtIndex:[self selectedRange].location-1] == '\\' // Or the user is inserting a character not matching the characters used to quote this string || [[self string] characterAtIndex:[self selectedRange].location] != insertedCharacter ) ) { [super keyDown: theEvent]; return; } // If the caret is inside a text string, without any selection, and not adjoined to an alphanumeric char // (exception for '(' ) skip autopairing. // There is one exception to this - if the caret is before a linked pair character, // processing continues in order to check whether the next character should be jumped // over; e.g. [| := caret]: "foo|" and press " => only caret will be moved "foo"| if( ([self isCaretAdjacentToAlphanumCharWithInsertionOf:insertedCharacter] && ![self isNextCharMarkedBy:kAPlinked withValue:kAPval] && ![self selectedRange].length) || (![self isNextCharMarkedBy:kAPlinked withValue:kAPval] && [self isNextCharMarkedBy:kLEXToken withValue:kLEXTokenValue] && ![self selectedRange].length)) { [super keyDown:theEvent]; return; } // Check whether the submitted character should trigger autopair processing. switch (insertedCharacter) { case '(': matchingCharacter = @")"; processAutopair = YES; break; case '"': matchingCharacter = @"\""; processAutopair = YES; skipTypedLinkedCharacter = YES; break; case '`': matchingCharacter = @"`"; processAutopair = YES; skipTypedLinkedCharacter = YES; break; case '\'': matchingCharacter = @"'"; processAutopair = YES; skipTypedLinkedCharacter = YES; break; case ')': skipTypedLinkedCharacter = YES; break; } // Check to see whether the next character should be compared to the typed character; // if it matches the typed character, and is marked with the is-linked-pair attribute, // select the next character and replace it with the typed character. This allows // a normally quoted string to be typed in full, with the autopair appearing as a hint and // then being automatically replaced when the user types it. if (skipTypedLinkedCharacter) { currentRange = [self selectedRange]; if (currentRange.location != NSNotFound && currentRange.length == 0) { if ([self isNextCharMarkedBy:kAPlinked withValue:kAPval]) { if ([[[self textStorage] string] characterAtIndex:currentRange.location] == insertedCharacter) { currentRange.length = 1; [self setSelectedRange:currentRange]; processAutopair = NO; } } } } // If an appropriate character has been typed, and a matching character has been set, // some form of autopairing is required. if (processAutopair && matchingCharacter) { // Check to see whether several characters are selected, and if so, wrap them with // the auto-paired characters. This returns false if the selection has zero length. if ([self wrapSelectionWithPrefix:characters suffix:matchingCharacter]) return; // Otherwise, start by inserting the original character - the first half of the autopair. [super keyDown:theEvent]; // Then process the second half of the autopair - the matching character. currentRange = [self selectedRange]; if (currentRange.location != NSNotFound) { NSTextStorage *textStorage = [self textStorage]; // Register the auto-pairing for undo [self shouldChangeTextInRange:currentRange replacementString:matchingCharacter]; // Insert the matching character and give it the is-linked-pair-character attribute [self replaceCharactersInRange:currentRange withString:matchingCharacter]; currentRange.length = 1; [textStorage addAttribute:kAPlinked value:kAPval range:currentRange]; // Restore the original selection. currentRange.length=0; [self setSelectedRange:currentRange]; } return; } } // break down the undo grouping level for better undo behavior [self breakUndoCoalescing]; // The default action is to perform the normal key-down action. [super keyDown:theEvent]; } - (void) deleteBackward:(id)sender { // If the caret is currently inside a marked auto-pair, delete the characters on both sides // of the caret. NSRange currentRange = [self selectedRange]; if (currentRange.length == 0 && currentRange.location > 0 && [self areAdjacentCharsLinked]) [self setSelectedRange:NSMakeRange(currentRange.location - 1,2)]; // Avoid auto-uppercasing if resulting word would be a SQL keyword; // e.g. type inta| and deleteBackward: delBackwardsWasPressed = YES; [super deleteBackward:sender]; } /* * Handle special commands - see NSResponder.h for a sample list. * This subclass currently handles insertNewline: in order to preserve indentation * when adding newlines. */ - (void) doCommandBySelector:(SEL)aSelector { // Handle newlines, adding any indentation found on the current line to the new line - ignoring the enter key if appropriate if (aSelector == @selector(insertNewline:) && [prefs boolForKey:SPCustomQueryAutoIndent] && (!autoindentIgnoresEnter || [[NSApp currentEvent] keyCode] != 0x4C)) { NSString *textViewString = [[self textStorage] string]; NSString *currentLine, *indentString = nil; NSScanner *whitespaceScanner; NSRange currentLineRange; NSInteger lineCursorLocation; // Extract the current line based on the text caret or selection start position currentLineRange = [textViewString lineRangeForRange:NSMakeRange([self selectedRange].location, 0)]; currentLine = [[NSString alloc] initWithString:[textViewString substringWithRange:currentLineRange]]; lineCursorLocation = [self selectedRange].location - currentLineRange.location; // Scan all indentation characters on the line into a string whitespaceScanner = [[NSScanner alloc] initWithString:currentLine]; [whitespaceScanner setCharactersToBeSkipped:nil]; [whitespaceScanner scanCharactersFromSet:[NSCharacterSet whitespaceCharacterSet] intoString:&indentString]; [whitespaceScanner release]; [currentLine release]; // Always add the newline, whether or not we want to indent the next line [self insertNewline:self]; // Replicate the indentation on the previous line if one was found. if (indentString) { if (lineCursorLocation < [indentString length]) { [self insertText:[indentString substringWithRange:NSMakeRange(0, lineCursorLocation)]]; } else { [self insertText:indentString]; } } // Return to avoid the original implementation, preventing double linebreaks return; } [super doCommandBySelector:aSelector]; } /* * 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]; // } /* * List of keywords for autocompletion. If you add a keyword here, * it should also be added to the flex file SPEditorTokens.l */ -(NSArray *)keywords { return [NSArray arrayWithObjects: @"ACCESSIBLE", @"ACTION", @"ADD", @"AFTER", @"AGAINST", @"AGGREGATE", @"ALGORITHM", @"ALL", @"ALTER", @"ALTER COLUMN", @"ALTER DATABASE", @"ALTER EVENT", @"ALTER FUNCTION", @"ALTER LOGFILE GROUP", @"ALTER PROCEDURE", @"ALTER SCHEMA", @"ALTER SERVER", @"ALTER TABLE", @"ALTER TABLESPACE", @"ALTER VIEW", @"ANALYZE", @"ANALYZE TABLE", @"AND", @"ANY", @"AS", @"ASC", @"ASCII", @"ASENSITIVE", @"AT", @"AUTHORS", @"AUTOEXTEND_SIZE", @"AUTO_INCREMENT", @"AVG", @"AVG_ROW_LENGTH", @"BACKUP", @"BACKUP TABLE", @"BEFORE", @"BEGIN", @"BETWEEN", @"BIGINT", @"BINARY", @"BINLOG", @"BIT", @"BLOB", @"BOOL", @"BOOLEAN", @"BOTH", @"BTREE", @"BY", @"BYTE", @"CACHE", @"CACHE INDEX", @"CALL", @"CASCADE", @"CASCADED", @"CASE", @"CHAIN", @"CHANGE", @"CHANGED", @"CHAR", @"CHARACTER", @"CHARACTER SET", @"CHARSET", @"CHECK", @"CHECK TABLE", @"CHECKSUM", @"CHECKSUM TABLE", @"CIPHER", @"CLIENT", @"CLOSE", @"COALESCE", @"CODE", @"COLLATE", @"COLLATION", @"COLUMN", @"COLUMNS", @"COLUMN_FORMAT" @"COMMENT", @"COMMIT", @"COMMITTED", @"COMPACT", @"COMPLETION", @"COMPRESSED", @"CONCURRENT", @"CONDITION", @"CONNECTION", @"CONSISTENT", @"CONSTRAINT", @"CONTAINS", @"CONTINUE", @"CONTRIBUTORS", @"CONVERT", @"CREATE", @"CREATE DATABASE", @"CREATE EVENT", @"CREATE FUNCTION", @"CREATE INDEX", @"CREATE LOGFILE GROUP", @"CREATE PROCEDURE", @"CREATE SCHEMA", @"CREATE TABLE", @"CREATE TABLESPACE", @"CREATE TRIGGER", @"CREATE USER", @"CREATE VIEW", @"CROSS", @"CUBE", @"CURRENT_DATE", @"CURRENT_TIME", @"CURRENT_TIMESTAMP", @"CURRENT_USER", @"CURSOR", @"DATA", @"DATABASE", @"DATABASES", @"DATAFILE", @"DATE", @"DATETIME", @"DAY", @"DAY_HOUR", @"DAY_MICROSECOND", @"DAY_MINUTE", @"DAY_SECOND", @"DEALLOCATE", @"DEALLOCATE PREPARE", @"DEC", @"DECIMAL", @"DECLARE", @"DEFAULT", @"DEFINER", @"DELAYED", @"DELAY_KEY_WRITE", @"DELETE", @"DELIMITER ", @"DELIMITER ;\n", @"DELIMITER ;;\n", @"DESC", @"DESCRIBE", @"DES_KEY_FILE", @"DETERMINISTIC", @"DIRECTORY", @"DISABLE", @"DISCARD", @"DISK", @"DISTINCT", @"DISTINCTROW", @"DIV", @"DO", @"DOUBLE", @"DROP", @"DROP DATABASE", @"DROP EVENT", @"DROP FOREIGN KEY", @"DROP FUNCTION", @"DROP INDEX", @"DROP LOGFILE GROUP", @"DROP PREPARE", @"DROP PRIMARY KEY", @"DROP PREPARE", @"DROP PROCEDURE", @"DROP SCHEMA", @"DROP SERVER", @"DROP TABLE", @"DROP TABLESPACE", @"DROP TRIGGER", @"DROP USER", @"DROP VIEW", @"DUAL", @"DUMPFILE", @"DUPLICATE", @"DYNAMIC", @"EACH", @"ELSE", @"ELSEIF", @"ENABLE", @"ENCLOSED", @"END", @"ENDS", @"ENGINE", @"ENGINES", @"ENUM", @"ERRORS", @"ESCAPE", @"ESCAPED", @"EVENT", @"EVENTS", @"EVERY", @"EXECUTE", @"EXISTS", @"EXIT", @"EXPANSION", @"EXPLAIN", @"EXTENDED", @"EXTENT_SIZE", @"FALSE", @"FAST", @"FETCH", @"FIELDS", @"FIELDS TERMINATED BY", @"FILE", @"FIRST", @"FIXED", @"FLOAT", @"FLOAT4", @"FLOAT8", @"FLUSH", @"FOR", @"FOR UPDATE", @"FORCE", @"FOREIGN", @"FOREIGN KEY", @"FOUND", @"FRAC_SECOND", @"FROM", @"FULL", @"FULLTEXT", @"FUNCTION", @"GEOMETRY", @"GEOMETRYCOLLECTION", @"GET_FORMAT", @"GLOBAL", @"GRANT", @"GRANTS", @"GROUP", @"GROUP BY", @"HANDLER", @"HASH", @"HAVING", @"HELP", @"HIGH_PRIORITY", @"HOSTS", @"HOUR", @"HOUR_MICROSECOND", @"HOUR_MINUTE", @"HOUR_SECOND", @"IDENTIFIED", @"IF", @"IGNORE", @"IMPORT", @"IN", @"INDEX", @"INDEXES", @"INFILE", @"INITIAL_SIZE", @"INNER", @"INNOBASE", @"INNODB", @"INOUT", @"INSENSITIVE", @"INSERT", @"INSERT_METHOD", @"INSTALL", @"INSTALL PLUGIN", @"INT", @"INT1", @"INT2", @"INT3", @"INT4", @"INT8", @"INTEGER", @"INTERVAL", @"INTO", @"INTO DUMPFILE", @"INTO OUTFILE", @"INTO TABLE", @"INVOKER", @"IO_THREAD", @"IS", @"ISOLATION", @"ISSUER", @"ITERATE", @"JOIN", @"KEY", @"KEYS", @"KEY_BLOCK_SIZE", @"KILL", @"LANGUAGE", @"LAST", @"LEADING", @"LEAVE", @"LEAVES", @"LEFT", @"LESS", @"LEVEL", @"LIKE", @"LIMIT", @"LINEAR", @"LINES", @"LINES TERMINATED BY", @"LINESTRING", @"LIST", @"LOAD DATA", @"LOAD INDEX INTO CACHE", @"LOAD XML", @"LOCAL", @"LOCALTIME", @"LOCALTIMESTAMP", @"LOCK", @"LOCK IN SHARE MODE", @"LOCK TABLES", @"LOCKS", @"LOGFILE", @"LOGS", @"LONG", @"LONGBLOB", @"LONGTEXT", @"LOOP", @"LOW_PRIORITY", @"MASTER", @"MASTER_CONNECT_RETRY", @"MASTER_HOST", @"MASTER_LOG_FILE", @"MASTER_LOG_POS", @"MASTER_PASSWORD", @"MASTER_PORT", @"MASTER_SERVER_ID", @"MASTER_SSL", @"MASTER_SSL_CA", @"MASTER_SSL_CAPATH", @"MASTER_SSL_CERT", @"MASTER_SSL_CIPHER", @"MASTER_SSL_KEY", @"MASTER_USER", @"MATCH", @"MAXVALUE", @"MAX_CONNECTIONS_PER_HOUR", @"MAX_QUERIES_PER_HOUR", @"MAX_ROWS", @"MAX_SIZE", @"MAX_UPDATES_PER_HOUR", @"MAX_USER_CONNECTIONS", @"MEDIUM", @"MEDIUMBLOB", @"MEDIUMINT", @"MEDIUMTEXT", @"MEMORY", @"MERGE", @"MICROSECOND", @"MIDDLEINT", @"MIGRATE", @"MINUTE", @"MINUTE_MICROSECOND", @"MINUTE_SECOND", @"MIN_ROWS", @"MOD", @"MODE", @"MODIFIES", @"MODIFY", @"MONTH", @"MULTILINESTRING", @"MULTIPOINT", @"MULTIPOLYGON", @"MUTEX", @"NAME", @"NAMES", @"NATIONAL", @"NATURAL", @"NCHAR", @"NDB", @"NDBCLUSTER", @"NEW", @"NEXT", @"NO", @"NODEGROUP", @"NONE", @"NOT", @"NO_WAIT", @"NO_WRITE_TO_BINLOG", @"NULL", @"NUMERIC", @"NVARCHAR", @"OFFSET", @"OLD_PASSWORD", @"ON", @"ONE", @"ONE_SHOT", @"OPEN", @"OPTIMIZE", @"OPTIMIZE TABLE", @"OPTION", @"OPTIONALLY", @"OPTIONALLY ENCLOSED BY", @"OPTIONS", @"OR", @"ORDER", @"ORDER BY", @"OUT", @"OUTER", @"OUTFILE", @"PACK_KEYS", @"PARSER", @"PARTIAL", @"PARTITION", @"PARTITIONING", @"PARTITIONS", @"PASSWORD", @"PHASE", @"PLUGIN", @"PLUGINS", @"POINT", @"POLYGON", @"PRECISION", @"PREPARE", @"PRESERVE", @"PREV", @"PRIMARY", @"PRIMARY KEY", @"PRIVILEGES", @"PROCEDURE", @"PROCEDURE ANALYSE", @"PROCESS", @"PROCESSLIST", @"PURGE", @"QUARTER", @"QUERY", @"QUICK", @"RANGE", @"READ", @"READS", @"READ_ONLY", @"READ_WRITE", @"REAL", @"REBUILD", @"RECOVER", @"REDOFILE", @"REDO_BUFFER_SIZE", @"REDUNDANT", @"REFERENCES", @"REGEXP", @"RELAY_LOG_FILE", @"RELAY_LOG_POS", @"RELAY_THREAD", @"RELEASE", @"RELOAD", @"REMOVE", @"RENAME", @"RENAME DATABASE", @"RENAME TABLE", @"REORGANIZE", @"REPAIR", @"REPAIR TABLE", @"REPEAT", @"REPEATABLE", @"REPLACE", @"REPLICATION", @"REQUIRE", @"RESET", @"RESET MASTER", @"RESTORE", @"RESTORE TABLE", @"RESTRICT", @"RESUME", @"RETURN", @"RETURNS", @"REVOKE", @"RIGHT", @"RLIKE", @"ROLLBACK", @"ROLLUP", @"ROUTINE", @"ROW", @"ROWS", @"ROWS IDENTIFIED BY" @"ROW_FORMAT", @"RTREE", @"SAVEPOINT", @"SCHEDULE", @"SCHEDULER", @"SCHEMA", @"SCHEMAS", @"SECOND", @"SECOND_MICROSECOND", @"SECURITY", @"SELECT", @"SELECT DISTINCT", @"SENSITIVE", @"SEPARATOR", @"SERIAL", @"SERIALIZABLE", @"SESSION", @"SET", @"SET GLOBAL", @"SET NAMES", @"SET PASSWORD", @"SHARE", @"SHOW", @"SHOW BINARY LOGS", @"SHOW BINLOG EVENTS", @"SHOW CHARACTER SET", @"SHOW COLLATION", @"SHOW COLUMNS", @"SHOW CONTRIBUTORS", @"SHOW CREATE DATABASE", @"SHOW CREATE EVENT", @"SHOW CREATE FUNCTION", @"SHOW CREATE PROCEDURE", @"SHOW CREATE SCHEMA", @"SHOW CREATE TABLE", @"SHOW CREATE TRIGGERS", @"SHOW CREATE VIEW", @"SHOW DATABASES", @"SHOW ENGINE", @"SHOW ENGINES", @"SHOW ERRORS", @"SHOW EVENTS", @"SHOW FIELDS", @"SHOW FULL PROCESSLIST", @"SHOW FUNCTION CODE", @"SHOW FUNCTION STATUS", @"SHOW GRANTS", @"SHOW INDEX", @"SHOW INNODB STATUS", @"SHOW KEYS", @"SHOW MASTER LOGS", @"SHOW MASTER STATUS", @"SHOW OPEN TABLES", @"SHOW PLUGINS", @"SHOW PRIVILEGES", @"SHOW PROCEDURE CODE", @"SHOW PROCEDURE STATUS", @"SHOW PROFILE", @"SHOW PROFILES", @"SHOW PROCESSLIST", @"SHOW SCHEDULER STATUS", @"SHOW SCHEMAS", @"SHOW SLAVE HOSTS", @"SHOW SLAVE STATUS", @"SHOW STATUS", @"SHOW STORAGE ENGINES", @"SHOW TABLE STATUS", @"SHOW TABLE TYPES", @"SHOW TABLES", @"SHOW TRIGGERS", @"SHOW VARIABLES", @"SHOW WARNINGS", @"SHUTDOWN", @"SIGNED", @"SIMPLE", @"SLAVE", @"SMALLINT", @"SNAPSHOT", @"SOME", @"SONAME", @"SOUNDS", @"SPATIAL", @"SPECIFIC", @"SQL_AUTO_IS_NULL", @"SQL_BIG_RESULT", @"SQL_BIG_SELECTS", @"SQL_BIG_TABLES", @"SQL_BUFFER_RESULT", @"SQL_CACHE", @"SQL_CALC_FOUND_ROWS", @"SQL_LOG_BIN", @"SQL_LOG_OFF", @"SQL_LOG_UPDATE", @"SQL_LOW_PRIORITY_UPDATES", @"SQL_MAX_JOIN_SIZE", @"SQL_NO_CACHE", @"SQL_QUOTE_SHOW_CREATE", @"SQL_SAFE_UPDATES", @"SQL_SELECT_LIMIT", @"SQL_SLAVE_SKIP_COUNTER", @"SQL_SMALL_RESULT", @"SQL_THREAD", @"SQL_TSI_DAY", @"SQL_TSI_FRAC_SECOND", @"SQL_TSI_HOUR", @"SQL_TSI_MINUTE", @"SQL_TSI_MONTH", @"SQL_TSI_QUARTER", @"SQL_TSI_SECOND", @"SQL_TSI_WEEK", @"SQL_TSI_YEAR", @"SQL_WARNINGS", @"SSL", @"START", @"START TRANSACTION", @"STARTING", @"STARTS", @"STATUS", @"STOP", @"STORAGE", @"STRAIGHT_JOIN", @"STRING", @"SUBJECT", @"SUBPARTITION", @"SUBPARTITIONS", @"SUPER", @"SUSPEND", @"TABLE", @"TABLES", @"TABLESPACE", @"TEMPORARY", @"TEMPTABLE", @"TERMINATED", @"TEXT", @"THAN", @"THEN", @"TIME", @"TIMESTAMP", @"TIMESTAMPADD", @"TIMESTAMPDIFF", @"TINYBLOB", @"TINYINT", @"TINYTEXT", @"TO", @"TRAILING", @"TRANSACTION", @"TRIGGER", @"TRIGGERS", @"TRUE", @"TRUNCATE", @"TYPE", @"TYPES", @"UNCOMMITTED", @"UNDEFINED", @"UNDO", @"UNDOFILE", @"UNDO_BUFFER_SIZE", @"UNICODE", @"UNINSTALL", @"UNINSTALL PLUGIN", @"UNION", @"UNIQUE", @"UNKNOWN", @"UNLOCK", @"UNLOCK TABLES", @"UNSIGNED", @"UNTIL", @"UPDATE", @"UPGRADE", @"USAGE", @"USE", @"USER", @"USER_RESOURCES", @"USE_FRM", @"USING", @"UTC_DATE", @"UTC_TIME", @"UTC_TIMESTAMP", @"VALUE", @"VALUES", @"VARBINARY", @"VARCHAR", @"VARCHARACTER", @"VARIABLES", @"VARYING", @"VIEW", @"WAIT", @"WARNINGS", @"WEEK", @"WHEN", @"WHERE", @"WHILE", @"WITH", @"WITH CONSISTENT SNAPSHOT", @"WORK", @"WRITE", @"X509", @"XA", @"XOR", @"YEAR", @"YEAR_MONTH", @"ZEROFILL", nil]; } /* * List of fucntions for autocompletion. If you add a keyword here, * it should also be added to the flex file SPEditorTokens.l */ -(NSArray *)functions { return [NSArray arrayWithObjects: @"ABS", @"ACOS", @"ADDDATE", @"ADDTIME", @"AES_DECRYPT", @"AES_ENCRYPT", @"AREA", @"ASBINARY", @"ASCII", @"ASIN", @"ASTEXT", @"ATAN", @"ATAN2", @"AVG", @"BDMPOLYFROMTEXT", @"BDMPOLYFROMWKB", @"BDPOLYFROMTEXT", @"BDPOLYFROMWKB", @"BENCHMARK", @"BIN", @"BIT_AND", @"BIT_COUNT", @"BIT_LENGTH", @"BIT_OR", @"BIT_XOR", @"BOUNDARY", @"BUFFER", @"CAST", @"CEIL", @"CEILING", @"CENTROID", @"CHAR", @"CHARACTER_LENGTH", @"CHARSET", @"CHAR_LENGTH", @"COALESCE", @"COERCIBILITY", @"COLLATION", @"COMPRESS", @"CONCAT", @"CONCAT_WS", @"CONNECTION_ID", @"CONTAINS", @"CONV", @"CONVERT", @"CONVERT_TZ", @"CONVEXHULL", @"COS", @"COT", @"COUNT", @"COUNT(*)", @"CRC32", @"CROSSES", @"CURDATE", @"CURRENT_DATE", @"CURRENT_TIME", @"CURRENT_TIMESTAMP", @"CURRENT_USER", @"CURTIME", @"DATABASE", @"DATE", @"DATEDIFF", @"DATE_ADD", @"DATE_DIFF", @"DATE_FORMAT", @"DATE_SUB", @"DAY", @"DAYNAME", @"DAYOFMONTH", @"DAYOFWEEK", @"DAYOFYEAR", @"DECODE", @"DEFAULT", @"DEGREES", @"DES_DECRYPT", @"DES_ENCRYPT", @"DIFFERENCE", @"DIMENSION", @"DISJOINT", @"DISTANCE", @"ELT", @"ENCODE", @"ENCRYPT", @"ENDPOINT", @"ENVELOPE", @"EQUALS", @"EXP", @"EXPORT_SET", @"EXTERIORRING", @"EXTRACT", @"EXTRACTVALUE", @"FIELD", @"FIND_IN_SET", @"FLOOR", @"FORMAT", @"FOUND_ROWS", @"FROM_DAYS", @"FROM_UNIXTIME", @"GEOMCOLLFROMTEXT", @"GEOMCOLLFROMWKB", @"GEOMETRYCOLLECTION", @"GEOMETRYCOLLECTIONFROMTEXT", @"GEOMETRYCOLLECTIONFROMWKB", @"GEOMETRYFROMTEXT", @"GEOMETRYFROMWKB", @"GEOMETRYN", @"GEOMETRYTYPE", @"GEOMFROMTEXT", @"GEOMFROMWKB", @"GET_FORMAT", @"GET_LOCK", @"GLENGTH", @"GREATEST", @"GROUP_CONCAT", @"GROUP_UNIQUE_USERS", @"HEX", @"HOUR", @"IF", @"IFNULL", @"INET_ATON", @"INET_NTOA", @"INSERT", @"INSERT_ID", @"INSTR", @"INTERIORRINGN", @"INTERSECTION", @"INTERSECTS", @"INTERVAL", @"ISCLOSED", @"ISEMPTY", @"ISNULL", @"ISRING", @"ISSIMPLE", @"IS_FREE_LOCK", @"IS_USED_LOCK", @"LAST_DAY", @"LAST_INSERT_ID", @"LCASE", @"LEAST", @"LEFT", @"LENGTH", @"LINEFROMTEXT", @"LINEFROMWKB", @"LINESTRING", @"LINESTRINGFROMTEXT", @"LINESTRINGFROMWKB", @"LN", @"LOAD_FILE", @"LOCALTIME", @"LOCALTIMESTAMP", @"LOCATE", @"LOG", @"LOG10", @"LOG2", @"LOWER", @"LPAD", @"LTRIM", @"MAKEDATE", @"MAKETIME", @"MAKE_SET", @"MASTER_POS_WAIT", @"MAX", @"MBRCONTAINS", @"MBRDISJOINT", @"MBREQUAL", @"MBRINTERSECTS", @"MBROVERLAPS", @"MBRTOUCHES", @"MBRWITHIN", @"MD5", @"MICROSECOND", @"MID", @"MIN", @"MINUTE", @"MLINEFROMTEXT", @"MLINEFROMWKB", @"MOD", @"MONTH", @"MONTHNAME", @"NOW", @"MPOINTFROMTEXT", @"MPOINTFROMWKB", @"MPOLYFROMTEXT", @"MPOLYFROMWKB", @"MULTILINESTRING", @"MULTILINESTRINGFROMTEXT", @"MULTILINESTRINGFROMWKB", @"MULTIPOINT", @"MULTIPOINTFROMTEXT", @"MULTIPOINTFROMWKB", @"MULTIPOLYGON", @"MULTIPOLYGONFROMTEXT", @"MULTIPOLYGONFROMWKB", @"NAME_CONST", @"NOW", @"NULLIF", @"NUMGEOMETRIES", @"NUMINTERIORRINGS", @"NUMPOINTS", @"OCT", @"OCTET_LENGTH", @"OLD_PASSWORD", @"ORD", @"OVERLAPS", @"PASSWORD", @"PERIOD_ADD", @"PERIOD_DIFF", @"PI", @"POINT", @"POINTFROMTEXT", @"POINTFROMWKB", @"POINTN", @"POINTONSURFACE", @"POLYFROMTEXT", @"POLYFROMWKB", @"POLYGON", @"POLYGONFROMTEXT", @"POLYGONFROMWKB", @"POSITION", @"POW", @"POWER", @"QUARTER", @"QUOTE", @"RADIANS", @"RAND", @"RELATED", @"RELEASE_LOCK", @"REPEAT", @"REPLACE", @"REVERSE", @"RIGHT", @"ROUND", @"ROW_COUNT", @"RPAD", @"RTRIM", @"SCHEMA", @"SECOND", @"SEC_TO_TIME", @"SESSION_USER", @"SHA", @"SHA1", @"SIGN", @"SIN", @"SLEEP", @"SOUNDEX", @"SPACE", @"SQRT", @"SRID", @"STARTPOINT", @"STD", @"STDDEV", @"STDDEV_POP", @"STDDEV_SAMP", @"STRCMP", @"STR_TO_DATE", @"SUBDATE", @"SUBSTR", @"SUBSTRING", @"SUBSTRING_INDEX", @"SUBTIME", @"SUM", @"SYMDIFFERENCE", @"SYSDATE", @"SYSTEM_USER", @"TAN", @"TIME", @"TIMEDIFF", @"TIMESTAMP", @"TIMESTAMPADD", @"TIMESTAMPDIFF", @"TIME_FORMAT", @"TIME_TO_SEC", @"TOUCHES", @"TO_DAYS", @"TRIM", @"TRUNCATE", @"UCASE", @"UNCOMPRESS", @"UNCOMPRESSED_LENGTH", @"UNHEX", @"UNIQUE_USERS", @"UNIX_TIMESTAMP", @"UPDATEXML", @"UPPER", @"USER", @"UTC_DATE", @"UTC_TIME", @"UTC_TIMESTAMP", @"UUID", @"VARIANCE", @"VAR_POP", @"VAR_SAMP", @"VERSION", @"WEEK", @"WEEKDAY", @"WEEKOFYEAR", @"WITHIN", @"YEAR", @"YEARWEEK", nil]; } /* * Set whether this text view should apply the indentation on the current line to new lines. */ - (void)setAutoindent:(BOOL)enableAutoindent { autoindentEnabled = enableAutoindent; } /* * Retrieve whether this text view applies indentation on the current line to new lines. */ - (BOOL)autoindent { return autoindentEnabled; } /* * Set whether this text view should not autoindent when the Enter key is used, as opposed * to the return key. Also catches function-return. */ - (void)setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter { autoindentIgnoresEnter = enableAutoindentIgnoresEnter; } /* * Retrieve whether this text view should not autoindent when the Enter key is used. */ - (BOOL)autoindentIgnoresEnter { return autoindentIgnoresEnter; } /* * Set whether this text view should automatically create the matching closing char for ", ', ` and ( chars. */ - (void)setAutopair:(BOOL)enableAutopair { autopairEnabled = enableAutopair; } /* * Retrieve whether this text view automatically creates the matching closing char for ", ', ` and ( chars. */ - (BOOL)autopair { return autopairEnabled; } /* * Set whether MySQL Help should be automatically invoked while typing. */ - (void)setAutohelp:(BOOL)enableAutohelp { autohelpEnabled = enableAutohelp; } /* * Retrieve whether MySQL Help should be automatically invoked while typing. */ - (BOOL)autohelp { return autohelpEnabled; } /* * Set whether SQL keywords should be automatically uppercased. */ - (void)setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords { autouppercaseKeywordsEnabled = enableAutouppercaseKeywords; } /* * Retrieve whether SQL keywords should be automatically uppercased. */ - (BOOL)autouppercaseKeywords { return autouppercaseKeywordsEnabled; } /* * If enabled it shows the MySQL Help for the current word (not inside quotes) or for the selection * after an adjustable delay if the textView is idle, i.e. no user interaction. */ - (void)autoHelp { if(![prefs boolForKey:SPCustomQueryUpdateAutoHelp]) return; // If selection show Help for it if([self selectedRange].length) { [[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] performSelector:@selector(showAutoHelpForCurrentWord:) withObject:self afterDelay:0.1]; return; } // Otherwise show Help if caret is not inside quotes NSUInteger cursorPosition = [self selectedRange].location; if (cursorPosition >= [[self string] length]) cursorPosition--; if(cursorPosition > -1 && (![[self textStorage] attribute:kQuote atIndex:cursorPosition effectiveRange:nil]||[[self textStorage] attribute:kSQLkeyword atIndex:cursorPosition effectiveRange:nil])) [[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] performSelector:@selector(showAutoHelpForCurrentWord:) withObject:self afterDelay:0.1]; } /* * Syntax Highlighting. * * (The main bottleneck is the [NSTextStorage addAttribute:value:range:] method - the parsing itself is really fast!) * Some sample code from Andrew Choi ( http://members.shaw.ca/akochoi-old/blog/2003/11-09/index.html#3 ) has been reused. */ - (void)doSyntaxHighlighting { NSTextStorage *textStore = [self textStorage]; NSString *selfstr = [self string]; NSUInteger strlength = [selfstr length]; NSRange textRange; // If text larger than SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING // do highlighting partly (max SP_SYNTAX_HILITE_BIAS*2). // The approach is to take the middle position of the current view port // and highlight only ±SP_SYNTAX_HILITE_BIAS of that middle position // considering of line starts resp. ends if(strlength > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) { // Cancel all doSyntaxHighlighting requests [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doSyntaxHighlighting) object:nil]; // Get the text range currently displayed in the view port NSRect visibleRect = [[[self enclosingScrollView] contentView] documentVisibleRect]; NSRange visibleRange = [[self layoutManager] glyphRangeForBoundingRectWithoutAdditionalLayout:visibleRect inTextContainer:[self textContainer]]; if(!visibleRange.length) return; // Take roughly the middle position in the current view port NSInteger curPos = visibleRange.location+(NSInteger)(visibleRange.length/2); // get the last line to parse due to SP_SYNTAX_HILITE_BIAS NSInteger end = curPos + SP_SYNTAX_HILITE_BIAS; if (end > strlength ) { end = strlength; } else { while(end < strlength) { if([selfstr characterAtIndex:end]=='\n') break; end++; } } // get the first line to parse due to SP_SYNTAX_HILITE_BIAS NSInteger start = end - (SP_SYNTAX_HILITE_BIAS*2); if (start > 0) while(start>-1) { if([selfstr characterAtIndex:start]=='\n') break; start--; } else start = 0; textRange = NSMakeRange(start, end-start); // only to be sure that nothing went wrongly textRange = NSIntersectionRange(textRange, NSMakeRange(0, [textStore length])); if (!textRange.length) return; } else { // If text size is less SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING // process syntax highlighting for the entire text view buffer textRange = NSMakeRange(0,strlength); } NSColor *tokenColor; NSColor *commentColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorCommentColor]] retain]; NSColor *quoteColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorQuoteColor]] retain]; NSColor *keywordColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorSQLKeywordColor]] retain]; NSColor *backtickColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorBacktickColor]] retain]; NSColor *numericColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorNumericColor]] retain]; NSColor *variableColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorVariableColor]] retain]; NSColor *textColor = [[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorTextColor]] retain]; BOOL autouppercaseKeywords = [prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]; NSUInteger tokenEnd, token; NSRange tokenRange; // first remove the old colors and kQuote [textStore removeAttribute:NSForegroundColorAttributeName range:textRange]; // mainly for suppressing auto-pairing in [textStore removeAttribute:kLEXToken range:textRange]; // initialise flex yyuoffset = textRange.location; yyuleng = 0; yy_switch_to_buffer(yy_scan_string(NSStringUTF8String([selfstr substringWithRange:textRange]))); // NO if lexer doesn't find a token to suppress auto-uppercasing // and continue earlier. BOOL allowToCheckForUpperCase; // now loop through all the tokens while (token=yylex()){ allowToCheckForUpperCase = YES; switch (token) { case SPT_SINGLE_QUOTED_TEXT: case SPT_DOUBLE_QUOTED_TEXT: tokenColor = quoteColor; break; case SPT_BACKTICK_QUOTED_TEXT: tokenColor = backtickColor; break; case SPT_RESERVED_WORD: tokenColor = keywordColor; break; case SPT_NUMERIC: tokenColor = numericColor; break; case SPT_COMMENT: tokenColor = commentColor; break; case SPT_VARIABLE: tokenColor = variableColor; break; case SPT_WHITESPACE: continue; break; default: tokenColor = textColor; allowToCheckForUpperCase = NO; } tokenRange = NSMakeRange(yyuoffset, yyuleng); // make sure that tokenRange is valid (and therefore within textRange) // otherwise a bug in the lex code could cause the the TextView to crash // NOTE Disabled for testing purposes for speed it up tokenRange = NSIntersectionRange(tokenRange, textRange); if (!tokenRange.length) continue; // If the current token is marked as SQL keyword, uppercase it if required. tokenEnd = tokenRange.location+tokenRange.length-1; // Check the end of the token if (allowToCheckForUpperCase && autouppercaseKeywords && !delBackwardsWasPressed && [[textStore attribute:kSQLkeyword atIndex:tokenEnd effectiveRange:nil] isEqualToString:kValue]) // check if next char is not a kSQLkeyword or current kSQLkeyword is at the end; // if so then upper case keyword if not already done // @try catch() for catching valid index esp. after deleteBackward: { NSString* curTokenString = [selfstr substringWithRange:tokenRange]; NSString* upperCaseCurTokenString = [curTokenString uppercaseString]; BOOL doIt = NO; @try { doIt = ![[textStore attribute:kSQLkeyword atIndex:tokenEnd+1 effectiveRange:nil] isEqualToString:kValue]; } @catch(id ae) { doIt = NO; } if(doIt && ![upperCaseCurTokenString isEqualToString:curTokenString]) { // Register it for undo works only partly for now, at least the uppercased keyword will be selected [self shouldChangeTextInRange:tokenRange replacementString:curTokenString]; [self replaceCharactersInRange:tokenRange withString:upperCaseCurTokenString]; } } NSMutableAttributedStringAddAttributeValueRange(textStore, NSForegroundColorAttributeName, tokenColor, tokenRange); if(!allowToCheckForUpperCase) continue; // Add an attribute to be used in the auto-pairing (keyDown:) // to disable auto-pairing if caret is inside of any token found by lex. // For discussion: maybe change it later (only for quotes not keywords?) if(token < 6) NSMutableAttributedStringAddAttributeValueRange(textStore, kLEXToken, kLEXTokenValue, tokenRange); // Mark each SQL keyword for auto-uppercasing and do it for the next textStorageDidProcessEditing: event. // Performing it one token later allows words which start as reserved keywords to be entered. if(token == SPT_RESERVED_WORD) NSMutableAttributedStringAddAttributeValueRange(textStore, kSQLkeyword, kValue, tokenRange); // Add an attribute to be used to distinguish quotes from keywords etc. // used e.g. in completion suggestions else if(token < 4) NSMutableAttributedStringAddAttributeValueRange(textStore, kQuote, kQuoteValue, tokenRange); //distinguish backtick quoted word for completion else if(token == SPT_BACKTICK_QUOTED_TEXT) NSMutableAttributedStringAddAttributeValueRange(textStore, kBTQuote, kBTQuoteValue, tokenRange); } [commentColor release]; [quoteColor release]; [keywordColor release]; [backtickColor release]; [numericColor release]; [variableColor release]; [textColor release]; } - (void)drawRect:(NSRect)rect { [super drawRect:rect]; // Highlight snippets coming from the Query Favorite text macro if(snippetControlCounter > -1) { NSInteger i; [[NSColor colorWithCalibratedRed:0.0 green:0.5 blue:0.0 alpha:0.1] setFill]; for(i=0; i -1) { NSRange glRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(snippetControlArray[i][0],snippetControlArray[i][1]) actualCharacterRange:NULL]; if(glRange.length) { NSRect boundingRect = [[self layoutManager] boundingRectForGlyphRange:glRange inTextContainer:[self textContainer]]; boundingRect = NSInsetRect(boundingRect, 0, 2); NSBezierPath *aBezierPath = [NSBezierPath bezierPathWithRoundedRect:boundingRect xRadius:4 yRadius:15]; [aBezierPath fill]; } } } } } #pragma mark - #pragma mark context menu /* * Add a menu item to context menu for looking up mysql documentation. */ - (NSMenu *)menuForEvent:(NSEvent *)event { // Set title of the menu item if([self selectedRange].length) showMySQLHelpFor = NSLocalizedString(@"MySQL Help for Selection", @"MySQL Help for Selection"); else showMySQLHelpFor = NSLocalizedString(@"MySQL Help for Word", @"MySQL Help for Word"); // Add the menu items for // - MySQL Help for Word/Selection // - Copy as RTF // - Select Active Query // if it doesn't yet exist NSMenu *menu = [[self class] defaultMenu]; if ([[[self class] defaultMenu] itemWithTag:SP_CQ_SEARCH_IN_MYSQL_HELP_MENU_ITEM_TAG] == nil) { [menu insertItem:[NSMenuItem separatorItem] atIndex:3]; NSMenuItem *showMySQLHelpForMenuItem = [[NSMenuItem alloc] initWithTitle:showMySQLHelpFor action:@selector(showMySQLHelpForCurrentWord:) keyEquivalent:@"h"]; [showMySQLHelpForMenuItem setTag:SP_CQ_SEARCH_IN_MYSQL_HELP_MENU_ITEM_TAG]; [showMySQLHelpForMenuItem setKeyEquivalentModifierMask:NSControlKeyMask]; [menu insertItem:showMySQLHelpForMenuItem atIndex:4]; [showMySQLHelpForMenuItem release]; } else { [[menu itemWithTag:SP_CQ_SEARCH_IN_MYSQL_HELP_MENU_ITEM_TAG] setTitle:showMySQLHelpFor]; } if ([[[self class] defaultMenu] itemWithTag:SP_CQ_COPY_AS_RTF_MENU_ITEM_TAG] == nil) { NSMenuItem *copyAsRTFMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Copy as RTF", @"Copy as RTF") action:@selector(copyAsRTF) keyEquivalent:@"c"]; [copyAsRTFMenuItem setTag:SP_CQ_COPY_AS_RTF_MENU_ITEM_TAG]; [copyAsRTFMenuItem setKeyEquivalentModifierMask:NSControlKeyMask]; [menu insertItem:copyAsRTFMenuItem atIndex:2]; [copyAsRTFMenuItem release]; } if ([[[self class] defaultMenu] itemWithTag:SP_CQ_SELECT_CURRENT_QUERY_MENU_ITEM_TAG] == nil) { NSMenuItem *selectCurrentQueryMenuItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Select Active Query", @"Select Active Query") action:@selector(selectCurrentQuery) keyEquivalent:@"y"]; [selectCurrentQueryMenuItem setTag:SP_CQ_SELECT_CURRENT_QUERY_MENU_ITEM_TAG]; [selectCurrentQueryMenuItem setKeyEquivalentModifierMask:NSControlKeyMask]; [menu insertItem:selectCurrentQueryMenuItem atIndex:4]; [selectCurrentQueryMenuItem release]; } // Hide "Select Active Query" if self is not editable [[menu itemAtIndex:4] setHidden:![self isEditable]]; if([[[self window] delegate] valueForKeyPath:@"customQueryInstance"]) { [[menu itemAtIndex:5] setHidden:NO]; [[menu itemAtIndex:6] setHidden:NO]; } else { [[menu itemAtIndex:5] setHidden:YES]; [[menu itemAtIndex:6] setHidden:YES]; } return menu; } /* * Disable the search in the MySQL help function when getRangeForCurrentWord returns zero length. */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { // Enable or disable the search in the MySQL help menu item depending on whether there is a // selection and whether it is a reasonable length. if ([menuItem action] == @selector(showMySQLHelpForCurrentWord:)) { NSUInteger stringSize = [self getRangeForCurrentWord].length; return (stringSize || stringSize > 64); } // Enable Copy as RTF if something is selected if ([menuItem action] == @selector(copyAsRTF)) { return ([self selectedRange].length>0); } // Validate Select Active Query if ([menuItem action] == @selector(selectCurrentQuery)) { return ([self isEditable] && [[self delegate] isKindOfClass:[CustomQuery class]]); } // Disable "Copy with Column Names" and "Copy as SQL INSERT" // in the main menu if ( [menuItem tag] == MENU_EDIT_COPY_WITH_COLUMN || [menuItem tag] == MENU_EDIT_COPY_AS_SQL ) { return NO; } return YES; } #pragma mark - #pragma mark delegates /* * Update colors by setting them in the Preference pane. */ - (void)changeColor:(id)sender { [self setInsertionPointColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorCaretColor]]]; // Remember the old selected range NSRange oldRange = [self selectedRange]; // Invoke syntax highlighting [self setSelectedRange:NSMakeRange(oldRange.location,0)]; [self insertText:@""]; // Reset old selected range [self setSelectedRange:oldRange]; } /* * Scrollview delegate after the textView's view port was changed. * Manily used to update the syntax highlighting for a large text size. */ - (void) boundsDidChangeNotification:(NSNotification *)notification { // Invoke syntax highlighting if text view port was changed for large text if(startListeningToBoundChanges && [[self string] length] > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doSyntaxHighlighting) object:nil]; if(![[self textStorage] changeInLength]) [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.4]; } } /* * Performs syntax highlighting, re-init autohelp, and re-calculation of snippets after a text change */ - (void)textStorageDidProcessEditing:(NSNotification *)notification { NSTextStorage *textStore = [notification object]; // Make sure that the notification is from the correct textStorage object if (textStore!=[self textStorage]) return; NSInteger editedMask = [textStore editedMask]; // Start autohelp only if the user really changed the text (not e.g. for setting a background color) if([prefs boolForKey:SPCustomQueryUpdateAutoHelp] && editedMask != 1) { [self performSelector:@selector(autoHelp) withObject:nil afterDelay:[[[prefs valueForKey:SPCustomQueryAutoHelpDelay] retain] doubleValue]]; } // Cancel calling doSyntaxHighlighting for large text if([[self string] length] > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(doSyntaxHighlighting) object:nil]; // Do syntax highlighting/re-calculate snippet ranges only if the user really changed the text if(editedMask != 1) { // Re-calculate snippet ranges if snippet session is active if(snippetControlCounter > -1 && !snippetWasJustInserted) { if([self checkForCaretInsideSnippet]) { NSInteger editStartPosition = [textStore editedRange].location; NSInteger changeInLength = [textStore changeInLength]; NSInteger i; // Remove any fully nested snippets relative to the current snippet which is was edited if(snippetControlArray[i][0] > -1 && i != snippetControlMax) { NSInteger currentSnippetLocation = snippetControlArray[currentSnippetIndex][0]; NSInteger currentSnippetMaxRange = snippetControlArray[currentSnippetIndex][0] + snippetControlArray[currentSnippetIndex][1]; for(i=0; i -1 && i != currentSnippetIndex && snippetControlArray[i][0] >= currentSnippetLocation && snippetControlArray[i][0] <= currentSnippetMaxRange && snippetControlArray[i][0] + snippetControlArray[i][1] >= currentSnippetLocation && snippetControlArray[i][0] + snippetControlArray[i][1] <= currentSnippetMaxRange ) { snippetControlArray[i][0] = -1; snippetControlArray[i][1] = -1; snippetControlArray[i][2] = -1; } } } // Adjust length change to current snippet snippetControlArray[currentSnippetIndex][1] += changeInLength; // If length < 0 break snippet input if(snippetControlArray[currentSnippetIndex][1] < 0) { [self endSnippetSession]; } else { // Adjust start position of snippets after caret position for(i=0; i<=snippetControlMax; i++) { if(snippetControlArray[i][0] > -1 && i != currentSnippetIndex) { if(editStartPosition <= snippetControlArray[i][0]) { snippetControlArray[i][0] += changeInLength; } else if(editStartPosition >= snippetControlArray[i][0] && editStartPosition <= snippetControlArray[i][0] + snippetControlArray[i][1]) { snippetControlArray[i][1] += changeInLength; } } } } } else { [self endSnippetSession]; } } [self doSyntaxHighlighting]; } startListeningToBoundChanges = YES; } /* * Show only setable modes in the font panel */ - (NSUInteger)validModesForFontPanel:(NSFontPanel *)fontPanel { return (NSFontPanelFaceModeMask | NSFontPanelSizeModeMask); } #pragma mark - #pragma mark drag&drop /////////////////////////// // Dragging methods /////////////////////////// /* * Insert the content of a dragged file path or if ⌘ is pressed * while dragging insert the file path */ - (BOOL)performDragOperation:(id )sender { NSPasteboard *pboard = [sender draggingPasteboard]; if ( [[pboard types] containsObject:NSFilenamesPboardType] && [[pboard types] containsObject:@"CorePasteboardFlavorType 0x54455854"]) return [super performDragOperation:sender]; if ( [[pboard types] containsObject:NSFilenamesPboardType] ) { NSArray *files = [pboard propertyListForType:NSFilenamesPboardType]; // Only one file path is allowed if([files count] > 1) { NSLog(@"%@", NSLocalizedString(@"Only one dragged item allowed.",@"Only one dragged item allowed.")); return YES; } NSString *filepath = [[pboard propertyListForType:NSFilenamesPboardType] objectAtIndex:0]; // if (([filenamesAttributes fileHFSTypeCode] == 'clpt' && [filenamesAttributes fileHFSCreatorCode] == 'MACS') || [[filename pathExtension] isEqualToString:@"textClipping"] == YES) { // // } // Set the new insertion point NSPoint draggingLocation = [sender draggingLocation]; draggingLocation = [self convertPoint:draggingLocation fromView:nil]; NSUInteger characterIndex = [self characterIndexOfPoint:draggingLocation]; [self setSelectedRange:NSMakeRange(characterIndex,0)]; // Check if user pressed ⌘ while dragging for inserting only the file path if([sender draggingSourceOperationMask] == 4) { [self insertText:filepath]; return YES; } // Check size and NSFileType NSDictionary *attr = [[NSFileManager defaultManager] fileAttributesAtPath:filepath traverseLink:YES]; if(attr) { NSNumber *filesize = [attr objectForKey:NSFileSize]; NSString *filetype = [attr objectForKey:NSFileType]; if(filetype == NSFileTypeRegular && filesize) { // Ask for confirmation if file content is larger than 1MB if([filesize unsignedLongValue] > 1000000) { NSAlert *alert = [[NSAlert alloc] init]; [alert addButtonWithTitle:NSLocalizedString(@"OK", @"OK button")]; [alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"cancel button")]; [alert setInformativeText:[NSString stringWithFormat:NSLocalizedString(@"Do you really want to proceed with %.1f MB of data?", @"message of panel asking for confirmation for inserting large text from dragging action"), [filesize unsignedLongValue]/1048576.0]]; [alert setHelpAnchor:filepath]; [alert setMessageText:NSLocalizedString(@"Warning",@"warning")]; [alert setAlertStyle:NSWarningAlertStyle]; [alert beginSheetModalForWindow:[self window] modalDelegate:self didEndSelector:@selector(dragAlertSheetDidEnd:returnCode:contextInfo:) contextInfo:nil]; [alert release]; } else [self insertFileContentOfFile:filepath]; } } return YES; } return [super performDragOperation:sender]; } /* * Confirmation sheetDidEnd method */ - (void)dragAlertSheetDidEnd:(NSAlert *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { [[sheet window] orderOut:nil]; if ( returnCode == NSAlertFirstButtonReturn ) [self insertFileContentOfFile:[sheet helpAnchor]]; } /* * Convert a NSPoint, usually the mouse location, to * a character index of the text view. */ - (NSUInteger)characterIndexOfPoint:(NSPoint)aPoint { NSUInteger glyphIndex; NSLayoutManager *layoutManager = [self layoutManager]; CGFloat fraction; NSRange range; range = [layoutManager glyphRangeForTextContainer:[self textContainer]]; glyphIndex = [layoutManager glyphIndexForPoint:aPoint inTextContainer:[self textContainer] fractionOfDistanceThroughGlyph:&fraction]; if( fraction > 0.5 ) glyphIndex++; if( glyphIndex == NSMaxRange(range) ) return [[self textStorage] length]; else return [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; } /* * Insert content of a plain text file for a given path. * In addition it tries to figure out the file's text encoding heuristically. */ - (void)insertFileContentOfFile:(NSString *)aPath { NSError *err = nil; NSStringEncoding enc; NSString *content = nil; // Make usage of the UNIX command "file" to get an info // about file type and encoding. NSTask *task=[[NSTask alloc] init]; NSPipe *pipe=[[NSPipe alloc] init]; NSFileHandle *handle; NSString *result; [task setLaunchPath:@"/usr/bin/file"]; [task setArguments:[NSArray arrayWithObjects:aPath, @"-Ib", nil]]; [task setStandardOutput:pipe]; handle=[pipe fileHandleForReading]; [task launch]; result=[[NSString alloc] initWithData:[handle readDataToEndOfFile] encoding:NSASCIIStringEncoding]; [pipe release]; [task release]; // UTF16/32 files are detected as application/octet-stream resp. audio/mpeg if( [result hasPrefix:@"text/plain"] || [[[aPath pathExtension] lowercaseString] isEqualToString:@"sql"] || [[[aPath pathExtension] lowercaseString] isEqualToString:@"txt"] || [result hasPrefix:@"audio/mpeg"] || [result hasPrefix:@"application/octet-stream"] ) { // if UTF16/32 cocoa will try to find the correct encoding if([result hasPrefix:@"application/octet-stream"] || [result hasPrefix:@"audio/mpeg"] || [result rangeOfString:@"utf-16"].length) enc = 0; else if([result rangeOfString:@"utf-8"].length) enc = NSUTF8StringEncoding; else if([result rangeOfString:@"iso-8859-1"].length) enc = NSISOLatin1StringEncoding; else if([result rangeOfString:@"us-ascii"].length) enc = NSASCIIStringEncoding; else enc = 0; if(enc == 0) // cocoa tries to detect the encoding content = [NSString stringWithContentsOfFile:aPath usedEncoding:&enc error:&err]; else content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err]; if(content) { [self insertText:content]; [result release]; // [self insertText:@""]; // Invoke keyword uppercasing return; } // If UNIX "file" failed try cocoa's encoding detection content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err]; if(content) { [self insertText:content]; [result release]; // [self insertText:@""]; // Invoke keyword uppercasing return; } } [result release]; NSLog(@"%@ ‘%@’.", NSLocalizedString(@"Couldn't read the file content of", @"Couldn't read the file content of"), aPath); } - (void) dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [lineNumberView release]; [super dealloc]; } @end