Source

Murky / Source / SourceHighlighting.m

Full commit
//
//  SourceHighlighting.m
//  Murky
//
//  Created by Jens Alfke on 9/2/09.
//  Copyright 2009 Jens Alfke. All rights reserved.
//

#import "SourceHighlighting.h"

static NSDictionary *sAddedStyle, *sDeletedStyle, *sSectionStyle;

static void initStyles() {
    // Fun fact: These styles match the color scheme used for diffs by bitbucket.org.
    if (!sAddedStyle)
        sAddedStyle = $dict({NSBackgroundColorAttributeName, 
            [NSColor colorWithCalibratedRed:0.87f green:1.0f 
                                       blue:0.86f alpha:1.0f]} );
    if (!sDeletedStyle)
        sDeletedStyle = $dict({NSBackgroundColorAttributeName, 
            [NSColor colorWithCalibratedRed:1.0f green:0.86f 
                                       blue:0.87f alpha:1.0f]} );
    if (!sSectionStyle)
        sSectionStyle = $dict({NSBackgroundColorAttributeName, 
            [NSColor colorWithDeviceRed:0.87f green:0.91f 
                                   blue:0.93f alpha:1.0f]} );
}


/** Applies a default source-code text style to the string. */
NSMutableAttributedString* AttributedStringForSourceCode (NSString *code) {
    if (!code) return nil;
    NSString *fontName = [[NSUserDefaults standardUserDefaults] objectForKey: @"CodeFont"];
    if (!fontName)
        fontName = @"Monaco";
    float fontSize = [[NSUserDefaults standardUserDefaults] floatForKey: @"CodeFontSize"];
    if (!fontSize)
        fontSize = 10.0f;
    NSFont *font = [NSFont fontWithName: fontName size: fontSize];
    NSMutableParagraphStyle *paraStyle = [[NSMutableParagraphStyle alloc] init];
    paraStyle.lineBreakMode = NSLineBreakByClipping;
    NSDictionary *attrs = $dict({NSFontAttributeName, font},
                                {NSParagraphStyleAttributeName, paraStyle});
    return [[NSMutableAttributedString alloc] initWithString: code
                                                  attributes: attrs];
}


/** Colorize text in unified-diff format:
    Each line begins with a "+", "-" or "@". */
void HighlightDiffs (NSMutableAttributedString* text) {
    initStyles();
    [text addAttribute: NSForegroundColorAttributeName
                 value: [NSColor colorWithCalibratedRed:0.25f green:0.25f blue:0.25f alpha:1.0f]
                 range: NSMakeRange(0,text.length)];
    
    NSUInteger nextStart, firstLineStart=0;
    for (NSUInteger start = 0,lineNo = 0; start < text.length; start=nextStart,lineNo++) {
        NSUInteger end;
        [text.string getLineStart: &start end: &nextStart contentsEnd: &end 
                         forRange: NSMakeRange(start,1)];
        NSDictionary *attrs;
        switch ([text.string characterAtIndex: start]) {
            case '+':   attrs = sAddedStyle; break;
            case '-':   attrs = sDeletedStyle; break;
            case '@':
                attrs = sSectionStyle; 
                if (firstLineStart == 0)
                    firstLineStart = start;
                break;
            default:
                attrs = nil; break;
        }
        if (attrs)
            [text addAttributes: attrs range: NSMakeRange(start,nextStart-start)];
    }
    [text deleteCharactersInRange: NSMakeRange(0,firstLineStart)];
}


/** Colorize text in "hg annotate" (aka "hg blame") format,
    where each line is prefixed with a revision number and a ":". */
void HighlightAnnotatedFile (NSMutableAttributedString* text,
                             int curRevNo, int maxRevNo,
                             NSArray *revToolTips) {
    CAssert(curRevNo>=0);
    CAssert(curRevNo<=maxRevNo);
    initStyles();
    NSDictionary* revisionStyles[maxRevNo+1];
    memset(&revisionStyles, 0, sizeof(revisionStyles));
    revisionStyles[curRevNo] = sAddedStyle;
    NSString *string = text.string;
    NSUInteger nextStart;
    for (NSUInteger start = 0,lineNo = 0; start < text.length; start=nextStart,lineNo++) {
        // Find the line's character range:
        NSUInteger end;
        [string getLineStart: &start end: &nextStart contentsEnd: &end 
                    forRange: NSMakeRange(start,1)];
        // Get the revision number of this line:
        NSRange colon = [string rangeOfString: @":" options: 0 
                                        range: NSMakeRange(start,string.length-start)];
        CAssert(colon.length==1);
        int revNo = [[string substringWithRange: NSMakeRange(start, colon.location-start)] intValue];
        
        // Make the rev number uniform width:
        int n = 4 - (colon.location-start);
        if (n > 0) {
            NSString *spaces = [@"    " substringToIndex: n];
            [text.mutableString insertString: spaces atIndex: start];
            end += n;
            nextStart += n;
        }
        int lineStart = start + 0;
        
        // Colorize:
        NSDictionary *attrs = revisionStyles[revNo];
        if (!attrs && revNo < curRevNo) {
            // Get darker the older this rev is from the current.
            float white = 1.0f - 0.75f*(curRevNo-revNo)/(float)maxRevNo;
            attrs = $dict({NSBackgroundColorAttributeName,
                            [NSColor colorWithCalibratedWhite: white alpha: 1.0f]},
                          {RevisionNumberAttributeName, $object(revNo)});
            revisionStyles[revNo] = attrs;
        }
        if (attrs) {
            [text addAttributes: attrs range: NSMakeRange(lineStart,nextStart-lineStart)];
            NSString *tooltip = [revToolTips objectAtIndex: revNo];
            [text addAttributes: $dict({NSToolTipAttributeName, tooltip},
                                       {NSLinkAttributeName, $object(revNo)})
                          range: NSMakeRange(lineStart,lineStart+4)];
        }
    }
}