The R Project SVN R-packages

Rev

Rev 5676 | Rev 5707 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

/*
 *  R.app : a Cocoa front end to: "R A Computer Language for Statistical Data Analysis"
 *  
 *  R.app Copyright notes:
 *                     Copyright (C) 2004-5  The R Foundation
 *                     written by Stefano M. Iacus and Simon Urbanek
 *
 *                  
 *  R Copyright notes:
 *                     Copyright (C) 1995-1996   Robert Gentleman and Ross Ihaka
 *                     Copyright (C) 1998-2001   The R Development Core Team
 *                     Copyright (C) 2002-2004   The R Foundation
 *
 *  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.
 *
 *  A copy of the GNU General Public License is available via WWW at
 *  http://www.gnu.org/copyleft/gpl.html.  You can also obtain it by
 *  writing to the Free Software Foundation, Inc., 59 Temple Place,
 *  Suite 330, Boston, MA  02111-1307  USA.
 *
 *  Created by Simon Urbanek on 1/11/05.
 */

#import "RGUI.h"
#import "RDocumentWinCtrl.h"
#import "PreferenceKeys.h"
#import "RController.h"
#import "RDocumentController.h"
#import "REngine/REngine.h"
#import "Tools/FileCompletion.h"
#import "Tools/CodeCompletion.h"
#import "RegexKitLite.h"

BOOL defaultsInitialized = NO;

NSColor *shColorNormal;
NSColor *shColorString;
NSColor *shColorNumber;
NSColor *shColorKeyword;
NSColor *shColorComment;
NSColor *shColorIdentifier;

// note: those must match tags in the NIB!
#define hsTypeExact   1
#define hsTypeApprox  2


@implementation RDocumentWinCtrl

+ (void) setDefaultSyntaxHighlightingColors
{
    NSColor *c=[Preferences unarchivedObjectForKey:normalSyntaxColorKey withDefault:nil];
    if (c) shColorNormal = c;
    else shColorNormal=[NSColor colorWithDeviceRed:0.025 green:0.085 blue:0.600 alpha:1.0];
    [shColorNormal retain];
    c=[Preferences unarchivedObjectForKey:stringSyntaxColorKey withDefault:nil];
    if (c) shColorString = c;
    else shColorString=[NSColor colorWithDeviceRed:0.690 green:0.075 blue:0.000 alpha:1.0];
    [shColorString retain]; 
    c=[Preferences unarchivedObjectForKey:numberSyntaxColorKey withDefault:nil];
    if (c) shColorNumber = c;
    else shColorNumber=[NSColor colorWithDeviceRed:0.020 green:0.320 blue:0.095 alpha:1.0];
    [shColorNumber retain];
    c=[Preferences unarchivedObjectForKey:keywordSyntaxColorKey withDefault:nil];
    if (c) shColorKeyword = c;
    else shColorKeyword=[NSColor colorWithDeviceRed:0.765 green:0.535 blue:0.035 alpha:1.0];
    [shColorKeyword retain];
    c=[Preferences unarchivedObjectForKey:commentSyntaxColorKey withDefault:nil];
    if (c) shColorComment = c;
    else shColorComment=[NSColor colorWithDeviceRed:0.312 green:0.309 blue:0.309 alpha:1.0];
    [shColorComment retain];
    c=[Preferences unarchivedObjectForKey:identifierSyntaxColorKey withDefault:nil];
    if (c) shColorIdentifier = c;
    else shColorIdentifier=[NSColor colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:1.0];
    [shColorIdentifier retain]; 
    //  NSLog(@"shColorIdentifier %f %f %f %f", [c redComponent], [c greenComponent], [c blueComponent], [c alphaComponent]);

}

//- (id)init { // NOTE: init is *not* used! put any initialization in windowDidLoad

static RDocumentWinCtrl *staticCodedRWC = nil;

// FIXME: this is a very, very ugly hack to work around a bug in Cocoa: 
// "Customize Toolbar.." creates a copy of the custom views in the tollbar and
// one of it is the help search view (defined in the RDocument NIB). It turns
// out that a copy is made by encoding and decoding it. However, due to some
// strange bug in Cocoa this leads to instantiation of RDocumentWinCtrl via initWithCoder:
// which is then released immediately. This leads to a crash, so we work
// around this by retaining that copy thus making sure it won't be released.
// In order to reduce the memory overhead we keep around only one instance
// of this "special" controller and keep returning it.
- (id)initWithCoder: (NSCoder*) coder {
    SLog(@"RDocumentWinCtrl.initWithCoder<%@>: %@ **** this is due to a bug in Cocoa! Working around it:", self, coder);
    if (!staticCodedRWC) {
        staticCodedRWC = [super initWithCoder:coder];
        SLog(@" - creating static answer: %@", staticCodedRWC);
    } else {
        SLog(@" - release original, return static answer %@", staticCodedRWC);
        [self release];
        self = staticCodedRWC;
    }
    return [self retain]; // add a retain because it will be matched by the caller
}

