/UKSyntaxColoredTextViewController.m
http://github.com/bububa/MongoHub-Mac · Objective C · 1355 lines · 840 code · 244 blank · 271 comment · 207 complexity · 84f575989b941de20e5b3da798c28198 MD5 · raw file
- //
- // UKSyntaxColoredTextViewController.m
- // UKSyntaxColoredDocument
- //
- // Created by Uli Kusterer on 13.03.10.
- // Copyright 2010 Uli Kusterer.
- //
- // This software is provided 'as-is', without any express or implied
- // warranty. In no event will the authors be held liable for any damages
- // arising from the use of this software.
- //
- // Permission is granted to anyone to use this software for any purpose,
- // including commercial applications, and to alter it and redistribute it
- // freely, subject to the following restrictions:
- //
- // 1. The origin of this software must not be misrepresented; you must not
- // claim that you wrote the original software. If you use this software
- // in a product, an acknowledgment in the product documentation would be
- // appreciated but is not required.
- //
- // 2. Altered source versions must be plainly marked as such, and must not be
- // misrepresented as being the original software.
- //
- // 3. This notice may not be removed or altered from any source
- // distribution.
- //
- // -----------------------------------------------------------------------------
- // Headers:
- // -----------------------------------------------------------------------------
- #import "UKSyntaxColoredTextViewController.h"
- #import "NSArray+Color.h"
- #import "NSScanner+SkipUpToCharset.h"
- // -----------------------------------------------------------------------------
- // Globals:
- // -----------------------------------------------------------------------------
- static BOOL sSyntaxColoredTextDocPrefsInited = NO;
- // -----------------------------------------------------------------------------
- // Macros:
- // -----------------------------------------------------------------------------
- #define TEXTVIEW ((NSTextView*)[self view])
- @implementation UKSyntaxColoredTextViewController
- +(void) makeSurePrefsAreInited
- {
- if( !sSyntaxColoredTextDocPrefsInited )
- {
- NSUserDefaults* prefs = [NSUserDefaults standardUserDefaults];
- [prefs registerDefaults: [NSDictionary dictionaryWithContentsOfFile: [[NSBundle mainBundle] pathForResource: @"SyntaxColorDefaults" ofType: @"plist"]]];
-
- sSyntaxColoredTextDocPrefsInited = YES;
- }
- }
- /* -----------------------------------------------------------------------------
- init:
- Constructor that inits sourceCode member variable as a flag. It's
- storage for the text until the NIB's been loaded.
- -------------------------------------------------------------------------- */
- -(id) initWithNibName: (NSString*)inNibName bundle: (NSBundle*)inBundle
- {
- self = [super initWithNibName: inNibName bundle: inBundle];
- if( self )
- {
- autoSyntaxColoring = YES;
- maintainIndentation = YES;
- recolorTimer = nil;
- syntaxColoringBusy = NO;
- }
- return self;
- }
- -(void) dealloc
- {
- [[NSNotificationCenter defaultCenter] removeObserver: self];
-
- [recolorTimer invalidate];
- [recolorTimer release];
- recolorTimer = nil;
-
- [replacementString release];
- replacementString = nil;
-
- [super dealloc];
- }
- -(void) setUpSyntaxColoring
- {
- // Set up some sensible defaults for syntax coloring:
- [[self class] makeSurePrefsAreInited];
-
- // Register for "text changed" notifications of our text storage:
- [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(processEditing:)
- name: NSTextStorageDidProcessEditingNotification
- object: [TEXTVIEW textStorage]];
-
- // Make sure text isn't wrapped:
- [self turnOffWrapping];
-
- // Do initial syntax coloring of our file:
- [self recolorCompleteFile: nil];
-
- // Put selection at top like Project Builder has it, so user sees it:
- [TEXTVIEW setSelectedRange: NSMakeRange(0,0)];
-
- [self textView: TEXTVIEW willChangeSelectionFromCharacterRange: NSMakeRange(0,0)
- toCharacterRange: NSMakeRange(0,0)];
-
- // Make sure we can use "find" if we're on 10.3:
- if( [TEXTVIEW respondsToSelector: @selector(setUsesFindPanel:)] )
- [TEXTVIEW setUsesFindPanel: YES];
- }
- -(void) setDelegate: (id<UKSyntaxColoredTextViewDelegate>)inDelegate
- {
- delegate = inDelegate;
- }
- -(id) delegate
- {
- return delegate;
- }
- /* -----------------------------------------------------------------------------
- setView:
- We've just been given a view! Apply initial syntax coloring.
- -------------------------------------------------------------------------- */
- -(void) setView: (NSView*)theView
- {
- [super setView: theView];
-
- [(NSTextView*)theView setDelegate: self];
- [self setUpSyntaxColoring]; // +++ If someone calls this twice, we should only call part of this twice!
- }
- /* -----------------------------------------------------------------------------
- processEditing:
- Part of the text was changed. Recolor it.
- -------------------------------------------------------------------------- */
- -(void) processEditing: (NSNotification*)notification
- {
- NSTextStorage *textStorage = [notification object];
- NSRange range = [textStorage editedRange];
- int changeInLen = [textStorage changeInLength];
- BOOL wasInUndoRedo = [[self undoManager] isUndoing] || [[self undoManager] isRedoing];
- BOOL textLengthMayHaveChanged = NO;
-
- // Was delete op or undo that could have changed text length?
- if( wasInUndoRedo )
- {
- textLengthMayHaveChanged = YES;
- range = [TEXTVIEW selectedRange];
- }
- if( changeInLen <= 0 )
- textLengthMayHaveChanged = YES;
-
- // Try to get chars around this to recolor any identifier we're in:
- if( textLengthMayHaveChanged )
- {
- if( range.location > 0 )
- range.location--;
- if( (range.location +range.length +2) < [textStorage length] )
- range.length += 2;
- else if( (range.location +range.length +1) < [textStorage length] )
- range.length += 1;
- }
-
- NSRange currRange = range;
-
- // Perform the syntax coloring:
- if( autoSyntaxColoring && range.length > 0 )
- {
- NSRange effectiveRange;
- NSString* rangeMode;
-
-
- rangeMode = [textStorage attribute: TD_SYNTAX_COLORING_MODE_ATTR
- atIndex: currRange.location
- effectiveRange: &effectiveRange];
-
- unsigned int x = range.location;
-
- /* TODO: If we're in a multi-line comment and we're typing a comment-end
- character, or we're in a string and we're typing a quote character,
- this should include the rest of the text up to the next comment/string
- end character in the recalc. */
-
- // Scan up to prev line break:
- while( x > 0 )
- {
- unichar theCh = [[textStorage string] characterAtIndex: x];
- if( theCh == '\n' || theCh == '\r' )
- break;
- --x;
- }
-
- currRange.location = x;
-
- // Scan up to next line break:
- x = range.location +range.length;
-
- while( x < [textStorage length] )
- {
- unichar theCh = [[textStorage string] characterAtIndex: x];
- if( theCh == '\n' || theCh == '\r' )
- break;
- ++x;
- }
-
- currRange.length = x -currRange.location;
-
- // Open identifier, comment etc.? Make sure we include the whole range.
- if( rangeMode != nil )
- currRange = NSUnionRange( currRange, effectiveRange );
-
- // Actually recolor the changed part:
- [self recolorRange: currRange];
- }
- }
- /* -----------------------------------------------------------------------------
- textView:shouldChangeTextinRange:replacementString:
- Perform indentation-maintaining if we're supposed to.
- -------------------------------------------------------------------------- */
- -(BOOL) textView:(NSTextView *)tv shouldChangeTextInRange:(NSRange)afcr replacementString:(NSString *)rps
- {
- if( maintainIndentation )
- {
- affectedCharRange = afcr;
- if( replacementString )
- {
- [replacementString release];
- replacementString = nil;
- }
- replacementString = [rps retain];
- [self performSelector: @selector(didChangeText) withObject: nil afterDelay: 0.0]; // Queue this up on the event loop. If we change the text here, we only confuse the undo stack.
- }
-
- return YES;
- }
- -(void) didChangeText // This actually does what we want to do in textView:shouldChangeTextInRange:
- {
- if( maintainIndentation && replacementString && ([replacementString isEqualToString:@"\n"]
- || [replacementString isEqualToString:@"\r"]) )
- {
- NSMutableAttributedString* textStore = [TEXTVIEW textStorage];
- BOOL hadSpaces = NO;
- unsigned int lastSpace = affectedCharRange.location,
- prevLineBreak = 0;
- NSRange spacesRange = { 0, 0 };
- unichar theChar = 0;
- unsigned int x = (affectedCharRange.location == 0) ? 0 : affectedCharRange.location -1;
- NSString* tsString = [textStore string];
-
- while( YES )
- {
- if( x > ([tsString length] -1) )
- break;
-
- theChar = [tsString characterAtIndex: x];
-
- switch( theChar )
- {
- case '\n':
- case '\r':
- prevLineBreak = x +1;
- x = 0; // Terminate the loop.
- break;
-
- case ' ':
- case '\t':
- if( !hadSpaces )
- {
- lastSpace = x;
- hadSpaces = YES;
- }
- break;
-
- default:
- hadSpaces = NO;
- break;
- }
-
- if( x == 0 )
- break;
-
- x--;
- }
-
- if( hadSpaces )
- {
- spacesRange.location = prevLineBreak;
- spacesRange.length = lastSpace -prevLineBreak +1;
- if( spacesRange.length > 0 )
- [TEXTVIEW insertText: [tsString substringWithRange:spacesRange]];
- }
- }
- }
- /* -----------------------------------------------------------------------------
- toggleAutoSyntaxColoring:
- Action for menu item that toggles automatic syntax coloring on and off.
- -------------------------------------------------------------------------- */
- -(IBAction) toggleAutoSyntaxColoring: (id)sender
- {
- [self setAutoSyntaxColoring: ![self autoSyntaxColoring]];
- [self recolorCompleteFile: nil];
- }
- /* -----------------------------------------------------------------------------
- setAutoSyntaxColoring:
- Accessor to turn automatic syntax coloring on or off.
- -------------------------------------------------------------------------- */
- -(void) setAutoSyntaxColoring: (BOOL)state
- {
- autoSyntaxColoring = state;
- }
- /* -----------------------------------------------------------------------------
- autoSyntaxColoring:
- Accessor for determining whether automatic syntax coloring is on or off.
- -------------------------------------------------------------------------- */
- -(BOOL) autoSyntaxColoring
- {
- return autoSyntaxColoring;
- }
- /* -----------------------------------------------------------------------------
- toggleMaintainIndentation:
- Action for menu item that toggles indentation maintaining on and off.
- -------------------------------------------------------------------------- */
- -(IBAction) toggleMaintainIndentation: (id)sender
- {
- [self setMaintainIndentation: ![self maintainIndentation]];
- }
- /* -----------------------------------------------------------------------------
- setMaintainIndentation:
- Accessor to turn indentation maintaining on or off.
- -------------------------------------------------------------------------- */
- -(void) setMaintainIndentation: (BOOL)state
- {
- maintainIndentation = state;
- }
- /* -----------------------------------------------------------------------------
- maintainIndentation:
- Accessor for determining whether indentation maintaining is on or off.
- -------------------------------------------------------------------------- */
- -(BOOL) maintainIndentation
- {
- return maintainIndentation;
- }
- /* -----------------------------------------------------------------------------
- goToLine:
- This selects the specified line of the document.
- -------------------------------------------------------------------------- */
- -(void) goToLine: (int)lineNum
- {
- NSRange theRange = { 0, 0 };
- NSString* vString = [TEXTVIEW string];
- unsigned currLine = 1;
- NSCharacterSet* vSet = [NSCharacterSet characterSetWithCharactersInString: @"\n\r"];
- unsigned x;
- unsigned lastBreakOffs = 0;
- unichar lastBreakChar = 0;
-
- for( x = 0; x < [vString length]; x++ )
- {
- unichar theCh = [vString characterAtIndex: x];
-
- // Skip non-linebreak chars:
- if( ![vSet characterIsMember: theCh] )
- continue;
-
- // If this is the LF in a CRLF sequence, only count it as one line break:
- if( theCh == '\n' && lastBreakOffs == (x-1)
- && lastBreakChar == '\r' )
- {
- lastBreakOffs = 0;
- lastBreakChar = 0;
- theRange.location++;
- continue;
- }
-
- // Calc range and increase line number:
- theRange.length = x -theRange.location +1;
- if( currLine >= lineNum )
- break;
- currLine++;
- theRange.location = theRange.location +theRange.length;
- lastBreakOffs = x;
- lastBreakChar = theCh;
- }
-
- [TEXTVIEW scrollRangeToVisible: theRange];
- [TEXTVIEW setSelectedRange: theRange];
- }
- /* -----------------------------------------------------------------------------
- turnOffWrapping:
- Makes the view so wide that text won't wrap anymore.
- -------------------------------------------------------------------------- */
- #define REALLY_LARGE_NUMBER 1.0e7 // FLT_MAX is too large and causes our rect to be shortened again.
- -(void) turnOffWrapping
- {
- NSTextContainer* textContainer = [TEXTVIEW textContainer];
- NSRect frame = { { 0, 0 }, { 0, 0 } };
- NSScrollView* scrollView = [TEXTVIEW enclosingScrollView];
-
- // Make sure we can see right edge of line:
- [scrollView setHasHorizontalScroller: YES];
-
- // Make text container so wide it won't wrap:
- [textContainer setContainerSize: NSMakeSize(REALLY_LARGE_NUMBER, REALLY_LARGE_NUMBER)];
- [textContainer setWidthTracksTextView: NO];
- [textContainer setHeightTracksTextView: NO];
-
- // Make sure text view is wide enough:
- frame.origin = NSMakePoint( 0.0, 0.0 );
- frame.size = [scrollView contentSize];
-
- [TEXTVIEW setMaxSize: NSMakeSize(REALLY_LARGE_NUMBER, REALLY_LARGE_NUMBER)];
- [TEXTVIEW setHorizontallyResizable: YES];
- [TEXTVIEW setVerticallyResizable: YES];
- [TEXTVIEW setAutoresizingMask: NSViewNotSizable];
- }
- /* -----------------------------------------------------------------------------
- goToCharacter:
- This selects the specified character in the document.
- -------------------------------------------------------------------------- */
- -(void) goToCharacter: (int)charNum
- {
- [self goToRangeFrom: charNum toChar: charNum +1];
- }
- /* -----------------------------------------------------------------------------
- goToRangeFrom:
- Main bottleneck for selecting ranges in our file.
- -------------------------------------------------------------------------- */
- -(void) goToRangeFrom: (int)startCh toChar: (int)endCh
- {
- NSRange theRange = { 0, 0 };
-
- theRange.location = startCh -1;
- theRange.length = endCh -startCh;
-
- if( startCh == 0 || startCh > [[TEXTVIEW string] length] )
- return;
-
- [TEXTVIEW scrollRangeToVisible: theRange];
- [TEXTVIEW setSelectedRange: theRange];
- }
- // -----------------------------------------------------------------------------
- // restoreText:
- // Main bottleneck for our (very primitive and inefficient) undo
- // implementation. This takes a string of the previous state of the
- // *entire text* and restores it.
- // -----------------------------------------------------------------------------
- -(void) restoreText: (NSString*)textToRestore
- {
- [[self undoManager] disableUndoRegistration];
- [TEXTVIEW setString: textToRestore];
- [[self undoManager] enableUndoRegistration];
- }
- // -----------------------------------------------------------------------------
- // indentSelection:
- // Indent the selected lines by one more level (i.e. one more tab).
- // -----------------------------------------------------------------------------
- -(IBAction) indentSelection: (id)sender
- {
- [[self undoManager] beginUndoGrouping];
- NSString* prevText = [[[[TEXTVIEW textStorage] string] copy] autorelease];
- [[self undoManager] registerUndoWithTarget: self selector: @selector(restoreText:) object: prevText];
-
- NSRange selRange = [TEXTVIEW selectedRange],
- nuSelRange = selRange;
- unsigned x;
- NSMutableString* str = [[TEXTVIEW textStorage] mutableString];
-
- // Unselect any trailing returns so we don't indent the next line after a full-line selection.
- if( selRange.length > 1 && ([str characterAtIndex: selRange.location +selRange.length -1] == '\n'
- || [str characterAtIndex: selRange.location +selRange.length -1] == '\r') )
- selRange.length--;
-
- for( x = selRange.location +selRange.length -1; x >= selRange.location; x-- )
- {
- if( [str characterAtIndex: x] == '\n'
- || [str characterAtIndex: x] == '\r' )
- {
- [str insertString: @"\t" atIndex: x+1];
- nuSelRange.length++;
- }
-
- if( x == 0 )
- break;
- }
-
- [str insertString: @"\t" atIndex: nuSelRange.location];
- nuSelRange.length++;
- [TEXTVIEW setSelectedRange: nuSelRange];
- [[self undoManager] endUndoGrouping];
- }
- // -----------------------------------------------------------------------------
- // unindentSelection:
- // Un-indent the selected lines by one level (i.e. remove one tab from each
- // line's start).
- // -----------------------------------------------------------------------------
- -(IBAction) unindentSelection: (id)sender
- {
- NSRange selRange = [TEXTVIEW selectedRange],
- nuSelRange = selRange;
- unsigned x, n;
- unsigned lastIndex = selRange.location +selRange.length -1;
- NSMutableString* str = [[TEXTVIEW textStorage] mutableString];
-
- // Unselect any trailing returns so we don't indent the next line after a full-line selection.
- if( selRange.length > 1 && ([str characterAtIndex: selRange.location +selRange.length -1] == '\n'
- || [str characterAtIndex: selRange.location +selRange.length -1] == '\r') )
- selRange.length--;
-
- if( selRange.length == 0 )
- return;
-
- [[self undoManager] beginUndoGrouping];
- NSString* prevText = [[[[TEXTVIEW textStorage] string] copy] autorelease];
- [[self undoManager] registerUndoWithTarget: self selector: @selector(restoreText:) object: prevText];
-
- for( x = lastIndex; x >= selRange.location; x-- )
- {
- if( [str characterAtIndex: x] == '\n'
- || [str characterAtIndex: x] == '\r' )
- {
- if( (x +1) <= lastIndex)
- {
- if( [str characterAtIndex: x+1] == '\t' )
- {
- [str deleteCharactersInRange: NSMakeRange(x+1,1)];
- nuSelRange.length--;
- }
- else
- {
- for( n = x+1; (n <= (x+4)) && (n <= lastIndex); n++ )
- {
- if( [str characterAtIndex: x+1] != ' ' )
- break;
- [str deleteCharactersInRange: NSMakeRange(x+1,1)];
- nuSelRange.length--;
- }
- }
- }
- }
-
- if( x == 0 )
- break;
- }
-
- if( [str characterAtIndex: nuSelRange.location] == '\t' )
- {
- [str deleteCharactersInRange: NSMakeRange(nuSelRange.location,1)];
- nuSelRange.length--;
- }
- else
- {
- for( n = 1; (n <= 4) && (n <= lastIndex); n++ )
- {
- if( [str characterAtIndex: nuSelRange.location] != ' ' )
- break;
- [str deleteCharactersInRange: NSMakeRange(nuSelRange.location,1)];
- nuSelRange.length--;
- }
- }
-
- [TEXTVIEW setSelectedRange: nuSelRange];
- [[self undoManager] endUndoGrouping];
- }
- /* -----------------------------------------------------------------------------
- toggleCommentForSelection:
- Add a comment to the start of this line/remove an existing comment.
- -------------------------------------------------------------------------- */
- -(IBAction) toggleCommentForSelection: (id)sender
- {
- NSRange selRange = [TEXTVIEW selectedRange];
- unsigned x;
- NSMutableString* str = [[TEXTVIEW textStorage] mutableString];
-
- if( selRange.length == 0 )
- selRange.length++;
-
- // Are we at the end of a line?
- if ([str characterAtIndex: selRange.location] == '\n' ||
- [str characterAtIndex: selRange.location] == '\r')
- {
- if( selRange.location > 0 )
- {
- selRange.location--;
- selRange.length++;
- }
- }
-
- // Move the selection to the start of a line
- while( selRange.location > 0 )
- {
- if( [str characterAtIndex: selRange.location] == '\n'
- || [str characterAtIndex: selRange.location] == '\r')
- {
- selRange.location++;
- selRange.length--;
- break;
- }
- selRange.location--;
- selRange.length++;
- }
-
- // Select up to the end of a line
- while ( (selRange.location +selRange.length) < [str length]
- && !([str characterAtIndex:selRange.location+selRange.length-1] == '\n'
- || [str characterAtIndex:selRange.location+selRange.length-1] == '\r') )
- {
- selRange.length++;
- }
-
- if (selRange.length == 0)
- return;
-
- [[self undoManager] beginUndoGrouping];
- NSString* prevText = [[[[TEXTVIEW textStorage] string] copy] autorelease];
- [[self undoManager] registerUndoWithTarget: self selector: @selector(restoreText:) object: prevText];
-
- // Unselect any trailing returns so we don't comment the next line after a full-line selection.
- while( [str characterAtIndex: selRange.location +selRange.length -1] == '\n' ||
- [str characterAtIndex: selRange.location +selRange.length -1] == '\r'
- && selRange.length > 0 )
- {
- selRange.length--;
- }
-
-
- NSRange nuSelRange = selRange;
-
- NSString* commentPrefix = [[self syntaxDefinitionDictionary] objectForKey: @"OneLineCommentPrefix"];
- if( !commentPrefix || [commentPrefix length] == 0 )
- commentPrefix = @"# ";
- NSInteger commentPrefixLength = [commentPrefix length];
- NSString* trimmedCommentPrefix = [commentPrefix stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]];
- if( !trimmedCommentPrefix || [trimmedCommentPrefix length] == 0 ) // Comments apparently *are* whitespace.
- trimmedCommentPrefix = commentPrefix;
- NSInteger trimmedCommentPrefixLength = [trimmedCommentPrefix length];
-
- for( x = selRange.location +selRange.length -1; x >= selRange.location; x-- )
- {
- BOOL hitEnd = (x == selRange.location);
- BOOL hitLineBreak = [str characterAtIndex: x] == '\n' || [str characterAtIndex: x] == '\r';
- if( hitLineBreak || hitEnd )
- {
- NSUInteger startOffs = x+1;
- if( hitEnd && !hitLineBreak )
- startOffs = x;
- NSInteger possibleCommentLength = 0;
- if( commentPrefixLength <= (selRange.length +selRange.location -startOffs) )
- possibleCommentLength = commentPrefixLength;
- else if( trimmedCommentPrefixLength <= (selRange.length +selRange.location -startOffs) )
- possibleCommentLength = trimmedCommentPrefixLength;
-
- NSString * lineStart = [str substringWithRange: NSMakeRange( startOffs, possibleCommentLength )];
- BOOL haveWhitespaceToo = [lineStart hasPrefix: commentPrefix];
- if( [lineStart hasPrefix: trimmedCommentPrefix] )
- {
- NSInteger commentLength = haveWhitespaceToo ? commentPrefixLength : trimmedCommentPrefixLength;
- [str deleteCharactersInRange: NSMakeRange(startOffs, commentLength)];
- nuSelRange.length -= commentLength;
- }
- else
- {
- [str insertString: commentPrefix atIndex: startOffs];
- nuSelRange.length += commentPrefixLength;
- }
- }
-
- if( x == 0 )
- break;
- }
-
- [TEXTVIEW setSelectedRange: nuSelRange];
- [[self undoManager] endUndoGrouping];
-
- }
- /* -----------------------------------------------------------------------------
- validateMenuItem:
- Make sure check marks of the "Toggle auto syntax coloring" and "Maintain
- indentation" menu items are set up properly.
- -------------------------------------------------------------------------- */
- -(BOOL) validateMenuItem:(NSMenuItem*)menuItem
- {
- if( [menuItem action] == @selector(toggleAutoSyntaxColoring:) )
- {
- [menuItem setState: [self autoSyntaxColoring]];
- return YES;
- }
- else if( [menuItem action] == @selector(toggleMaintainIndentation:) )
- {
- [menuItem setState: [self maintainIndentation]];
- return YES;
- }
- else
- return [super validateMenuItem: menuItem];
- }
- /* -----------------------------------------------------------------------------
- recolorCompleteFile:
- IBAction to do a complete recolor of the whole friggin' document.
- This is called once after the document's been loaded and leaves some
- custom styles in the document which are used by recolorRange to properly
- perform recoloring of parts.
- -------------------------------------------------------------------------- */
- -(IBAction) recolorCompleteFile: (id)sender
- {
- NSRange range = NSMakeRange( 0, [[TEXTVIEW textStorage] length] );
- [self recolorRange: range];
- }
- /* -----------------------------------------------------------------------------
- recolorRange:
- Try to apply syntax coloring to the text in our text view. This
- overwrites any styles the text may have had before. This function
- guarantees that it'll preserve the selection.
-
- Note that the order in which the different things are colorized is
- important. E.g. identifiers go first, followed by comments, since that
- way colors are removed from identifiers inside a comment and replaced
- with the comment color, etc.
-
- The range passed in here is special, and may not include partial
- identifiers or the end of a comment. Make sure you include the entire
- multi-line comment etc. or it'll lose color.
-
- This calls oldRecolorRange to handle old-style syntax definitions.
- -------------------------------------------------------------------------- */
- -(void) recolorRange: (NSRange)range
- {
- if( syntaxColoringBusy ) // Prevent endless loop when recoloring's replacement of text causes processEditing to fire again.
- return;
-
- if( TEXTVIEW == nil || range.length == 0 // Don't like doing useless stuff.
- || recolorTimer ) // And don't like recoloring partially if a full recolorization is pending.
- return;
-
- @try
- {
- syntaxColoringBusy = YES;
- if( [delegate respondsToSelector: @selector(textViewControllerWillStartSyntaxRecoloring:)] )
- [delegate textViewControllerWillStartSyntaxRecoloring: self];
-
- // Kludge fix for case where we sometimes exceed text length:ra
- int diff = [[TEXTVIEW textStorage] length] -(range.location +range.length);
- if( diff < 0 )
- range.length += diff;
-
- // Get the text we'll be working with:
- NSMutableAttributedString* vString = [[NSMutableAttributedString alloc] initWithString: [[[TEXTVIEW textStorage] string] substringWithRange: range]];
- [vString autorelease];
-
- // Load colors and fonts to use from preferences:
- // Load our dictionary which contains info on coloring this language:
- NSDictionary* vSyntaxDefinition = [self syntaxDefinitionDictionary];
- NSEnumerator* vComponentsEnny = [[vSyntaxDefinition objectForKey: @"Components"] objectEnumerator];
-
- if( vComponentsEnny == nil ) // No new-style list of components to colorize?
- {
- // @finally takes care of cleaning up syntaxColoringBusy etc. here.
- return;
- }
-
- // Loop over all available components:
- NSDictionary* vCurrComponent = nil;
- NSDictionary* vStyles = [self defaultTextAttributes];
- NSUserDefaults* vPrefs = [NSUserDefaults standardUserDefaults];
-
- NSDictionary* dStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- [NSColor whiteColor], NSForegroundColorAttributeName,
- @"JSONStrings", TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
-
- [vString addAttributes: dStyles range: NSMakeRange( 0, [vString length] )];
-
- while( (vCurrComponent = [vComponentsEnny nextObject]) )
- {
- NSString* vComponentType = [vCurrComponent objectForKey: @"Type"];
- NSString* vComponentName = [vCurrComponent objectForKey: @"Name"];
- NSString* vColorKeyName = [@"SyntaxColoring:Color:" stringByAppendingString: vComponentName];
- NSColor* vColor = [[vPrefs arrayForKey: vColorKeyName] colorValue];
-
- if( !vColor )
- vColor = [[vCurrComponent objectForKey: @"Color"] colorValue];
-
- if( [vComponentType isEqualToString: @"BlockComment"] )
- {
- [self colorCommentsFrom: [vCurrComponent objectForKey: @"Start"]
- to: [vCurrComponent objectForKey: @"End"] inString: vString
- withColor: vColor andMode: vComponentName];
- }
- else if( [vComponentType isEqualToString: @"OneLineComment"] )
- {
- [self colorOneLineComment: [vCurrComponent objectForKey: @"Start"]
- inString: vString withColor: vColor andMode: vComponentName];
- }
- else if( [vComponentType isEqualToString: @"String"] )
- {
- [self colorStringsFrom: [vCurrComponent objectForKey: @"Start"]
- to: [vCurrComponent objectForKey: @"End"]
- inString: vString withColor: vColor andMode: vComponentName
- andEscapeChar: [vCurrComponent objectForKey: @"EscapeChar"]];
- }
- else if( [vComponentType isEqualToString: @"Tag"] )
- {
- [self colorTagFrom: [vCurrComponent objectForKey: @"Start"]
- to: [vCurrComponent objectForKey: @"End"] inString: vString
- withColor: vColor andMode: vComponentName
- exceptIfMode: [vCurrComponent objectForKey: @"IgnoredComponent"]];
- }
- else if( [vComponentType isEqualToString: @"Keywords"] )
- {
- NSArray* vIdents = [vCurrComponent objectForKey: @"Keywords"];
- if( !vIdents )
- vIdents = [[NSUserDefaults standardUserDefaults] objectForKey: [@"SyntaxColoring:Keywords:" stringByAppendingString: vComponentName]];
- if( !vIdents && [vComponentName isEqualToString: @"UserIdentifiers"] )
- vIdents = [[NSUserDefaults standardUserDefaults] objectForKey: TD_USER_DEFINED_IDENTIFIERS];
- if( vIdents )
- {
- NSCharacterSet* vIdentCharset = nil;
- NSString* vCurrIdent = nil;
- NSString* vCsStr = [vCurrComponent objectForKey: @"Charset"];
- if( vCsStr )
- vIdentCharset = [NSCharacterSet characterSetWithCharactersInString: vCsStr];
-
- NSEnumerator* vItty = [vIdents objectEnumerator];
- while( vCurrIdent = [vItty nextObject] )
- [self colorIdentifier: vCurrIdent inString: vString withColor: vColor
- andMode: vComponentName charset: vIdentCharset];
- }
- }
- }
-
- // Replace the range with our recolored part:
- [vString addAttributes: vStyles range: NSMakeRange( 0, [vString length] )];
- [[TEXTVIEW textStorage] replaceCharactersInRange: range withAttributedString: vString];
- }
- @finally
- {
- if( [delegate respondsToSelector: @selector(textViewControllerDidFinishSyntaxRecoloring:)] )
- [delegate textViewControllerDidFinishSyntaxRecoloring: self];
- syntaxColoringBusy = NO;
- [self textView: TEXTVIEW willChangeSelectionFromCharacterRange: [TEXTVIEW selectedRange]
- toCharacterRange: [TEXTVIEW selectedRange]];
- }
- }
- /* -----------------------------------------------------------------------------
- textView:willChangeSelectionFromCharacterRange:toCharacterRange:
- Delegate method called when our selection changes. Updates our status
- display to indicate which characters are selected.
- -------------------------------------------------------------------------- */
- -(NSRange) textView: (NSTextView*)theTextView willChangeSelectionFromCharacterRange: (NSRange)oldSelectedCharRange
- toCharacterRange: (NSRange)newSelectedCharRange
- {
- unsigned startCh = newSelectedCharRange.location,
- endCh = newSelectedCharRange.location +newSelectedCharRange.length;
- unsigned lineNo = 0,
- lastLineStart = 0,
- x = 0;
- unsigned startChLine = 0, endChLine = 0;
- unichar lastBreakChar = 0;
- unsigned lastBreakOffs = 0;
-
- // Calc line number:
- for( x = 0; (x < startCh) && (x < [[theTextView string] length]); x++ )
- {
- unichar theCh = [[theTextView string] characterAtIndex: x];
- switch( theCh )
- {
- case '\n':
- if( lastBreakOffs == (x-1) && lastBreakChar == '\r' ) // LF in CRLF sequence? Treat this as a single line break.
- {
- lastBreakOffs = 0;
- lastBreakChar = 0;
- continue;
- }
- // Else fall through!
-
- case '\r':
- lineNo++;
- lastLineStart = x +1;
- lastBreakOffs = x;
- lastBreakChar = theCh;
- break;
- }
- }
-
- startChLine = (newSelectedCharRange.location -lastLineStart);
- endChLine = (newSelectedCharRange.location -lastLineStart) +newSelectedCharRange.length;
-
- // Let delegate know what to display:
- if( [delegate respondsToSelector: @selector(selectionInTextViewController:changedToStartCharacter:endCharacter:inLine:startCharacterInDocument:endCharacterInDocument:)] )
- [delegate selectionInTextViewController: self
- changedToStartCharacter: startChLine endCharacter: endChLine
- inLine: lineNo startCharacterInDocument: startCh
- endCharacterInDocument: endCh];
-
- return newSelectedCharRange;
- }
- /* -----------------------------------------------------------------------------
- syntaxDefinitionFilename:
- Like nibName, this should return the name of the syntax
- definition file to use. Advanced users may use this to allow different
- coloring to take place depending on the file extension by returning
- different file names here.
-
- Note that the ".plist" extension is automatically appended to the file
- name.
- -------------------------------------------------------------------------- */
- -(NSString*) syntaxDefinitionFilename
- {
- NSString* syntaxDefFN = nil;
- if( [delegate respondsToSelector: @selector(syntaxDefinitionFilenameForTextViewController:)] )
- syntaxDefFN = [delegate syntaxDefinitionFilenameForTextViewController: self];
-
- if( !syntaxDefFN )
- syntaxDefFN = @"SyntaxDefinition";
-
- return syntaxDefFN;
- }
- /* -----------------------------------------------------------------------------
- syntaxDefinitionDictionary:
- This returns the syntax definition dictionary to use, which indicates
- what ranges of text to colorize. Advanced users may use this to allow
- different coloring to take place depending on the file extension by
- returning different dictionaries here.
-
- By default, this simply reads a dictionary from the .plist file
- indicated by -syntaxDefinitionFilename.
- -------------------------------------------------------------------------- */
- -(NSDictionary*) syntaxDefinitionDictionary
- {
- NSDictionary* theDict = nil;
-
- if( [delegate respondsToSelector: @selector(syntaxDefinitionDictionaryForTextViewController:)] )
- theDict = [delegate syntaxDefinitionDictionaryForTextViewController: self];
-
- if( !theDict )
- {
- NSBundle* theBundle = [self nibBundle];
- if( !theBundle )
- theBundle = [NSBundle bundleForClass: [self class]]; // Usually the main bundle, but be nice to plugins.
- theDict = [NSDictionary dictionaryWithContentsOfFile: [theBundle pathForResource: [self syntaxDefinitionFilename] ofType: @"plist"]];
- }
-
- return theDict;
- }
- /* -----------------------------------------------------------------------------
- colorStringsFrom:
- Apply syntax coloring to all strings. This is basically the same code
- as used for multi-line comments, except that it ignores the end
- character if it is preceded by a backslash.
- -------------------------------------------------------------------------- */
- -(void) colorStringsFrom: (NSString*) startCh to: (NSString*) endCh inString: (NSMutableAttributedString*) s
- withColor: (NSColor*) col andMode:(NSString*)attr andEscapeChar: (NSString*)vStringEscapeCharacter
- {
- NS_DURING
- NSScanner* vScanner = [NSScanner scannerWithString: [s string]];
- NSDictionary* vStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- col, NSForegroundColorAttributeName,
- attr, TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
- BOOL vIsEndChar = NO;
- unichar vEscChar = '\\';
- BOOL vDelegateHandlesProgress = [delegate respondsToSelector: @selector(textViewControllerProgressedWhileSyntaxRecoloring:)];
-
- if( vStringEscapeCharacter )
- {
- if( [vStringEscapeCharacter length] != 0 )
- vEscChar = [vStringEscapeCharacter characterAtIndex: 0];
- }
-
- while( ![vScanner isAtEnd] )
- {
- int vStartOffs,
- vEndOffs;
- vIsEndChar = NO;
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
-
- // Look for start of string:
- [vScanner scanUpToString: startCh intoString: nil];
- vStartOffs = [vScanner scanLocation];
- if( ![vScanner scanString:startCh intoString:nil] )
- NS_VOIDRETURN;
-
- while( !vIsEndChar && ![vScanner isAtEnd] ) // Loop until we find end-of-string marker or our text to color is finished:
- {
- [vScanner scanUpToString: endCh intoString: nil];
- if( ([vStringEscapeCharacter length] == 0) || [[s string] characterAtIndex: ([vScanner scanLocation] -1)] != vEscChar ) // Backslash before the end marker? That means ignore the end marker.
- vIsEndChar = YES; // A real one! Terminate loop.
- if( ![vScanner scanString:endCh intoString:nil] ) // But skip this char before that.
- return;
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
- }
-
- vEndOffs = [vScanner scanLocation];
-
- // Now mess with the string's styles:
- [s setAttributes: vStyles range: NSMakeRange( vStartOffs, vEndOffs -vStartOffs )];
- }
- NS_HANDLER
- // Just ignore it, syntax coloring isn't that important.
- NS_ENDHANDLER
- }
- /* -----------------------------------------------------------------------------
- colorCommentsFrom:
- Colorize block-comments in the text view.
-
- REVISIONS:
- 2004-05-18 witness Documented.
- -------------------------------------------------------------------------- */
- -(void) colorCommentsFrom: (NSString*) startCh to: (NSString*) endCh inString: (NSMutableAttributedString*) s
- withColor: (NSColor*) col andMode:(NSString*)attr
- {
- @try
- {
- NSScanner* vScanner = [NSScanner scannerWithString: [s string]];
- NSDictionary* vStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- col, NSForegroundColorAttributeName,
- attr, TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
- BOOL vDelegateHandlesProgress = [delegate respondsToSelector: @selector(textViewControllerProgressedWhileSyntaxRecoloring:)];
-
- while( ![vScanner isAtEnd] )
- {
- int vStartOffs,
- vEndOffs;
-
- // Look for start of multi-line comment:
- [vScanner scanUpToString: startCh intoString: nil];
- vStartOffs = [vScanner scanLocation];
- if( ![vScanner scanString:startCh intoString:nil] )
- return;
-
- // Look for associated end-of-comment marker:
- [vScanner scanUpToString: endCh intoString: nil];
- if( ![vScanner scanString: endCh intoString: nil] )
- /*return*/; // Don't exit. If user forgot trailing marker, indicate this by "bleeding" until end of string.
- vEndOffs = [vScanner scanLocation];
-
- // Now mess with the string's styles:
- [s setAttributes: vStyles range: NSMakeRange( vStartOffs, vEndOffs -vStartOffs )];
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
- }
- }
- @catch( ... )
- {
- // Just ignore it, syntax coloring isn't that important.
- }
- }
- /* -----------------------------------------------------------------------------
- colorOneLineComment:
- Colorize one-line-comments in the text view.
-
- REVISIONS:
- 2004-05-18 witness Documented.
- -------------------------------------------------------------------------- */
- -(void) colorOneLineComment: (NSString*) startCh inString: (NSMutableAttributedString*) s
- withColor: (NSColor*) col andMode:(NSString*)attr
- {
- @try
- {
- NSScanner* vScanner = [NSScanner scannerWithString: [s string]];
- NSDictionary* vStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- col, NSForegroundColorAttributeName,
- attr, TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
- BOOL vDelegateHandlesProgress = [delegate respondsToSelector: @selector(textViewControllerProgressedWhileSyntaxRecoloring:)];
-
- while( ![vScanner isAtEnd] )
- {
- int vStartOffs,
- vEndOffs;
-
- // Look for start of one-line comment:
- [vScanner scanUpToString: startCh intoString: nil];
- vStartOffs = [vScanner scanLocation];
- if( ![vScanner scanString:startCh intoString:nil] )
- return;
-
- // Look for associated line break:
- if( ![vScanner skipUpToCharactersFromSet:[NSCharacterSet characterSetWithCharactersInString: @"\n\r"]] )
- ;
-
- vEndOffs = [vScanner scanLocation];
-
- // Now mess with the string's styles:
- [s setAttributes: vStyles range: NSMakeRange( vStartOffs, vEndOffs -vStartOffs )];
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
- }
- }
- @catch( ... )
- {
- // Just ignore it, syntax coloring isn't that important.
- }
- }
- /* -----------------------------------------------------------------------------
- colorIdentifier:
- Colorize keywords in the text view.
-
- REVISIONS:
- 2004-05-18 witness Documented.
- -------------------------------------------------------------------------- */
- -(void) colorIdentifier: (NSString*) ident inString: (NSMutableAttributedString*) s
- withColor: (NSColor*) col andMode:(NSString*)attr charset: (NSCharacterSet*)cset
- {
- @try
- {
- NSScanner* vScanner = [NSScanner scannerWithString: [s string]];
- NSDictionary* vStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- col, NSForegroundColorAttributeName,
- attr, TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
- int vStartOffs = 0;
- BOOL vDelegateHandlesProgress = [delegate respondsToSelector: @selector(textViewControllerProgressedWhileSyntaxRecoloring:)];
-
- // Skip any leading whitespace chars, somehow NSScanner doesn't do that:
- if( cset )
- {
- while( vStartOffs < [[s string] length] )
- {
- if( [cset characterIsMember: [[s string] characterAtIndex: vStartOffs]] )
- break;
- vStartOffs++;
- }
- }
-
- [vScanner setScanLocation: vStartOffs];
-
- while( ![vScanner isAtEnd] )
- {
- // Look for start of identifier:
- [vScanner scanUpToString: ident intoString: nil];
- vStartOffs = [vScanner scanLocation];
- if( ![vScanner scanString:ident intoString:nil] )
- return;
-
- if( vStartOffs > 0 ) // Check that we're not in the middle of an identifier:
- {
- // Alphanum character before identifier start?
- if( [cset characterIsMember: [[s string] characterAtIndex: (vStartOffs -1)]] ) // If charset is NIL, this evaluates to NO.
- continue;
- }
-
- if( (vStartOffs +[ident length] +1) < [s length] )
- {
- // Alphanum character following our identifier?
- if( [cset characterIsMember: [[s string] characterAtIndex: (vStartOffs +[ident length])]] ) // If charset is NIL, this evaluates to NO.
- continue;
- }
-
- // Now mess with the string's styles:
- [s setAttributes: vStyles range: NSMakeRange( vStartOffs, [ident length] )];
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
- }
- }
- @catch( ... )
- {
- // Just ignore it, syntax coloring isn't that important.
- }
- }
- /* -----------------------------------------------------------------------------
- colorTagFrom:
- Colorize HTML tags or similar constructs in the text view.
-
- REVISIONS:
- 2004-05-18 witness Documented.
- -------------------------------------------------------------------------- */
- -(void) colorTagFrom: (NSString*) startCh to: (NSString*)endCh inString: (NSMutableAttributedString*) s
- withColor: (NSColor*) col andMode:(NSString*)attr exceptIfMode: (NSString*)ignoreAttr
- {
- @try
- {
- NSScanner* vScanner = [NSScanner scannerWithString: [s string]];
- NSDictionary* vStyles = [NSDictionary dictionaryWithObjectsAndKeys:
- col, NSForegroundColorAttributeName,
- attr, TD_SYNTAX_COLORING_MODE_ATTR,
- nil];
- BOOL vDelegateHandlesProgress = [delegate respondsToSelector: @selector(textViewControllerProgressedWhileSyntaxRecoloring:)];
-
- while( ![vScanner isAtEnd] )
- {
- int vStartOffs,
- vEndOffs;
-
- // Look for start of one-line comment:
- [vScanner scanUpToString: startCh intoString: nil];
- vStartOffs = [vScanner scanLocation];
- if( vStartOffs >= [s length] )
- return;
- NSString* scMode = [[s attributesAtIndex:vStartOffs effectiveRange: nil] objectForKey: TD_SYNTAX_COLORING_MODE_ATTR];
- if( ![vScanner scanString:startCh intoString:nil] )
- return;
-
- // If start lies in range of ignored style, don't colorize it:
- if( ignoreAttr != nil && [scMode isEqualToString: ignoreAttr] )
- continue;
-
- // Look for matching end marker:
- while( ![vScanner isAtEnd] )
- {
- // Scan up to the next occurence of the terminating sequence:
- (BOOL) [vScanner scanUpToString: endCh intoString:nil];
-
- // Now, if the mode of the end marker is not the mode we were told to ignore,
- // we're finished now and we can exit the inner loop:
- vEndOffs = [vScanner scanLocation];
- if( vEndOffs < [s length] )
- {
- scMode = [[s attributesAtIndex:vEndOffs effectiveRange: nil] objectForKey: TD_SYNTAX_COLORING_MODE_ATTR];
- [vScanner scanString: endCh intoString: nil]; // Also skip the terminating sequence.
- if( ignoreAttr == nil || ![scMode isEqualToString: ignoreAttr] )
- break;
- }
-
- // Otherwise we keep going, look for the next occurence of endCh and hope it isn't in that style.
- }
-
- vEndOffs = [vScanner scanLocation];
-
- if( vDelegateHandlesProgress )
- [delegate textViewControllerProgressedWhileSyntaxRecoloring: self];
-
- // Now mess with the string's styles:
- [s setAttributes: vStyles range: NSMakeRange( vStartOffs, vEndOffs -vStartOffs )];
- }
- }
- @catch( ... )
- {
- // Just ignore it, syntax coloring isn't that important.
- }
- }
- /* -----------------------------------------------------------------------------
- defaultTextAttributes:
- Return the text attributes to use for the text in our text view.
-
- REVISIONS:
- 2004-05-18 witness Documented.
- -------------------------------------------------------------------------- */
- -(NSDictionary*) defaultTextAttributes
- {
- return [NSDictionary dictionaryWithObjectsAndKeys: [NSFont userFixedPitchFontOfSize: 10.0], NSFontAttributeName, nil];
- }
- @end