apps-projectcenter/Modules/Editors/ProjectCenter/SyntaxHighlighter.m

829 lines
23 KiB
Objective-C

/*
SyntaxHighlighter.m
Implementation of the SyntaxHighlighter class for the
ProjectManager application.
Copyright (C) 2005 Saso Kiselkov
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., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#import "SyntaxHighlighter.h"
#import <Foundation/NSBundle.h>
#import <Foundation/NSString.h>
#import <Foundation/NSArray.h>
#import <Foundation/NSDictionary.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSDebug.h>
#import <Foundation/NSAutoreleasePool.h>
#import <Foundation/NSCharacterSet.h>
#import <Foundation/NSValue.h>
#import <Foundation/NSNull.h>
#import <Foundation/NSException.h>
#import <Foundation/NSLock.h>
#import <AppKit/NSTextStorage.h>
#import <AppKit/NSAttributedString.h>
#import "PCEditorView.h"
#import "SyntaxDefinition.h"
static NSString * const KeywordsNotFixedAttributeName = @"KNF";
static NSString * const ContextAttributeName = @"C";
static inline BOOL
my_isspace(unichar c)
{
if (c == ' ' || c == '\t' || c == '\f')
{
return YES;
}
else
{
return NO;
}
}
/**
* This function looks ahead and after `startRange' in `string' and
* tries to return the range of a whitespace delimited word at the
* specified range. E.g. string = @"abc def ghi" and startRange = {5, 1},
* then {4, 3} is returned, because the word "def" lies within the range.
* Please note that even when the range points to a whitespace area
* (e.g. string = @"abc def" and startRange = {3, 1}), the lookup
* will occur and not return `not found' (e.g. in the above example it
* would return {0, 7}). When the range is also surrounded by whitespace
* (e.g. @" " and startRange = {1, 1}) the startRange itself is returned.
*/
static NSRange
RangeOfWordInString(NSString * string, NSRange startRange)
{
SEL sel = @selector(characterAtIndex:);
unichar (*characterAtIndex)(id, SEL, unsigned int) =
(unichar (*)(id, SEL, unsigned int)) [string methodForSelector: sel];
NSInteger ahead, after;
NSUInteger length = [string length];
for (ahead = 1; ahead <= (NSInteger) startRange.location; ahead++)
{
if (my_isspace(characterAtIndex(string,
sel,
startRange.location - ahead)))
{
break;
}
}
ahead--;
for (after = 0; (after + NSMaxRange(startRange)) < length; after++)
{
if (my_isspace(characterAtIndex(string,
sel,
(after + NSMaxRange(startRange)))))
{
break;
}
}
{
NSUInteger start = startRange.location - ahead;
NSUInteger length = startRange.length + ahead + after;
if (start > 0)
{
start--;
length++;
}
if (length + 1 < length)
{
length++;
}
return NSMakeRange(start, length);
}
}
@interface SyntaxHighlighter (Private)
- (void) fixUpContextsInRange: (NSRange) r;
- (void) fixUpKeywordsInRange: (NSRange) r;
- (void) lazilyFixUpKeywordsInRange: (NSRange) r;
- (void) assignGraphicalAttributesOfContext: (NSUInteger) context
toRange: (NSRange) r;
- (void) assignGraphicalAttributesOfKeyword: (NSUInteger) keyword
inContext: (NSUInteger) context
toRange: (NSRange) r;
- (NSUInteger) contextBeforeRange: (NSRange) r;
- (NSUInteger) contextAfterRange: (NSRange) r;
- (NSUInteger) contextAtEndOfRange: (NSRange) r;
- (void) beginEditingIfNeeded;
- (void) endEditingIfNeeded;
@end
@implementation SyntaxHighlighter (Private)
/**
* Fixes up the contexts inside the text storage in range `r'. A context
* is recognized by the "Context" attribute which holds the number of
* the context. This method also applies graphical attributes of the
* corresponding contexts to the context ranges.
*/
- (void) fixUpContextsInRange: (NSRange) r
{
TextPattern ** beginnings = [syntax contextBeginnings];
const char * beginningChars = [syntax contextBeginningCharacters];
unsigned numBeginningChars = [syntax numberOfContextBeginningCharacters];
NSUInteger i;
unichar * string;
NSUInteger context;
string = (unichar *) malloc(r.length * sizeof(unichar));
[[textStorage string] getCharacters: string range: r];
i = 0;
context = [self contextBeforeRange: r];
while (i < r.length)
{
// marks the beginning of the currently processed range
unsigned int mark = i;
// default context - look for beginning symbols
if (context == 0)
{
NSUInteger j = 0;
TextPattern * pattern = NULL;
NSRange ctxtRange;
NSInteger l = 0;
TextPattern ** skips = [syntax contextSkipsForContext: 0];
const char * skipChars = [syntax contextSkipCharactersForContext: 0];
unsigned int numSkipChars = [syntax
numberOfContextSkipCharactersForContext: 0];
for (;i < r.length; i++)
{
unichar c = string[i];
// Optimize - look into the skip characters array if the
// character could be the beginning of a skip sequence.
// If not, don't perform skip sequence recognition at all.
if (c < numSkipChars && skipChars[c])
{
for (j = 0; (pattern = skips[j]) != NULL; j++)
{
l = CheckTextPatternPresenceInString(pattern,
string,
r.length,
i);
if (l > 0)
{
break;
}
}
if (l > 0)
{
i += l - 1;
continue;
}
}
// optimize - skip unneeded characters
if (c < numBeginningChars && !beginningChars[c])
{
continue;
}
for (j = 0; (pattern = beginnings[j]) != NULL; j++)
{
l = CheckTextPatternPresenceInString(pattern, string,
r.length, i);
if (l > 0)
{
break;
}
}
if (l > 0)
{
break;
}
}
// non-default contexts begin with number 1, not zero
j++;
ctxtRange = NSMakeRange(r.location + mark, i - mark);
if (ctxtRange.length > 0)
{
// add an attribute telling the context into the text storage
[textStorage addAttribute: ContextAttributeName
value: [NSNumber numberWithInt: 0]
range: ctxtRange];
[self assignGraphicalAttributesOfContext: 0 toRange: ctxtRange];
}
ctxtRange = NSMakeRange(r.location + i, l);
if (ctxtRange.length > 0)
{
[textStorage addAttribute: ContextAttributeName
value: [NSNumber numberWithInt: j]
range: ctxtRange];
[self assignGraphicalAttributesOfContext: j
toRange: ctxtRange];
}
i += l;
// switch to the found context again
context = j;
}
// specific context - look for it's terminator, but skip it's
// exceptions
else
{
NSInteger l = 0;
TextPattern * ending = [syntax contextEndingForContext: context - 1];
NSRange ctxtRange;
TextPattern ** skips = [syntax contextSkipsForContext: context];
const char * skipChars = [syntax contextSkipCharactersForContext:
context];
NSUInteger numSkipChars = [syntax
numberOfContextSkipCharactersForContext: context];
for (;i < r.length; i++)
{
unichar c = string[i];
if (c < numSkipChars && skipChars[c])
{
unsigned int j;
TextPattern * pattern;
for (j = 0; (pattern = skips[j]) != NULL; j++)
{
l = CheckTextPatternPresenceInString(pattern, string,
r.length, i);
if (l > 0)
{
break;
}
}
if (l > 0)
{
i += l - 1;
continue;
}
}
l = CheckTextPatternPresenceInString(ending, string,
r.length, i);
if (l > 0)
{
break;
}
}
ctxtRange = NSMakeRange(r.location + mark, i - mark);
if (ctxtRange.length > 0)
{
// add an attribute telling the context into the
// text storage
[textStorage addAttribute: ContextAttributeName
value: [NSNumber numberWithInteger: context]
range: ctxtRange];
[self assignGraphicalAttributesOfContext: context
toRange: ctxtRange];
}
ctxtRange = NSMakeRange(r.location + i, l);
if (ctxtRange.length > 0)
{
[textStorage addAttribute: ContextAttributeName
value: [NSNumber numberWithInteger: 0]
range: ctxtRange];
[self assignGraphicalAttributesOfContext: context
toRange: ctxtRange];
}
i += l;
// switch to the default context again
context = 0;
}
}
free(string);
}
- (void) fixUpKeywordsInRange: (NSRange) r
{
unichar * string;
NSUInteger i;
string = malloc(r.length * sizeof(unichar));
[[textStorage string] getCharacters: string range: r];
for (i = 0; i < r.length;)
{
NSRange contextRange;
TextPattern ** patterns;
NSInteger context;
context = [[textStorage attribute: ContextAttributeName
atIndex: i + r.location
effectiveRange: &contextRange] integerValue];
contextRange = NSIntersectionRange(r, contextRange);
contextRange.location -= r.location;
patterns = [syntax keywordsInContext: context];
while (i < NSMaxRange(contextRange))
{
unichar c = string[i];
NSUInteger l = 0;
NSUInteger j;
TextPattern * pattern;
// skip whitespace - it can't start a keyword
if (my_isspace(c) || c == '\r' || c == '\n')
{
i++;
continue;
}
for (j = 0; (pattern = patterns[j]) != NULL; j++)
{
l = CheckTextPatternPresenceInString(pattern,
string,
r.length,
i);
if (l > 0)
{
break;
}
}
// found a pattern?
if (pattern != NULL)
{
NSRange keywordRange = NSMakeRange(i + r.location, l);
[self assignGraphicalAttributesOfKeyword: j
inContext: context
toRange: keywordRange];
i += l;
}
else
{
i++;
}
}
}
free(string);
}
- (void) lazilyFixUpKeywordsInRange: (NSRange) r
{
NSUInteger i;
BOOL localDidBeginEditing = NO;
for (i = r.location; i < NSMaxRange(r);)
{
NSRange effectiveRange;
// locate non-fixed areas and fix them up
if ([textStorage attribute: KeywordsNotFixedAttributeName
atIndex: i
longestEffectiveRange: &effectiveRange
inRange: r] != nil)
{
if (localDidBeginEditing == NO)
{
localDidBeginEditing = YES;
[textStorage beginEditing];
}
effectiveRange = NSIntersectionRange(effectiveRange, r);
[self fixUpKeywordsInRange: effectiveRange];
[textStorage removeAttribute: KeywordsNotFixedAttributeName
range: effectiveRange];
i += effectiveRange.length;
}
// skip over fixed areas
else
{
i += effectiveRange.length;
}
}
if (localDidBeginEditing == YES)
{
[textStorage endEditing];
}
}
- (void) assignGraphicalAttributesOfContext: (NSUInteger) ctxt
toRange: (NSRange) r
{
BOOL bold, italic;
NSColor * color;
color = [syntax foregroundColorForContext: ctxt];
if (color != nil)
{
[textStorage addAttribute: NSForegroundColorAttributeName
value: color
range: r];
}
else
{
[textStorage removeAttribute: NSForegroundColorAttributeName range: r];
}
color = [syntax backgroundColorForContext: ctxt];
if (color != nil)
{
[textStorage addAttribute: NSBackgroundColorAttributeName
value: color
range: r];
}
else
{
[textStorage removeAttribute: NSBackgroundColorAttributeName range: r];
}
bold = [syntax isBoldFontForContext: ctxt];
italic = [syntax isItalicFontForContext: ctxt];
if (bold && italic)
{
[textStorage addAttribute: NSFontAttributeName
value: boldItalicFont
range: r];
}
else if (bold)
{
[textStorage addAttribute: NSFontAttributeName
value: boldFont
range: r];
}
else if (italic)
{
[textStorage addAttribute: NSFontAttributeName
value: italicFont
range: r];
}
else
{
[textStorage addAttribute: NSFontAttributeName
value: normalFont
range: r];
}
}
- (void) assignGraphicalAttributesOfKeyword: (NSUInteger) keyword
inContext: (NSUInteger) context
toRange: (NSRange) r
{
BOOL bold, italic;
NSColor * color;
color = [syntax foregroundColorForKeyword: keyword inContext: context];
if (color != nil)
{
[textStorage addAttribute: NSForegroundColorAttributeName
value: color
range: r];
}
else
{
color = [syntax foregroundColorForContext: context];
if (color != nil)
{
[textStorage addAttribute: NSForegroundColorAttributeName
value: color
range: r];
}
else
{
[textStorage removeAttribute: NSForegroundColorAttributeName
range: r];
}
}
color = [syntax backgroundColorForKeyword: keyword inContext: context];
if (color != nil)
{
[textStorage addAttribute: NSBackgroundColorAttributeName
value: color
range: r];
}
else
{
color = [syntax backgroundColorForContext: context];
if (color != nil)
{
[textStorage addAttribute: NSBackgroundColorAttributeName
value: color
range: r];
}
else
{
[textStorage removeAttribute: NSBackgroundColorAttributeName
range: r];
}
}
bold = [syntax isBoldFontForKeyword: keyword inContext: context];
italic = [syntax isItalicFontForKeyword: keyword inContext: context];
if (bold && italic)
{
[textStorage addAttribute: NSFontAttributeName
value: boldItalicFont
range: r];
}
else if (bold)
{
[textStorage addAttribute: NSFontAttributeName
value: boldFont
range: r];
}
else if (italic)
{
[textStorage addAttribute: NSFontAttributeName
value: italicFont
range: r];
}
else
{
[textStorage addAttribute: NSFontAttributeName
value: normalFont
range: r];
}
}
- (NSUInteger) contextBeforeRange: (NSRange) r
{
NSRange tmp;
if (r.location == 0)
{
return 0;
}
else
{
return [[textStorage attribute: ContextAttributeName
atIndex: r.location - 1
effectiveRange: &tmp] intValue];
}
}
- (NSUInteger) contextAfterRange: (NSRange) r
{
NSRange tmp;
NSUInteger i;
NSUInteger length;
i = NSMaxRange(r);
length = [textStorage length];
if (length == 0)
{
return 0;
}
else if (i < length)
{
return [[textStorage attribute: ContextAttributeName
atIndex: i
effectiveRange: &tmp] intValue];
}
else
{
return 0;
}
}
- (NSUInteger) contextAtEndOfRange: (NSRange) r
{
NSRange tmp;
NSInteger i = (int) NSMaxRange(r) - 1;
if (i < 0)
{
return 0;
}
else
{
return [[textStorage attribute: ContextAttributeName
atIndex: i
effectiveRange: &tmp] integerValue];
}
}
- (void) beginEditingIfNeeded
{
if (didBeginEditing == NO)
{
didBeginEditing = YES;
[textStorage beginEditing];
}
}
- (void) endEditingIfNeeded
{
if (didBeginEditing == YES)
{
didBeginEditing = NO;
[textStorage endEditing];
}
}
@end
@implementation SyntaxHighlighter
- initWithFileType:(NSString *)fileType
textStorage:(NSTextStorage *)aStorage
{
if ([self init])
{
NSRange r;
ASSIGN(textStorage, aStorage);
ASSIGN(syntax, [SyntaxDefinition
syntaxDefinitionForFileType:fileType textStorage:textStorage]);
// no syntax definition - no highlighting possible
if (syntax == nil)
{
[self release];
return nil;
}
// mark all of the text storage as requiring keyword fixing
r = NSMakeRange(0, [textStorage length]);
[textStorage addAttribute: KeywordsNotFixedAttributeName
value: [NSNull null]
range: r];
[[NSNotificationCenter defaultCenter]
addObserver: self
selector: @selector(textStorageWillProcessEditing:)
name: NSTextStorageWillProcessEditingNotification
object: textStorage];
ASSIGN(normalFont, [PCEditorView defaultEditorFont]);
ASSIGN(boldFont, [PCEditorView defaultEditorBoldFont]);
ASSIGN(italicFont, [PCEditorView defaultEditorItalicFont]);
ASSIGN(boldItalicFont, [PCEditorView defaultEditorBoldItalicFont]);
return self;
}
else
{
return nil;
}
}
- (void)setNormalFont:(NSFont *)f
{
ASSIGN(normalFont, f);
}
- (void)setBoldFont:(NSFont *)f
{
ASSIGN(boldFont, f);
}
- (void)setItalicFont:(NSFont *)f
{
ASSIGN(italicFont, f);
}
- (void)setBoldItalicFont:(NSFont *)f
{
ASSIGN(boldItalicFont, f);
}
- (void) dealloc
{
NSDebugLLog(@"SyntaxHighlighter", @"SyntaxHighlighter: dealloc");
[[NSNotificationCenter defaultCenter] removeObserver: self];
TEST_RELEASE(textStorage);
TEST_RELEASE(syntax);
TEST_RELEASE(normalFont);
TEST_RELEASE(boldFont);
TEST_RELEASE(italicFont);
TEST_RELEASE(boldItalicFont);
[super dealloc];
}
- (void) highlightRange: (NSRange) r
{
if (delayedProcessedRange.length > 0)
{
[self beginEditingIfNeeded];
[self fixUpContextsInRange: delayedProcessedRange];
[self fixUpKeywordsInRange: delayedProcessedRange];
if ([self contextAtEndOfRange: delayedProcessedRange] !=
[self contextAfterRange: delayedProcessedRange])
{
NSRange invalidatedRange;
lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);
invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
[textStorage length] - NSMaxRange(delayedProcessedRange));
[textStorage addAttribute: KeywordsNotFixedAttributeName
value: [NSNull null]
range: invalidatedRange];
}
}
else
{
if (delayedProcessedRange.location > 0 &&
[self contextBeforeRange: delayedProcessedRange] !=
[self contextAfterRange: delayedProcessedRange])
{
NSRange invalidatedRange;
lastProcessedContextIndex = NSMaxRange(delayedProcessedRange);
[self beginEditingIfNeeded];
invalidatedRange = NSMakeRange(NSMaxRange(delayedProcessedRange),
[textStorage length] - NSMaxRange(delayedProcessedRange));
[textStorage addAttribute: KeywordsNotFixedAttributeName
value: [NSNull null]
range: invalidatedRange];
}
}
delayedProcessedRange = NSMakeRange(0, 0);
r = RangeOfWordInString([textStorage string], r);
// need to fixup contexts?
if (NSMaxRange(r) > lastProcessedContextIndex)
{
NSRange fixupRange;
fixupRange = NSMakeRange(lastProcessedContextIndex,
NSMaxRange(r) - lastProcessedContextIndex);
[self beginEditingIfNeeded];
[self fixUpContextsInRange: fixupRange];
lastProcessedContextIndex = NSMaxRange(r);
}
[self lazilyFixUpKeywordsInRange: r];
[self endEditingIfNeeded];
}
- (void) textStorageWillProcessEditing: (NSNotification *) notif
{
if ([textStorage editedMask] & NSTextStorageEditedCharacters)
{
NSRange editedRange = [textStorage editedRange];
delayedProcessedRange = RangeOfWordInString([textStorage string],
editedRange);
if (lastProcessedContextIndex > editedRange.location)
{
lastProcessedContextIndex += [textStorage changeInLength];
}
}
}
@end