- (void)dealloc {
    SLog(@"RDocumentWinCtrl.dealloc<%@>", self);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [[Preferences sharedPreferences] removeDependent:self];
    if (helpTempFile) [[NSFileManager defaultManager] removeFileAtPath:helpTempFile handler:nil];
    if (functionMenuInvalidAttribute) [functionMenuInvalidAttribute release];
    if (functionMenuCommentAttribute) [functionMenuCommentAttribute release];
    [super dealloc];
}

- (void) replaceContentsWithRtf: (NSData*) rtfContents
{
    [textView replaceCharactersInRange:
        NSMakeRange(0, [[textView textStorage] length])
                               withRTF:rtfContents];
    [textView setSelectedRange:NSMakeRange(0,0)];
}

- (void) replaceContentsWithString: (NSString*) strContents
{
    [[textView layoutManager] setAllowsNonContiguousLayout:YES];
    [textView setString:strContents];
    [textView setSelectedRange:NSMakeRange(0,0)];
    [textView performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.0];
    [[textView layoutManager] setAllowsNonContiguousLayout:NO];
}

- (NSData*) contentsAsRtf
{
    return [textView RTFFromRange:
        NSMakeRange(0, [[textView string] length])];
}

- (NSString*) contentsAsString
{
    return [textView string];
}   

- (NSTextView *) textView {
    return textView;
}

- (void) setPlain: (BOOL) plain
{
    plainFile=plain;
    if (plain && useHighlighting && textView)
        [textView setTextColor:[NSColor blackColor] range:NSMakeRange(0,[[textView textStorage] length])];
    else if (!plain && useHighlighting && textView)
        [textView performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.0];
}

- (BOOL) plain
{
    return plainFile;
}

// fileEncoding is passed through to the document - bound by the save box
- (int) fileEncoding
{
    SLog(@"%@ fileEncoding (%@ gives %d)", self, [self document], [[self document] fileEncoding]);
    return [[self document] fileEncoding];
}

- (void) setFileEncoding: (int) encoding
{
    SLog(@"%@ setFileEncoding: %d (doc %@)", self, encoding, [self document]);
    [[self document] setFileEncoding:encoding];
}

- (id) initWithWindowNibName:(NSString*) nib
{
    self = [super initWithWindowNibName:nib];
    SLog(@"RDocumentWinCtrl<%@>.initWithNibName:%@", self, nib);
    if (self) {
        plainFile=NO;
        hsType=1;
        currentHighlight=-1;
        updating=NO;
        helpTempFile=nil;
        execNewlineFlag=NO;

        functionMenuInvalidAttribute = [[NSDictionary dictionaryWithObjectsAndKeys:
            [NSColor redColor], NSForegroundColorAttributeName,
            [NSFont menuFontOfSize:0], NSFontAttributeName,
        nil] retain];
        functionMenuCommentAttribute =[[NSDictionary dictionaryWithObjectsAndKeys:
            [NSColor grayColor], NSForegroundColorAttributeName,
            [NSFont menuFontOfSize:0], NSFontAttributeName,
        nil] retain];

        [self setShouldCloseDocument:YES];
    }
    return self;
}

// we don't need this one, because the default implementation automatically calls the one w/o owner
// - (id) initWithWindowNibName:(NSString*) nib owner: (id) owner

- (void) windowDidLoad
{

    SLog(@"RDocumentWinCtrl(%@).windowDidLoad", self);

    showMatchingBraces = [Preferences flagForKey:showBraceHighlightingKey withDefault: YES];
    argsHints = [Preferences flagForKey:prefShowArgsHints withDefault:YES];

    SLog(@" - load document contents into textView");
    [(RDocument*)[self document] loadInitialContents];

    SLog(@" - scan document for functions");
    [self functionRescan];

    [[textView undoManager] removeAllActions];

    [super windowDidLoad];

    SLog(@" - windowDidLoad is done");

    return;

}

- (NSView*) saveOpenAccView
{
    return saveOpenAccView;
}

- (NSUndoManager*) windowWillReturnUndoManager: (NSWindow*) sender
{
    return [[self document] undoManager];
}

- (void) setStatusLineText: (NSString*) text
{
    [statusLine setStringValue:text?text:@""];
}

- (BOOL) hintForFunction: (NSString*) fn
{

    BOOL success = NO;

    if([[self document] hasREditFlag]) {
        [self setStatusLineText:NLS(@"(arguments lookup is disabled while R is busy)")];
        return NO;
    }

    if (preventReentrance && insideR>0) {
        [self setStatusLineText:NLS(@"(arguments lookup is disabled while R is busy)")];
        return NO;
    }
    if (![[REngine mainEngine] beginProtected]) {
        [self setStatusLineText:NLS(@"(arguments lookup is disabled while R is busy)")];
        return NO;      
    }
    RSEXP *x = [[REngine mainEngine] evaluateString:[NSString stringWithFormat:@"try(gsub('\\\\s+',' ',paste(capture.output(print(args(%@))),collapse='')),silent=TRUE)", fn]];
    if (x) {
        NSString *res = [x string];
        if (res && [res length]>10 && [res hasPrefix:@"function"]) {
            if ([res hasSuffix:@" NULL"]) res=[res substringToIndex:[res length]-5];
            res = [fn stringByAppendingString:[res substringFromIndex:9]];
            success = YES;
            [self setStatusLineText:res];
        }
        [x release];
    }
    [[REngine mainEngine] endProtected];
    return success;
}

