From c06ad0189beb544710b6d47b2978a5614d036cdc Mon Sep 17 00:00:00 2001 From: stuconnolly Date: Thu, 27 May 2010 17:35:20 +0000 Subject: Rename CMTextView to SPTextView. --- Source/CMTextView.h | 149 -- Source/CMTextView.m | 3319 -------------------------------------- Source/CustomQuery.h | 4 +- Source/SPContentFilterManager.m | 2 +- Source/SPFieldMapperController.h | 4 +- Source/SPFieldMapperController.m | 2 +- Source/SPNarrowDownCompletion.m | 2 +- Source/SPQueryFavoriteManager.h | 4 +- Source/SPQueryFavoriteManager.m | 4 +- Source/SPTextView.h | 148 ++ Source/SPTextView.m | 3319 ++++++++++++++++++++++++++++++++++++++ 11 files changed, 3478 insertions(+), 3479 deletions(-) delete mode 100644 Source/CMTextView.h delete mode 100644 Source/CMTextView.m create mode 100644 Source/SPTextView.h create mode 100644 Source/SPTextView.m (limited to 'Source') diff --git a/Source/CMTextView.h b/Source/CMTextView.h deleted file mode 100644 index 51f2153d..00000000 --- a/Source/CMTextView.h +++ /dev/null @@ -1,149 +0,0 @@ -// -// $Id$ -// -// CMTextView.h -// 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 -#import - -#import "NoodleLineNumberView.h" -#import "CMCopyTable.h" -#import "CMTextView.h" - -#define SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING 10000 - -@class SPNarrowDownCompletion, TableDocument, TablesList, CustomQuery; - -@interface CMTextView : NSTextView -{ - IBOutlet TableDocument *tableDocumentInstance; - IBOutlet TablesList *tablesListInstance; - IBOutlet CustomQuery *customQueryInstance; - - BOOL autoindentEnabled; - BOOL autopairEnabled; - BOOL autoindentIgnoresEnter; - BOOL autouppercaseKeywordsEnabled; - BOOL delBackwardsWasPressed; - BOOL autohelpEnabled; - NoodleLineNumberView *lineNumberView; - - BOOL startListeningToBoundChanges; - BOOL textBufferSizeIncreased; - - NSString *showMySQLHelpFor; - - IBOutlet NSScrollView *scrollView; - SPNarrowDownCompletion *completionPopup; - - NSUserDefaults *prefs; - - MCPConnection *mySQLConnection; - NSInteger mySQLmajorVersion; - - NSInteger snippetControlArray[20][3]; - NSInteger snippetMirroredControlArray[20][3]; - NSInteger snippetControlCounter; - NSInteger snippetControlMax; - NSInteger currentSnippetIndex; - NSInteger mirroredCounter; - BOOL snippetWasJustInserted; - BOOL isProcessingMirroredSnippets; - - BOOL completionIsOpen; - BOOL completionWasReinvokedAutomatically; - BOOL completionWasRefreshed; - BOOL completionFuzzyMode; - NSUInteger completionParseRangeLocation; - - NSColor *queryHiliteColor; - NSColor *queryEditorBackgroundColor; - NSColor *commentColor; - NSColor *quoteColor; - NSColor *keywordColor; - NSColor *backtickColor; - NSColor *numericColor; - NSColor *variableColor; - NSColor *otherTextColor; - NSRange queryRange; - BOOL shouldHiliteQuery; -} - -@property(retain) NSColor* queryHiliteColor; -@property(retain) NSColor* queryEditorBackgroundColor; -@property(retain) NSColor* commentColor; -@property(retain) NSColor* quoteColor; -@property(retain) NSColor* keywordColor; -@property(retain) NSColor* backtickColor; -@property(retain) NSColor* numericColor; -@property(retain) NSColor* variableColor; -@property(retain) NSColor* otherTextColor; -@property(assign) NSRange queryRange; -@property(assign) BOOL shouldHiliteQuery; -@property(assign) BOOL completionIsOpen; -@property(assign) BOOL completionWasReinvokedAutomatically; - -- (IBAction)showMySQLHelpForCurrentWord:(id)sender; - -- (BOOL) isNextCharMarkedBy:(id)attribute withValue:(id)aValue; -- (BOOL) areAdjacentCharsLinked; -- (BOOL) isCaretAdjacentToAlphanumCharWithInsertionOf:(unichar)aChar; -- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix; -- (BOOL) shiftSelectionRight; -- (BOOL) shiftSelectionLeft; -- (void) setAutoindent:(BOOL)enableAutoindent; -- (BOOL) autoindent; -- (void) setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter; -- (BOOL) autoindentIgnoresEnter; -- (void) setAutopair:(BOOL)enableAutopair; -- (BOOL) autopair; -- (void) setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords; -- (BOOL) autouppercaseKeywords; -- (void) setAutohelp:(BOOL)enableAutohelp; -- (BOOL) autohelp; -- (void) setTabStops; -- (void) selectLineNumber:(NSUInteger)lineNumber ignoreLeadingNewLines:(BOOL)ignLeadingNewLines; -- (NSUInteger) getLineNumberForCharacterIndex:(NSUInteger)anIndex; -- (void) autoHelp; -- (void) doSyntaxHighlighting; -- (NSBezierPath*)roundedBezierPathAroundRange:(NSRange)aRange; -- (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion; -- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch autoCompleteMode:(BOOL)autoCompleteMode; -- (void) doAutoCompletion; -- (void) refreshCompletion; -- (NSArray *)suggestionsForSQLCompletionWith:(NSString *)currentWord dictMode:(BOOL)isDictMode browseMode:(BOOL)dbBrowseMode withTableName:(NSString*)aTableName withDbName:(NSString*)aDbName; -- (void) selectCurrentQuery; -- (void) processMirroredSnippets; - -- (BOOL)checkForCaretInsideSnippet; -- (void)insertAsSnippet:(NSString*)theSnippet atRange:(NSRange)targetRange; - -- (void)showCompletionListFor:(NSString*)kind atRange:(NSRange)aRange fuzzySearch:(BOOL)fuzzySearchMode; - -- (NSUInteger)characterIndexOfPoint:(NSPoint)aPoint; -- (void)insertFileContentOfFile:(NSString *)aPath; - -- (BOOL)isSnippetMode; - -- (NSString *)runBashCommand:(NSString *)command; - -@end diff --git a/Source/CMTextView.m b/Source/CMTextView.m deleted file mode 100644 index 55c759a0..00000000 --- a/Source/CMTextView.m +++ /dev/null @@ -1,3319 +0,0 @@ -// -// $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" -#import "SPTooltip.h" -#import "TablesList.h" -#import "SPNavigatorController.h" -#import "SPAlertSheets.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 NSUInteger yylex(); -extern NSUInteger 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 @"s" // attribute for found SQL keywords -#define kQuote @"Quote" -#define kQuoteValue @"isQuoted" -#define kValue @"x" -#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 SP_MAX_TEXT_SIZE_FOR_SYNTAX_HIGHLIGHTING 20000000 - -#define MYSQL_DOC_SEARCH_URL @"http://dev.mysql.com/doc/refman/%@/en/%@.html" - -#pragma mark - - -// some helper functions for handling rectangles and points -// needed in roundedBezierPathAroundRange: -static inline CGFloat SPRectTop(NSRect rectangle) { return rectangle.origin.y; } -static inline CGFloat SPRectBottom(NSRect rectangle) { return rectangle.origin.y+rectangle.size.height; } -static inline CGFloat SPRectLeft(NSRect rectangle) { return rectangle.origin.x; } -static inline CGFloat SPRectRight(NSRect rectangle) { return rectangle.origin.x+rectangle.size.width; } -static inline CGFloat SPPointDistance(NSPoint a, NSPoint b) { return sqrt( (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y) ); } -static inline NSPoint SPPointOnLine(NSPoint a, NSPoint b, CGFloat t) { return NSMakePoint(a.x*(1.-t) + b.x*t, a.y*(1.-t) + b.y*t); } - -@implementation CMTextView - -@synthesize queryHiliteColor; -@synthesize queryEditorBackgroundColor; -@synthesize commentColor; -@synthesize quoteColor; -@synthesize keywordColor; -@synthesize backtickColor; -@synthesize numericColor; -@synthesize variableColor; -@synthesize otherTextColor; -@synthesize queryRange; -@synthesize shouldHiliteQuery; -@synthesize completionIsOpen; -@synthesize completionWasReinvokedAutomatically; - -/* - * 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 -{ - - prefs = [[NSUserDefaults standardUserDefaults] retain]; - [self setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; - - // Set self as delegate for the textView's textStorage to enable syntax highlighting, - [[self textStorage] setDelegate:self]; - - // Set defaults for general usage - autoindentEnabled = NO; - autopairEnabled = YES; - autoindentIgnoresEnter = NO; - autouppercaseKeywordsEnabled = NO; - autohelpEnabled = NO; - delBackwardsWasPressed = NO; - startListeningToBoundChanges = NO; - textBufferSizeIncreased = NO; - snippetControlCounter = -1; - mirroredCounter = -1; - completionPopup = nil; - completionIsOpen = NO; - isProcessingMirroredSnippets = NO; - completionWasRefreshed = NO; - - lineNumberView = [[NoodleLineNumberView alloc] initWithScrollView:scrollView]; - [scrollView setVerticalRulerView:lineNumberView]; - [scrollView setHasHorizontalRuler:NO]; - [scrollView setHasVerticalRuler:YES]; - [scrollView setRulersVisible:YES]; - [self setAllowsDocumentBackgroundColorChange:YES]; - [self setContinuousSpellCheckingEnabled:NO]; - [self setAutoindent:[prefs boolForKey:SPCustomQueryAutoIndent]]; - [self setAutoindentIgnoresEnter:YES]; - [self setAutopair:[prefs boolForKey:SPCustomQueryAutoPairCharacters]]; - [self setAutohelp:[prefs boolForKey:SPCustomQueryUpdateAutoHelp]]; - [self setAutouppercaseKeywords:[prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]]; - [self setCompletionWasReinvokedAutomatically:NO]; - - - // Re-define tab stops for a better editing - [self setTabStops]; - - // 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]]; - - [self setQueryHiliteColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorHighlightQueryColor]]]; - [self setQueryEditorBackgroundColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorBackgroundColor]]]; - [self setCommentColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorCommentColor]]]; - [self setQuoteColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorQuoteColor]]]; - [self setKeywordColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorSQLKeywordColor]]]; - [self setBacktickColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorBacktickColor]]]; - [self setNumericColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorNumericColor]]]; - [self setVariableColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorVariableColor]]]; - [self setOtherTextColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorTextColor]]]; - [self setTextColor:[self otherTextColor]]; - [self setInsertionPointColor:[self otherTextColor]]; - [self setShouldHiliteQuery:[prefs boolForKey:SPCustomQueryHighlightCurrentQuery]]; - - // Register observers for the when editor background colors preference changes - [prefs addObserver:self forKeyPath:SPCustomQueryEditorFont options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorBackgroundColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorHighlightQueryColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryHighlightCurrentQuery options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorCommentColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorQuoteColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorSQLKeywordColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorBacktickColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorNumericColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorVariableColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorTextColor options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryEditorTabStopWidth options:NSKeyValueObservingOptionNew context:NULL]; - [prefs addObserver:self forKeyPath:SPCustomQueryAutoUppercaseKeywords options:NSKeyValueObservingOptionNew context:NULL]; - -} - -- (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion -{ - mySQLConnection = theConnection; - mySQLmajorVersion = majorVersion; -} - -/** - * This method is called as part of Key Value Observing which is used to watch for prefernce changes which effect the interface. - */ -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - if ([keyPath isEqualToString:SPCustomQueryEditorBackgroundColor]) { - [self setQueryEditorBackgroundColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - [self setNeedsDisplay:YES]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorFont]) { - [self setFont:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - [self setNeedsDisplay:YES]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorHighlightQueryColor]) { - [self setQueryHiliteColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - [self setNeedsDisplay:YES]; - } else if ([keyPath isEqualToString:SPCustomQueryHighlightCurrentQuery]) { - [self setShouldHiliteQuery:[[change objectForKey:NSKeyValueChangeNewKey] boolValue]]; - [self setNeedsDisplay:YES]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorCommentColor]) { - [self setCommentColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorQuoteColor]) { - [self setQuoteColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorSQLKeywordColor]) { - [self setKeywordColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorBacktickColor]) { - [self setBacktickColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorNumericColor]) { - [self setNumericColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorVariableColor]) { - [self setVariableColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorTextColor]) { - [self setOtherTextColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; - [self setTextColor:[self otherTextColor]]; - if([[self string] length]<100000 && [self isEditable]) - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; - } else if ([keyPath isEqualToString:SPCustomQueryEditorTabStopWidth]) { - [self setTabStops]; - } else if ([keyPath isEqualToString:SPCustomQueryAutoUppercaseKeywords]) { - [self setAutouppercaseKeywords:[prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]]; - } -} - -/* - * 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(currentWord == nil) currentWord = [NSString stringWithString:@""]; - // 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 6MB - if([[self string] length] && [[self string] length]<6000000) - { - NSMutableSet *uniqueArray = [NSMutableSet setWithCapacity:5]; - - for(id w in [[self textStorage] words]) - [uniqueArray addObject:[w string]]; - // Remove current word from list - - [uniqueArray removeObject:currentWord]; - - NSInteger reverseSort = NO; - - for(id w in [[uniqueArray allObjects] sortedArrayUsingFunction:alphabeticSort context:&reverseSort]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; - - } - - if(!isDictMode) { - // Add predefined keywords - NSArray *keywordList = [[NSArray arrayWithArray:[[SPQueryController sharedQueryController] keywordList]] retain]; - for(id s in keywordList) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"dummy-small", @"image", nil]]; - - // Add predefined functions - NSArray *functionList = [[NSArray arrayWithArray:[[SPQueryController sharedQueryController] functionList]] retain]; - for(id s in functionList) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"func-small", @"image", nil]]; - - [functionList release]; - [keywordList release]; - } - - } - - if(!isDictMode && [mySQLConnection isConnected]) - { - // Add structural db/table/field data to completions list or fallback to gathering TablesList data - - NSString* connectionID; - if(tableDocumentInstance) - connectionID = [tableDocumentInstance connectionID]; - else - connectionID = @"_"; - - // Try to get structure data - NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[SPNavigatorController sharedNavigatorController] dbStructureForConnection:connectionID]]; - - if(dbs != nil && [dbs isKindOfClass:[NSDictionary class]] && [dbs count]) { - NSMutableArray *allDbs = [NSMutableArray array]; - [allDbs addObjectsFromArray:[dbs allKeys]]; - - NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; - NSMutableArray *sortedDbs = [NSMutableArray array]; - [sortedDbs addObjectsFromArray:[allDbs sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; - - NSString *currentDb = nil; - NSString *currentTable = nil; - - if (tablesListInstance && [tablesListInstance selectedDatabase]) - currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [tablesListInstance selectedDatabase]]; - if (tablesListInstance && [tablesListInstance tableName]) - currentTable = [tablesListInstance tableName]; - - // Put current selected db at the top - if(aTableName == nil && aDbName == nil && [tablesListInstance selectedDatabase]) { - currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [tablesListInstance selectedDatabase]]; - [sortedDbs removeObject:currentDb]; - [sortedDbs insertObject:currentDb atIndex:0]; - } - - NSString* aTableName_id; - NSString* aDbName_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, aDbName]; - if(aDbName && aTableName) - aTableName_id = [NSString stringWithFormat:@"%@%@%@", aDbName_id, SPUniqueSchemaDelimiter, aTableName]; - else - aTableName_id = [NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, aTableName]; - - - // Put information_schema and/or mysql db at the end if not selected - NSString* mysql_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, @"mysql"]; - NSString* inf_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, @"information_schema"]; - if(currentDb && ![currentDb isEqualToString:mysql_id] && [sortedDbs containsObject:mysql_id]) { - [sortedDbs removeObject:mysql_id]; - [sortedDbs addObject:mysql_id]; - } - if(currentDb && ![currentDb isEqualToString:inf_id] && [sortedDbs containsObject:inf_id]) { - [sortedDbs removeObject:inf_id]; - [sortedDbs addObject:inf_id]; - } - - BOOL aTableNameExists = NO; - if(!aDbName) { - - // Try to suggest only items which are uniquely valid for the parsed string - NSArray *uniqueSchema = [[SPNavigatorController sharedNavigatorController] getUniqueDbIdentifierFor:[aTableName lowercaseString] andConnection:[[[self delegate] valueForKeyPath:@"tableDocumentInstance"] connectionID]]; - NSInteger uniqueSchemaKind = [[uniqueSchema objectAtIndex:0] intValue]; - - // 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] isKindOfClass:[NSDictionary class]] - && [[dbs objectForKey:currentDb] objectForKey:[NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, [uniqueSchema objectAtIndex:1]]] - && uniqueSchemaKind == 2) { - aTableNameExists = YES; - aTableName = [uniqueSchema objectAtIndex:1]; - aTableName_id = [NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, aTableName]; - aDbName_id = [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_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [uniqueSchema objectAtIndex:1]]; - aTableNameExists = NO; - } - - } else if (aDbName && [aDbName length]) { - if(aTableName && [aTableName length] - && [dbs objectForKey:aDbName_id] && [[dbs objectForKey:aDbName_id] isKindOfClass:[NSDictionary class]] - && [[dbs objectForKey:aDbName_id] objectForKey:[NSString stringWithFormat:@"%@%@%@", aDbName_id, SPUniqueSchemaDelimiter, aTableName]]) { - aTableNameExists = YES; - } - } - - // If aDbName exist show only those table - if([allDbs containsObject:aDbName_id]) { - [sortedDbs removeAllObjects]; - [sortedDbs addObject:aDbName_id]; - } - - for(id db in sortedDbs) { - - NSArray *allTables; - if([[dbs objectForKey:db] isKindOfClass:[NSDictionary class]]) - allTables = [[dbs objectForKey:db] allKeys]; - else { - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[[[dbs objectForKey:db] description] componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"database-small", @"image", @"", @"isRef", nil]]; - continue; - } - - NSString *dbpath = [db substringFromIndex:[db rangeOfString:SPUniqueSchemaDelimiter].location]; - - NSMutableArray *sortedTables = [NSMutableArray array]; - if(aTableNameExists) { - [sortedTables addObject:aTableName_id]; - } else { - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[db componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"database-small", @"image", @"", @"isRef", nil]]; - [sortedTables addObjectsFromArray:[allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; - if([sortedTables count] > 1 && [sortedTables containsObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable]]) { - [sortedTables removeObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable]]; - [sortedTables insertObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable] atIndex:0]; - } - } - for(id table in sortedTables) { - NSDictionary *theTable = [[dbs objectForKey:db] objectForKey:table]; - NSString *tablepath = [table substringFromIndex:[table rangeOfString:SPUniqueSchemaDelimiter].location]; - 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 componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"table-small-square", @"image", tablepath, @"path", @"", @"isRef", nil]]; - break; - case 1: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"table-view-small-square", @"image", tablepath, @"path", @"", @"isRef", nil]]; - break; - case 2: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"proc-small", @"image", tablepath, @"path", @"", @"isRef", nil]]; - breakFlag = YES; - break; - case 3: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"func-small", @"image", tablepath, @"path", @"", @"isRef", nil]]; - breakFlag = YES; - break; - } - if(!breakFlag) { - NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; - for(id field in sortedFields) { - if(![field hasPrefix:@" "]) { - NSString *fieldpath = [field substringFromIndex:[field rangeOfString:SPUniqueSchemaDelimiter].location]; - NSArray *def = [theTable objectForKey:field]; - NSString *typ = [NSString stringWithFormat:@"%@ %@ %@", [def objectAtIndex:0], [def objectAtIndex:3], [def objectAtIndex:5]]; - // 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 componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", - @"field-small-square", @"image", - fieldpath, @"path", - t, @"type", - lst, @"list", - @"", @"isRef", - nil]]; - } else { - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: - [[field componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", - @"field-small-square", @"image", - fieldpath, @"path", - typ, @"type", - @"", @"isRef", - nil]]; - } - } - } - } - } - } - if(desc) [desc release]; - } else { - - // [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 [tablesListInstance allDatabaseNames]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; - - // Add all system database names to completions list - for (id obj in [tablesListInstance allSystemDatabaseNames]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; - - // Add table names to completions list - for (id obj in [tablesListInstance allTableNames]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-small-square", @"image", @"", @"isRef", nil]]; - - // Add view names to completions list - for (id obj in [tablesListInstance 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 ([tableDocumentInstance table] != nil) - for (id obj in [[tableDocumentInstance 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 [tablesListInstance 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 [tablesListInstance allFunctionNames]) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"func-small", @"image", @"", @"isRef", nil]]; - } - } - } - - return [possibleCompletions autorelease]; - -} - -- (void) doAutoCompletion -{ - if(completionIsOpen || !self || ![self delegate]) return; - - // Cancel autocompletion trigger - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - - NSRange r = [self selectedRange]; - - if(![self delegate] || ![[self delegate] isKindOfClass:[CustomQuery class]] || r.length || snippetControlCounter > -1) return; - - if(r.location) { - NSCharacterSet *ignoreCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\"'`;,()[]{}=+/<> \t\n\r"]; - - // Check the previous character and don't autocomplete if the character is whitespace or certain types of punctuation - if ([ignoreCharacterSet characterIsMember:[[self string] characterAtIndex:r.location - 1]]) return; - - // Suppress auto-completion if the window isn't active anymore - if ([[NSApp keyWindow] firstResponder] != self) return; - - // Trigger the completion - [self doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:YES]; - } - -} - -- (void) refreshCompletion -{ - if(completionWasRefreshed) return; - completionWasRefreshed = YES; - [self doCompletionByUsingSpellChecker:NO fuzzyMode:completionFuzzyMode autoCompleteMode:NO]; -} - -- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch autoCompleteMode:(BOOL)autoCompleteMode -{ - - // Cancel autocompletion trigger - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - if(![self isEditable] || (completionIsOpen && !completionWasReinvokedAutomatically)) { - return; - } - - [self breakUndoCoalescing]; - - // Remember state for refreshCompletion - completionFuzzyMode = fuzzySearch; - - NSUInteger caretPos = NSMaxRange([self selectedRange]); - - BOOL caretMovedLeft = NO; - - // Check if caret is located after a ` - if so move caret inside - if(!autoCompleteMode && [[self string] length] && caretPos > 0 && [[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; - - // Break for long stuff - if(completionRange.length>100000) return; - - NSString* allow; // additional chars which won't close the suggestion list window - 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(tablesListInstance && [tablesListInstance selectedDatabase]) - currentDb = [tablesListInstance 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(NSUInteger i=0; i<[lineHead length]; i++) - if([lineHead characterAtIndex:i]=='`') caretIsInsideBackticks = !caretIsInsideBackticks; - - NSCharacterSet *whiteSpaceCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; - NSUInteger 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); - - // Break for long stuff - if(parseRange.length>100000) return; - - 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]; - } - - // Cancel autocompletion trigger again if user typed something in while parsing - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - if (completionIsOpen) [completionPopup close], completionPopup = nil; - completionIsOpen = YES; - 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:[self isSnippetMode] - fuzzySearch:fuzzySearch - backtickMode:backtickMode - withDbName:dbName - withTableName:tableName - selectedDb:currentDb - caretMovedLeft:caretMovedLeft - autoComplete:autoCompleteMode - oneColumn:isDictMode - isQueryingDBStructure:[mySQLConnection isQueryingDatabaseStructure]]; - - completionParseRangeLocation = parseRange.location; - - //Get the NSPoint of the first character of the current word - NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(completionRange.location,0) 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 == '('); - NSUInteger bufferLength = [[self string] length]; - - if(!bufferLength) return NO; - - // Check previous/next character for being alphanum - // @try block for bounds checking - @try - { - if(caretPosition==0) - leftIsAlphanum = NO; - else - leftIsAlphanum = [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition-1]] && !charIsOpenBracket; - } @catch(id ae) { } - @try { - if(caretPosition >= bufferLength) - rightIsAlphanum = NO; - else - 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 if (leftChar == '{') - matchingChar = '}'; - 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 - -- (IBAction)printDocument:(id)sender -{ - - // If Extended Table Info tab is active delegate the print call to the SPPrintController - // if the user doesn't select anything in self - if([[[[self delegate] class] description] isEqualToString:@"SPExtendedTableInfo"] && ![self selectedRange].length) { - [[[self delegate] valueForKeyPath:@"tableDocumentInstance"] printDocument:sender]; - return; - } - - // This will scale the view to fit the page without centering it. - [[NSPrintInfo sharedPrintInfo] setHorizontalPagination:NSFitPagination]; - [[NSPrintInfo sharedPrintInfo] setHorizontallyCentered:NO]; - [[NSPrintInfo sharedPrintInfo] setVerticallyCentered:NO]; - - NSRange r = NSMakeRange(0, [[self string] length]); - - // Remove all colors before printing for large text buffer - if(r.length > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) { - // Cancel all doSyntaxHighlighting requests - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doSyntaxHighlighting) - object:nil]; - [[self textStorage] removeAttribute:NSForegroundColorAttributeName range:r]; - [[self textStorage] removeAttribute:kLEXToken range:r]; - [[self textStorage] ensureAttributesAreFixedInRange:r]; - - } - [[self textStorage] ensureAttributesAreFixedInRange:r]; - - // Setup the print operation with the print info and view - NSPrintOperation *printOperation = [NSPrintOperation printOperationWithView:self printInfo:[NSPrintInfo sharedPrintInfo]]; - - // Order out print sheet - [printOperation runOperationModalForWindow:[self window] delegate:nil didRunSelector:NULL contextInfo:NULL]; - -} - -- (void)printOperationDidRun:(NSPrintOperation *)printOperation success:(BOOL)success contextInfo:(void *)contextInfo -{ - // Refresh syntax highlighting - [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.01]; -} - -/* - * Search for the current selection or current word in the MySQL Help - */ -- (IBAction) showMySQLHelpForCurrentWord:(id)sender -{ - [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 -{ - - NSRange currentRange = [self selectedRange]; - - // Only proceed if a selection is active - if (currentRange.length == 0 || ![self isEditable]) - return NO; - - NSString *selString = [[self string] substringWithRange:currentRange]; - - // Replace the current selection with the selected string wrapped in prefix and suffix - [self insertText:[NSString stringWithFormat:@"%@%@%@", prefix, selString, suffix]]; - - // Re-select original selection - NSRange innerSelectionRange = NSMakeRange(currentRange.location+1, [selString length]); - [self setSelectedRange:innerSelectionRange]; - - // If autopair is enabled mark last autopair character as autopair-linked - if([prefs boolForKey:SPCustomQueryAutoPairCharacters]) - [[self textStorage] addAttribute:kAPlinked value:kAPval range:NSMakeRange(NSMaxRange(innerSelectionRange), 1)]; - - 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 -{ - if([self isEditable]) - [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 - { - NSUInteger arrayCount = [lineRanges count]; - NSUInteger 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]; - if (lineNumber < 1) lineNumber = 1; - - // 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"; - NSUInteger i, indentedLinesLength = 0; - - if ([self selectedRange].location == NSNotFound || ![self isEditable]) return NO; - - // Indent the currently selected line if the caret is within a single line - if ([self selectedRange].length == 0) { - - // 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; - NSUInteger 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) { - - // 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; - mirroredCounter = -1; - snippetWasJustInserted = NO; -} - -/* - * Shows pre-defined completion list - */ -- (void)showCompletionListFor:(NSString*)kind atRange:(NSRange)aRange fuzzySearch:(BOOL)fuzzySearchMode -{ - - // Cancel auto-completion timer - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - NSMutableArray *possibleCompletions = [[[NSMutableArray alloc] initWithCapacity:0] autorelease]; - - NSString *connectionID; - if(tableDocumentInstance) - connectionID = [tableDocumentInstance connectionID]; - else - connectionID = @"_"; - - NSArray *arr = nil; - if([kind isEqualToString:@"$SP_ASLIST_ALL_TABLES"]) { - NSString *currentDb = nil; - - if (tablesListInstance && [tablesListInstance selectedDatabase]) - currentDb = [tablesListInstance selectedDatabase]; - - NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[mySQLConnection getDbStructure] objectForKey:connectionID]]; - - if(currentDb != nil && dbs != nil && [dbs count] && [dbs objectForKey:currentDb]) { - NSArray *allTables = [[dbs objectForKey:currentDb] allKeys]; - NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; - NSArray *sortedTables = [allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; - [desc release]; - for(id table in sortedTables) { - NSDictionary * theTable = [[dbs objectForKey:currentDb] objectForKey:table]; - NSInteger structtype = [[theTable objectForKey:@" struct_type "] intValue]; - switch(structtype) { - case 0: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-small-square", @"image", currentDb, @"path", @"", @"isRef", nil]]; - break; - case 1: - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-view-small-square", @"image", currentDb, @"path", @"", @"isRef", nil]]; - break; - } - } - } else { - arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allTableAndViewNames]]; - if(arr == nil) { - arr = [NSArray array]; - } - for(id w in arr) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"table-small-square", @"image", @"", @"isRef", nil]]; - } - } - else if([kind isEqualToString:@"$SP_ASLIST_ALL_DATABASES"]) { - arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allDatabaseNames]]; - if(arr == nil) { - arr = [NSArray array]; - } - for(id w in arr) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"database-small", @"image", @"", @"isRef", nil]]; - arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allSystemDatabaseNames]]; - if(arr == nil) { - arr = [NSArray array]; - } - for(id w in arr) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"database-small", @"image", @"", @"isRef", nil]]; - } - else if([kind isEqualToString:@"$SP_ASLIST_ALL_FIELDS"]) { - - NSString *currentDb = nil; - NSString *currentTable = nil; - - if (tablesListInstance && [tablesListInstance selectedDatabase]) - currentDb = [tablesListInstance selectedDatabase]; - if (tablesListInstance && [tablesListInstance tableName]) - currentTable = [tablesListInstance tableName]; - - NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[mySQLConnection getDbStructure] objectForKey:connectionID]]; - if(currentDb != nil && currentTable != nil && dbs != nil && [dbs count] && [dbs objectForKey:currentDb] && [[dbs objectForKey:currentDb] objectForKey:currentTable]) { - NSDictionary * theTable = [[dbs objectForKey:currentDb] objectForKey:currentTable]; - NSArray *allFields = [theTable allKeys]; - NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; - NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; - [desc release]; - for(id field in sortedFields) { - if(![field hasPrefix:@" "]) { - NSArray *def = [theTable objectForKey:field]; - NSString *typ = [NSString stringWithFormat:@"%@ %@ %@", [def objectAtIndex:0], [def objectAtIndex:1], [def objectAtIndex:2]]; - // 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:@"%@@%%@",currentTable,currentDb], @"path", SPUniqueSchemaDelimiter, - t, @"type", - lst, @"list", - @"", @"isRef", - nil]]; - } else { - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: - field, @"display", - @"field-small-square", @"image", - [NSString stringWithFormat:@"%@%@%@",currentTable,currentDb], @"path", SPUniqueSchemaDelimiter, - typ, @"type", - @"", @"isRef", - nil]]; - } - } - } - } else { - arr = [NSArray arrayWithArray:[[tableDocumentInstance valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"]]; - if(arr == nil) { - arr = [NSArray array]; - } - for(id w in arr) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"field-small-square", @"image", @"", @"isRef", nil]]; - } - } - else { - NSLog(@"“%@” is not a valid completion list", kind); - NSBeep(); - return; - } - - if (completionIsOpen) [completionPopup close], completionPopup = nil; - completionIsOpen = YES; - completionPopup = [[SPNarrowDownCompletion alloc] initWithItems:possibleCompletions - alreadyTyped:@"" - staticPrefix:@"" - additionalWordCharacters:@"_." - caseSensitive:NO - charRange:aRange - parseRange:aRange - inView:self - dictMode:NO - dbMode:YES - tabTriggerMode:[self isSnippetMode] - fuzzySearch:fuzzySearchMode - backtickMode:NO - withDbName:@"" - withTableName:@"" - selectedDb:@"" - caretMovedLeft:NO - autoComplete:NO - oneColumn:NO - isQueryingDBStructure:NO]; - - //Get the NSPoint of the first character of the current word - NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(aRange.location,0) 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)]; - // 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]; - -} - -/* - * Update all mirrored snippets and adjust any involved instances - */ -- (void)processMirroredSnippets -{ - if(mirroredCounter > -1) { - - isProcessingMirroredSnippets = YES; - - NSInteger i, j, k, deltaLength; - NSRange mirroredRange; - - // Go through each defined mirrored snippet and update it - for(i=0; i<=mirroredCounter; i++) { - if(snippetMirroredControlArray[i][0] == currentSnippetIndex) { - - deltaLength = snippetControlArray[currentSnippetIndex][1]-snippetMirroredControlArray[i][2]; - - mirroredRange = NSMakeRange(snippetMirroredControlArray[i][1], snippetMirroredControlArray[i][2]); - NSString *mirroredString = nil; - - // For safety reasons - @try{ - mirroredString = [[self string] substringWithRange:NSMakeRange(snippetControlArray[currentSnippetIndex][0], snippetControlArray[currentSnippetIndex][1])]; - } - @catch(id ae) { - NSLog(@"Error while parsing for mirrored snippets. %@", [ae description]); - NSBeep(); - [self endSnippetSession]; - return; - } - - // Register for undo - [self shouldChangeTextInRange:mirroredRange replacementString:mirroredString]; - - [self replaceCharactersInRange:mirroredRange withString:mirroredString]; - snippetMirroredControlArray[i][2] = snippetControlArray[currentSnippetIndex][1]; - - // If a completion list is open adjust the theCharRange and theParseRange if a mirrored snippet - // was updated which is located before the initial position - if(completionIsOpen && snippetMirroredControlArray[i][1] < completionParseRangeLocation) - [completionPopup adjustWorkingRangeByDelta:deltaLength]; - - // Adjust all other snippets accordingly - for(j=0; j<=snippetControlMax; j++) { - if(snippetControlArray[j][0] > -1) { - if(snippetControlArray[j][0]+snippetControlArray[j][1]>=snippetMirroredControlArray[i][1]) { - snippetControlArray[j][0] += deltaLength; - } - } - } - // Adjust all mirrored snippets accordingly - for(k=0; k<=mirroredCounter; k++) { - if(i != k) { - if(snippetMirroredControlArray[k][1] > snippetMirroredControlArray[i][1]) { - snippetMirroredControlArray[k][1] += deltaLength; - } - } - } - } - } - - isProcessingMirroredSnippets = NO; - [self didChangeText]; - - } -} - - -/* - * Selects the current snippet defined by “currentSnippetIndex” - */ -- (void)selectCurrentSnippet -{ - if( snippetControlCounter > -1 - && currentSnippetIndex >= 0 - && currentSnippetIndex <= snippetControlMax - ) - { - - [self breakUndoCoalescing]; - - // 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) { - - NSRange r1 = NSMakeRange(snippetControlArray[currentSnippetIndex][0], snippetControlArray[currentSnippetIndex][1]); - - NSRange r2; - // Ensure the selection for nested snippets if it is at very end of the text buffer - // because NSIntersectionRange returns {0, 0} in such a case - if(r1.location == [[self string] length]) - r2 = NSMakeRange([[self string] length], 0); - else - r2 = NSIntersectionRange(NSMakeRange(0,[[self string] length]), r1); - - if(r1.location == r2.location && r1.length == r2.length) { - [self setSelectedRange:r2]; - NSString *snip = [[self string] substringWithRange:r2]; - - if([snip length] > 2 && [snip hasPrefix:@"¦"] && [snip hasSuffix:@"¦"]) { - BOOL fuzzySearchMode = ([snip hasPrefix:@"¦¦"] && [snip hasSuffix:@"¦¦"]) ? YES : NO; - NSInteger offset = (fuzzySearchMode) ? 2 : 1; - NSRange insertRange = NSMakeRange(r2.location,0); - NSString *newSnip = [snip substringWithRange:NSMakeRange(1*offset,[snip length]-(2*offset))]; - if([newSnip hasPrefix:@"$SP_ASLIST_"]) { - [self showCompletionListFor:newSnip atRange:NSMakeRange(r2.location, 0) fuzzySearch:fuzzySearchMode]; - return; - } else { - NSArray *list = [[snip substringWithRange:NSMakeRange(1*offset,[snip length]-(2*offset))] componentsSeparatedByString:@"¦"]; - NSMutableArray *possibleCompletions = [[[NSMutableArray alloc] initWithCapacity:[list count]] autorelease]; - for(id w in list) - [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; - - if (completionIsOpen) [completionPopup close], completionPopup = nil; - completionIsOpen = YES; - completionPopup = [[SPNarrowDownCompletion alloc] initWithItems:possibleCompletions - alreadyTyped:@"" - staticPrefix:@"" - additionalWordCharacters:@"_." - caseSensitive:NO - charRange:insertRange - parseRange:insertRange - inView:self - dictMode:NO - dbMode:NO - tabTriggerMode:[self isSnippetMode] - fuzzySearch:fuzzySearchMode - backtickMode:NO - withDbName:@"" - withTableName:@"" - selectedDb:@"" - caretMovedLeft:NO - autoComplete:NO - oneColumn:YES - isQueryingDBStructure:NO]; - - //Get the NSPoint of the first character of the current word - NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(r2.location,0) 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)]; - // 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]; - } - } - } else { - [self endSnippetSession]; - } - } - } 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)insertAsSnippet:(NSString*)theSnippet atRange:(NSRange)targetRange -{ - - // Do not allow the insertion of a query favorite if snippets are active - if(snippetControlCounter > -1) { - NSBeep(); - return; - } - - NSInteger i, j; - mirroredCounter = -1; - - // 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 - snippetMirroredControlArray[i][0] = -1; // mirrored snippet index - snippetMirroredControlArray[i][1] = -1; // mirrored snippet location - snippetMirroredControlArray[i][2] = -1; // mirrored snippet length - } - - if(theSnippet == nil || ![theSnippet length]) return; - - NSMutableString *snip = [[NSMutableString alloc] initWithCapacity:[theSnippet length]]; - - @try{ - NSString *re = @"(?s)(?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; - - // Replace internal variables - NSMutableString *theHintString = [[NSMutableString alloc] initWithCapacity:hintRange.length]; - [theHintString setString:[snip substringWithRange:hintRange]]; - if([theHintString isMatchedByRegex:@"(?"]; - } - [theHintString flushCachedRegexData]; - } - - while([theHintString isMatchedByRegex:@"(?"]; - } - [theHintString flushCachedRegexData]; - } - } - - // Handle escaped characters - [theHintString replaceOccurrencesOfRegex:@"\\\\(\\$\\(|\\}|\\$SP_)" withString:@"$1"]; - [theHintString flushCachedRegexData]; - - // If inside the snippet hint $(…) is defined run … as BASH command - // and replace $(…) by the return string of that command. Please note - // only one $(…) statement is allowed within one ${…} snippet environment. - NSRange tagRange = [theHintString rangeOfRegex:@"(?s)(? -1 && i != snipCnt && snippetControlArray[i][0] > snippetControlArray[snipCnt][0]) - snippetControlArray[i][0] -= 3+((snipCnt>9)?2:1); - - } - - // Parse for mirrored snippets - while([snip isMatchedByRegex:mirror_re]) { - mirroredCounter++; - if(mirroredCounter > 19) { - NSLog(@"Only 20 mirrored snippet placeholders allowed."); - NSBeep(); - break; - } else { - - NSRange snipRange = [snip rangeOfRegex:mirror_re capture:0L]; - NSInteger snipCnt = [[snip substringWithRange:[snip rangeOfRegex:mirror_re capture:1L]] intValue]; - - // Check for snippet number 19 (to simplify regexp) - if(snipCnt>18 || snipCnt<0) { - NSLog(@"Only snippets in the range of 0…18 allowed."); - [self endSnippetSession]; - break; - } - - [snip replaceCharactersInRange:snipRange withString:@""]; - [snip flushCachedRegexData]; - - // Store found mirrored snippet range - snippetMirroredControlArray[mirroredCounter][0] = snipCnt; - snippetMirroredControlArray[mirroredCounter][1] = snipRange.location + targetRange.location; - snippetMirroredControlArray[mirroredCounter][2] = 0; - - // Adjust successive snippets - for(i=0; i<20; i++) - if(snippetControlArray[i][0] > -1 && snippetControlArray[i][0] > snippetMirroredControlArray[mirroredCounter][1]) - snippetControlArray[i][0] -= 1+((snipCnt>9)?2:1); - - [snip flushCachedRegexData]; - } - } - // Preset mirrored snippets with according snippet content - if(mirroredCounter > -1) { - for(i=0; i<=mirroredCounter; i++) { - if(snippetControlArray[snippetMirroredControlArray[i][0]][0] > -1 && snippetControlArray[snippetMirroredControlArray[i][0]][1] > 0) { - [snip replaceCharactersInRange:NSMakeRange(snippetMirroredControlArray[i][1]-targetRange.location, snippetMirroredControlArray[i][2]) - withString:[snip substringWithRange:NSMakeRange(snippetControlArray[snippetMirroredControlArray[i][0]][0]-targetRange.location, snippetControlArray[snippetMirroredControlArray[i][0]][1])]]; - snippetMirroredControlArray[i][2] = snippetControlArray[snippetMirroredControlArray[i][0]][1]; - } - // Adjust successive snippets - for(j=0; j<20; j++) - if(snippetControlArray[j][0] > -1 && snippetControlArray[j][0] > snippetMirroredControlArray[i][1]) - snippetControlArray[j][0] += snippetControlArray[snippetMirroredControlArray[i][0]][1]; - // Adjust successive mirrored snippets - for(j=0; j<=mirroredCounter; j++) - if(snippetMirroredControlArray[j][1] > snippetMirroredControlArray[i][1]) - snippetMirroredControlArray[j][1] += snippetControlArray[snippetMirroredControlArray[i][0]][1]; - } - } - - 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; - } - - // unescape escaped snippets and re-adjust successive snippet locations : \${1:a} → ${1:a} - NSString *ure = @"(?s)\\\\\\$\\{(1?\\d):(.{0}|.*?[^\\\\])\\}"; - while([snip isMatchedByRegex:ure]) { - NSRange escapeRange = [snip rangeOfRegex:ure capture:0L]; - [snip replaceCharactersInRange:escapeRange withString:[snip substringWithRange:NSMakeRange(escapeRange.location+1,escapeRange.length-1)]]; - NSUInteger loc = escapeRange.location + targetRange.location; - [snip flushCachedRegexData]; - for(i=0; i<=snippetControlMax; i++) - if(snippetControlArray[i][0] > -1 && snippetControlArray[i][0] > loc) - snippetControlArray[i][0]--; - // Adjust mirrored snippets - if(mirroredCounter > -1) - for(i=0; i<=mirroredCounter; i++) - if(snippetMirroredControlArray[i][0] > -1 && snippetMirroredControlArray[i][1] > loc) - snippetMirroredControlArray[i][1]--; - } - - // Insert favorite query by selecting the tab trigger if any - [self setSelectedRange:targetRange]; - - // Registering for undo - [self breakUndoCoalescing]; - [self insertText:snip]; - - // If autopair is enabled check whether snip begins with ( and ends with ), if so mark ) as pair-linked - if([prefs boolForKey:SPCustomQueryAutoPairCharacters] && ([snip hasPrefix:@"("] && [snip hasSuffix:@")"] || ([snip hasPrefix:@"`"] && [snip hasSuffix:@"`"]) || ([snip hasPrefix:@"'"] && [snip hasSuffix:@"'"]) || ([snip hasPrefix:@"\""] && [snip hasSuffix:@"\""]))) - [[self textStorage] addAttribute:kAPlinked value:kAPval range:NSMakeRange([self selectedRange].location - 1, 1)]; - - // 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]; - -} - -/* - * Run 'command' as BASH command(s) and return the result. - * This task can be interrupted by pressing ⌘. - */ -- (NSString *)runBashCommand:(NSString *)command -{ - BOOL userTerminated = NO; - - NSTask *bashTask = [[NSTask alloc] init]; - [bashTask setLaunchPath: @"/bin/bash"]; - [bashTask setArguments:[NSArray arrayWithObjects: @"-c", command, nil]]; - - NSPipe *stdout_pipe = [NSPipe pipe]; - [bashTask setStandardOutput:stdout_pipe]; - NSFileHandle *stdout_file = [stdout_pipe fileHandleForReading]; - - NSPipe *stderr_pipe = [NSPipe pipe]; - [bashTask setStandardError:stderr_pipe]; - NSFileHandle *stderr_file = [stderr_pipe fileHandleForReading]; - [bashTask launch]; - - // Listen to ⌘. to terminate - while(1) { - if(![bashTask isRunning] || [bashTask processIdentifier] == 0) break; - NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask - untilDate:[NSDate distantPast] - inMode:NSDefaultRunLoopMode - dequeue:YES]; - usleep(10000); - if(!event) continue; - if ([event type] == NSKeyDown) { - unichar key = [[event characters] length] == 1 ? [[event characters] characterAtIndex:0] : 0; - if (([event modifierFlags] & NSCommandKeyMask) && key == '.') { - [bashTask terminate]; - userTerminated = YES; - break; - } - } else { - [NSApp sendEvent:event]; - } - } - - [bashTask waitUntilExit]; - - if(userTerminated) { - if(bashTask) [bashTask release]; - NSBeep(); - NSLog(@"“%@” was terminated by user.", command); - return @""; - } - - // If return from bash re-activate Sequel Pro - [NSApp activateIgnoringOtherApps:YES]; - - NSInteger status = [bashTask terminationStatus]; - NSData *outdata = [stdout_file readDataToEndOfFile]; - NSData *errdata = [stderr_file readDataToEndOfFile]; - - if(outdata != nil) { - NSString *stdout = [[NSString alloc] initWithData:outdata encoding:NSUTF8StringEncoding]; - NSString *error = [[[NSString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; - if(bashTask) [bashTask release]; - if(stdout != nil) { - if (status == 0) { - return [stdout autorelease]; - } else { - NSString *error = [[[NSString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; - SPBeginAlertSheet(NSLocalizedString(@"BASH Error", @"bash error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, - [NSString stringWithFormat:@"%@ “%@”:\n%@", NSLocalizedString(@"Error for", @"error for message"), command, [error description]]); - [stdout release]; - NSBeep(); - return @""; - } - } else { - NSLog(@"Couldn't read return string from “%@” by using UTF-8 encoding.", command); - NSBeep(); - } - } else { - if(bashTask) [bashTask release]; - NSLog(@"Couldn't read data from command “%@”.", command); - NSBeep(); - return @""; - } - -} - -/* - * 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; - } - - [[self textStorage] ensureAttributesAreFixedInRange:[self selectedRange]]; - NSUInteger 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; - - } - } - // 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 if(![self selectedRange].length) { - 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; - -} - -/* - * Return YES if user interacts with snippets (is needed mainly for suppressing - * the highlighting of the current query) - */ -- (BOOL)isSnippetMode -{ - return (snippetControlCounter > -1) ? YES : NO; -} - -#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]; - - // Cancel auto-completion timer - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - 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]]; - } - - // Cancel auto-completion timer - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - - 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 - - [self setCompletionWasReinvokedAutomatically:NO]; - completionWasRefreshed = NO; - // Cancel autocompletion trigger - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - if(curFlags==(NSControlKeyMask)) - [self doCompletionByUsingSpellChecker:NO fuzzyMode:YES autoCompleteMode:NO]; - else - [self doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:NO]; - return; - } - if (insertedCharacter == NSF5FunctionKey && [self isEditable]){ // F5 for completion based on spell checker - [self setCompletionWasReinvokedAutomatically:NO]; - [self doCompletionByUsingSpellChecker:YES fuzzyMode:NO autoCompleteMode: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(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 && [tableDocumentInstance fileURL]) { - NSArray *snippets = [[SPQueryController sharedQueryController] queryFavoritesForFileURL:[tableDocumentInstance fileURL] andTabTrigger:tabTrigger includeGlobals:YES]; - if([snippets count] > 0 && [(NSString*)[(NSDictionary*)[snippets objectAtIndex:0] objectForKey:@"query"] length]) { - [self insertAsSnippet:[(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:@"+"] || [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; - } - if([charactersIgnMod isEqualToString:@"0"]) { // reset font to default - BOOL editableStatus = [self isEditable]; - [self setEditable:YES]; - [self setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; - [self setEditable:editableStatus]; - 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; - case '{': - matchingCharacter = @"}"; - processAutopair = 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]; - - [self didChangeText]; - } - 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; - NSUInteger 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]; -} - -/* - * 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) - { - [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])) - [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]; - - if(strlength > SP_MAX_TEXT_SIZE_FOR_SYNTAX_HIGHLIGHTING) return; - - 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) - { - - // 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 - NSUInteger curPos = visibleRange.location+(NSUInteger)(visibleRange.length/2); - - // get the last line to parse due to SP_SYNTAX_HILITE_BIAS - // but look for only SP_SYNTAX_HILITE_BIAS chars forwards - NSUInteger end = curPos + SP_SYNTAX_HILITE_BIAS; - NSInteger lengthChecker = SP_SYNTAX_HILITE_BIAS; - if (end > strlength ) { - end = strlength; - } else { - while(end < strlength && lengthChecker > 0) { - if([selfstr characterAtIndex:end]=='\n') - break; - end++; - lengthChecker--; - } - } - if(lengthChecker <= 0) - end = curPos + SP_SYNTAX_HILITE_BIAS; - - // get the first line to parse due to SP_SYNTAX_HILITE_BIAS - // but look for only SP_SYNTAX_HILITE_BIAS chars backwards - NSUInteger start, start_temp; - if(end <= (SP_SYNTAX_HILITE_BIAS*2)) - start = 0; - else - start = end - (SP_SYNTAX_HILITE_BIAS*2); - - start_temp = start; - lengthChecker = SP_SYNTAX_HILITE_BIAS; - if (start > 0) - while(start>0 && lengthChecker > 0) { - if([selfstr characterAtIndex:start]=='\n') - break; - start--; - lengthChecker--; - } - if(lengthChecker <= 0) - start = start_temp; - - 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; - - size_t 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 = otherTextColor; - 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 (textBufferSizeIncreased && allowToCheckForUpperCase && autouppercaseKeywordsEnabled && !delBackwardsWasPressed - && [(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword, tokenEnd, nil) length]) - // 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]; - BOOL doIt = NO; - @try - { - doIt = ![(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword,tokenEnd+1,nil) length]; - } @catch(id ae) { doIt = NO; } - - if(doIt) - { - // 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:[curTokenString uppercaseString]]; - } - } - - 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); - - } - -} - -- (void) setTabStops -{ - NSFont *tvFont = [self font]; - NSInteger i; - NSTextTab *aTab; - NSMutableArray *myArrayOfTabs; - NSMutableParagraphStyle *paragraphStyle; - - BOOL oldEditableStatus = [self isEditable]; - [self setEditable:YES]; - - NSInteger tabStopWidth = [prefs integerForKey:SPCustomQueryEditorTabStopWidth]; - if(tabStopWidth < 1) tabStopWidth = 1; - - float tabWidth = NSSizeToCGSize([[NSString stringWithString:@" "] sizeWithAttributes:[NSDictionary dictionaryWithObject:tvFont forKey:NSFontAttributeName]]).width; - tabWidth = (float)tabStopWidth * tabWidth; - - NSInteger numberOfTabs = 256/tabStopWidth; - myArrayOfTabs = [NSMutableArray arrayWithCapacity:numberOfTabs]; - aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabWidth]; - [myArrayOfTabs addObject:aTab]; - [aTab release]; - for(i=1; i -1) { - // Is the caret still inside a snippet - if([self checkForCaretInsideSnippet]) { - for(NSUInteger i=0; i -1) { - // choose the colors for the snippet parts - if(i == currentSnippetIndex) { - [[NSColor colorWithCalibratedRed:1.0 green:0.6 blue:0.0 alpha:0.4] setFill]; - [[NSColor colorWithCalibratedRed:1.0 green:0.6 blue:0.0 alpha:0.8] setStroke]; - } else { - [[NSColor colorWithCalibratedRed:1.0 green:0.8 blue:0.2 alpha:0.2] setFill]; - [[NSColor colorWithCalibratedRed:1.0 green:0.8 blue:0.2 alpha:0.5] setStroke]; - } - NSBezierPath *snippetPath = [self roundedBezierPathAroundRange: NSMakeRange(snippetControlArray[i][0],snippetControlArray[i][1]) ]; - [snippetPath fill]; - [snippetPath stroke]; - } - } - } else { - [self endSnippetSession]; - } - } - - } - } - - [super drawRect:rect]; -} - -- (NSBezierPath*)roundedBezierPathAroundRange:(NSRange)aRange -{ - // parameters for snippet highlighting - CGFloat kappa = 0.5522847498; // magic number from http://www.whizkidtech.redprince.net/bezier/circle/ - CGFloat radius = 6; - CGFloat horzInset = -3; - CGFloat vertInset = 0.3; - BOOL connectDisconnectedPartsWithLine = NO; - - NSBezierPath *funkyPath = [NSBezierPath bezierPath]; - NSUInteger rectCount; - NSRectArray rects = [[self layoutManager] rectArrayForCharacterRange: aRange - withinSelectedCharacterRange: aRange - inTextContainer: [self textContainer] - rectCount: &rectCount ]; - if (rectCount>2 || (rectCount>1 && (SPRectRight(rects[1]) >= SPRectLeft(rects[0]) || connectDisconnectedPartsWithLine))) { - // highlight complicated multiline snippet - NSRect lineRects[4]; - lineRects[0] = rects[0]; - lineRects[1] = rects[1]; - lineRects[2] = rects[rectCount-2]; - lineRects[3] = rects[rectCount-1]; - for(int j=0;j<4;j++) lineRects[j] = NSInsetRect(lineRects[j], horzInset, vertInset); - NSPoint vertices[8]; - vertices[0] = NSMakePoint( SPRectLeft(lineRects[0]), SPRectTop(lineRects[0]) ); // point a - vertices[1] = NSMakePoint( SPRectRight(lineRects[0]), SPRectTop(lineRects[0]) ); // point b - vertices[2] = NSMakePoint( SPRectRight(lineRects[2]), SPRectBottom(lineRects[2]) ); // point c - vertices[3] = NSMakePoint( SPRectRight(lineRects[3]), SPRectBottom(lineRects[2]) ); // point d - vertices[4] = NSMakePoint( SPRectRight(lineRects[3]), SPRectBottom(lineRects[3]) ); // point e - vertices[5] = NSMakePoint( SPRectLeft(lineRects[3]), SPRectBottom(lineRects[3]) ); // point f - vertices[6] = NSMakePoint( SPRectLeft(lineRects[1]), SPRectTop(lineRects[1]) ); // point g - vertices[7] = NSMakePoint( SPRectLeft(lineRects[0]), SPRectTop(lineRects[1]) ); // point h - - for (NSUInteger j=0; j<8; j++) { - NSPoint curr = vertices[j]; - NSPoint prev = vertices[(j+8-1)%8]; - NSPoint next = vertices[(j+1)%8]; - - CGFloat s = radius/SPPointDistance(prev, curr); - if (s>0.5) s = 0.5; - CGFloat t = radius/SPPointDistance(curr, next); - if (t>0.5) t = 0.5; - - NSPoint a = SPPointOnLine(curr, prev, 0.5); - NSPoint b = SPPointOnLine(curr, prev, s); - NSPoint c = curr; - NSPoint d = SPPointOnLine(curr, next, t); - NSPoint e = SPPointOnLine(curr, next, 0.5); - - if (j==0) [funkyPath moveToPoint:a]; - [funkyPath lineToPoint: b]; - [funkyPath curveToPoint:d controlPoint1:SPPointOnLine(b, c, kappa) controlPoint2:SPPointOnLine(d, c, kappa)]; - [funkyPath lineToPoint: e]; - } - } else { - //highlight disconnected snippet parts (or single line snippet) - for (NSUInteger j=0; j 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 - -/* - * 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; - - // Cancel autocompletion trigger - if([prefs boolForKey:SPCustomQueryAutoComplete]) - [NSObject cancelPreviousPerformRequestsWithTarget:self - selector:@selector(doAutoCompletion) - object:nil]; - - 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] doubleValue]]; - } - - // Start autocompletion if enabled - if([[NSApp keyWindow] firstResponder] == self && [prefs boolForKey:SPCustomQueryAutoComplete] && !completionIsOpen && editedMask != 1 && [textStore editedRange].length) - [self performSelector:@selector(doAutoCompletion) withObject:nil afterDelay:[[prefs valueForKey:SPCustomQueryAutoCompleteDelay] 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 && !isProcessingMirroredSnippets) { - // Remove any fully nested snippets relative to the current snippet which was edited - NSUInteger currentSnippetLocation = snippetControlArray[currentSnippetIndex][0]; - NSUInteger currentSnippetMaxRange = snippetControlArray[currentSnippetIndex][0] + snippetControlArray[currentSnippetIndex][1]; - NSInteger i; - 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; - } - } - - NSUInteger editStartPosition = [textStore editedRange].location; - NSUInteger changeInLength = [textStore changeInLength]; - - // 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; - } - } - } - // Adjust start position of mirrored snippets after caret position - if(mirroredCounter > -1) - for(i=0; i<=mirroredCounter; i++) { - if(editStartPosition < snippetMirroredControlArray[i][1]) { - snippetMirroredControlArray[i][1] += changeInLength; - } - } - } - - if(mirroredCounter > -1 && snippetControlCounter > -1) { - [self performSelector:@selector(processMirroredSnippets) withObject:nil afterDelay:0.0]; - } - - - } - if([[self textStorage] changeInLength] > 0) - textBufferSizeIncreased = YES; - else - textBufferSizeIncreased = NO; - - if([[self textStorage] changeInLength] < SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) - [self doSyntaxHighlighting]; - - } else { - textBufferSizeIncreased = NO; - } - - startListeningToBoundChanges = YES; - -} - -/* - * Set font panel's valid modes - */ -- (NSUInteger)validModesForFontPanel:(NSFontPanel *)fontPanel -{ - return (NSFontPanelSizeModeMask|NSFontPanelCollectionModeMask); -} - -#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 %@ of data?", @"message of panel asking for confirmation for inserting large text from dragging action"), - [NSString stringForByteSize:[filesize longLongValue]]]]; - [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; - } - - // Insert selected items coming from the Navigator - if ( [[pboard types] containsObject:@"SPDragFromNavigatorPboardType"] ) { - NSPoint draggingLocation = [sender draggingLocation]; - draggingLocation = [self convertPoint:draggingLocation fromView:nil]; - NSUInteger characterIndex = [self characterIndexOfPoint:draggingLocation]; - [self setSelectedRange:NSMakeRange(characterIndex,0)]; - - NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[pboard dataForType:@"SPDragFromNavigatorPboardType"]] autorelease]; - NSArray *draggedItems = [[NSArray alloc] initWithArray:(NSArray *)[unarchiver decodeObjectForKey:@"itemdata"]]; - [unarchiver finishDecoding]; - - NSMutableString *dragString = [NSMutableString string]; - NSMutableString *aPath = [NSMutableString string]; - - NSString *currentDb = nil; - NSString *currentTable = nil; - - if (tablesListInstance && [tablesListInstance selectedDatabase]) - currentDb = [tablesListInstance selectedDatabase]; - if (tablesListInstance && [tablesListInstance tableName]) - currentTable = [tablesListInstance tableName]; - - if(!currentDb) currentDb = @""; - if(!currentTable) currentTable = @""; - - for(NSString* item in draggedItems) { - if([dragString length]) [dragString appendString:@", "]; - [aPath setString:item]; - // Insert path relative to the current selected db and table if any - [aPath replaceOccurrencesOfRegex:[NSString stringWithFormat:@"^%@%@", currentDb, SPUniqueSchemaDelimiter] withString:@""]; - [aPath replaceOccurrencesOfRegex:[NSString stringWithFormat:@"^%@%@", currentTable, SPUniqueSchemaDelimiter] withString:@""]; - [dragString appendString:[[aPath componentsSeparatedByString:SPUniqueSchemaDelimiter] componentsJoinedByPeriodAndBacktickQuoted]]; - } - [self breakUndoCoalescing]; - [self insertText:dragString]; - if (draggedItems) [draggedItems release]; - 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)changeFont:(id)sender -{ - if (prefs && [self font] != nil) { - [prefs setObject:[NSArchiver archivedDataWithRootObject:[self font]] forKey:SPCustomQueryEditorFont]; - NSFont *nf = [[NSFontPanel sharedFontPanel] panelConvertFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; - BOOL oldEditable = [self isEditable]; - [self setEditable:YES]; - [self setFont:nf]; - [self setEditable:oldEditable]; - [self setNeedsDisplay:YES]; - [prefs setObject:[NSArchiver archivedDataWithRootObject:nf] forKey:SPCustomQueryEditorFont]; - } -} - -- (void) dealloc -{ - - // Cancel any deferred calls - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - - // Remove observers - [[NSNotificationCenter defaultCenter] removeObserver:self]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorFont]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorBackgroundColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorHighlightQueryColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryHighlightCurrentQuery]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorCommentColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorQuoteColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorSQLKeywordColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorBacktickColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorNumericColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorVariableColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorTextColor]; - [prefs removeObserver:self forKeyPath:SPCustomQueryEditorTabStopWidth]; - [prefs removeObserver:self forKeyPath:SPCustomQueryAutoUppercaseKeywords]; - - if (completionIsOpen) [completionPopup close], completionIsOpen = NO; - [prefs release]; - [lineNumberView release]; - if(queryHiliteColor) [queryHiliteColor release]; - if(queryEditorBackgroundColor) [queryEditorBackgroundColor release]; - if(commentColor) [commentColor release]; - if(quoteColor) [quoteColor release]; - if(keywordColor) [keywordColor release]; - if(backtickColor) [backtickColor release]; - if(numericColor) [numericColor release]; - if(variableColor) [variableColor release]; - if(otherTextColor) [otherTextColor release]; - [super dealloc]; -} - -@end diff --git a/Source/CustomQuery.h b/Source/CustomQuery.h index 829083fe..f4299db0 100644 --- a/Source/CustomQuery.h +++ b/Source/CustomQuery.h @@ -28,7 +28,7 @@ #import #import "CMCopyTable.h" -#import "CMTextView.h" +#import "SPTextView.h" #import "RegexKitLite.h" #define SP_HELP_TOC_SEARCH_STRING @"contents" @@ -75,7 +75,7 @@ IBOutlet NSMenuItem *copyHistoryMenuItem; IBOutlet NSPopUpButton *encodingPopUp; - IBOutlet CMTextView *textView; + IBOutlet SPTextView *textView; IBOutlet CMCopyTable *customQueryView; IBOutlet NSScrollView *customQueryScrollView; IBOutlet id errorText; diff --git a/Source/SPContentFilterManager.m b/Source/SPContentFilterManager.m index ac9bef86..de2564b1 100644 --- a/Source/SPContentFilterManager.m +++ b/Source/SPContentFilterManager.m @@ -208,7 +208,7 @@ } /** - * This method is only implemented to be compatible with CMTextView. + * This method is only implemented to be compatible with SPTextView. */ - (id)customQueryInstance { diff --git a/Source/SPFieldMapperController.h b/Source/SPFieldMapperController.h index f6187d48..266986f4 100644 --- a/Source/SPFieldMapperController.h +++ b/Source/SPFieldMapperController.h @@ -25,7 +25,7 @@ #import #import -@class CMTextView; +@class SPTextView; @interface SPFieldMapperController : NSWindowController { @@ -64,7 +64,7 @@ IBOutlet id lowPriorityUpdateCheckBox; IBOutlet id highPriorityCheckBox; IBOutlet id skipexistingRowsCheckBox; - IBOutlet CMTextView *onupdateTextView; + IBOutlet SPTextView *onupdateTextView; IBOutlet id advancedButton; diff --git a/Source/SPFieldMapperController.m b/Source/SPFieldMapperController.m index 0a51e20f..8ba0ebbb 100644 --- a/Source/SPFieldMapperController.m +++ b/Source/SPFieldMapperController.m @@ -30,7 +30,7 @@ #import "SPStringAdditions.h" #import "SPConstants.h" #import "SPNotLoaded.h" -#import "CMTextView.h" +#import "SPTextView.h" @implementation SPFieldMapperController diff --git a/Source/SPNarrowDownCompletion.m b/Source/SPNarrowDownCompletion.m index 668e01c1..eaa3c43c 100644 --- a/Source/SPNarrowDownCompletion.m +++ b/Source/SPNarrowDownCompletion.m @@ -36,7 +36,7 @@ #import "SPConstants.h" #import "SPQueryController.h" #import "RegexKitLite.h" -#import "CMTextView.h" +#import "SPTextView.h" #import "SPConstants.h" diff --git a/Source/SPQueryFavoriteManager.h b/Source/SPQueryFavoriteManager.h index c3b93ab5..e62244ef 100644 --- a/Source/SPQueryFavoriteManager.h +++ b/Source/SPQueryFavoriteManager.h @@ -25,7 +25,7 @@ #import -@class BWAnchoredButtonBar, CMTextView, TableDocument; +@class BWAnchoredButtonBar, SPTextView, TableDocument; @interface NSObject (SPQueryFavoriteManagerDelegate) @@ -43,7 +43,7 @@ IBOutlet NSTableView *favoritesTableView; IBOutlet NSTextField *favoriteNameTextField; IBOutlet NSTextField *favoriteTabTriggerTextField; - IBOutlet CMTextView *favoriteQueryTextView; + IBOutlet SPTextView *favoriteQueryTextView; IBOutlet NSButton *removeButton; IBOutlet BWAnchoredButtonBar *splitViewButtonBar; diff --git a/Source/SPQueryFavoriteManager.m b/Source/SPQueryFavoriteManager.m index 40648f91..3840bd1e 100644 --- a/Source/SPQueryFavoriteManager.m +++ b/Source/SPQueryFavoriteManager.m @@ -30,7 +30,7 @@ #import "SPConstants.h" #import "SPConnectionController.h" #import "RegexKitLite.h" -#import "CMTextView.h" +#import "SPTextView.h" #define SP_MULTIPLE_SELECTION_PLACEHOLDER_STRING NSLocalizedString(@"[multiple selection]", @"[multiple selection]") #define SP_NO_SELECTION_PLACEHOLDER_STRING NSLocalizedString(@"[no selection]", @"[no selection]") @@ -178,7 +178,7 @@ } /** - * This method is only implemented to be compatible with CMTextView. + * This method is only implemented to be compatible with SPTextView. */ - (id)customQueryInstance { diff --git a/Source/SPTextView.h b/Source/SPTextView.h new file mode 100644 index 00000000..2964845a --- /dev/null +++ b/Source/SPTextView.h @@ -0,0 +1,148 @@ +// +// $Id: CMTextView.h 2240 2010-05-23 21:44:59Z rowanb $ +// +// SPTextView.h +// 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 +#import + +#import "NoodleLineNumberView.h" +#import "CMCopyTable.h" + +#define SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING 10000 + +@class SPNarrowDownCompletion, TableDocument, TablesList, CustomQuery; + +@interface SPTextView : NSTextView +{ + IBOutlet TableDocument *tableDocumentInstance; + IBOutlet TablesList *tablesListInstance; + IBOutlet CustomQuery *customQueryInstance; + + BOOL autoindentEnabled; + BOOL autopairEnabled; + BOOL autoindentIgnoresEnter; + BOOL autouppercaseKeywordsEnabled; + BOOL delBackwardsWasPressed; + BOOL autohelpEnabled; + NoodleLineNumberView *lineNumberView; + + BOOL startListeningToBoundChanges; + BOOL textBufferSizeIncreased; + + NSString *showMySQLHelpFor; + + IBOutlet NSScrollView *scrollView; + SPNarrowDownCompletion *completionPopup; + + NSUserDefaults *prefs; + + MCPConnection *mySQLConnection; + NSInteger mySQLmajorVersion; + + NSInteger snippetControlArray[20][3]; + NSInteger snippetMirroredControlArray[20][3]; + NSInteger snippetControlCounter; + NSInteger snippetControlMax; + NSInteger currentSnippetIndex; + NSInteger mirroredCounter; + BOOL snippetWasJustInserted; + BOOL isProcessingMirroredSnippets; + + BOOL completionIsOpen; + BOOL completionWasReinvokedAutomatically; + BOOL completionWasRefreshed; + BOOL completionFuzzyMode; + NSUInteger completionParseRangeLocation; + + NSColor *queryHiliteColor; + NSColor *queryEditorBackgroundColor; + NSColor *commentColor; + NSColor *quoteColor; + NSColor *keywordColor; + NSColor *backtickColor; + NSColor *numericColor; + NSColor *variableColor; + NSColor *otherTextColor; + NSRange queryRange; + BOOL shouldHiliteQuery; +} + +@property(retain) NSColor* queryHiliteColor; +@property(retain) NSColor* queryEditorBackgroundColor; +@property(retain) NSColor* commentColor; +@property(retain) NSColor* quoteColor; +@property(retain) NSColor* keywordColor; +@property(retain) NSColor* backtickColor; +@property(retain) NSColor* numericColor; +@property(retain) NSColor* variableColor; +@property(retain) NSColor* otherTextColor; +@property(assign) NSRange queryRange; +@property(assign) BOOL shouldHiliteQuery; +@property(assign) BOOL completionIsOpen; +@property(assign) BOOL completionWasReinvokedAutomatically; + +- (IBAction)showMySQLHelpForCurrentWord:(id)sender; + +- (BOOL) isNextCharMarkedBy:(id)attribute withValue:(id)aValue; +- (BOOL) areAdjacentCharsLinked; +- (BOOL) isCaretAdjacentToAlphanumCharWithInsertionOf:(unichar)aChar; +- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix; +- (BOOL) shiftSelectionRight; +- (BOOL) shiftSelectionLeft; +- (void) setAutoindent:(BOOL)enableAutoindent; +- (BOOL) autoindent; +- (void) setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter; +- (BOOL) autoindentIgnoresEnter; +- (void) setAutopair:(BOOL)enableAutopair; +- (BOOL) autopair; +- (void) setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords; +- (BOOL) autouppercaseKeywords; +- (void) setAutohelp:(BOOL)enableAutohelp; +- (BOOL) autohelp; +- (void) setTabStops; +- (void) selectLineNumber:(NSUInteger)lineNumber ignoreLeadingNewLines:(BOOL)ignLeadingNewLines; +- (NSUInteger) getLineNumberForCharacterIndex:(NSUInteger)anIndex; +- (void) autoHelp; +- (void) doSyntaxHighlighting; +- (NSBezierPath*)roundedBezierPathAroundRange:(NSRange)aRange; +- (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion; +- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch autoCompleteMode:(BOOL)autoCompleteMode; +- (void) doAutoCompletion; +- (void) refreshCompletion; +- (NSArray *)suggestionsForSQLCompletionWith:(NSString *)currentWord dictMode:(BOOL)isDictMode browseMode:(BOOL)dbBrowseMode withTableName:(NSString*)aTableName withDbName:(NSString*)aDbName; +- (void) selectCurrentQuery; +- (void) processMirroredSnippets; + +- (BOOL)checkForCaretInsideSnippet; +- (void)insertAsSnippet:(NSString*)theSnippet atRange:(NSRange)targetRange; + +- (void)showCompletionListFor:(NSString*)kind atRange:(NSRange)aRange fuzzySearch:(BOOL)fuzzySearchMode; + +- (NSUInteger)characterIndexOfPoint:(NSPoint)aPoint; +- (void)insertFileContentOfFile:(NSString *)aPath; + +- (BOOL)isSnippetMode; + +- (NSString *)runBashCommand:(NSString *)command; + +@end diff --git a/Source/SPTextView.m b/Source/SPTextView.m new file mode 100644 index 00000000..5a096b93 --- /dev/null +++ b/Source/SPTextView.m @@ -0,0 +1,3319 @@ +// +// $Id$ +// +// SPTextView.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 "SPTextView.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" +#import "SPTooltip.h" +#import "TablesList.h" +#import "SPNavigatorController.h" +#import "SPAlertSheets.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 NSUInteger yylex(); +extern NSUInteger 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 @"s" // attribute for found SQL keywords +#define kQuote @"Quote" +#define kQuoteValue @"isQuoted" +#define kValue @"x" +#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 SP_MAX_TEXT_SIZE_FOR_SYNTAX_HIGHLIGHTING 20000000 + +#define MYSQL_DOC_SEARCH_URL @"http://dev.mysql.com/doc/refman/%@/en/%@.html" + +#pragma mark - + +// some helper functions for handling rectangles and points +// needed in roundedBezierPathAroundRange: +static inline CGFloat SPRectTop(NSRect rectangle) { return rectangle.origin.y; } +static inline CGFloat SPRectBottom(NSRect rectangle) { return rectangle.origin.y+rectangle.size.height; } +static inline CGFloat SPRectLeft(NSRect rectangle) { return rectangle.origin.x; } +static inline CGFloat SPRectRight(NSRect rectangle) { return rectangle.origin.x+rectangle.size.width; } +static inline CGFloat SPPointDistance(NSPoint a, NSPoint b) { return sqrt( (a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y) ); } +static inline NSPoint SPPointOnLine(NSPoint a, NSPoint b, CGFloat t) { return NSMakePoint(a.x*(1.-t) + b.x*t, a.y*(1.-t) + b.y*t); } + +@implementation SPTextView + +@synthesize queryHiliteColor; +@synthesize queryEditorBackgroundColor; +@synthesize commentColor; +@synthesize quoteColor; +@synthesize keywordColor; +@synthesize backtickColor; +@synthesize numericColor; +@synthesize variableColor; +@synthesize otherTextColor; +@synthesize queryRange; +@synthesize shouldHiliteQuery; +@synthesize completionIsOpen; +@synthesize completionWasReinvokedAutomatically; + +/* + * 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 +{ + + prefs = [[NSUserDefaults standardUserDefaults] retain]; + [self setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; + + // Set self as delegate for the textView's textStorage to enable syntax highlighting, + [[self textStorage] setDelegate:self]; + + // Set defaults for general usage + autoindentEnabled = NO; + autopairEnabled = YES; + autoindentIgnoresEnter = NO; + autouppercaseKeywordsEnabled = NO; + autohelpEnabled = NO; + delBackwardsWasPressed = NO; + startListeningToBoundChanges = NO; + textBufferSizeIncreased = NO; + snippetControlCounter = -1; + mirroredCounter = -1; + completionPopup = nil; + completionIsOpen = NO; + isProcessingMirroredSnippets = NO; + completionWasRefreshed = NO; + + lineNumberView = [[NoodleLineNumberView alloc] initWithScrollView:scrollView]; + [scrollView setVerticalRulerView:lineNumberView]; + [scrollView setHasHorizontalRuler:NO]; + [scrollView setHasVerticalRuler:YES]; + [scrollView setRulersVisible:YES]; + [self setAllowsDocumentBackgroundColorChange:YES]; + [self setContinuousSpellCheckingEnabled:NO]; + [self setAutoindent:[prefs boolForKey:SPCustomQueryAutoIndent]]; + [self setAutoindentIgnoresEnter:YES]; + [self setAutopair:[prefs boolForKey:SPCustomQueryAutoPairCharacters]]; + [self setAutohelp:[prefs boolForKey:SPCustomQueryUpdateAutoHelp]]; + [self setAutouppercaseKeywords:[prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]]; + [self setCompletionWasReinvokedAutomatically:NO]; + + + // Re-define tab stops for a better editing + [self setTabStops]; + + // 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]]; + + [self setQueryHiliteColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorHighlightQueryColor]]]; + [self setQueryEditorBackgroundColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorBackgroundColor]]]; + [self setCommentColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorCommentColor]]]; + [self setQuoteColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorQuoteColor]]]; + [self setKeywordColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorSQLKeywordColor]]]; + [self setBacktickColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorBacktickColor]]]; + [self setNumericColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorNumericColor]]]; + [self setVariableColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorVariableColor]]]; + [self setOtherTextColor:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorTextColor]]]; + [self setTextColor:[self otherTextColor]]; + [self setInsertionPointColor:[self otherTextColor]]; + [self setShouldHiliteQuery:[prefs boolForKey:SPCustomQueryHighlightCurrentQuery]]; + + // Register observers for the when editor background colors preference changes + [prefs addObserver:self forKeyPath:SPCustomQueryEditorFont options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorBackgroundColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorHighlightQueryColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryHighlightCurrentQuery options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorCommentColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorQuoteColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorSQLKeywordColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorBacktickColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorNumericColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorVariableColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorTextColor options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryEditorTabStopWidth options:NSKeyValueObservingOptionNew context:NULL]; + [prefs addObserver:self forKeyPath:SPCustomQueryAutoUppercaseKeywords options:NSKeyValueObservingOptionNew context:NULL]; + +} + +- (void) setConnection:(MCPConnection *)theConnection withVersion:(NSInteger)majorVersion +{ + mySQLConnection = theConnection; + mySQLmajorVersion = majorVersion; +} + +/** + * This method is called as part of Key Value Observing which is used to watch for prefernce changes which effect the interface. + */ +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:SPCustomQueryEditorBackgroundColor]) { + [self setQueryEditorBackgroundColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + [self setNeedsDisplay:YES]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorFont]) { + [self setFont:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + [self setNeedsDisplay:YES]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorHighlightQueryColor]) { + [self setQueryHiliteColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + [self setNeedsDisplay:YES]; + } else if ([keyPath isEqualToString:SPCustomQueryHighlightCurrentQuery]) { + [self setShouldHiliteQuery:[[change objectForKey:NSKeyValueChangeNewKey] boolValue]]; + [self setNeedsDisplay:YES]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorCommentColor]) { + [self setCommentColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorQuoteColor]) { + [self setQuoteColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorSQLKeywordColor]) { + [self setKeywordColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorBacktickColor]) { + [self setBacktickColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorNumericColor]) { + [self setNumericColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorVariableColor]) { + [self setVariableColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorTextColor]) { + [self setOtherTextColor:[NSUnarchiver unarchiveObjectWithData:[change objectForKey:NSKeyValueChangeNewKey]]]; + [self setTextColor:[self otherTextColor]]; + if([[self string] length]<100000 && [self isEditable]) + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.1]; + } else if ([keyPath isEqualToString:SPCustomQueryEditorTabStopWidth]) { + [self setTabStops]; + } else if ([keyPath isEqualToString:SPCustomQueryAutoUppercaseKeywords]) { + [self setAutouppercaseKeywords:[prefs boolForKey:SPCustomQueryAutoUppercaseKeywords]]; + } +} + +/* + * 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(currentWord == nil) currentWord = [NSString stringWithString:@""]; + // 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 6MB + if([[self string] length] && [[self string] length]<6000000) + { + NSMutableSet *uniqueArray = [NSMutableSet setWithCapacity:5]; + + for(id w in [[self textStorage] words]) + [uniqueArray addObject:[w string]]; + // Remove current word from list + + [uniqueArray removeObject:currentWord]; + + NSInteger reverseSort = NO; + + for(id w in [[uniqueArray allObjects] sortedArrayUsingFunction:alphabeticSort context:&reverseSort]) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; + + } + + if(!isDictMode) { + // Add predefined keywords + NSArray *keywordList = [[NSArray arrayWithArray:[[SPQueryController sharedQueryController] keywordList]] retain]; + for(id s in keywordList) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"dummy-small", @"image", nil]]; + + // Add predefined functions + NSArray *functionList = [[NSArray arrayWithArray:[[SPQueryController sharedQueryController] functionList]] retain]; + for(id s in functionList) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:s, @"display", @"func-small", @"image", nil]]; + + [functionList release]; + [keywordList release]; + } + + } + + if(!isDictMode && [mySQLConnection isConnected]) + { + // Add structural db/table/field data to completions list or fallback to gathering TablesList data + + NSString* connectionID; + if(tableDocumentInstance) + connectionID = [tableDocumentInstance connectionID]; + else + connectionID = @"_"; + + // Try to get structure data + NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[SPNavigatorController sharedNavigatorController] dbStructureForConnection:connectionID]]; + + if(dbs != nil && [dbs isKindOfClass:[NSDictionary class]] && [dbs count]) { + NSMutableArray *allDbs = [NSMutableArray array]; + [allDbs addObjectsFromArray:[dbs allKeys]]; + + NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; + NSMutableArray *sortedDbs = [NSMutableArray array]; + [sortedDbs addObjectsFromArray:[allDbs sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; + + NSString *currentDb = nil; + NSString *currentTable = nil; + + if (tablesListInstance && [tablesListInstance selectedDatabase]) + currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [tablesListInstance selectedDatabase]]; + if (tablesListInstance && [tablesListInstance tableName]) + currentTable = [tablesListInstance tableName]; + + // Put current selected db at the top + if(aTableName == nil && aDbName == nil && [tablesListInstance selectedDatabase]) { + currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [tablesListInstance selectedDatabase]]; + [sortedDbs removeObject:currentDb]; + [sortedDbs insertObject:currentDb atIndex:0]; + } + + NSString* aTableName_id; + NSString* aDbName_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, aDbName]; + if(aDbName && aTableName) + aTableName_id = [NSString stringWithFormat:@"%@%@%@", aDbName_id, SPUniqueSchemaDelimiter, aTableName]; + else + aTableName_id = [NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, aTableName]; + + + // Put information_schema and/or mysql db at the end if not selected + NSString* mysql_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, @"mysql"]; + NSString* inf_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, @"information_schema"]; + if(currentDb && ![currentDb isEqualToString:mysql_id] && [sortedDbs containsObject:mysql_id]) { + [sortedDbs removeObject:mysql_id]; + [sortedDbs addObject:mysql_id]; + } + if(currentDb && ![currentDb isEqualToString:inf_id] && [sortedDbs containsObject:inf_id]) { + [sortedDbs removeObject:inf_id]; + [sortedDbs addObject:inf_id]; + } + + BOOL aTableNameExists = NO; + if(!aDbName) { + + // Try to suggest only items which are uniquely valid for the parsed string + NSArray *uniqueSchema = [[SPNavigatorController sharedNavigatorController] getUniqueDbIdentifierFor:[aTableName lowercaseString] andConnection:[[[self delegate] valueForKeyPath:@"tableDocumentInstance"] connectionID]]; + NSInteger uniqueSchemaKind = [[uniqueSchema objectAtIndex:0] intValue]; + + // 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] isKindOfClass:[NSDictionary class]] + && [[dbs objectForKey:currentDb] objectForKey:[NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, [uniqueSchema objectAtIndex:1]]] + && uniqueSchemaKind == 2) { + aTableNameExists = YES; + aTableName = [uniqueSchema objectAtIndex:1]; + aTableName_id = [NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, aTableName]; + aDbName_id = [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_id = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [uniqueSchema objectAtIndex:1]]; + aTableNameExists = NO; + } + + } else if (aDbName && [aDbName length]) { + if(aTableName && [aTableName length] + && [dbs objectForKey:aDbName_id] && [[dbs objectForKey:aDbName_id] isKindOfClass:[NSDictionary class]] + && [[dbs objectForKey:aDbName_id] objectForKey:[NSString stringWithFormat:@"%@%@%@", aDbName_id, SPUniqueSchemaDelimiter, aTableName]]) { + aTableNameExists = YES; + } + } + + // If aDbName exist show only those table + if([allDbs containsObject:aDbName_id]) { + [sortedDbs removeAllObjects]; + [sortedDbs addObject:aDbName_id]; + } + + for(id db in sortedDbs) { + + NSArray *allTables; + if([[dbs objectForKey:db] isKindOfClass:[NSDictionary class]]) + allTables = [[dbs objectForKey:db] allKeys]; + else { + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[[[dbs objectForKey:db] description] componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"database-small", @"image", @"", @"isRef", nil]]; + continue; + } + + NSString *dbpath = [db substringFromIndex:[db rangeOfString:SPUniqueSchemaDelimiter].location]; + + NSMutableArray *sortedTables = [NSMutableArray array]; + if(aTableNameExists) { + [sortedTables addObject:aTableName_id]; + } else { + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[db componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"database-small", @"image", @"", @"isRef", nil]]; + [sortedTables addObjectsFromArray:[allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]]; + if([sortedTables count] > 1 && [sortedTables containsObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable]]) { + [sortedTables removeObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable]]; + [sortedTables insertObject:[NSString stringWithFormat:@"%@%@%@", db, SPUniqueSchemaDelimiter, currentTable] atIndex:0]; + } + } + for(id table in sortedTables) { + NSDictionary *theTable = [[dbs objectForKey:db] objectForKey:table]; + NSString *tablepath = [table substringFromIndex:[table rangeOfString:SPUniqueSchemaDelimiter].location]; + 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 componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"table-small-square", @"image", tablepath, @"path", @"", @"isRef", nil]]; + break; + case 1: + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"table-view-small-square", @"image", tablepath, @"path", @"", @"isRef", nil]]; + break; + case 2: + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"proc-small", @"image", tablepath, @"path", @"", @"isRef", nil]]; + breakFlag = YES; + break; + case 3: + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"func-small", @"image", tablepath, @"path", @"", @"isRef", nil]]; + breakFlag = YES; + break; + } + if(!breakFlag) { + NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; + for(id field in sortedFields) { + if(![field hasPrefix:@" "]) { + NSString *fieldpath = [field substringFromIndex:[field rangeOfString:SPUniqueSchemaDelimiter].location]; + NSArray *def = [theTable objectForKey:field]; + NSString *typ = [NSString stringWithFormat:@"%@ %@ %@", [def objectAtIndex:0], [def objectAtIndex:3], [def objectAtIndex:5]]; + // 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 componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", + @"field-small-square", @"image", + fieldpath, @"path", + t, @"type", + lst, @"list", + @"", @"isRef", + nil]]; + } else { + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: + [[field componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", + @"field-small-square", @"image", + fieldpath, @"path", + typ, @"type", + @"", @"isRef", + nil]]; + } + } + } + } + } + } + if(desc) [desc release]; + } else { + + // [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 [tablesListInstance allDatabaseNames]) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; + + // Add all system database names to completions list + for (id obj in [tablesListInstance allSystemDatabaseNames]) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]]; + + // Add table names to completions list + for (id obj in [tablesListInstance allTableNames]) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-small-square", @"image", @"", @"isRef", nil]]; + + // Add view names to completions list + for (id obj in [tablesListInstance 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 ([tableDocumentInstance table] != nil) + for (id obj in [[tableDocumentInstance 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 [tablesListInstance 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 [tablesListInstance allFunctionNames]) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"func-small", @"image", @"", @"isRef", nil]]; + } + } + } + + return [possibleCompletions autorelease]; + +} + +- (void) doAutoCompletion +{ + if(completionIsOpen || !self || ![self delegate]) return; + + // Cancel autocompletion trigger + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + + NSRange r = [self selectedRange]; + + if(![self delegate] || ![[self delegate] isKindOfClass:[CustomQuery class]] || r.length || snippetControlCounter > -1) return; + + if(r.location) { + NSCharacterSet *ignoreCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@"\"'`;,()[]{}=+/<> \t\n\r"]; + + // Check the previous character and don't autocomplete if the character is whitespace or certain types of punctuation + if ([ignoreCharacterSet characterIsMember:[[self string] characterAtIndex:r.location - 1]]) return; + + // Suppress auto-completion if the window isn't active anymore + if ([[NSApp keyWindow] firstResponder] != self) return; + + // Trigger the completion + [self doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:YES]; + } + +} + +- (void) refreshCompletion +{ + if(completionWasRefreshed) return; + completionWasRefreshed = YES; + [self doCompletionByUsingSpellChecker:NO fuzzyMode:completionFuzzyMode autoCompleteMode:NO]; +} + +- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch autoCompleteMode:(BOOL)autoCompleteMode +{ + + // Cancel autocompletion trigger + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + if(![self isEditable] || (completionIsOpen && !completionWasReinvokedAutomatically)) { + return; + } + + [self breakUndoCoalescing]; + + // Remember state for refreshCompletion + completionFuzzyMode = fuzzySearch; + + NSUInteger caretPos = NSMaxRange([self selectedRange]); + + BOOL caretMovedLeft = NO; + + // Check if caret is located after a ` - if so move caret inside + if(!autoCompleteMode && [[self string] length] && caretPos > 0 && [[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; + + // Break for long stuff + if(completionRange.length>100000) return; + + NSString* allow; // additional chars which won't close the suggestion list window + 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(tablesListInstance && [tablesListInstance selectedDatabase]) + currentDb = [tablesListInstance 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(NSUInteger i=0; i<[lineHead length]; i++) + if([lineHead characterAtIndex:i]=='`') caretIsInsideBackticks = !caretIsInsideBackticks; + + NSCharacterSet *whiteSpaceCharSet = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + NSUInteger 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); + + // Break for long stuff + if(parseRange.length>100000) return; + + 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]; + } + + // Cancel autocompletion trigger again if user typed something in while parsing + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + if (completionIsOpen) [completionPopup close], completionPopup = nil; + completionIsOpen = YES; + 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:[self isSnippetMode] + fuzzySearch:fuzzySearch + backtickMode:backtickMode + withDbName:dbName + withTableName:tableName + selectedDb:currentDb + caretMovedLeft:caretMovedLeft + autoComplete:autoCompleteMode + oneColumn:isDictMode + isQueryingDBStructure:[mySQLConnection isQueryingDatabaseStructure]]; + + completionParseRangeLocation = parseRange.location; + + //Get the NSPoint of the first character of the current word + NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(completionRange.location,0) 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 SPTextView + */ +- (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 == '('); + NSUInteger bufferLength = [[self string] length]; + + if(!bufferLength) return NO; + + // Check previous/next character for being alphanum + // @try block for bounds checking + @try + { + if(caretPosition==0) + leftIsAlphanum = NO; + else + leftIsAlphanum = [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition-1]] && !charIsOpenBracket; + } @catch(id ae) { } + @try { + if(caretPosition >= bufferLength) + rightIsAlphanum = NO; + else + 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 if (leftChar == '{') + matchingChar = '}'; + 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 + +- (IBAction)printDocument:(id)sender +{ + + // If Extended Table Info tab is active delegate the print call to the SPPrintController + // if the user doesn't select anything in self + if([[[[self delegate] class] description] isEqualToString:@"SPExtendedTableInfo"] && ![self selectedRange].length) { + [[[self delegate] valueForKeyPath:@"tableDocumentInstance"] printDocument:sender]; + return; + } + + // This will scale the view to fit the page without centering it. + [[NSPrintInfo sharedPrintInfo] setHorizontalPagination:NSFitPagination]; + [[NSPrintInfo sharedPrintInfo] setHorizontallyCentered:NO]; + [[NSPrintInfo sharedPrintInfo] setVerticallyCentered:NO]; + + NSRange r = NSMakeRange(0, [[self string] length]); + + // Remove all colors before printing for large text buffer + if(r.length > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) { + // Cancel all doSyntaxHighlighting requests + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doSyntaxHighlighting) + object:nil]; + [[self textStorage] removeAttribute:NSForegroundColorAttributeName range:r]; + [[self textStorage] removeAttribute:kLEXToken range:r]; + [[self textStorage] ensureAttributesAreFixedInRange:r]; + + } + [[self textStorage] ensureAttributesAreFixedInRange:r]; + + // Setup the print operation with the print info and view + NSPrintOperation *printOperation = [NSPrintOperation printOperationWithView:self printInfo:[NSPrintInfo sharedPrintInfo]]; + + // Order out print sheet + [printOperation runOperationModalForWindow:[self window] delegate:nil didRunSelector:NULL contextInfo:NULL]; + +} + +- (void)printOperationDidRun:(NSPrintOperation *)printOperation success:(BOOL)success contextInfo:(void *)contextInfo +{ + // Refresh syntax highlighting + [self performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.01]; +} + +/* + * Search for the current selection or current word in the MySQL Help + */ +- (IBAction) showMySQLHelpForCurrentWord:(id)sender +{ + [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 +{ + + NSRange currentRange = [self selectedRange]; + + // Only proceed if a selection is active + if (currentRange.length == 0 || ![self isEditable]) + return NO; + + NSString *selString = [[self string] substringWithRange:currentRange]; + + // Replace the current selection with the selected string wrapped in prefix and suffix + [self insertText:[NSString stringWithFormat:@"%@%@%@", prefix, selString, suffix]]; + + // Re-select original selection + NSRange innerSelectionRange = NSMakeRange(currentRange.location+1, [selString length]); + [self setSelectedRange:innerSelectionRange]; + + // If autopair is enabled mark last autopair character as autopair-linked + if([prefs boolForKey:SPCustomQueryAutoPairCharacters]) + [[self textStorage] addAttribute:kAPlinked value:kAPval range:NSMakeRange(NSMaxRange(innerSelectionRange), 1)]; + + 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 +{ + if([self isEditable]) + [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 + { + NSUInteger arrayCount = [lineRanges count]; + NSUInteger 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]; + if (lineNumber < 1) lineNumber = 1; + + // 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"; + NSUInteger i, indentedLinesLength = 0; + + if ([self selectedRange].location == NSNotFound || ![self isEditable]) return NO; + + // Indent the currently selected line if the caret is within a single line + if ([self selectedRange].length == 0) { + + // 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; + NSUInteger 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) { + + // 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; + mirroredCounter = -1; + snippetWasJustInserted = NO; +} + +/* + * Shows pre-defined completion list + */ +- (void)showCompletionListFor:(NSString*)kind atRange:(NSRange)aRange fuzzySearch:(BOOL)fuzzySearchMode +{ + + // Cancel auto-completion timer + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + NSMutableArray *possibleCompletions = [[[NSMutableArray alloc] initWithCapacity:0] autorelease]; + + NSString *connectionID; + if(tableDocumentInstance) + connectionID = [tableDocumentInstance connectionID]; + else + connectionID = @"_"; + + NSArray *arr = nil; + if([kind isEqualToString:@"$SP_ASLIST_ALL_TABLES"]) { + NSString *currentDb = nil; + + if (tablesListInstance && [tablesListInstance selectedDatabase]) + currentDb = [tablesListInstance selectedDatabase]; + + NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[mySQLConnection getDbStructure] objectForKey:connectionID]]; + + if(currentDb != nil && dbs != nil && [dbs count] && [dbs objectForKey:currentDb]) { + NSArray *allTables = [[dbs objectForKey:currentDb] allKeys]; + NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; + NSArray *sortedTables = [allTables sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; + [desc release]; + for(id table in sortedTables) { + NSDictionary * theTable = [[dbs objectForKey:currentDb] objectForKey:table]; + NSInteger structtype = [[theTable objectForKey:@" struct_type "] intValue]; + switch(structtype) { + case 0: + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-small-square", @"image", currentDb, @"path", @"", @"isRef", nil]]; + break; + case 1: + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:table, @"display", @"table-view-small-square", @"image", currentDb, @"path", @"", @"isRef", nil]]; + break; + } + } + } else { + arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allTableAndViewNames]]; + if(arr == nil) { + arr = [NSArray array]; + } + for(id w in arr) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"table-small-square", @"image", @"", @"isRef", nil]]; + } + } + else if([kind isEqualToString:@"$SP_ASLIST_ALL_DATABASES"]) { + arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allDatabaseNames]]; + if(arr == nil) { + arr = [NSArray array]; + } + for(id w in arr) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"database-small", @"image", @"", @"isRef", nil]]; + arr = [NSArray arrayWithArray:[[[self delegate] valueForKeyPath:@"tablesListInstance"] allSystemDatabaseNames]]; + if(arr == nil) { + arr = [NSArray array]; + } + for(id w in arr) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"database-small", @"image", @"", @"isRef", nil]]; + } + else if([kind isEqualToString:@"$SP_ASLIST_ALL_FIELDS"]) { + + NSString *currentDb = nil; + NSString *currentTable = nil; + + if (tablesListInstance && [tablesListInstance selectedDatabase]) + currentDb = [tablesListInstance selectedDatabase]; + if (tablesListInstance && [tablesListInstance tableName]) + currentTable = [tablesListInstance tableName]; + + NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[mySQLConnection getDbStructure] objectForKey:connectionID]]; + if(currentDb != nil && currentTable != nil && dbs != nil && [dbs count] && [dbs objectForKey:currentDb] && [[dbs objectForKey:currentDb] objectForKey:currentTable]) { + NSDictionary * theTable = [[dbs objectForKey:currentDb] objectForKey:currentTable]; + NSArray *allFields = [theTable allKeys]; + NSSortDescriptor *desc = [[NSSortDescriptor alloc] initWithKey:nil ascending:YES selector:@selector(localizedCompare:)]; + NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]]; + [desc release]; + for(id field in sortedFields) { + if(![field hasPrefix:@" "]) { + NSArray *def = [theTable objectForKey:field]; + NSString *typ = [NSString stringWithFormat:@"%@ %@ %@", [def objectAtIndex:0], [def objectAtIndex:1], [def objectAtIndex:2]]; + // 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:@"%@@%%@",currentTable,currentDb], @"path", SPUniqueSchemaDelimiter, + t, @"type", + lst, @"list", + @"", @"isRef", + nil]]; + } else { + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys: + field, @"display", + @"field-small-square", @"image", + [NSString stringWithFormat:@"%@%@%@",currentTable,currentDb], @"path", SPUniqueSchemaDelimiter, + typ, @"type", + @"", @"isRef", + nil]]; + } + } + } + } else { + arr = [NSArray arrayWithArray:[[tableDocumentInstance valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"]]; + if(arr == nil) { + arr = [NSArray array]; + } + for(id w in arr) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"field-small-square", @"image", @"", @"isRef", nil]]; + } + } + else { + NSLog(@"“%@” is not a valid completion list", kind); + NSBeep(); + return; + } + + if (completionIsOpen) [completionPopup close], completionPopup = nil; + completionIsOpen = YES; + completionPopup = [[SPNarrowDownCompletion alloc] initWithItems:possibleCompletions + alreadyTyped:@"" + staticPrefix:@"" + additionalWordCharacters:@"_." + caseSensitive:NO + charRange:aRange + parseRange:aRange + inView:self + dictMode:NO + dbMode:YES + tabTriggerMode:[self isSnippetMode] + fuzzySearch:fuzzySearchMode + backtickMode:NO + withDbName:@"" + withTableName:@"" + selectedDb:@"" + caretMovedLeft:NO + autoComplete:NO + oneColumn:NO + isQueryingDBStructure:NO]; + + //Get the NSPoint of the first character of the current word + NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(aRange.location,0) 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)]; + // 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]; + +} + +/* + * Update all mirrored snippets and adjust any involved instances + */ +- (void)processMirroredSnippets +{ + if(mirroredCounter > -1) { + + isProcessingMirroredSnippets = YES; + + NSInteger i, j, k, deltaLength; + NSRange mirroredRange; + + // Go through each defined mirrored snippet and update it + for(i=0; i<=mirroredCounter; i++) { + if(snippetMirroredControlArray[i][0] == currentSnippetIndex) { + + deltaLength = snippetControlArray[currentSnippetIndex][1]-snippetMirroredControlArray[i][2]; + + mirroredRange = NSMakeRange(snippetMirroredControlArray[i][1], snippetMirroredControlArray[i][2]); + NSString *mirroredString = nil; + + // For safety reasons + @try{ + mirroredString = [[self string] substringWithRange:NSMakeRange(snippetControlArray[currentSnippetIndex][0], snippetControlArray[currentSnippetIndex][1])]; + } + @catch(id ae) { + NSLog(@"Error while parsing for mirrored snippets. %@", [ae description]); + NSBeep(); + [self endSnippetSession]; + return; + } + + // Register for undo + [self shouldChangeTextInRange:mirroredRange replacementString:mirroredString]; + + [self replaceCharactersInRange:mirroredRange withString:mirroredString]; + snippetMirroredControlArray[i][2] = snippetControlArray[currentSnippetIndex][1]; + + // If a completion list is open adjust the theCharRange and theParseRange if a mirrored snippet + // was updated which is located before the initial position + if(completionIsOpen && snippetMirroredControlArray[i][1] < completionParseRangeLocation) + [completionPopup adjustWorkingRangeByDelta:deltaLength]; + + // Adjust all other snippets accordingly + for(j=0; j<=snippetControlMax; j++) { + if(snippetControlArray[j][0] > -1) { + if(snippetControlArray[j][0]+snippetControlArray[j][1]>=snippetMirroredControlArray[i][1]) { + snippetControlArray[j][0] += deltaLength; + } + } + } + // Adjust all mirrored snippets accordingly + for(k=0; k<=mirroredCounter; k++) { + if(i != k) { + if(snippetMirroredControlArray[k][1] > snippetMirroredControlArray[i][1]) { + snippetMirroredControlArray[k][1] += deltaLength; + } + } + } + } + } + + isProcessingMirroredSnippets = NO; + [self didChangeText]; + + } +} + + +/* + * Selects the current snippet defined by “currentSnippetIndex” + */ +- (void)selectCurrentSnippet +{ + if( snippetControlCounter > -1 + && currentSnippetIndex >= 0 + && currentSnippetIndex <= snippetControlMax + ) + { + + [self breakUndoCoalescing]; + + // 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) { + + NSRange r1 = NSMakeRange(snippetControlArray[currentSnippetIndex][0], snippetControlArray[currentSnippetIndex][1]); + + NSRange r2; + // Ensure the selection for nested snippets if it is at very end of the text buffer + // because NSIntersectionRange returns {0, 0} in such a case + if(r1.location == [[self string] length]) + r2 = NSMakeRange([[self string] length], 0); + else + r2 = NSIntersectionRange(NSMakeRange(0,[[self string] length]), r1); + + if(r1.location == r2.location && r1.length == r2.length) { + [self setSelectedRange:r2]; + NSString *snip = [[self string] substringWithRange:r2]; + + if([snip length] > 2 && [snip hasPrefix:@"¦"] && [snip hasSuffix:@"¦"]) { + BOOL fuzzySearchMode = ([snip hasPrefix:@"¦¦"] && [snip hasSuffix:@"¦¦"]) ? YES : NO; + NSInteger offset = (fuzzySearchMode) ? 2 : 1; + NSRange insertRange = NSMakeRange(r2.location,0); + NSString *newSnip = [snip substringWithRange:NSMakeRange(1*offset,[snip length]-(2*offset))]; + if([newSnip hasPrefix:@"$SP_ASLIST_"]) { + [self showCompletionListFor:newSnip atRange:NSMakeRange(r2.location, 0) fuzzySearch:fuzzySearchMode]; + return; + } else { + NSArray *list = [[snip substringWithRange:NSMakeRange(1*offset,[snip length]-(2*offset))] componentsSeparatedByString:@"¦"]; + NSMutableArray *possibleCompletions = [[[NSMutableArray alloc] initWithCapacity:[list count]] autorelease]; + for(id w in list) + [possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:w, @"display", @"dummy-small", @"image", nil]]; + + if (completionIsOpen) [completionPopup close], completionPopup = nil; + completionIsOpen = YES; + completionPopup = [[SPNarrowDownCompletion alloc] initWithItems:possibleCompletions + alreadyTyped:@"" + staticPrefix:@"" + additionalWordCharacters:@"_." + caseSensitive:NO + charRange:insertRange + parseRange:insertRange + inView:self + dictMode:NO + dbMode:NO + tabTriggerMode:[self isSnippetMode] + fuzzySearch:fuzzySearchMode + backtickMode:NO + withDbName:@"" + withTableName:@"" + selectedDb:@"" + caretMovedLeft:NO + autoComplete:NO + oneColumn:YES + isQueryingDBStructure:NO]; + + //Get the NSPoint of the first character of the current word + NSRange glyphRange = [[self layoutManager] glyphRangeForCharacterRange:NSMakeRange(r2.location,0) 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)]; + // 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]; + } + } + } else { + [self endSnippetSession]; + } + } + } 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)insertAsSnippet:(NSString*)theSnippet atRange:(NSRange)targetRange +{ + + // Do not allow the insertion of a query favorite if snippets are active + if(snippetControlCounter > -1) { + NSBeep(); + return; + } + + NSInteger i, j; + mirroredCounter = -1; + + // 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 + snippetMirroredControlArray[i][0] = -1; // mirrored snippet index + snippetMirroredControlArray[i][1] = -1; // mirrored snippet location + snippetMirroredControlArray[i][2] = -1; // mirrored snippet length + } + + if(theSnippet == nil || ![theSnippet length]) return; + + NSMutableString *snip = [[NSMutableString alloc] initWithCapacity:[theSnippet length]]; + + @try{ + NSString *re = @"(?s)(?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; + + // Replace internal variables + NSMutableString *theHintString = [[NSMutableString alloc] initWithCapacity:hintRange.length]; + [theHintString setString:[snip substringWithRange:hintRange]]; + if([theHintString isMatchedByRegex:@"(?"]; + } + [theHintString flushCachedRegexData]; + } + + while([theHintString isMatchedByRegex:@"(?"]; + } + [theHintString flushCachedRegexData]; + } + } + + // Handle escaped characters + [theHintString replaceOccurrencesOfRegex:@"\\\\(\\$\\(|\\}|\\$SP_)" withString:@"$1"]; + [theHintString flushCachedRegexData]; + + // If inside the snippet hint $(…) is defined run … as BASH command + // and replace $(…) by the return string of that command. Please note + // only one $(…) statement is allowed within one ${…} snippet environment. + NSRange tagRange = [theHintString rangeOfRegex:@"(?s)(? -1 && i != snipCnt && snippetControlArray[i][0] > snippetControlArray[snipCnt][0]) + snippetControlArray[i][0] -= 3+((snipCnt>9)?2:1); + + } + + // Parse for mirrored snippets + while([snip isMatchedByRegex:mirror_re]) { + mirroredCounter++; + if(mirroredCounter > 19) { + NSLog(@"Only 20 mirrored snippet placeholders allowed."); + NSBeep(); + break; + } else { + + NSRange snipRange = [snip rangeOfRegex:mirror_re capture:0L]; + NSInteger snipCnt = [[snip substringWithRange:[snip rangeOfRegex:mirror_re capture:1L]] intValue]; + + // Check for snippet number 19 (to simplify regexp) + if(snipCnt>18 || snipCnt<0) { + NSLog(@"Only snippets in the range of 0…18 allowed."); + [self endSnippetSession]; + break; + } + + [snip replaceCharactersInRange:snipRange withString:@""]; + [snip flushCachedRegexData]; + + // Store found mirrored snippet range + snippetMirroredControlArray[mirroredCounter][0] = snipCnt; + snippetMirroredControlArray[mirroredCounter][1] = snipRange.location + targetRange.location; + snippetMirroredControlArray[mirroredCounter][2] = 0; + + // Adjust successive snippets + for(i=0; i<20; i++) + if(snippetControlArray[i][0] > -1 && snippetControlArray[i][0] > snippetMirroredControlArray[mirroredCounter][1]) + snippetControlArray[i][0] -= 1+((snipCnt>9)?2:1); + + [snip flushCachedRegexData]; + } + } + // Preset mirrored snippets with according snippet content + if(mirroredCounter > -1) { + for(i=0; i<=mirroredCounter; i++) { + if(snippetControlArray[snippetMirroredControlArray[i][0]][0] > -1 && snippetControlArray[snippetMirroredControlArray[i][0]][1] > 0) { + [snip replaceCharactersInRange:NSMakeRange(snippetMirroredControlArray[i][1]-targetRange.location, snippetMirroredControlArray[i][2]) + withString:[snip substringWithRange:NSMakeRange(snippetControlArray[snippetMirroredControlArray[i][0]][0]-targetRange.location, snippetControlArray[snippetMirroredControlArray[i][0]][1])]]; + snippetMirroredControlArray[i][2] = snippetControlArray[snippetMirroredControlArray[i][0]][1]; + } + // Adjust successive snippets + for(j=0; j<20; j++) + if(snippetControlArray[j][0] > -1 && snippetControlArray[j][0] > snippetMirroredControlArray[i][1]) + snippetControlArray[j][0] += snippetControlArray[snippetMirroredControlArray[i][0]][1]; + // Adjust successive mirrored snippets + for(j=0; j<=mirroredCounter; j++) + if(snippetMirroredControlArray[j][1] > snippetMirroredControlArray[i][1]) + snippetMirroredControlArray[j][1] += snippetControlArray[snippetMirroredControlArray[i][0]][1]; + } + } + + 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; + } + + // unescape escaped snippets and re-adjust successive snippet locations : \${1:a} → ${1:a} + NSString *ure = @"(?s)\\\\\\$\\{(1?\\d):(.{0}|.*?[^\\\\])\\}"; + while([snip isMatchedByRegex:ure]) { + NSRange escapeRange = [snip rangeOfRegex:ure capture:0L]; + [snip replaceCharactersInRange:escapeRange withString:[snip substringWithRange:NSMakeRange(escapeRange.location+1,escapeRange.length-1)]]; + NSUInteger loc = escapeRange.location + targetRange.location; + [snip flushCachedRegexData]; + for(i=0; i<=snippetControlMax; i++) + if(snippetControlArray[i][0] > -1 && snippetControlArray[i][0] > loc) + snippetControlArray[i][0]--; + // Adjust mirrored snippets + if(mirroredCounter > -1) + for(i=0; i<=mirroredCounter; i++) + if(snippetMirroredControlArray[i][0] > -1 && snippetMirroredControlArray[i][1] > loc) + snippetMirroredControlArray[i][1]--; + } + + // Insert favorite query by selecting the tab trigger if any + [self setSelectedRange:targetRange]; + + // Registering for undo + [self breakUndoCoalescing]; + [self insertText:snip]; + + // If autopair is enabled check whether snip begins with ( and ends with ), if so mark ) as pair-linked + if([prefs boolForKey:SPCustomQueryAutoPairCharacters] && ([snip hasPrefix:@"("] && [snip hasSuffix:@")"] || ([snip hasPrefix:@"`"] && [snip hasSuffix:@"`"]) || ([snip hasPrefix:@"'"] && [snip hasSuffix:@"'"]) || ([snip hasPrefix:@"\""] && [snip hasSuffix:@"\""]))) + [[self textStorage] addAttribute:kAPlinked value:kAPval range:NSMakeRange([self selectedRange].location - 1, 1)]; + + // 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]; + +} + +/* + * Run 'command' as BASH command(s) and return the result. + * This task can be interrupted by pressing ⌘. + */ +- (NSString *)runBashCommand:(NSString *)command +{ + BOOL userTerminated = NO; + + NSTask *bashTask = [[NSTask alloc] init]; + [bashTask setLaunchPath: @"/bin/bash"]; + [bashTask setArguments:[NSArray arrayWithObjects: @"-c", command, nil]]; + + NSPipe *stdout_pipe = [NSPipe pipe]; + [bashTask setStandardOutput:stdout_pipe]; + NSFileHandle *stdout_file = [stdout_pipe fileHandleForReading]; + + NSPipe *stderr_pipe = [NSPipe pipe]; + [bashTask setStandardError:stderr_pipe]; + NSFileHandle *stderr_file = [stderr_pipe fileHandleForReading]; + [bashTask launch]; + + // Listen to ⌘. to terminate + while(1) { + if(![bashTask isRunning] || [bashTask processIdentifier] == 0) break; + NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + usleep(10000); + if(!event) continue; + if ([event type] == NSKeyDown) { + unichar key = [[event characters] length] == 1 ? [[event characters] characterAtIndex:0] : 0; + if (([event modifierFlags] & NSCommandKeyMask) && key == '.') { + [bashTask terminate]; + userTerminated = YES; + break; + } + } else { + [NSApp sendEvent:event]; + } + } + + [bashTask waitUntilExit]; + + if(userTerminated) { + if(bashTask) [bashTask release]; + NSBeep(); + NSLog(@"“%@” was terminated by user.", command); + return @""; + } + + // If return from bash re-activate Sequel Pro + [NSApp activateIgnoringOtherApps:YES]; + + NSInteger status = [bashTask terminationStatus]; + NSData *outdata = [stdout_file readDataToEndOfFile]; + NSData *errdata = [stderr_file readDataToEndOfFile]; + + if(outdata != nil) { + NSString *stdout = [[NSString alloc] initWithData:outdata encoding:NSUTF8StringEncoding]; + NSString *error = [[[NSString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; + if(bashTask) [bashTask release]; + if(stdout != nil) { + if (status == 0) { + return [stdout autorelease]; + } else { + NSString *error = [[[NSString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; + SPBeginAlertSheet(NSLocalizedString(@"BASH Error", @"bash error"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, + [NSString stringWithFormat:@"%@ “%@”:\n%@", NSLocalizedString(@"Error for", @"error for message"), command, [error description]]); + [stdout release]; + NSBeep(); + return @""; + } + } else { + NSLog(@"Couldn't read return string from “%@” by using UTF-8 encoding.", command); + NSBeep(); + } + } else { + if(bashTask) [bashTask release]; + NSLog(@"Couldn't read data from command “%@”.", command); + NSBeep(); + return @""; + } + +} + +/* + * 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; + } + + [[self textStorage] ensureAttributesAreFixedInRange:[self selectedRange]]; + NSUInteger 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; + + } + } + // 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 if(![self selectedRange].length) { + 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; + +} + +/* + * Return YES if user interacts with snippets (is needed mainly for suppressing + * the highlighting of the current query) + */ +- (BOOL)isSnippetMode +{ + return (snippetControlCounter > -1) ? YES : NO; +} + +#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]; + + // Cancel auto-completion timer + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + 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]]; + } + + // Cancel auto-completion timer + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + + 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 + + [self setCompletionWasReinvokedAutomatically:NO]; + completionWasRefreshed = NO; + // Cancel autocompletion trigger + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + if(curFlags==(NSControlKeyMask)) + [self doCompletionByUsingSpellChecker:NO fuzzyMode:YES autoCompleteMode:NO]; + else + [self doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:NO]; + return; + } + if (insertedCharacter == NSF5FunctionKey && [self isEditable]){ // F5 for completion based on spell checker + [self setCompletionWasReinvokedAutomatically:NO]; + [self doCompletionByUsingSpellChecker:YES fuzzyMode:NO autoCompleteMode:NO]; + return; + } + + // Check for {SHIFT}TAB to try to insert query favorite via TAB trigger if SPTextView 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(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 && [tableDocumentInstance fileURL]) { + NSArray *snippets = [[SPQueryController sharedQueryController] queryFavoritesForFileURL:[tableDocumentInstance fileURL] andTabTrigger:tabTrigger includeGlobals:YES]; + if([snippets count] > 0 && [(NSString*)[(NSDictionary*)[snippets objectAtIndex:0] objectForKey:@"query"] length]) { + [self insertAsSnippet:[(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:@"+"] || [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; + } + if([charactersIgnMod isEqualToString:@"0"]) { // reset font to default + BOOL editableStatus = [self isEditable]; + [self setEditable:YES]; + [self setFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; + [self setEditable:editableStatus]; + 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; + case '{': + matchingCharacter = @"}"; + processAutopair = 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]; + + [self didChangeText]; + } + 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; + NSUInteger 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]; +} + +/* + * 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) + { + [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])) + [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]; + + if(strlength > SP_MAX_TEXT_SIZE_FOR_SYNTAX_HIGHLIGHTING) return; + + 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) + { + + // 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 + NSUInteger curPos = visibleRange.location+(NSUInteger)(visibleRange.length/2); + + // get the last line to parse due to SP_SYNTAX_HILITE_BIAS + // but look for only SP_SYNTAX_HILITE_BIAS chars forwards + NSUInteger end = curPos + SP_SYNTAX_HILITE_BIAS; + NSInteger lengthChecker = SP_SYNTAX_HILITE_BIAS; + if (end > strlength ) { + end = strlength; + } else { + while(end < strlength && lengthChecker > 0) { + if([selfstr characterAtIndex:end]=='\n') + break; + end++; + lengthChecker--; + } + } + if(lengthChecker <= 0) + end = curPos + SP_SYNTAX_HILITE_BIAS; + + // get the first line to parse due to SP_SYNTAX_HILITE_BIAS + // but look for only SP_SYNTAX_HILITE_BIAS chars backwards + NSUInteger start, start_temp; + if(end <= (SP_SYNTAX_HILITE_BIAS*2)) + start = 0; + else + start = end - (SP_SYNTAX_HILITE_BIAS*2); + + start_temp = start; + lengthChecker = SP_SYNTAX_HILITE_BIAS; + if (start > 0) + while(start>0 && lengthChecker > 0) { + if([selfstr characterAtIndex:start]=='\n') + break; + start--; + lengthChecker--; + } + if(lengthChecker <= 0) + start = start_temp; + + 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; + + size_t 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 = otherTextColor; + 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 (textBufferSizeIncreased && allowToCheckForUpperCase && autouppercaseKeywordsEnabled && !delBackwardsWasPressed + && [(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword, tokenEnd, nil) length]) + // 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]; + BOOL doIt = NO; + @try + { + doIt = ![(NSString*)NSMutableAttributedStringAttributeAtIndex(textStore, kSQLkeyword,tokenEnd+1,nil) length]; + } @catch(id ae) { doIt = NO; } + + if(doIt) + { + // 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:[curTokenString uppercaseString]]; + } + } + + 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); + + } + +} + +- (void) setTabStops +{ + NSFont *tvFont = [self font]; + NSInteger i; + NSTextTab *aTab; + NSMutableArray *myArrayOfTabs; + NSMutableParagraphStyle *paragraphStyle; + + BOOL oldEditableStatus = [self isEditable]; + [self setEditable:YES]; + + NSInteger tabStopWidth = [prefs integerForKey:SPCustomQueryEditorTabStopWidth]; + if(tabStopWidth < 1) tabStopWidth = 1; + + float tabWidth = NSSizeToCGSize([[NSString stringWithString:@" "] sizeWithAttributes:[NSDictionary dictionaryWithObject:tvFont forKey:NSFontAttributeName]]).width; + tabWidth = (float)tabStopWidth * tabWidth; + + NSInteger numberOfTabs = 256/tabStopWidth; + myArrayOfTabs = [NSMutableArray arrayWithCapacity:numberOfTabs]; + aTab = [[NSTextTab alloc] initWithType:NSLeftTabStopType location:tabWidth]; + [myArrayOfTabs addObject:aTab]; + [aTab release]; + for(i=1; i -1) { + // Is the caret still inside a snippet + if([self checkForCaretInsideSnippet]) { + for(NSUInteger i=0; i -1) { + // choose the colors for the snippet parts + if(i == currentSnippetIndex) { + [[NSColor colorWithCalibratedRed:1.0 green:0.6 blue:0.0 alpha:0.4] setFill]; + [[NSColor colorWithCalibratedRed:1.0 green:0.6 blue:0.0 alpha:0.8] setStroke]; + } else { + [[NSColor colorWithCalibratedRed:1.0 green:0.8 blue:0.2 alpha:0.2] setFill]; + [[NSColor colorWithCalibratedRed:1.0 green:0.8 blue:0.2 alpha:0.5] setStroke]; + } + NSBezierPath *snippetPath = [self roundedBezierPathAroundRange: NSMakeRange(snippetControlArray[i][0],snippetControlArray[i][1]) ]; + [snippetPath fill]; + [snippetPath stroke]; + } + } + } else { + [self endSnippetSession]; + } + } + + } + } + + [super drawRect:rect]; +} + +- (NSBezierPath*)roundedBezierPathAroundRange:(NSRange)aRange +{ + // parameters for snippet highlighting + CGFloat kappa = 0.5522847498; // magic number from http://www.whizkidtech.redprince.net/bezier/circle/ + CGFloat radius = 6; + CGFloat horzInset = -3; + CGFloat vertInset = 0.3; + BOOL connectDisconnectedPartsWithLine = NO; + + NSBezierPath *funkyPath = [NSBezierPath bezierPath]; + NSUInteger rectCount; + NSRectArray rects = [[self layoutManager] rectArrayForCharacterRange: aRange + withinSelectedCharacterRange: aRange + inTextContainer: [self textContainer] + rectCount: &rectCount ]; + if (rectCount>2 || (rectCount>1 && (SPRectRight(rects[1]) >= SPRectLeft(rects[0]) || connectDisconnectedPartsWithLine))) { + // highlight complicated multiline snippet + NSRect lineRects[4]; + lineRects[0] = rects[0]; + lineRects[1] = rects[1]; + lineRects[2] = rects[rectCount-2]; + lineRects[3] = rects[rectCount-1]; + for(int j=0;j<4;j++) lineRects[j] = NSInsetRect(lineRects[j], horzInset, vertInset); + NSPoint vertices[8]; + vertices[0] = NSMakePoint( SPRectLeft(lineRects[0]), SPRectTop(lineRects[0]) ); // point a + vertices[1] = NSMakePoint( SPRectRight(lineRects[0]), SPRectTop(lineRects[0]) ); // point b + vertices[2] = NSMakePoint( SPRectRight(lineRects[2]), SPRectBottom(lineRects[2]) ); // point c + vertices[3] = NSMakePoint( SPRectRight(lineRects[3]), SPRectBottom(lineRects[2]) ); // point d + vertices[4] = NSMakePoint( SPRectRight(lineRects[3]), SPRectBottom(lineRects[3]) ); // point e + vertices[5] = NSMakePoint( SPRectLeft(lineRects[3]), SPRectBottom(lineRects[3]) ); // point f + vertices[6] = NSMakePoint( SPRectLeft(lineRects[1]), SPRectTop(lineRects[1]) ); // point g + vertices[7] = NSMakePoint( SPRectLeft(lineRects[0]), SPRectTop(lineRects[1]) ); // point h + + for (NSUInteger j=0; j<8; j++) { + NSPoint curr = vertices[j]; + NSPoint prev = vertices[(j+8-1)%8]; + NSPoint next = vertices[(j+1)%8]; + + CGFloat s = radius/SPPointDistance(prev, curr); + if (s>0.5) s = 0.5; + CGFloat t = radius/SPPointDistance(curr, next); + if (t>0.5) t = 0.5; + + NSPoint a = SPPointOnLine(curr, prev, 0.5); + NSPoint b = SPPointOnLine(curr, prev, s); + NSPoint c = curr; + NSPoint d = SPPointOnLine(curr, next, t); + NSPoint e = SPPointOnLine(curr, next, 0.5); + + if (j==0) [funkyPath moveToPoint:a]; + [funkyPath lineToPoint: b]; + [funkyPath curveToPoint:d controlPoint1:SPPointOnLine(b, c, kappa) controlPoint2:SPPointOnLine(d, c, kappa)]; + [funkyPath lineToPoint: e]; + } + } else { + //highlight disconnected snippet parts (or single line snippet) + for (NSUInteger j=0; j 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 + +/* + * 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; + + // Cancel autocompletion trigger + if([prefs boolForKey:SPCustomQueryAutoComplete]) + [NSObject cancelPreviousPerformRequestsWithTarget:self + selector:@selector(doAutoCompletion) + object:nil]; + + 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] doubleValue]]; + } + + // Start autocompletion if enabled + if([[NSApp keyWindow] firstResponder] == self && [prefs boolForKey:SPCustomQueryAutoComplete] && !completionIsOpen && editedMask != 1 && [textStore editedRange].length) + [self performSelector:@selector(doAutoCompletion) withObject:nil afterDelay:[[prefs valueForKey:SPCustomQueryAutoCompleteDelay] 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 && !isProcessingMirroredSnippets) { + // Remove any fully nested snippets relative to the current snippet which was edited + NSUInteger currentSnippetLocation = snippetControlArray[currentSnippetIndex][0]; + NSUInteger currentSnippetMaxRange = snippetControlArray[currentSnippetIndex][0] + snippetControlArray[currentSnippetIndex][1]; + NSInteger i; + 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; + } + } + + NSUInteger editStartPosition = [textStore editedRange].location; + NSUInteger changeInLength = [textStore changeInLength]; + + // 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; + } + } + } + // Adjust start position of mirrored snippets after caret position + if(mirroredCounter > -1) + for(i=0; i<=mirroredCounter; i++) { + if(editStartPosition < snippetMirroredControlArray[i][1]) { + snippetMirroredControlArray[i][1] += changeInLength; + } + } + } + + if(mirroredCounter > -1 && snippetControlCounter > -1) { + [self performSelector:@selector(processMirroredSnippets) withObject:nil afterDelay:0.0]; + } + + + } + if([[self textStorage] changeInLength] > 0) + textBufferSizeIncreased = YES; + else + textBufferSizeIncreased = NO; + + if([[self textStorage] changeInLength] < SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING) + [self doSyntaxHighlighting]; + + } else { + textBufferSizeIncreased = NO; + } + + startListeningToBoundChanges = YES; + +} + +/* + * Set font panel's valid modes + */ +- (NSUInteger)validModesForFontPanel:(NSFontPanel *)fontPanel +{ + return (NSFontPanelSizeModeMask|NSFontPanelCollectionModeMask); +} + +#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 %@ of data?", @"message of panel asking for confirmation for inserting large text from dragging action"), + [NSString stringForByteSize:[filesize longLongValue]]]]; + [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; + } + + // Insert selected items coming from the Navigator + if ( [[pboard types] containsObject:@"SPDragFromNavigatorPboardType"] ) { + NSPoint draggingLocation = [sender draggingLocation]; + draggingLocation = [self convertPoint:draggingLocation fromView:nil]; + NSUInteger characterIndex = [self characterIndexOfPoint:draggingLocation]; + [self setSelectedRange:NSMakeRange(characterIndex,0)]; + + NSKeyedUnarchiver *unarchiver = [[[NSKeyedUnarchiver alloc] initForReadingWithData:[pboard dataForType:@"SPDragFromNavigatorPboardType"]] autorelease]; + NSArray *draggedItems = [[NSArray alloc] initWithArray:(NSArray *)[unarchiver decodeObjectForKey:@"itemdata"]]; + [unarchiver finishDecoding]; + + NSMutableString *dragString = [NSMutableString string]; + NSMutableString *aPath = [NSMutableString string]; + + NSString *currentDb = nil; + NSString *currentTable = nil; + + if (tablesListInstance && [tablesListInstance selectedDatabase]) + currentDb = [tablesListInstance selectedDatabase]; + if (tablesListInstance && [tablesListInstance tableName]) + currentTable = [tablesListInstance tableName]; + + if(!currentDb) currentDb = @""; + if(!currentTable) currentTable = @""; + + for(NSString* item in draggedItems) { + if([dragString length]) [dragString appendString:@", "]; + [aPath setString:item]; + // Insert path relative to the current selected db and table if any + [aPath replaceOccurrencesOfRegex:[NSString stringWithFormat:@"^%@%@", currentDb, SPUniqueSchemaDelimiter] withString:@""]; + [aPath replaceOccurrencesOfRegex:[NSString stringWithFormat:@"^%@%@", currentTable, SPUniqueSchemaDelimiter] withString:@""]; + [dragString appendString:[[aPath componentsSeparatedByString:SPUniqueSchemaDelimiter] componentsJoinedByPeriodAndBacktickQuoted]]; + } + [self breakUndoCoalescing]; + [self insertText:dragString]; + if (draggedItems) [draggedItems release]; + 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)changeFont:(id)sender +{ + if (prefs && [self font] != nil) { + [prefs setObject:[NSArchiver archivedDataWithRootObject:[self font]] forKey:SPCustomQueryEditorFont]; + NSFont *nf = [[NSFontPanel sharedFontPanel] panelConvertFont:[NSUnarchiver unarchiveObjectWithData:[prefs dataForKey:SPCustomQueryEditorFont]]]; + BOOL oldEditable = [self isEditable]; + [self setEditable:YES]; + [self setFont:nf]; + [self setEditable:oldEditable]; + [self setNeedsDisplay:YES]; + [prefs setObject:[NSArchiver archivedDataWithRootObject:nf] forKey:SPCustomQueryEditorFont]; + } +} + +- (void) dealloc +{ + + // Cancel any deferred calls + [NSObject cancelPreviousPerformRequestsWithTarget:self]; + + // Remove observers + [[NSNotificationCenter defaultCenter] removeObserver:self]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorFont]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorBackgroundColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorHighlightQueryColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryHighlightCurrentQuery]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorCommentColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorQuoteColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorSQLKeywordColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorBacktickColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorNumericColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorVariableColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorTextColor]; + [prefs removeObserver:self forKeyPath:SPCustomQueryEditorTabStopWidth]; + [prefs removeObserver:self forKeyPath:SPCustomQueryAutoUppercaseKeywords]; + + if (completionIsOpen) [completionPopup close], completionIsOpen = NO; + [prefs release]; + [lineNumberView release]; + if(queryHiliteColor) [queryHiliteColor release]; + if(queryEditorBackgroundColor) [queryEditorBackgroundColor release]; + if(commentColor) [commentColor release]; + if(quoteColor) [quoteColor release]; + if(keywordColor) [keywordColor release]; + if(backtickColor) [backtickColor release]; + if(numericColor) [numericColor release]; + if(variableColor) [variableColor release]; + if(otherTextColor) [otherTextColor release]; + [super dealloc]; +} + +@end -- cgit v1.2.3