//
// $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"
#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;
completionIsOpen = NO;
isProcessingMirroredSnippets = 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([[[self window] delegate] respondsToSelector:@selector(connectionID)])
connectionID = [[[self window] delegate] connectionID];
else
connectionID = @"_";
NSLog(@"cid %@", connectionID);
NSDictionary *dbs = [NSDictionary dictionaryWithDictionary:[[mySQLConnection getDbStructure] objectForKey:connectionID]];
if(dbs != nil && [dbs count]) {
NSMutableArray *allDbs = [NSMutableArray array];
[allDbs addObjectsFromArray:[dbs allKeys]];
// Add database names having no tables since they don't appear in the information_schema
if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"] != nil)
for(id db in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"])
if(![allDbs containsObject:[NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, db]])
[allDbs addObject:[NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, db]];
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 ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"selectedDatabase"] != nil)
currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]];
if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"tableName"] != nil)
currentTable = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"tableName"];
// Put current selected db at the top
if(aTableName == nil && aDbName == nil && [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"]) {
currentDb = [NSString stringWithFormat:@"%@%@%@", connectionID, SPUniqueSchemaDelimiter, [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"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
NSInteger uniqueSchemaKind = [mySQLConnection getUniqueDbIdentifierFor:[aTableName lowercaseString]];
// If no db name but table name check if table name is a valid name in the current selected db
if(aTableName && [aTableName length] && [dbs objectForKey:currentDb] && [[dbs objectForKey:currentDb] objectForKey:[NSString stringWithFormat:@"%@%@%@", currentDb, SPUniqueSchemaDelimiter, aTableName]] && uniqueSchemaKind == 2) {
aTableNameExists = YES;
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, aTableName];
aTableNameExists = NO;
}
} else if (aDbName && [aDbName length]) {
if(aTableName && [aTableName length] && [dbs objectForKey:aDbName_id] && [[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 = [[dbs objectForKey:db] allKeys];
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];
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", db, @"path", @"", @"isRef", nil]];
break;
case 1:
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"table-view-small-square", @"image", db, @"path", @"", @"isRef", nil]];
break;
case 2:
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"proc-small", @"image", db, @"path", @"", @"isRef", nil]];
breakFlag = YES;
break;
case 3:
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:[[table componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display", @"func-small", @"image", db, @"path", @"", @"isRef", nil]];
breakFlag = YES;
break;
}
if(!breakFlag) {
NSArray *sortedFields = [allFields sortedArrayUsingDescriptors:[NSArray arrayWithObject:desc]];
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 componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display",
@"field-small-square", @"image",
table, @"path",
t, @"type",
lst, @"list",
@"", @"isRef",
nil]];
} else {
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:
[[field componentsSeparatedByString:SPUniqueSchemaDelimiter] lastObject], @"display",
@"field-small-square", @"image",
table, @"path",
typ, @"type",
@"", @"isRef",
nil]];
}
}
}
}
}
}
if(desc) [desc release];
} else {
// Fallback for MySQL < 5 and if the data gathering is in progress
if(mySQLmajorVersion > 4)
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:NSLocalizedString(@"fetching table data…", @"fetching table data for completion in progress message"), @"path", @"", @"noCompletion", nil]];
// Add all database names to completions list
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allDatabaseNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]];
// Add all system database names to completions list
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allSystemDatabaseNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"database-small", @"image", @"", @"isRef", nil]];
// Add table names to completions list
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allTableNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-small-square", @"image", @"", @"isRef", nil]];
// Add view names to completions list
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allViewNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"table-view-small-square", @"image", @"", @"isRef", nil]];
// Add field names to completions list for currently selected table
if ([[[self window] delegate] table] != nil)
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"field-small-square", @"image", @"", @"isRef", nil]];
// Add proc/func only for MySQL version 5 or higher
if(mySQLmajorVersion > 4) {
// Add all procedures to completions list for currently selected table
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allProcedureNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"proc-small", @"image", @"", @"isRef", nil]];
// Add all function to completions list for currently selected table
for (id obj in [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"allFunctionNames"])
[possibleCompletions addObject:[NSDictionary dictionaryWithObjectsAndKeys:obj, @"display", @"func-small", @"image", @"", @"isRef", nil]];
}
}
}
return [possibleCompletions autorelease];
}
- (void) doAutoCompletion
{
NSRange r = [self selectedRange];
if(![[self delegate] isKindOfClass:[CustomQuery class]] || r.length || snippetControlCounter > -1) return;
if(r.location) {
if([[[self textStorage] attribute:kQuote atIndex:r.location-1 effectiveRange:nil] isEqualToString:kQuoteValue])
return;
if(![[NSCharacterSet whitespaceAndNewlineCharacterSet] characterIsMember:[[self string] characterAtIndex:r.location-1]])
// Suppress auto-completion if window isn't active anymore
if([[NSApp keyWindow] firstResponder] == self)
[self doCompletionByUsingSpellChecker:NO fuzzyMode:NO autoCompleteMode:YES];
}
}
- (void) doCompletionByUsingSpellChecker:(BOOL)isDictMode fuzzyMode:(BOOL)fuzzySearch autoCompleteMode:(BOOL)autoCompleteMode
{
if(![self isEditable]) return;
[self breakUndoCoalescing];
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([[[self window] delegate] isKindOfClass:[TableDocument class]] && [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"])
currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"];
else
currentDb = @"";
BOOL caretIsInsideBackticks = NO;
// Is the caret inside backticks
// Do not using attribute:atIndex: since it could return wrong results due to editing.
// This approach counts the number of ` up to the beginning of the current line from caret position
NSRange lineHeadRange = [[self string] lineRangeForRange:NSMakeRange(caretPos, 0)];
NSString *lineHead = [[self string] substringWithRange:NSMakeRange(lineHeadRange.location, caretPos - lineHeadRange.location)];
for(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];
}
SPNarrowDownCompletion* completionPopUp = [[SPNarrowDownCompletion alloc] initWithItems:[self suggestionsForSQLCompletionWith:currentWord dictMode:isDictMode browseMode:dbBrowseMode withTableName:tableName withDbName:dbName]
alreadyTyped:filter
staticPrefix:prefix
additionalWordCharacters:allow
caseSensitive:!caseInsensitive
charRange:completionRange
parseRange:parseRange
inView:self
dictMode:isDictMode
dbMode:dbBrowseMode
tabTriggerMode:[self isSnippetMode]
fuzzySearch:fuzzySearch
backtickMode:backtickMode
withDbName:dbName
withTableName:tableName
selectedDb:currentDb
caretMovedLeft:caretMovedLeft
autoComplete:autoCompleteMode
oneColumn:isDictMode];
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];
completionIsOpen = YES;
[completionPopUp orderFront:self];
if(!autoCompleteMode)
[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
{
[[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] showHelpForCurrentWord:self];
}
/*
* If the textview has a selection, wrap it with the supplied prefix and suffix strings;
* return whether or not any wrap was performed.
*/
- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix
{
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])
[[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] selectCurrentQuery];
}
/*
* Selects the line lineNumber relatively to a selection (if given) and scrolls to it
*/
- (void) selectLineNumber:(NSUInteger)lineNumber ignoreLeadingNewLines:(BOOL)ignLeadingNewLines
{
NSRange selRange;
NSArray *lineRanges;
if([self selectedRange].length)
lineRanges = [[[self string] substringWithRange:[self selectedRange]] lineRangesForRange:NSMakeRange(0, [self selectedRange].length)];
else
lineRanges = [[self string] lineRangesForRange:NSMakeRange(0, [[self string] length])];
if(ignLeadingNewLines) // ignore leading empty lines
{
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([[[self window] delegate] respondsToSelector:@selector(connectionID)])
connectionID = [[[self window] delegate] connectionID];
else
connectionID = @"_";
NSArray *arr = nil;
if([kind isEqualToString:@"$SP_ASLIST_ALL_TABLES"]) {
NSString *currentDb = nil;
if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"selectedDatabase"] != nil)
currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"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 ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"selectedDatabase"] != nil)
currentDb = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"selectedDatabase"];
if ([[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"tableName"] != nil)
currentTable = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKeyPath:@"tableName"];
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:[[[[self window] delegate] 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;
}
SPNarrowDownCompletion* 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];
//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];
completionIsOpen = YES;
[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;
id aCompletionList;
// Get the completion list pointer if open
if(completionIsOpen)
for(id w in [NSApp windows])
if([w isKindOfClass:[SPNarrowDownCompletion class]]) {
aCompletionList = w;
break;
}
// 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)
[aCompletionList 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);
SPNarrowDownCompletion* completionPopUp;
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]];
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];
//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];
completionIsOpen = YES;
[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, 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];
// 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 && [[[self window] delegate] fileURL]) {
NSArray *snippets = [[SPQueryController sharedQueryController] queryFavoritesForFileURL:[[[self window] delegate] fileURL] andTabTrigger:tabTrigger includeGlobals:YES];
if([snippets count] > 0 && [(NSString*)[(NSDictionary*)[snippets objectAtIndex:0] objectForKey:@"query"] length]) {
[self 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)
{
[[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] performSelector:@selector(showAutoHelpForCurrentWord:) withObject:self afterDelay:0.1];
return;
}
// Otherwise show Help if caret is not inside quotes
NSUInteger cursorPosition = [self selectedRange].location;
if (cursorPosition >= [[self string] length]) cursorPosition--;
if(cursorPosition > -1 && (![[self textStorage] attribute:kQuote atIndex:cursorPosition effectiveRange:nil]||[[self textStorage] attribute:kSQLkeyword atIndex:cursorPosition effectiveRange:nil]))
[[[[self window] delegate] valueForKeyPath:@"customQueryInstance"] performSelector:@selector(showAutoHelpForCurrentWord:) withObject:self afterDelay:0.1];
}
/*
* Syntax Highlighting.
*
* (The main bottleneck is the [NSTextStorage addAttribute:value:range:] method - the parsing itself is really fast!)
* Some sample code from Andrew Choi ( http://members.shaw.ca/akochoi-old/blog/2003/11-09/index.html#3 ) has been reused.
*/
- (void)doSyntaxHighlighting
{
NSTextStorage *textStore = [self textStorage];
NSString *selfstr = [self string];
NSUInteger strlength = [selfstr length];
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] retain] 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] retain] doubleValue]];
// Cancel calling doSyntaxHighlighting for large text
if([[self string] length] > SP_TEXT_SIZE_TRIGGER_FOR_PARTLY_PARSING)
[NSObject cancelPreviousPerformRequestsWithTarget:self
selector:@selector(doSyntaxHighlighting)
object:nil];
// Do syntax highlighting/re-calculate snippet ranges only if the user really changed the text
if(editedMask != 1) {
// Re-calculate snippet ranges if snippet session is active
if(snippetControlCounter > -1 && !snippetWasJustInserted && !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 %.1f MB of data?", @"message of panel asking for confirmation for inserting large text from dragging action"),
[filesize unsignedLongValue]/1048576.0]];
[alert setHelpAnchor:filepath];
[alert setMessageText:NSLocalizedString(@"Warning",@"warning")];
[alert setAlertStyle:NSWarningAlertStyle];
[alert beginSheetModalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(dragAlertSheetDidEnd:returnCode:contextInfo:)
contextInfo:nil];
[alert release];
} else
[self insertFileContentOfFile:filepath];
}
}
return YES;
}
return [super performDragOperation:sender];
}
/*
* Confirmation sheetDidEnd method
*/
- (void)dragAlertSheetDidEnd:(NSAlert *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo
{
[[sheet window] orderOut:nil];
if ( returnCode == NSAlertFirstButtonReturn )
[self insertFileContentOfFile:[sheet helpAnchor]];
}
/*
* Convert a NSPoint, usually the mouse location, to
* a character index of the text view.
*/
- (NSUInteger)characterIndexOfPoint:(NSPoint)aPoint
{
NSUInteger glyphIndex;
NSLayoutManager *layoutManager = [self layoutManager];
CGFloat fraction;
NSRange range;
range = [layoutManager glyphRangeForTextContainer:[self textContainer]];
glyphIndex = [layoutManager glyphIndexForPoint:aPoint
inTextContainer:[self textContainer]
fractionOfDistanceThroughGlyph:&fraction];
if( fraction > 0.5 ) glyphIndex++;
if( glyphIndex == NSMaxRange(range) )
return [[self textStorage] length];
else
return [layoutManager characterIndexForGlyphAtIndex:glyphIndex];
}
/*
* Insert content of a plain text file for a given path.
* In addition it tries to figure out the file's text encoding heuristically.
*/
- (void)insertFileContentOfFile:(NSString *)aPath
{
NSError *err = nil;
NSStringEncoding enc;
NSString *content = nil;
// Make usage of the UNIX command "file" to get an info
// about file type and encoding.
NSTask *task=[[NSTask alloc] init];
NSPipe *pipe=[[NSPipe alloc] init];
NSFileHandle *handle;
NSString *result;
[task setLaunchPath:@"/usr/bin/file"];
[task setArguments:[NSArray arrayWithObjects:aPath, @"-Ib", nil]];
[task setStandardOutput:pipe];
handle=[pipe fileHandleForReading];
[task launch];
result=[[NSString alloc] initWithData:[handle readDataToEndOfFile]
encoding:NSASCIIStringEncoding];
[pipe release];
[task release];
// UTF16/32 files are detected as application/octet-stream resp. audio/mpeg
if( [result hasPrefix:@"text/plain"]
|| [[[aPath pathExtension] lowercaseString] isEqualToString:@"sql"]
|| [[[aPath pathExtension] lowercaseString] isEqualToString:@"txt"]
|| [result hasPrefix:@"audio/mpeg"]
|| [result hasPrefix:@"application/octet-stream"]
)
{
// if UTF16/32 cocoa will try to find the correct encoding
if([result hasPrefix:@"application/octet-stream"] || [result hasPrefix:@"audio/mpeg"] || [result rangeOfString:@"utf-16"].length)
enc = 0;
else if([result rangeOfString:@"utf-8"].length)
enc = NSUTF8StringEncoding;
else if([result rangeOfString:@"iso-8859-1"].length)
enc = NSISOLatin1StringEncoding;
else if([result rangeOfString:@"us-ascii"].length)
enc = NSASCIIStringEncoding;
else
enc = 0;
if(enc == 0) // cocoa tries to detect the encoding
content = [NSString stringWithContentsOfFile:aPath usedEncoding:&enc error:&err];
else
content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err];
if(content)
{
[self insertText:content];
[result release];
// [self insertText:@""]; // Invoke keyword uppercasing
return;
}
// If UNIX "file" failed try cocoa's encoding detection
content = [NSString stringWithContentsOfFile:aPath encoding:enc error:&err];
if(content)
{
[self insertText:content];
[result release];
// [self insertText:@""]; // Invoke keyword uppercasing
return;
}
}
[result release];
NSLog(@"%@ ‘%@’.", NSLocalizedString(@"Couldn't read the file content of", @"Couldn't read the file content of"), aPath);
}
- (void)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
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[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