- (NSString*) statusLineText
{
    return [statusLine stringValue];
}

- (void) functionReset
{
    SLog(@"RDocumentWinCtrl.functionReset");
    if (fnListBox) {
        NSMenuItem *fmi = [[NSMenuItem alloc] initWithTitle:NLS(@"<functions>") action:nil keyEquivalent:@""];
        [fmi setTag:-1];
        [fnListBox removeAllItems];
        [[fnListBox menu] addItem:fmi];
        [fmi release];
    }
    SLog(@" - reset done");
}

- (void) functionAdd: (NSString*) fn atPosition: (int) pos
{
    if (fnListBox) {
        if ([[[fnListBox menu] itemAtIndex:0] tag]==-1)
            [fnListBox removeAllItems];
        NSMenuItem *mi = [fnListBox itemWithTitle:fn];
        if (!mi) {
            mi = [[NSMenuItem alloc] initWithTitle:fn action:@selector(goFunction:) keyEquivalent:@""];
            [mi setTag: pos];
            [[fnListBox menu] addItem:mi];
        } else {
            [mi setTag:pos];
        }
    }
}

- (void) functionGo: (id) sender
{
    NSString *s = [[textView textStorage] string];
    NSMenuItem *mi = (NSMenuItem*) sender;
    int pos = [mi tag];
    if (pos>=0 && pos<[s length]) {
        NSRange fr = NSMakeRange(pos,0);
        [textView setSelectedRange:fr];
        [textView scrollRangeToVisible:fr];
    }
}

- (void) functionRescan
{

    // Cancel pending functionRescan calls
    [NSObject cancelPreviousPerformRequestsWithTarget:self 
                            selector:@selector(functionRescan) 
                            object:nil];

    NSTextStorage *ts = [textView textStorage];
    NSString *s = [ts string];
    unsigned long strLength = [s length];
    int oix = 0;
    int pim = 0;
    int sit = 0;
    int fnf = 0;
    NSMenu *fnm = [fnListBox menu];
    NSRange sr = [textView selectedRange];
    [self functionReset];

    if([s length]<8) return;

    SLog(@"RDoumentWinCtrl.functionRescan");
    while (1) {
        NSRange r = [s rangeOfRegex:@"\\bfunction\\s*\\(" inRange:NSMakeRange(oix,strLength-oix)];
        if (r.length<8) break;
        oix=NSMaxRange(r);
        int li = r.location-1;
        SLog(@" - potential function at %d", li);
        unichar fc;
        while (li>0 && ((fc=CFStringGetCharacterAtIndex((CFStringRef)s, li)) ==' ' || fc=='\t' || fc=='\r' || fc=='\n')) li--;
        if (li>0) {
            fc=CFStringGetCharacterAtIndex((CFStringRef)s, li);
            if (fc=='=' || (fc=='-' && CFStringGetCharacterAtIndex((CFStringRef)s, --li)=='<')) {
                int lci;
                li--;
                SLog(@" - matched =/<- at %d", li);
                while (li>0 && ((fc=CFStringGetCharacterAtIndex((CFStringRef)s, li)) ==' ' || fc=='\t' || fc=='\r' || fc=='\n')) li--;
                lci=li;
                // while (li>=0 && (((fc=CFStringGetCharacterAtIndex((CFStringRef)s, li))>='0' && fc<='9')||(fc>='a' && fc<='z')||(fc>='A' && fc<='Z')|| //(fc>=0x00C && fc<=0xFF9F)||
                //               fc=='.'||fc=='_')) li--;
                while (li>=0 && ((fc=CFStringGetCharacterAtIndex((CFStringRef)s, li)) !='\n' && fc!=' ' && fc!='\r' && fc!='\t' && fc!=';' && fc!='#')) li--;
                if (lci!=li) {

                    NSString *fn = [s substringWithRange:NSMakeRange(li+1,lci-li)];

                    int type = 1; // invalid function name
                    if([textView parserContextForPosition:li+2] == 4)
                        type = 2; // function declaration is commented out
                    else if([fn isMatchedByRegex:@"^[[:alpha:]\\.]"])
                        type = 0; // function name is valid

                    int fp = li+1;

                    NSMenuItem *mi = nil;
                    SLog(@" - found identifier %d:%d \"%@\"", li+1, lci-li, fn);
                    fnf++;
                    if (fp<=sr.location) sit=pim;
                    mi = [[NSMenuItem alloc] initWithTitle:fn action:@selector(functionGo:) keyEquivalent:@""];
                    if(type == 1) {
                        NSAttributedString *fna = [[NSAttributedString alloc] initWithString:fn attributes:functionMenuInvalidAttribute];
                        [mi setAttributedTitle:fna];
                        [fna release];
                    }
                    else if(type == 2) {
                        NSAttributedString *fna = [[NSAttributedString alloc] initWithString:fn attributes:functionMenuCommentAttribute];
                        [mi setAttributedTitle:fna];
                        [fna release];
                    }
                    [mi setTag:fp];
                    [mi setTarget:self];
                    [fnm addItem:mi];
                    [mi release];
                    pim++;
                }
            }
        }
    }

    if (fnf) {
        [fnListBox removeItemAtIndex:0];
        [fnListBox selectItemAtIndex:sit];
    }

    SLog(@" - rescan finished (%d functions)", fnf);
}

