Source

Murky / Source / FileViewer.m

//
//  FileViewer.m
//  Murky
//
//  Created by Jens Alfke on 12/28/09.
//  Copyright 2009 Jens Alfke. All rights reserved.
//

#import "FileViewer.h"
#import "HgFile.h"
#import "HgDir.h"
#import "HgRevision.h"
#import "HgRepository.h"
#import "SourceHighlighting.h"
#import <tgmath.h>


@interface FileViewer ()
@property (readwrite) HgFile *file;
- (void) _showFileContents;
@end



@implementation FileViewer


@synthesize file=_file;


- (id) initWithFile: (HgFile*)file {
    self = [super initWithWindowNibName: @"FileViewer"];
    if (self) {
        _file = file;
        if (file.isDirectory) {
            [self release];
            return nil;
        }
    }
    return self;
}

+ (NSArray*) allFileViewers
{
    NSMutableArray *openRepos = [NSMutableArray array];
    for( NSWindow *window in [NSApp windows]) {
        id delegate = [window delegate];
        if( [window isVisible] && [delegate isKindOfClass: self] ) {
            [openRepos addObject: delegate];
        }
    }
    return openRepos;
}

+ (FileViewer*) existingFileViewerWithFile: (HgFile*)file
{
    NSString* path = file.path;
    HgRepository* repository = file.repository;
    for( FileViewer *fileViewer in [self allFileViewers]) {
        if (fileViewer.revision.repository == repository
               && [fileViewer.file.path isEqualToString: path])
            return fileViewer;
    }
    return nil;
}

+ (FileViewer*) fileViewerWithFile: (HgFile*)file
{
    FileViewer *controller = [self existingFileViewerWithFile: file];
    if (controller)
        controller.file = file;     // set revision to match
    else
        controller = [[self alloc] initWithFile: file];
    return controller;
}


- (void) awakeFromNib {
    HgFile *file = _file;
    [self.window setContentBorderThickness: 22 forEdge: NSMinYEdge];
    [self setTextWraps: NO];
    
    HgRepository *repository = file.repository;
    NSInteger nRevisions = repository.revisions.count;
    _revisionSlider.maxValue = nRevisions - 1;
    
    NSMutableArray *revisions = [file.revisions mutableCopy];
    if (file.status != kClean)
        [revisions addObject: repository.uncommittedRevision];
    [_revisionSlider.cell setRevisions: revisions];
    if (![revisions containsObject: file.revision])
        self.revision = [revisions lastObject];
    else
        self.file = file;
}


- (NSArray*) revTooltips {
    NSArray *revisions = _file.repository.revisions;
    NSUInteger nRevisions = revisions.count;
    if (!_revTooltips || _revTooltips.count != nRevisions) {
        NSMutableArray *tooltips = [NSMutableArray arrayWithCapacity: nRevisions];
        for (HgRevision *revision in revisions) {
            NSString *tooltip = revision.formattedDescription ?: @"";
            [tooltips addObject: tooltip];
        }
        setObj(&_revTooltips, tooltips);
    }
    return _revTooltips;
}


- (void) _showFileContents {
    HgRevision *revision = self.revision;
    NSString *source = nil;
    NSError *error = nil;
    NSMutableAttributedString *text = nil;
    
    FileViewMode mode = _viewMode;
    if (mode==kViewBlame && revision.isUncommitted)
        mode = kViewSource;     // "hg blame" won't show current edits, so disallow it
    
    switch (mode) {
        case kViewSource: {
            NSData *contents = [revision getFileContents: _file error: &error];
            if (contents) {
                source = [[NSString alloc] initWithData: contents encoding: NSUTF8StringEncoding];
            } else {
                source = error.localizedRecoverySuggestion;
            }
            text = AttributedStringForSourceCode(source);
            break;
        }
        case kViewDiff: {
            source = [revision diffFile: _file withRevision: revision.parent error: &error];
            if (!source)
                source = error.localizedRecoverySuggestion;
            text = AttributedStringForSourceCode(source);
            HighlightDiffs(text);
            break;
        }
        case kViewBlame: {
            source = [revision annotateFile: _file error: &error];
            if (!source)
                source = error.localizedRecoverySuggestion;
            text = AttributedStringForSourceCode(source);
            if (!error) {
                NSInteger maxRev = _file.repository.revisions.count-1;
                NSInteger revNo = revision.localNumber;
                if (revNo == NSNotFound)
                    revNo = maxRev;
                HighlightAnnotatedFile(text, revNo, maxRev, self.revTooltips);
            }
            break;
        }
    }

    [_textView setEditable: NO];
    if (text) {
        NSPoint scroll = _textView.visibleRect.origin;
        [_textView.textStorage setAttributedString: text];
        [_textView scrollPoint: scroll];
    } else {
        [_textView setString: @""];
    }
    [_textView setNeedsDisplay: YES];
}


#pragma mark -
#pragma mark ACCESSORS:


- (HgFile*) file {
    return _file;
}

