// // 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" /* 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 *); @implementation CMTextView /* * 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. if (aSelector == @selector(insertNewline:)) { NSString *textViewString = [[self textStorage] string]; NSString *currentLine, *indentString = nil; NSScanner *whitespaceScanner; NSUInteger lineStart, lineEnd; // Extract the current line based on the text caret or selection start position [textViewString getLineStart:&lineStart end:NULL contentsEnd:&lineEnd forRange:NSMakeRange([self selectedRange].location, 0)]; currentLine = [[NSString alloc] initWithString:[textViewString substringWithRange:NSMakeRange(lineStart, lineEnd - lineStart)]]; // 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]; } /* * Handle autocompletion, returning a list of suggested completions for the supplied character range. */ - (NSArray *)completionsForPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index { 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 ++) { if ([partialString isEqualToString:[[matchingCompletions objectAtIndex:i] substringToIndex:partialLength]]) { // Matches case --> Insert at beginning of completion list [compl insertObject:[matchingCompletions objectAtIndex:i] atIndex:insindex++]; } else { // Not matching case --> Insert at end of completion list [compl addObject:[matchingCompletions objectAtIndex:i]]; } } return [compl autorelease]; } /* List of keywords for autocompletion. If you add a keyword here, it should also be added to the flex file SPEditorTokens.l */ -(NSArray *)keywords { return [NSArray arrayWithObjects: @"ADD", @"ALL", @"ALTER TABLE", @"ALTER VIEW", @"ALTER SCHEMA", @"ALTER SCHEMA", @"ALTER FUNCTION", @"ALTER COLUMN", @"ALTER DATABASE", @"ALTER PROCEDURE", @"ANALYZE", @"AND", @"ASC", @"ASENSITIVE", @"BEFORE", @"BETWEEN", @"BIGINT", @"BINARY", @"BLOB", @"BOTH", @"CALL", @"CASCADE", @"CASE", @"CHANGE", @"CHAR", @"CHARACTER", @"CHECK", @"COLLATE", @"COLUMN", @"COLUMNS", @"CONDITION", @"CONNECTION", @"CONSTRAINT", @"CONTINUE", @"CONVERT", @"CREATE VIEW", @"CREATE INDEX", @"CREATE FUNCTION", @"CREATE DATABASE", @"CREATE PROCEDURE", @"CREATE SCHEMA", @"CREATE TRIGGER", @"CREATE TABLE", @"CREATE USER", @"CROSS", @"CURRENT_DATE", @"CURRENT_TIME", @"CURRENT_TIMESTAMP", @"CURRENT_USER", @"CURSOR", @"DATABASE", @"DATABASES", @"DAY_HOUR", @"DAY_MICROSECOND", @"DAY_MINUTE", @"DAY_SECOND", @"DEC", @"DECIMAL", @"DECLARE", @"DEFAULT", @"DELAYED", @"DELETE", @"DESC", @"DESCRIBE", @"DETERMINISTIC", @"DISTINCT", @"DISTINCTROW", @"DIV", @"DOUBLE", @"DROP TABLE", @"DROP TRIGGER", @"DROP VIEW", @"DROP SCHEMA", @"DROP USER", @"DROP PROCEDURE", @"DROP FUNCTION", @"DROP FOREIGN KEY", @"DROP INDEX", @"DROP PREPARE", @"DROP PRIMARY KEY", @"DROP DATABASE", @"DUAL", @"EACH", @"ELSE", @"ELSEIF", @"ENCLOSED", @"ESCAPED", @"EXISTS", @"EXIT", @"EXPLAIN", @"FALSE", @"FETCH", @"FIELDS", @"FLOAT", @"FOR", @"FORCE", @"FOREIGN KEY", @"FOUND", @"FROM", @"FULLTEXT", @"GOTO", @"GRANT", @"GROUP", @"HAVING", @"HIGH_PRIORITY", @"HOUR_MICROSECOND", @"HOUR_MINUTE", @"HOUR_SECOND", @"IGNORE", @"INDEX", @"INFILE", @"INNER", @"INOUT", @"INSENSITIVE", @"INSERT", @"INT", @"INTEGER", @"INTERVAL", @"INTO", @"ITERATE", @"JOIN", @"KEY", @"KEYS", @"KILL", @"LEADING", @"LEAVE", @"LEFT", @"LIKE", @"LIMIT", @"LINES", @"LOAD", @"LOCALTIME", @"LOCALTIMESTAMP", @"LOCK", @"LONG", @"LONGBLOB", @"LONGTEXT", @"LOOP", @"LOW_PRIORITY", @"MATCH", @"MEDIUMBLOB", @"MEDIUMINT", @"MEDIUMTEXT", @"MIDDLEINT", @"MINUTE_MICROSECOND", @"MINUTE_SECOND", @"MOD", @"NATURAL", @"NOT", @"NO_WRITE_TO_BINLOG", @"NULL", @"NUMERIC", @"ON", @"OPTIMIZE", @"OPTION", @"OPTIONALLY", @"ORDER", @"OUT", @"OUTER", @"OUTFILE", @"PRECISION", @"PRIMARY", @"PRIVILEGES", @"PROCEDURE", @"PURGE", @"READ", @"REAL", @"REFERENCES", @"REGEXP", @"RENAME", @"REPEAT", @"REPLACE", @"REQUIRE", @"RESTRICT", @"RETURN", @"REVOKE", @"RIGHT", @"RLIKE", @"SECOND_MICROSECOND", @"SELECT", @"SENSITIVE", @"SEPARATOR", @"SET", @"SHOW PROCEDURE STATUS", @"SHOW PROCESSLIST", @"SHOW SCHEMAS", @"SHOW SLAVE HOSTS", @"SHOW PRIVILEGES", @"SHOW OPEN TABLES", @"SHOW MASTER STATUS", @"SHOW SLAVE STATUS", @"SHOW PLUGIN", @"SHOW STORAGE ENGINES", @"SHOW VARIABLES", @"SHOW WARNINGS", @"SHOW TRIGGERS", @"SHOW TABLES", @"SHOW MASTER LOGS", @"SHOW TABLE STATUS", @"SHOW TABLE TYPES", @"SHOW STATUS", @"SHOW INNODB STATUS", @"SHOW CREATE DATABASE", @"SHOW CREATE FUNCTION", @"SHOW CREATE PROCEDURE", @"SHOW CREATE SCHEMA", @"SHOW COLUMNS", @"SHOW COLLATION", @"SHOW BINARY LOGS", @"SHOW BINLOG EVENTS", @"SHOW CHARACTER SET", @"SHOW CREATE TABLE", @"SHOW CREATE VIEW", @"SHOW FUNCTION STATUS", @"SHOW GRANTS", @"SHOW INDEX", @"SHOW FIELDS", @"SHOW ERRORS", @"SHOW DATABASES", @"SHOW ENGINE", @"SHOW ENGINES", @"SHOW KEYS", @"SMALLINT", @"SONAME", @"SPATIAL", @"SPECIFIC", @"SQL", @"SQLEXCEPTION", @"SQLSTATE", @"SQLWARNING", @"SQL_BIG_RESULT", @"SQL_CALC_FOUND_ROWS", @"SQL_SMALL_RESULT", @"SSL", @"STARTING", @"STRAIGHT_JOIN", @"TABLE", @"TABLES", @"TERMINATED", @"THEN", @"TINYBLOB", @"TINYINT", @"TINYTEXT", @"TRAILING", @"TRIGGER", @"TRUE", @"UNDO", @"UNION", @"UNIQUE", @"UNLOCK", @"UNSIGNED", @"UPDATE", @"USAGE", @"USE", @"USING", @"UTC_DATE", @"UTC_TIME", @"UTC_TIMESTAMP", @"VALUES", @"VARBINARY", @"VARCHAR", @"VARCHARACTER", @"VARYING", @"WHEN", @"WHERE", @"WHILE", @"WITH", @"WRITE", @"XOR", @"YEAR_MONTH", @"ZEROFILL", nil]; } /******************* SYNTAX HIGHLIGHTING! *******************/ - (void)awakeFromNib /* sets self as delegate for the textView's textStorage to enable syntax highlighting */ { [[self textStorage] setDelegate:self]; } - (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 a few 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 *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_RESERVED_WORD: tokenColor = keywordColor; break; case SPT_COMMENT: tokenColor = commentColor; 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; [textStore addAttribute: NSForegroundColorAttributeName value: tokenColor range: tokenRange ]; } } @end