- (void) updatePreferences {
    SLog(@"RDocumentWinCtrl.updatePreferences");
    // for sanity's sake
    if (!defaultsInitialized) {
        [RDocumentWinCtrl setDefaultSyntaxHighlightingColors];
        defaultsInitialized=YES;
    }
    
    NSColor *c = [Preferences unarchivedObjectForKey: backgColorKey withDefault: nil];
    if (c && c != [[self window] backgroundColor]) {
        [[self window] setBackgroundColor:c];
        //      [[self window] display];
    }
    c=[Preferences unarchivedObjectForKey:normalSyntaxColorKey withDefault:nil];
    if (c) { [shColorNormal release]; shColorNormal = [c retain]; [textView setInsertionPointColor:c]; }
    c=[Preferences unarchivedObjectForKey:stringSyntaxColorKey withDefault:nil];
    if (c) { [shColorString release]; shColorString = [c retain]; }
    c=[Preferences unarchivedObjectForKey:numberSyntaxColorKey withDefault:nil];
    if (c) { [shColorNumber release]; shColorNumber = [c retain]; }
    c=[Preferences unarchivedObjectForKey:keywordSyntaxColorKey withDefault:nil];
    if (c) { [shColorKeyword release]; shColorKeyword = [c retain]; }
    c=[Preferences unarchivedObjectForKey:commentSyntaxColorKey withDefault:nil];
    if (c) { [shColorComment release]; shColorComment = [c retain]; }
    c=[Preferences unarchivedObjectForKey:identifierSyntaxColorKey withDefault:nil];
    if (c) { [shColorIdentifier release]; shColorIdentifier = [c retain]; }

    argsHints=[Preferences flagForKey:prefShowArgsHints withDefault:YES];

    [self setHighlighting:[Preferences flagForKey:showSyntaxColoringKey withDefault: YES]];
    showMatchingBraces = [Preferences flagForKey:showBraceHighlightingKey withDefault: YES];
    [textView setNeedsDisplay:YES];
    SLog(@" - preferences updated");
}

- (IBAction)saveDocumentAs:(id)sender
{

    RDocument *cd = [[RDocumentController sharedDocumentController] currentDocument];

    // if cd document is a REdit call do not allow to save it under another name
    // to preserving REdit editing
    if (cd && [cd hasREditFlag]) {
        [cd saveDocument:sender];
        return;
    }
    [cd saveDocumentAs:sender];
}

- (IBAction)saveDocument:(id)sender
{

    RDocument *cd = [[RDocumentController sharedDocumentController] currentDocument];

    // if cd document is a REdit call ensure that the last character is a line ending
    // to avoid error in edit()
    if (cd && [cd hasREditFlag]) {
        NSRange selectedRange = [textView selectedRange];
        if(![[textView string] length])
            [[[textView textStorage] mutableString] setString:@"\n"];
        if([[textView string] characterAtIndex:[[textView string] length]-1] != '\n') {
            [[[textView textStorage] mutableString] appendString:@"\n"];
            [textView setSelectedRange:NSIntersectionRange(selectedRange, NSMakeRange(0, [[textView string] length]))];
        }
    }
    [cd saveDocument:sender];
}

- (IBAction)printDocument:(id)sender
{
    NSPrintInfo *printInfo;
    NSPrintInfo *sharedInfo;
    NSPrintOperation *printOp;
    NSMutableDictionary *printInfoDict;
    NSMutableDictionary *sharedDict;
    
    sharedInfo = [NSPrintInfo sharedPrintInfo];
    sharedDict = [sharedInfo dictionary];
    printInfoDict = [NSMutableDictionary dictionaryWithDictionary:
        sharedDict];
    
    printInfo = [[NSPrintInfo alloc] initWithDictionary: printInfoDict];
    [printInfo setHorizontalPagination: NSFitPagination];
    [printInfo setVerticalPagination: NSAutoPagination];
    [printInfo setVerticallyCentered:NO];
    
    [textView setBackgroundColor:[NSColor whiteColor]];
    printOp = [NSPrintOperation printOperationWithView:textView 
                                             printInfo:printInfo];
    [printOp setShowPanels:YES];

    [printOp runOperationModalForWindow:[self window] 
                               delegate:self 
                         didRunSelector:@selector(sheetDidEnd:returnCode:contextInfo:) 
                            contextInfo:@""];
    [self updatePreferences];
}