- (void) setFile: (HgFile*)file {
    Assert(file!=nil);
    _file = file;
    HgRevision *revision = _file.revision;
    NSString *name = _file.name;
    if (revision.isUncommitted) {
        _revisionSlider.intValue = (int)_revisionSlider.maxValue;
    } else {
        _revisionSlider.intValue = (int)revision.localNumber;
        name = [name stringByAppendingFormat: @" r%i", revision.localNumber];
    }
    self.window.title = $sprintf(NSLocalizedString(@"%@ in %@",
                                   @"FileViewer window title pattern (filename,repository)"),
                                 name, _file.repository.name, _file.directory.path);
    [self _showFileContents];
}


- (HgRevision*) revision {
    return _file.revision;
}

- (void) setRevision: (HgRevision*)revision {
    HgFile *file = [revision fileAtPath: _file.path];
    if (file && file != _file)
        self.file = file;
}

- (BOOL) skipRevision: (NSInteger)delta {
    NSArray *revisions = [_revisionSlider.cell revisions];
    NSInteger index = [revisions indexOfObject: self.revision];
    if (index==NSNotFound)
        return NO;
    index += delta;
    if (index<0 || index>=(NSInteger)revisions.count)
        return NO;
    self.revision = [revisions objectAtIndex: index];
    return YES;
}


- (FileViewMode) viewMode {return _viewMode;}

- (void) setViewMode:(FileViewMode)mode {
    if (mode != _viewMode) {
        _viewMode = mode;
        [self _showFileContents];
        [_revisionSlider setContinuous: (mode==kViewSource)];
    }
}


#pragma mark -
#pragma mark ACTIONS:


- (IBAction) sliderChanged: (id)sender {
    NSInteger revNo = _revisionSlider.intValue;
    self.revision = [[_file.repository revisions] objectAtIndex: revNo];
}

- (IBAction) prevRevision: (id)sender {
    if (![self skipRevision: -1])
        NSBeep();
}

- (IBAction) nextRevision: (id)sender {
    if (![self skipRevision: 1])
        NSBeep();
}


- (BOOL) textWraps {
    return [_textView isHorizontallyResizable];
}

- (void) setTextWraps: (BOOL)wraps {
    [_textView setHorizontallyResizable: !wraps];
    NSSize containerSize = _textView.textContainer.containerSize;
    containerSize.width = wraps ?_textView.enclosingScrollView.documentVisibleRect.size.width 
                                :FLT_MAX;
    [[_textView textContainer] setContainerSize: containerSize];
    [[_textView textContainer] setWidthTracksTextView: wraps];
    [[_textView enclosingScrollView] setHasHorizontalScroller: !wraps];
    [_textView setNeedsDisplay: YES];
}


- (BOOL)textView:(NSTextView *)textView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex {
    if ([link isKindOfClass: [NSNumber class]]) {
        // HighlightAnnotatedFile creates links to revision numbers: 
        _revisionSlider.intValue = [link intValue];
        [self sliderChanged:_revisionSlider];
        return YES;
    }
    return NO;
}

@end



#pragma mark -
@implementation RevisionSliderCell

static NSDictionary *sTickAttrs;

+ (void) initialize {
    if (!sTickAttrs)
        sTickAttrs = $dict({NSFontAttributeName,
                            [NSFont systemFontOfSize: 7]});
}    

- (NSArray*) revisions {
    return _revisions;
}

- (void) setRevisions: (NSArray*)revisions {
    _revisions = revisions;
    self.numberOfTickMarks = revisions.count;
}

- (double)tickMarkValueAtIndex:(NSInteger)index {
    NSInteger revNo = [[_revisions objectAtIndex: index] localNumber];
    if (revNo == NSNotFound)
        revNo = (NSInteger) self.maxValue;
    return revNo;
}

- (void)drawWithFrame:(NSRect)cellFrame inView:(NSView *)controlView {
    CGFloat xClear = cellFrame.origin.x - 1;
    NSInteger i = 0;
    for (HgRevision *rev in _revisions) {
        NSRect r = [self rectOfTickMarkAtIndex: i++];
        if (rev.localNumber == self.intValue)
            continue; // skip label that's drawn in knob
        NSPoint org = {cellFrame.origin.x + r.origin.x - 2,
                       cellFrame.origin.y + r.origin.y + 4};
        NSString *label = rev.localString;
        CGFloat width = [label sizeWithAttributes: sTickAttrs].width;
        org.x = org.x + 2.0f - floor(width/2.0f);      // center
        org.x = MIN(org.x, NSMaxX([controlView bounds]) - width);  // pin to right edge
        if (org.x >= xClear) {  // don't let labels overlap
            [label drawAtPoint: org withAttributes: sTickAttrs];
            xClear = org.x + width;
        }
    }
    [super drawWithFrame: cellFrame inView: controlView];
}

- (void)drawKnob:(NSRect)knobRect {
    [super drawKnob: knobRect];
    
    // Don't show the uncommitted revision
    NSInteger revNo = self.intValue;
    if (revNo == self.maxValue) {
        HgRevision *lastRev = _revisions.lastObject;
        if (lastRev.isUncommitted)
            return;
    }
    
    // Else show the revision number in the knob:
    NSString *label = $sprintf(@"%i", self.intValue);
    NSSize size = [label sizeWithAttributes: sTickAttrs];
    NSPoint org = {round(NSMidX(knobRect) - size.width/2.0f),
                   round(NSMidY(knobRect) - size.height/2.0f)};
    [label drawAtPoint: org withAttributes: sTickAttrs];
}

@end