//
// 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
// Or mail to
#import "CMTextView.h"
#import "SPStringAdditions.h"
/*
* Include all the extern variables and prototypes required for flex (used for syntax highlighting)
*/
#import "SPEditorTokens.h"
extern int yylex();
extern int 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 *);
#define kAPlinked @"Linked" // attribute for a via auto-pair inserted char
#define kAPval @"linked"
#define kWQquoted @"Quoted" // set via lex to indicate a quoted string
#define kWQval @"quoted"
#define kSQLkeyword @"SQLkw" // attribute for found SQL keywords
#define kQuote @"Quote"
@implementation CMTextView
/*
* Checks if the char after the current caret position/selection matches a supplied attribute
*/
- (BOOL) isNextCharMarkedBy:(id)attribute
{
unsigned int 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])
return YES;
return NO;
}
/*
* Checks if the caret is wrapped by auto-paired characters.
* e.g. [| := caret]: "|"
*/
- (BOOL) areAdjacentCharsLinked
{
unsigned int caretPosition = [self selectedRange].location;
unichar leftChar, matchingChar;
// Perform bounds checking
if ([self selectedRange].length) return NO;
if (caretPosition < 1) return NO;
if (caretPosition >= [[self string] length]) return NO;
// Check the character to the left of the cursor and set the pairing character if appropriate
leftChar = [[self string] characterAtIndex:caretPosition - 1];
if (leftChar == '(')
matchingChar = ')';
else if (leftChar == '"' || leftChar == '`' || leftChar == '\'')
matchingChar = leftChar;
else
return NO;
// Check that the pairing character exists after the caret, and is tagged with the link attribute
if (matchingChar == [[self string] characterAtIndex:caretPosition]
&& [[self textStorage] attribute:kAPlinked atIndex:caretPosition effectiveRange:nil]) {
return YES;
}
return NO;
}
/*
* If the textview has a selection, wrap it with the supplied prefix and suffix strings;
* return whether or not any wrap was performed.
*/
- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix
{
// Only proceed if a selection is active
if ([self selectedRange].length == 0)
return NO;
// Replace the current selection with the selected string wrapped in prefix and suffix
[self insertText:
[NSString stringWithFormat:@"%@%@%@",
prefix,
[[self string] substringWithRange:[self selectedRange]],
suffix
]
];
return YES;
}
/*
* Copy selected text chunk as RTF to preserve syntax highlighting
*/
- (void)copyAsRTF
{
NSPasteboard *pb = [NSPasteboard generalPasteboard];
NSTextStorage *textStorage = [self textStorage];
NSData *rtf = [textStorage RTFFromRange:[self selectedRange]
documentAttributes:nil];
if (rtf)
{
[pb declareTypes:[NSArray arrayWithObject:NSRTFPboardType] owner:self];
[pb setData:rtf forType:NSRTFPboardType];
}
}
/*
* Handle some keyDown events in order to provide autopairing functionality (if enabled).
*/
- (void) keyDown:(NSEvent *)theEvent
{
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);
// Note: switch(insertedCharacter) {} does not work instead use charactersIgnoringModifiers
if([charactersIgnMod isEqualToString:@"c"]) // ^C copy as RTF
if(curFlags==(NSControlKeyMask))
{
[self copyAsRTF];
return;
}
// Only process for character autopairing if autopairing is enabled and a single character is being added.
if (autopairEnabled && 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]
// And we are inside a quoted string
&& [self isNextCharMarkedBy:kWQquoted]
// 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, 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 isNextCharMarkedBy:kAPlinked] && [self isNextCharMarkedBy:kWQquoted] && ![self selectedRange].length) {
[super keyDown:theEvent];
return;
}
// Check whether the submitted character should trigger autopair processing.
switch (insertedCharacter)
{
case '(':
matchingCharacter = @")";
processAutopair = YES;
break;
case '"':
matchingCharacter = @"\"";
processAutopair = YES;
skipTypedLinkedCharacter = YES;
break;
case '`':
matchingCharacter = @"`";
processAutopair = YES;
skipTypedLinkedCharacter = YES;
break;
case '\'':
matchingCharacter = @"'";
processAutopair = YES;
skipTypedLinkedCharacter = YES;
break;
case ')':
skipTypedLinkedCharacter = YES;
break;
}
// Check to see whether the next character should be compared to the typed character;
// if it matches the typed character, and is marked with the is-linked-pair attribute,
// select the next character and replace it with the typed character. This allows
// a normally quoted string to be typed in full, with the autopair appearing as a hint and
// then being automatically replaced when the user types it.
if (skipTypedLinkedCharacter) {
currentRange = [self selectedRange];
if (currentRange.location != NSNotFound && currentRange.length == 0) {
if ([self isNextCharMarkedBy:kAPlinked]) {
if ([[[self textStorage] string] characterAtIndex:currentRange.location] == insertedCharacter) {
currentRange.length = 1;
[self setSelectedRange:currentRange];
processAutopair = NO;
}
}
}
}
// If an appropriate character has been typed, and a matching character has been set,
// some form of autopairing is required.
if (processAutopair && matchingCharacter) {
// Check to see whether several characters are selected, and if so, wrap them with
// the auto-paired characters. This returns false if the selection has zero length.
if ([self wrapSelectionWithPrefix:characters suffix:matchingCharacter])
return;
// Otherwise, start by inserting the original character - the first half of the autopair.
[super keyDown:theEvent];
// Then process the second half of the autopair - the matching character.
currentRange = [self selectedRange];
if (currentRange.location != NSNotFound) {
NSTextStorage *textStorage = [self textStorage];
// Register the auto-pairing for undo
[self shouldChangeTextInRange:currentRange replacementString:matchingCharacter];
// Insert the matching character and give it the is-linked-pair-character attribute
[self replaceCharactersInRange:currentRange withString:matchingCharacter];
currentRange.length = 1;
[textStorage addAttribute:kAPlinked value:kAPval range:currentRange];
// Restore the original selection.
currentRange.length=0;
[self setSelectedRange:currentRange];
}
return;
}
}
// 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:)
&& autoindentEnabled
&& (!autoindentIgnoresEnter || [[NSApp currentEvent] keyCode] != 0x4C))
{
NSString *textViewString = [[self textStorage] string];
NSString *currentLine, *indentString = nil;
NSScanner *whitespaceScanner;
NSRange currentLineRange;
// 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]];
// 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) [self insertText:indentString];
// Return to avoid the original implementation, preventing double linebreaks
return;
}
[super doCommandBySelector:aSelector];
}
/*
* 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";
int i, indentedLinesLength = 0;
if ([self selectedRange].location == NSNotFound) return NO;
// Indent the currently selected line if the caret is within a single line
if ([self selectedRange].length == 0) {
NSRange currentLineRange;
// Extract the current line range based on the text caret
currentLineRange = [textViewString lineRangeForRange:[self selectedRange]];
// Register the indent for undo
[self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 0) replacementString:tabString];
// Insert the new tab
[self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 0) withString:tabString];
return YES;
}
// Otherwise, the selection has a length - get an array of current line ranges for the specified selection
lineRanges = [textViewString lineRangesForRange:[self selectedRange]];
// Loop through the ranges, storing a count of the overall length.
for (i = 0; i < [lineRanges count]; i++) {
currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]);
indentedLinesLength += currentLineRange.length + 1;
// Register the indent for undo
[self shouldChangeTextInRange:NSMakeRange(currentLineRange.location+i, 0) replacementString:tabString];
// Insert the new tab
[self replaceCharactersInRange:NSMakeRange(currentLineRange.location+i, 0) withString:tabString];
}
// Select the entirety of the new range
[self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, indentedLinesLength)];
return YES;
}
/*
* Shifts the selection, if any, leftwards by un-indenting any selected lines by one tab if possible.
* If the caret is within a line, the selection is not changed after the undent; if the selection has
* length, all lines crossed by the length are un-indented and fully selected.
* Returns whether or not an indentation was performed.
*/
- (BOOL) shiftSelectionLeft
{
NSString *textViewString = [[self textStorage] string];
NSRange currentLineRange;
NSArray *lineRanges;
int i, unindentedLines = 0, unindentedLinesLength = 0;
if ([self selectedRange].location == NSNotFound) return NO;
// Undent the currently selected line if the caret is within a single line
if ([self selectedRange].length == 0) {
NSRange currentLineRange;
// Extract the current line range based on the text caret
currentLineRange = [textViewString lineRangeForRange:[self selectedRange]];
// Ensure that the line has length and that the first character is a tab
if (currentLineRange.length < 1
|| [textViewString characterAtIndex:currentLineRange.location] != '\t')
return NO;
// Register the undent for undo
[self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 1) replacementString:@""];
// Remove the tab
[self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 1) withString:@""];
return YES;
}
// Otherwise, the selection has a length - get an array of current line ranges for the specified selection
lineRanges = [textViewString lineRangesForRange:[self selectedRange]];
// Loop through the ranges, storing a count of the total lines changed and the new length.
for (i = 0; i < [lineRanges count]; i++) {
currentLineRange = NSRangeFromString([lineRanges objectAtIndex:i]);
unindentedLinesLength += currentLineRange.length;
// Ensure that the line has length and that the first character is a tab
if (currentLineRange.length < 1
|| [textViewString characterAtIndex:currentLineRange.location-unindentedLines] != '\t')
continue;
// Register the undent for undo
[self shouldChangeTextInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) replacementString:@""];
// Remove the tab
[self replaceCharactersInRange:NSMakeRange(currentLineRange.location-unindentedLines, 1) withString:@""];
// As a line has been unindented, modify counts and lengths
unindentedLines++;
unindentedLinesLength--;
}
// If a change was made, select the entirety of the new range and return success
if (unindentedLines) {
[self setSelectedRange:NSMakeRange(NSRangeFromString([lineRanges objectAtIndex:0]).location, unindentedLinesLength)];
return YES;
}
return NO;
}
/*
* Handle autocompletion, returning a list of suggested completions for the supplied character range.
*/
- (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index
{
// Check if the caret is inside quotes "" or ''; if so
// return the normal word suggestion due to the spelling's settings
if([[self textStorage] attribute:kQuote atIndex:charRange.location effectiveRange:nil])
return [[NSSpellChecker sharedSpellChecker] completionsForPartialWordRange:NSMakeRange(0,charRange.length) inString:[[self string] substringWithRange:charRange] language:nil inSpellDocumentWithTag:0];
NSCharacterSet *separators = [NSCharacterSet characterSetWithCharactersInString:@" \t\r\n,()\"'`-!"];
NSArray *textViewWords = [[self string] componentsSeparatedByCharactersInSet:separators];
NSString *partialString = [[self string] substringWithRange:charRange];
unsigned int partialLength = [partialString length];
id tableNames = [[[[self window] delegate] valueForKeyPath:@"tablesListInstance"] valueForKey:@"tables"];
//unsigned int options = NSCaseInsensitiveSearch | NSAnchoredSearch;
//NSRange partialRange = NSMakeRange(0, partialLength);
NSMutableArray *compl = [[NSMutableArray alloc] initWithCapacity:32];
NSMutableArray *possibleCompletions = [NSMutableArray arrayWithArray:textViewWords];
[possibleCompletions addObjectsFromArray:[self keywords]];
[possibleCompletions addObjectsFromArray:tableNames];
// Add column names to completions list for currently selected table
if ([[[self window] delegate] table] != nil) {
id columnNames = [[[[self window] delegate] valueForKeyPath:@"tableDataInstance"] valueForKey:@"columnNames"];
[possibleCompletions addObjectsFromArray:columnNames];
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF beginswith[cd] %@ AND length > %d", partialString, partialLength];
NSArray *matchingCompletions = [[possibleCompletions filteredArrayUsingPredicate:predicate] sortedArrayUsingSelector:@selector(compare:)];
unsigned i, insindex;
insindex = 0;
for (i = 0; i < [matchingCompletions count]; i++)
{
NSString* obj = [matchingCompletions objectAtIndex:i];
if(![compl containsObject:obj])
if ([partialString isEqualToString:[obj substringToIndex:partialLength]])
// Matches case --> Insert at beginning of completion list
[compl insertObject:obj atIndex:insindex++];
else
// Not matching case --> Insert at end of completion list
[compl addObject:obj];
}
return [compl autorelease];
}
/*
* Hook to invoke the auto-uppercasing of SQL keywords after pasting
*/
- (void)paste:(id)sender
{
[super paste:sender];
// Invoke the auto-uppercasing of SQL keywords via an additional trigger
[self insertText:@""];
}
/*
* List of keywords for autocompletion. If you add a keyword here,
* it should also be added to the flex file SPEditorTokens.l
*/
-(NSArray *)keywords
{
return [NSArray arrayWithObjects:
@"ACCESSIBLE",
@"ACTION",
@"ADD",
@"AFTER",
@"AGAINST",
@"AGGREGATE",
@"ALGORITHM",
@"ALL",
@"ALTER",
@"ALTER COLUMN",
@"ALTER DATABASE",
@"ALTER EVENT",
@"ALTER FUNCTION",
@"ALTER LOGFILE GROUP",
@"ALTER PROCEDURE",
@"ALTER SCHEMA",
@"ALTER SERVER",
@"ALTER TABLE",
@"ALTER TABLESPACE",
@"ALTER VIEW",
@"ANALYZE",
@"ANALYZE TABLE",
@"AND",
@"ANY",
@"AS",
@"ASC",
@"ASCII",
@"ASENSITIVE",
@"AT",
@"AUTHORS",
@"AUTOEXTEND_SIZE",
@"AUTO_INCREMENT",
@"AVG",
@"AVG_ROW_LENGTH",
@"BACKUP",
@"BACKUP TABLE",
@"BEFORE",
@"BEGIN",
@"BETWEEN",
@"BIGINT",
@"BINARY",
@"BINLOG",
@"BIT",
@"BLOB",
@"BOOL",
@"BOOLEAN",
@"BOTH",
@"BTREE",
@"BY",
@"BYTE",
@"CACHE",
@"CACHE INDEX",
@"CALL",
@"CASCADE",
@"CASCADED",
@"CASE",
@"CHAIN",
@"CHANGE",
@"CHANGED",
@"CHAR",
@"CHARACTER",
@"CHARACTER SET",
@"CHARSET",
@"CHECK",
@"CHECK TABLE",
@"CHECKSUM",
@"CHECKSUM TABLE",
@"CIPHER",
@"CLIENT",
@"CLOSE",
@"COALESCE",
@"CODE",
@"COLLATE",
@"COLLATION",
@"COLUMN",
@"COLUMNS",
@"COMMENT",
@"COMMIT",
@"COMMITTED",
@"COMPACT",
@"COMPLETION",
@"COMPRESSED",
@"CONCURRENT",
@"CONDITION",
@"CONNECTION",
@"CONSISTENT",
@"CONSTRAINT",
@"CONTAINS",
@"CONTINUE",
@"CONTRIBUTORS",
@"CONVERT",
@"CREATE",
@"CREATE DATABASE",
@"CREATE EVENT",
@"CREATE FUNCTION",
@"CREATE INDEX",
@"CREATE LOGFILE GROUP",
@"CREATE PROCEDURE",
@"CREATE SCHEMA",
@"CREATE TABLE",
@"CREATE TABLESPACE",
@"CREATE TRIGGER",
@"CREATE USER",
@"CREATE VIEW",
@"CROSS",
@"CUBE",
@"CURRENT_DATE",
@"CURRENT_TIME",
@"CURRENT_TIMESTAMP",
@"CURRENT_USER",
@"CURSOR",
@"DATA",
@"DATABASE",
@"DATABASES",
@"DATAFILE",
@"DATE",
@"DATETIME",
@"DAY",
@"DAY_HOUR",
@"DAY_MICROSECOND",
@"DAY_MINUTE",
@"DAY_SECOND",
@"DEALLOCATE",
@"DEALLOCATE PREPARE",
@"DEC",
@"DECIMAL",
@"DECLARE",
@"DEFAULT",
@"DEFINER",
@"DELAYED",
@"DELAY_KEY_WRITE",
@"DELETE",
@"DESC",
@"DESCRIBE",
@"DES_KEY_FILE",
@"DETERMINISTIC",
@"DIRECTORY",
@"DISABLE",
@"DISCARD",
@"DISK",
@"DISTINCT",
@"DISTINCTROW",
@"DIV",
@"DO",
@"DOUBLE",
@"DROP",
@"DROP DATABASE",
@"DROP EVENT",
@"DROP FOREIGN KEY",
@"DROP FUNCTION",
@"DROP INDEX",
@"DROP LOGFILE GROUP",
@"DROP PREPARE",
@"DROP PRIMARY KEY",
@"DROP PREPARE",
@"DROP PROCEDURE",
@"DROP SCHEMA",
@"DROP SERVER",
@"DROP TABLE",
@"DROP TABLESPACE",
@"DROP TRIGGER",
@"DROP USER",
@"DROP VIEW",
@"DUAL",
@"DUMPFILE",
@"DUPLICATE",
@"DYNAMIC",
@"EACH",
@"ELSE",
@"ELSEIF",
@"ENABLE",
@"ENCLOSED",
@"END",
@"ENDS",
@"ENGINE",
@"ENGINES",
@"ENUM",
@"ERRORS",
@"ESCAPE",
@"ESCAPED",
@"EVENT",
@"EVENTS",
@"EVERY",
@"EXECUTE",
@"EXISTS",
@"EXIT",
@"EXPANSION",
@"EXPLAIN",
@"EXTENDED",
@"EXTENT_SIZE",
@"FALSE",
@"FAST",
@"FETCH",
@"FIELDS",
@"FILE",
@"FIRST",
@"FIXED",
@"FLOAT",
@"FLOAT4",
@"FLOAT8",
@"FLUSH",
@"FOR",
@"FORCE",
@"FOREIGN KEY",
@"FOREIGN",
@"FOUND",
@"FRAC_SECOND",
@"FROM",
@"FULL",
@"FULLTEXT",
@"FUNCTION",
@"GEOMETRY",
@"GEOMETRYCOLLECTION",
@"GET_FORMAT",
@"GLOBAL",
@"GRANT",
@"GRANTS",
@"GROUP",
@"HANDLER",
@"HASH",
@"HAVING",
@"HELP",
@"HIGH_PRIORITY",
@"HOSTS",
@"HOUR",
@"HOUR_MICROSECOND",
@"HOUR_MINUTE",
@"HOUR_SECOND",
@"IDENTIFIED",
@"IF",
@"IGNORE",
@"IMPORT",
@"IN",
@"INDEX",
@"INDEXES",
@"INFILE",
@"INITIAL_SIZE",
@"INNER",
@"INNOBASE",
@"INNODB",
@"INOUT",
@"INSENSITIVE",
@"INSERT",
@"INSERT_METHOD",
@"INSTALL",
@"INSTALL PLUGIN",
@"INT",
@"INT1",
@"INT2",
@"INT3",
@"INT4",
@"INT8",
@"INTEGER",
@"INTERVAL",
@"INTO",
@"INVOKER",
@"IO_THREAD",
@"IS",
@"ISOLATION",
@"ISSUER",
@"ITERATE",
@"JOIN",
@"KEY",
@"KEYS",
@"KEY_BLOCK_SIZE",
@"KILL",
@"LANGUAGE",
@"LAST",
@"LEADING",
@"LEAVE",
@"LEAVES",
@"LEFT",
@"LESS",
@"LEVEL",
@"LIKE",
@"LIMIT",
@"LINEAR",
@"LINES",
@"LINESTRING",
@"LIST",
@"LOAD DATA",
@"LOAD INDEX INTO CACHE",
@"LOCAL",
@"LOCALTIME",
@"LOCALTIMESTAMP",
@"LOCK",
@"LOCK TABLES",
@"LOCKS",
@"LOGFILE",
@"LOGS",
@"LONG",
@"LONGBLOB",
@"LONGTEXT",
@"LOOP",
@"LOW_PRIORITY",
@"MASTER",
@"MASTER_CONNECT_RETRY",
@"MASTER_HOST",
@"MASTER_LOG_FILE",
@"MASTER_LOG_POS",
@"MASTER_PASSWORD",
@"MASTER_PORT",
@"MASTER_SERVER_ID",
@"MASTER_SSL",
@"MASTER_SSL_CA",
@"MASTER_SSL_CAPATH",
@"MASTER_SSL_CERT",
@"MASTER_SSL_CIPHER",
@"MASTER_SSL_KEY",
@"MASTER_USER",
@"MATCH",
@"MAXVALUE",
@"MAX_CONNECTIONS_PER_HOUR",
@"MAX_QUERIES_PER_HOUR",
@"MAX_ROWS",
@"MAX_SIZE",
@"MAX_UPDATES_PER_HOUR",
@"MAX_USER_CONNECTIONS",
@"MEDIUM",
@"MEDIUMBLOB",
@"MEDIUMINT",
@"MEDIUMTEXT",
@"MEMORY",
@"MERGE",
@"MICROSECOND",
@"MIDDLEINT",
@"MIGRATE",
@"MINUTE",
@"MINUTE_MICROSECOND",
@"MINUTE_SECOND",
@"MIN_ROWS",
@"MOD",
@"MODE",
@"MODIFIES",
@"MODIFY",
@"MONTH",
@"MULTILINESTRING",
@"MULTIPOINT",
@"MULTIPOLYGON",
@"MUTEX",
@"NAME",
@"NAMES",
@"NATIONAL",
@"NATURAL",
@"NCHAR",
@"NDB",
@"NDBCLUSTER",
@"NEW",
@"NEXT",
@"NO",
@"NODEGROUP",
@"NONE",
@"NOT",
@"NO_WAIT",
@"NO_WRITE_TO_BINLOG",
@"NULL",
@"NUMERIC",
@"NVARCHAR",
@"OFFSET",
@"OLD_PASSWORD",
@"ON",
@"ONE",
@"ONE_SHOT",
@"OPEN",
@"OPTIMIZE",
@"OPTIMIZE TABLE",
@"OPTION",
@"OPTIONALLY",
@"OPTIONS",
@"OR",
@"ORDER",
@"OUT",
@"OUTER",
@"OUTFILE",
@"PACK_KEYS",
@"PARSER",
@"PARTIAL",
@"PARTITION",
@"PARTITIONING",
@"PARTITIONS",
@"PASSWORD",
@"PHASE",
@"PLUGIN",
@"PLUGINS",
@"POINT",
@"POLYGON",
@"PRECISION",
@"PREPARE",
@"PRESERVE",
@"PREV",
@"PRIMARY",
@"PRIVILEGES",
@"PROCEDURE",
@"PROCESS",
@"PROCESSLIST",
@"PURGE",
@"QUARTER",
@"QUERY",
@"QUICK",
@"RANGE",
@"READ",
@"READS",
@"READ_ONLY",
@"READ_WRITE",
@"REAL",
@"REBUILD",
@"RECOVER",
@"REDOFILE",
@"REDO_BUFFER_SIZE",
@"REDUNDANT",
@"REFERENCES",
@"REGEXP",
@"RELAY_LOG_FILE",
@"RELAY_LOG_POS",
@"RELAY_THREAD",
@"RELEASE",
@"RELOAD",
@"REMOVE",
@"RENAME",
@"RENAME DATABASE",
@"RENAME TABLE",
@"REORGANIZE",
@"REPAIR",
@"REPAIR TABLE",
@"REPEAT",
@"REPEATABLE",
@"REPLACE",
@"REPLICATION",
@"REQUIRE",
@"RESET",
@"RESET MASTER",
@"RESTORE",
@"RESTORE TABLE",
@"RESTRICT",
@"RESUME",
@"RETURN",
@"RETURNS",
@"REVOKE",
@"RIGHT",
@"RLIKE",
@"ROLLBACK",
@"ROLLUP",
@"ROUTINE",
@"ROW",
@"ROWS",
@"ROW_FORMAT",
@"RTREE",
@"SAVEPOINT",
@"SCHEDULE",
@"SCHEDULER",
@"SCHEMA",
@"SCHEMAS",
@"SECOND",
@"SECOND_MICROSECOND",
@"SECURITY",
@"SELECT",
@"SENSITIVE",
@"SEPARATOR",
@"SERIAL",
@"SERIALIZABLE",
@"SESSION",
@"SET",
@"SET PASSWORD",
@"SHARE",
@"SHOW",
@"SHOW BINARY LOGS",
@"SHOW BINLOG EVENTS",
@"SHOW CHARACTER SET",
@"SHOW COLLATION",
@"SHOW COLUMNS",
@"SHOW CONTRIBUTORS",
@"SHOW CREATE DATABASE",
@"SHOW CREATE EVENT",
@"SHOW CREATE FUNCTION",
@"SHOW CREATE PROCEDURE",
@"SHOW CREATE SCHEMA",
@"SHOW CREATE TABLE",
@"SHOW CREATE TRIGGERS",
@"SHOW CREATE VIEW",
@"SHOW DATABASES",
@"SHOW ENGINE",
@"SHOW ENGINES",
@"SHOW ERRORS",
@"SHOW EVENTS",
@"SHOW FIELDS",
@"SHOW FUNCTION CODE",
@"SHOW FUNCTION STATUS",
@"SHOW GRANTS",
@"SHOW INDEX",
@"SHOW INNODB STATUS",
@"SHOW KEYS",
@"SHOW MASTER LOGS",
@"SHOW MASTER STATUS",
@"SHOW OPEN TABLES",
@"SHOW PLUGINS",
@"SHOW PRIVILEGES",
@"SHOW PROCEDURE CODE",
@"SHOW PROCEDURE STATUS",
@"SHOW PROFILE",
@"SHOW PROFILES",
@"SHOW PROCESSLIST",
@"SHOW SCHEDULER STATUS",
@"SHOW SCHEMAS",
@"SHOW SLAVE HOSTS",
@"SHOW SLAVE STATUS",
@"SHOW STATUS",
@"SHOW STORAGE ENGINES",
@"SHOW TABLE STATUS",
@"SHOW TABLE TYPES",
@"SHOW TABLES",
@"SHOW TRIGGERS",
@"SHOW VARIABLES",
@"SHOW WARNINGS",
@"SHUTDOWN",
@"SIGNED",
@"SIMPLE",
@"SLAVE",
@"SMALLINT",
@"SNAPSHOT",
@"SOME",
@"SONAME",
@"SOUNDS",
@"SPATIAL",
@"SPECIFIC",
@"SQL",
@"SQLEXCEPTION",
@"SQLSTATE",
@"SQLWARNING",
@"SQL_BIG_RESULT",
@"SQL_BUFFER_RESULT",
@"SQL_CACHE",
@"SQL_CALC_FOUND_ROWS",
@"SQL_NO_CACHE",
@"SQL_SMALL_RESULT",
@"SQL_THREAD",
@"SQL_TSI_DAY",
@"SQL_TSI_FRAC_SECOND",
@"SQL_TSI_HOUR",
@"SQL_TSI_MINUTE",
@"SQL_TSI_MONTH",
@"SQL_TSI_QUARTER",
@"SQL_TSI_SECOND",
@"SQL_TSI_WEEK",
@"SQL_TSI_YEAR",
@"SSL",
@"START",
@"START TRANSACTION",
@"STARTING",
@"STARTS",
@"STATUS",
@"STOP",
@"STORAGE",
@"STRAIGHT_JOIN",
@"STRING",
@"SUBJECT",
@"SUBPARTITION",
@"SUBPARTITIONS",
@"SUPER",
@"SUSPEND",
@"TABLE",
@"TABLES",
@"TABLESPACE",
@"TEMPORARY",
@"TEMPTABLE",
@"TERMINATED",
@"TEXT",
@"THAN",
@"THEN",
@"TIME",
@"TIMESTAMP",
@"TIMESTAMPADD",
@"TIMESTAMPDIFF",
@"TINYBLOB",
@"TINYINT",
@"TINYTEXT",
@"TO",
@"TRAILING",
@"TRANSACTION",
@"TRIGGER",
@"TRIGGERS",
@"TRUE",
@"TRUNCATE",
@"TYPE",
@"TYPES",
@"UNCOMMITTED",
@"UNDEFINED",
@"UNDO",
@"UNDOFILE",
@"UNDO_BUFFER_SIZE",
@"UNICODE",
@"UNINSTALL",
@"UNINSTALL PLUGIN",
@"UNION",
@"UNIQUE",
@"UNKNOWN",
@"UNLOCK",
@"UNLOCK TABLES",
@"UNSIGNED",
@"UNTIL",
@"UPDATE",
@"UPGRADE",
@"USAGE",
@"USE",
@"USER",
@"USER_RESOURCES",
@"USE_FRM",
@"USING",
@"UTC_DATE",
@"UTC_TIME",
@"UTC_TIMESTAMP",
@"VALUE",
@"VALUES",
@"VARBINARY",
@"VARCHAR",
@"VARCHARACTER",
@"VARIABLES",
@"VARYING",
@"VIEW",
@"WAIT",
@"WARNINGS",
@"WEEK",
@"WHEN",
@"WHERE",
@"WHILE",
@"WITH",
@"WORK",
@"WRITE",
@"X509",
@"XA",
@"XOR",
@"YEAR",
@"YEAR_MONTH",
@"ZEROFILL",
nil];
}
/*
* Set whether this text view should apply the indentation on the current line to new lines.
*/
- (void)setAutoindent:(BOOL)enableAutoindent
{
autoindentEnabled = enableAutoindent;
}
/*
* Retrieve whether this text view applies indentation on the current line to new lines.
*/
- (BOOL)autoindent
{
return autoindentEnabled;
}
/*
* Set whether this text view should not autoindent when the Enter key is used, as opposed
* to the return key. Also catches function-return.
*/
- (void)setAutoindentIgnoresEnter:(BOOL)enableAutoindentIgnoresEnter
{
autoindentIgnoresEnter = enableAutoindentIgnoresEnter;
}
/*
* Retrieve whether this text view should not autoindent when the Enter key is used.
*/
- (BOOL)autoindentIgnoresEnter
{
return autoindentIgnoresEnter;
}
/*
* Set whether this text view should automatically create the matching closing char for ", ', ` and ( chars.
*/
- (void)setAutopair:(BOOL)enableAutopair
{
autopairEnabled = enableAutopair;
}
/*
* Retrieve whether this text view automatically creates the matching closing char for ", ', ` and ( chars.
*/
- (BOOL)autopair
{
return autopairEnabled;
}
/*
* Set whether SQL keywords should be automatically uppercased.
*/
- (void)setAutouppercaseKeywords:(BOOL)enableAutouppercaseKeywords
{
autouppercaseKeywordsEnabled = enableAutouppercaseKeywords;
}
/*
* Retrieve whether SQL keywords should be automaticallyuppercased.
*/
- (BOOL)autouppercaseKeywords
{
return autouppercaseKeywordsEnabled;
}
/*******************
SYNTAX HIGHLIGHTING!
*******************/
- (void)awakeFromNib
/*
* Sets self as delegate for the textView's textStorage to enable syntax highlighting,
* and set defaults for general usage
*/
{
[[self textStorage] setDelegate:self];
autoindentEnabled = YES;
autopairEnabled = YES;
autoindentIgnoresEnter = NO;
autouppercaseKeywordsEnabled = YES;
delBackwardsWasPressed = NO;
}
- (void)textStorageDidProcessEditing:(NSNotification *)notification
/*
* Performs syntax highlighting.
* This method recolors the entire text on every keypress. For performance reasons, this function does
* nothing if the text is more than 20 KB.
*
* 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.
*/
{
NSTextStorage *textStore = [notification object];
//make sure that the notification is from the correct textStorage object
if (textStore!=[self textStorage]) return;
NSColor *commentColor = [NSColor colorWithDeviceRed:0.000 green:0.455 blue:0.000 alpha:1.000];
NSColor *quoteColor = [NSColor colorWithDeviceRed:0.769 green:0.102 blue:0.086 alpha:1.000];
NSColor *keywordColor = [NSColor colorWithDeviceRed:0.200 green:0.250 blue:1.000 alpha:1.000];
NSColor *backtickColor = [NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.658 alpha:1.000];
NSColor *numericColor = [NSColor colorWithDeviceRed:0.506 green:0.263 blue:0.0 alpha:1.000];
NSColor *variableColor = [NSColor colorWithDeviceRed:0.5 green:0.5 blue:0.5 alpha:1.000];
NSColor *tokenColor;
int token;
NSRange textRange, tokenRange;
textRange = NSMakeRange(0, [textStore length]);
//don't color texts longer than about 20KB. would be too slow
if (textRange.length > 20000) return;
//first remove the old colors
[textStore removeAttribute:NSForegroundColorAttributeName range:textRange];
//initialise flex
yyuoffset = 0; yyuleng = 0;
yy_switch_to_buffer(yy_scan_string([[textStore string] UTF8String]));
//now loop through all the tokens
while (token=yylex()){
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;
default:
tokenColor = nil;
}
if (!tokenColor) continue;
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
tokenRange = NSIntersectionRange(tokenRange, textRange);
if (!tokenRange.length) continue;
// If the current token is marked as SQL keyword, uppercase it if required.
unsigned long tokenEnd = tokenRange.location+tokenRange.length-1;
// Check the end of the token
if (autouppercaseKeywordsEnabled && !delBackwardsWasPressed
&& [[self textStorage] attribute:kSQLkeyword atIndex:tokenEnd effectiveRange:nil])
// 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 = [[self string] substringWithRange:tokenRange];
BOOL doIt = NO;
@try
{
doIt = ![[self textStorage] attribute:kSQLkeyword atIndex:tokenEnd+1 effectiveRange:nil];
} @catch(id ae) { doIt = YES; }
if(doIt && ![[curTokenString uppercaseString] isEqualToString:curTokenString])
{
// Register it for undo works only partly for now, at least the uppercased keyword will be selected
[self shouldChangeTextInRange:tokenRange replacementString:[curTokenString uppercaseString]];
[self replaceCharactersInRange:tokenRange withString:[curTokenString uppercaseString]];
}
}
[textStore addAttribute: NSForegroundColorAttributeName
value: tokenColor
range: tokenRange ];
// 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?)
[textStore addAttribute: kWQquoted
value: kWQval
range: 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)
[textStore addAttribute: kSQLkeyword
value: kWQval
range: tokenRange ];
// Add an attribute to be used to distinguish quotes from keywords etc.
// used e.g. in completion suggestions
if(token == SPT_DOUBLE_QUOTED_TEXT || token == SPT_SINGLE_QUOTED_TEXT)
[textStore addAttribute: kQuote
value: kWQval
range: tokenRange ];
}
}
@end