- (IBAction)goToLine:(id)sender
{
    [NSApp beginSheet:goToLineSheet
       modalForWindow:[self window]
        modalDelegate:self
       didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
          contextInfo:@"goToLine"];
}

- (IBAction)goToLineCloseSheet:(id)sender
{
    [NSApp endSheet:goToLineSheet returnCode:[sender tag]];
}

- (void) setHighlighting: (BOOL) use
{
    useHighlighting=use;
    if (textView) {
        if (use)
            [textView performSelector:@selector(doSyntaxHighlighting) withObject:nil afterDelay:0.0];
        else
            [textView setTextColor:[NSColor blackColor] range:NSMakeRange(0,[[textView textStorage] length])];
    }
}

- (void)highlightBracesAfterDidProcessEditing
{
    [self highlightBracesWithShift:0 andWarn:YES];
}

- (void) highlightBracesWithShift: (int) shift andWarn: (BOOL) warn
{

    NSString *completeString = [[textView textStorage] string];
    unsigned int completeStringLength = [completeString length];
    if (completeStringLength < 2) return;
    
    NSRange selRange = [textView selectedRange];
    unsigned int cursorLocation = selRange.location;
    cursorLocation+=shift; // add any shift as cursor movement guys need it
    if (cursorLocation<0 || cursorLocation>=completeStringLength) return;
    
    unichar characterToCheck;
    unichar openingChar = 0;
    characterToCheck = [completeString characterAtIndex:cursorLocation];
    int skipMatchingBrace = 0;
    
    [textView resetHighlights];
    if (characterToCheck == ')') openingChar='(';
    else if (characterToCheck == ']') openingChar='[';
    else if (characterToCheck == '}') openingChar='{';
    
    // well, this is rather simple so far, because it ignores cross-quoting, but for a first shot it's not too bad ;)
    if (openingChar) {
        while (cursorLocation--) {
            unichar c = [completeString characterAtIndex:cursorLocation];
            if (c == openingChar) {
                if (!skipMatchingBrace) {
                    [textView highlightCharacter:cursorLocation];
                    return;
                } else
                    skipMatchingBrace--;
            } else if (c == characterToCheck)
                skipMatchingBrace++;
        }
        if (warn) NSBeep();
    } else { // ok, now reverse the roles and find the closing brace (if any)
        unsigned maxLimit=completeStringLength;
        //if (cursorLocation-maxLimit>4000) maxLimit=cursorLocation+4000; // just a soft limit to not search too far (but I think we're fast enough...)
        if (characterToCheck == '(') openingChar=')';
        else if (characterToCheck == '[') openingChar=']';
        else if (characterToCheck == '{') openingChar='}';      
        if (openingChar) {
            while ((++cursorLocation)<maxLimit) {
                unichar c = [completeString characterAtIndex:cursorLocation];
                if (c == openingChar) {
                    if (!skipMatchingBrace) {
                        [textView highlightCharacter:cursorLocation];
                        return;
                    } else
                        skipMatchingBrace--;
                } else if (c == characterToCheck)
                    skipMatchingBrace++;
            }
        }
    }
}

- (BOOL)textView:(NSTextView *)textViewSrc doCommandBySelector:(SEL)commandSelector {
    BOOL retval = NO;
    if (textViewSrc!=textView) return NO;
    //NSLog(@"RTextView commandSelector: %@\n", NSStringFromSelector(commandSelector));
    if (@selector(insertNewline:) == commandSelector && execNewlineFlag) {
        execNewlineFlag=NO;
        return YES;
    }
    if (@selector(insertNewline:) == commandSelector) {
        // handling of indentation
        // currently we just copy what we get and add tabs for additional non-matched { brackets
        NSTextStorage *ts = [textView textStorage];
        NSString *s = [ts string];
        NSRange csr = [textView selectedRange];
        NSRange ssr = NSMakeRange(csr.location, 0);
        NSRange lr = [s lineRangeForRange:ssr];
        if ([[self statusLineText] length]>0) [self setStatusLineText:@""];
        // line on which enter was pressed - this will be taken as guide
        if (csr.location>0) {
            int i=lr.location;
            int last=csr.location;
            int whiteSpaces=0, addShift=0;
            BOOL initial=YES;
            BOOL caretIsAdjacentCurlyBrackets = NO;
            NSString *wss=@"\n";
            NSString *wssForClosingCurlyBracket = @"";
            while (i<last) {
                unichar c=[s characterAtIndex:i];
                if (initial) {
                    if (c=='\t' || c==' ') {
                        whiteSpaces++;
                    }
                    else initial=NO;
                }
                if (c=='{') addShift++;
                if (c=='}' && addShift>0) addShift--;
                i++;
            }
            if (whiteSpaces>0)
                wss = [wss stringByAppendingString:[s substringWithRange:NSMakeRange(lr.location,whiteSpaces)]];
            if([s characterAtIndex:last-1] == '{' && [s characterAtIndex:last] == '}') {
                wssForClosingCurlyBracket = [NSString stringWithString:wss];
                caretIsAdjacentCurlyBrackets = YES;
            }
            while (addShift>0) { wss=[wss stringByAppendingString:@"\t"]; addShift--; }
            // add an undo checkpoint before actually committing the changes
            [textView breakUndoCoalescing];
            [textView insertText:wss];

            // if caret is adjacent by {} add new line with the original indention
            // and place the caret one line up at the line's end
            if(caretIsAdjacentCurlyBrackets) {
                [textView insertText:wssForClosingCurlyBracket];
                [textView doCommandBySelector:@selector(moveUp:)];
                [textView doCommandBySelector:@selector(moveToEndOfLine:)];
            }
            return YES;
        }
        }
    if (showMatchingBraces && ![self plain]) {
        if (commandSelector == @selector(deleteBackward:)) {
            [textView setDeleteBackward:YES];
        }
        if (commandSelector == @selector(moveLeft:))
            [self highlightBracesWithShift: -1 andWarn:NO];
        if(commandSelector == @selector(moveRight:))
            [self highlightBracesWithShift: 0 andWarn:NO];
    }
    return retval;
}

- (NSArray *)textView:(NSTextView *)aTextView completions:(NSArray *)words forPartialWordRange:(NSRange)charRange indexOfSelectedItem:(int *)index 
{

    NSRange sr = [aTextView selectedRange];

    unsigned caretPosition = NSMaxRange(sr);

    SLog(@"completion attempt; cursor at %d, complRange: %d-%d", sr.location, charRange.location, charRange.location+charRange.length);

    *index=0;

    // avoid selecting of token if nothing was found
    [textView setSelectedRange:NSMakeRange(NSMaxRange(sr), 0)];

    // For better current function detection we pass maximal 1000 left from caret
    return [CodeCompletion retrieveSuggestionsForScopeRange:(sr.location > 1000) ? NSMakeRange(caretPosition-1000, 1000) : NSMakeRange(0, caretPosition)
                                                 inTextView:aTextView];

}

- (void)textViewDidChangeSelection:(NSNotification *)aNotification
{

    if(argsHints && ![[self document] hasREditFlag] && ![self plain]) {

        // show functions hints due to current caret position or selection
        SLog(@"RDocumentWinCtrl: textViewDidChangeSelection and calls currentFunctionHint");

        RTextView *tv = [aNotification object];

        // Cancel pending currentFunctionHint calls
        [NSObject cancelPreviousPerformRequestsWithTarget:tv 
                                selector:@selector(currentFunctionHint) 
                                object:nil];

        [tv performSelector:@selector(currentFunctionHint) withObject:nil afterDelay:0.1];

    }
}

- (void)sheetDidEnd:(id)sheet returnCode:(int)returnCode contextInfo:(NSString *)contextInfo
{

    SLog(@"RDocumentWinCtrl: sheetDidEnd: returnCode: %d contextInfo: %@", returnCode, contextInfo);

    // Order out the sheet - could be a NSPanel or NSWindow
    if ([sheet respondsToSelector:@selector(orderOut:)]) {
        [sheet orderOut:nil];
    }
    else if ([sheet respondsToSelector:@selector(window)]) {
        [[sheet window] orderOut:nil];
    }

    // callback for "Go To Line Number"
    if([contextInfo isEqualToString:@"goToLine"]) {
        if(returnCode == 1) {
            NSRange currentLineRange = NSMakeRange(0, 0);
            NSString *s = [[textView textStorage] string];
            int lineCounter = 0;
            int l = [goToLineField intValue];

            while(lineCounter++ < l)
                currentLineRange = [s lineRangeForRange:NSMakeRange(NSMaxRange(currentLineRange), 0)];

            SLog(@" - go to line %d", l);
            // select found line
            [textView setSelectedRange:currentLineRange];
            // scroll to found line
            [textView centerSelectionInVisibleArea:nil];
            // remove selection after 300ms
            [textView performSelector:@selector(moveLeft:) withObject:nil afterDelay:0.3];

        }
    }

    // Make window at which the sheet was attached key window
    [[self window] makeKeyWindow];

}

- (BOOL)windowShouldClose:(id)sender
{

    SLog(@"RDocumentWinCtrl%@.windowShouldClose: (doc=%@, win=%@, self.rc=%d)", self, [self document], [self window], [self retainCount]);

    // Cancel pending calls
    [NSObject cancelPreviousPerformRequestsWithTarget:textView 
                            selector:@selector(currentFunctionHint) 
                            object:nil];

    [NSObject cancelPreviousPerformRequestsWithTarget:textView 
                            selector:@selector(doSyntaxHighlighting) 
                            object:nil];

    [NSObject cancelPreviousPerformRequestsWithTarget:self 
                            selector:@selector(functionRescan) 
                            object:nil];

    return YES;

}

- (void) close
{
    SLog(@"RDocumentWinCtrl<%@>.close", self);  
    [super close];
}

- (void) setEditable: (BOOL) editable
{
    [textView setEditable:editable];
}

- (IBAction)comment: (id)sender
{

    NSRange sr = [textView selectedRange];

    if (sr.length == 0) { // comment out the current line only by inserting a "# " after the indention

        SLog(@"RDocumentWinCtrl: comment current line");
        NSRange lineRange = [[textView string] lineRangeForRange:sr];
        // for empty line simply insert "# "
        if(!lineRange.length) {
            SLog(@" - empty line thus insert # only");
            [textView insertText:@"# "];
            return;
        }
        [textView setSelectedRange:lineRange];

        // set undo break point
        [textView breakUndoCoalescing];
        // insert commented string
        [textView insertText:
            [[[textView string] substringWithRange:lineRange] stringByReplacingOccurrencesOfRegex:@"^(\\s*)(.*)" 
                    withString:[NSString stringWithFormat:@"%@# %@", @"$1", @"$2"]]
            ];
        // restore cursor position
        sr.location+=2;
        [textView setSelectedRange:sr];
        return;
    }

    SLog(@"RDocumentWinCtrl: comment selected block");

    // comment out the selected block by inserting a "# " after the indention for each line;
    // empty lines won't be commented out
    NSMutableString *selectedString = [NSMutableString stringWithCapacity:sr.length];
    [selectedString setString:[[textView string] substringWithRange:sr] ];
    // handle first line separately since it doesn't start with a \n or \r
    NSRange firstLineRange = [selectedString lineRangeForRange:NSMakeRange(0,0)];
    NSString *firstLineString = [[selectedString substringWithRange:firstLineRange] stringByReplacingOccurrencesOfRegex:@"^(\\s*)(.*)" 
        withString:[NSString stringWithFormat:@"%@# %@", @"$1", @"$2"]];
    [selectedString replaceCharactersInRange:firstLineRange withString:firstLineString];
    NSString *commentedString = [selectedString stringByReplacingOccurrencesOfRegex:@"(?m)([\r\n]+)(\\s*)(?=\\S)" 
            withString:[NSString stringWithFormat:@"%@%@# ", @"$1", @"$2"]];
    [textView setSelectedRange:sr];

    // set undo break point
    [textView breakUndoCoalescing];
    // insert commented string
    [textView insertText:commentedString];
    // restore selection
    [textView setSelectedRange:NSMakeRange(sr.location, [commentedString length])];

}

- (IBAction)uncomment: (id)sender
{

    NSRange sr = [textView selectedRange];

    if (sr.length == 0) { // uncomment the current line only

        SLog(@"RDocumentWinCtrl: uncomment current line");
        NSRange lineRange = [[textView string] lineRangeForRange:sr];
        // for empty line does nothing
        if(!lineRange.length) {
            SLog(@" - no line found");
            return;
        }
        
        [textView setSelectedRange:lineRange];
        // set undo break point
        [textView breakUndoCoalescing];
        NSString *uncommentedString = [[[textView string] substringWithRange:lineRange] stringByReplacingOccurrencesOfRegex:
            @"^(\\s*)(# ?)" withString:@"$1"];
        [textView insertText:uncommentedString];
        // restore cursor position
        [textView setSelectedRange:NSMakeRange(sr.location - lineRange.length + [uncommentedString length], 0)];
        return;
    }

    SLog(@"RDocumentWinCtrl: uncomment selected block");

    // uncomment selected block
    NSString *uncommentedString = [[[textView string] substringWithRange:sr] stringByReplacingOccurrencesOfRegex:
        @"(?m)^(\\s*)(# ?)" withString:@"$1"];
    // set undo break point
    [textView breakUndoCoalescing];
    [textView insertText:uncommentedString];
    // restore selection
    [textView setSelectedRange:NSMakeRange(sr.location, [uncommentedString length])];

}

- (IBAction)executeSelection:(id)sender
{
    NSRange sr = [textView selectedRange];
    if (sr.length>0) {
        NSString *stx = [[[textView textStorage] string] substringWithRange:sr];
        [[RController sharedController] sendInput:stx];
    } else { // if nothing is selected, execute the current line
        NSRange lineRange = [[[textView textStorage] string] lineRangeForRange:sr];
        if (lineRange.length < 1)
            NSBeep(); // nothing to execute
        else
            [[RController sharedController] sendInput:
             [[[textView textStorage] string] substringWithRange: lineRange]];
    }
    execNewlineFlag=YES;
}

- (IBAction)sourceCurrentDocument:(id)sender
{
    if ([[self document] isDocumentEdited]) {
        RSEXP *x=[[REngine mainEngine] evaluateString:@"tempfile()"];
        NSString *fn=nil;
        if (x && (fn=[x string])) {
            if ([[self document] writeToFile:fn ofType:@"R"]) {
                [[RController sharedController] sendInput:[NSString stringWithFormat:@"source(\"%@\")\nunlink(\"%@\")", fn, fn]];
            }
        }
    } else {
        NSString *fn=[[self document] fileName];
        if (fn) {
            [[RController sharedController] sendInput:[NSString stringWithFormat:@"source(\"%@\")", fn]];
        }
    }
}


- (IBAction)setHelpSearchType:(id)sender
{
    NSMenuItem *mi = (NSMenuItem*) sender;
    NSMenu *m = [(NSSearchFieldCell*) searchToolbarField searchMenuTemplate];
    int hst = [mi tag];
    if (mi && m && hst!=hsType) {
        SLog(@"setHelpSearchType: old=%d, new=%d", hsType, hst);
        NSMenuItem *cmi = [m itemWithTag:hsType];
        if (cmi) [cmi setState:NSOffState];
        hsType = hst;
        cmi = (NSMenuItem*) [m itemWithTag:hsType];
        if (cmi) [cmi setState:NSOnState];
        // sounds weird, but we have to re-set the tempate to force sf to update the real menu
        [(NSSearchFieldCell*) searchToolbarField setSearchMenuTemplate:m];
    }
}

- (IBAction)goHelpSearch:(id)sender
{
    NSSearchField *sf = (NSSearchField*) sender;
    NSString *ss = [sf stringValue];
    SLog(@"RDocumentWinCtrl<%@>.goHelpSearch: \"%@\", type=%d", self, ss, hsType);
    if ([ss length]<1)
        [helpDrawer close];
    else {
        [helpDrawer open];
        switch (hsType) {
            case hsTypeExact: {
                RSEXP *x = [[REngine mainEngine] evaluateString:[NSString stringWithFormat:@"try(help(\"%@\"),silent=TRUE)", ss]];
                if (x) {
                    NSString *path = [[[x string] copy] autorelease];
                    [x release];
                    if (path) {
#if R_VERSION < R_Version(2, 10, 0)
                        NSString *url = [NSString stringWithFormat:@"file://%@", path];
#else
                        int port = [[RController sharedController] helpServerPort];
                        if (port == 0) {
                            NSRunInformationalAlertPanel(NLS(@"Cannot start HTML help server."), NLS(@"Help"), NLS(@"Ok"), nil, nil);
                            return;
                        }
                        // is will be ..../package/html/topic.html - so extract the package name from there - the internal help relies on the same logic
                        NSArray *pc = [path pathComponents];
                        if ([pc count] < 3) {
                            NSRunInformationalAlertPanel(NLS(@"Cannot determine package for the topic."), NLS(@"Help"), NLS(@"Ok"), nil, nil);
                            return;
                        }
                        NSString *pkg = (NSString*) [pc objectAtIndex:[pc count] - 3];
                        NSString *url = [NSString stringWithFormat:@"http://127.0.0.1:%d/library/%@/html/%@.html", port, pkg, ss];
#endif
                        [[helpWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
                    }
                }
                break;
            }
            case hsTypeApprox: {
                if (!helpTempFile)
                    helpTempFile = [NSString stringWithFormat:@"/tmp/RguiHS%p", self];
                SLog(@" - approx search will use %@", helpTempFile);
#if R_VERSION < R_Version(2, 10, 0)
                RSEXP * x= [[REngine mainEngine] evaluateString:[NSString stringWithFormat:@"try({function(a){sink('%@'); cat(paste('<html><table>',paste('<tr><td><b>',a[,1],'</b> (<a href=\"file://',a[,4],'/html/00Index.html\">',a[,3],'</a></td><td>',a[,2],'</td></tr>',sep='',collapse=''),'</table></html>',sep=''));sink();'OK'}}(help.search(\"%@\")$matches),silent=TRUE)", helpTempFile, ss]];
#else
                int port = [[RController sharedController] helpServerPort];
                if (port == 0) {
                    NSRunInformationalAlertPanel(NLS(@"Cannot start HTML help server."), NLS(@"Help"), NLS(@"Ok"), nil, nil);
                    return;
                }
                RSEXP * x= [[REngine mainEngine] evaluateString:[NSString stringWithFormat:@"try({function(a){sink('%@'); cat(paste('<html><table>',paste('<tr><td><b>',a[,1],'</b> (<a href=\"http://127.0.0.1:',tools:::httpdPort,'/library/',a[,3],'/html/',a[,1],'.html\">',a[,3],'</a>)</td><td>',a[,2],'</td></tr>',sep='',collapse=''),'</table></html>',sep=''));sink();'OK'}}(help.search(\"%@\")$matches),silent=TRUE)", helpTempFile, ss]];          
#endif
                SLog(@" - resulting SEXP=%@", x);
                if (x && [x string] && [[x string] isEqual:@"OK"]) {
                    NSString *url = [NSString stringWithFormat:@"file://%@",helpTempFile];
                    [[helpWebView mainFrame] loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]];
                }
                [x release];
                break;
            }
        }
    }
}

- (NSView*) searchToolbarView
{
    return searchToolbarView;
}

- (NSView*) fnListView
{
    return fnListView;
}

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{

    if ([menuItem action] == @selector(comment:) || [menuItem action] == @selector(uncomment:))  {
        id firstResponder = [[NSApp keyWindow] firstResponder];
        return ([firstResponder respondsToSelector:@selector(isEditable)] && [firstResponder isEditable]);
    }

    return YES;
}
@end