Rev 4826 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed
//// RConsoleWinCtrl.m// R//// Created by Simon Urbanek on 10/13/07.// Copyright 2007 __MyCompanyName__. All rights reserved.//#import "RGUI.h"#import "RConsoleWinCtrl.h"#import "Preferences.h"#import "RDocument.h"// size of the console output cache buffer#define DEFAULT_WRITE_BUFFER_SIZE 32768// high water-mark of the buffer - it's [length - x] where x is the smallest possible size to be flushed before a new string will be split.#define writeBufferHighWaterMark (DEFAULT_WRITE_BUFFER_SIZE-4096)// low water-mark of the buffer - if less than the water mark is available then the buffer will be flushed#define writeBufferLowWaterMark 2048// FIXME: those colors should become configurable#define outputColor [NSColor colorWithCalibratedRed: 0.0 green: 0.0 blue: 0.5 alpha: 1.0]#define promptColor [NSColor colorWithCalibratedRed: 0.5 green: 0.0 blue: 0.5 alpha: 1.0]#define inputColor [NSColor colorWithCalibratedRed: 0.0 green: 0.0 blue: 0.0 alpha: 1.0]@implementation RConsoleWinCtrl#pragma mark --- Initialization ---- (id)initWithWindowNibName:(NSString *)windowNibName{self = [super initWithWindowNibName:windowNibName];if (self) {outputPosition = promptPosition = committedLength = 0;consoleInputQueue = [[NSMutableArray alloc] initWithCapacity:8];currentConsoleInput = nil;writeBufferLen = DEFAULT_WRITE_BUFFER_SIZE;writeBufferPos = writeBuffer = (char*) malloc(writeBufferLen);readConsTransBufferSize = 1024; // initial size - will grow as neededreadConsTransBuffer = (char*) malloc(readConsTransBufferSize);hist = [[History alloc] init];currentFontSize = [Preferences floatForKey: kConsoleFontSize withDefault: 11.0];textFont = [[NSFont userFixedPitchFontOfSize:currentFontSize] retain];}return self;}/* initializes R engine used by this console instance, sets engine cv accordingly */- (void) initEngine{SLog(@" - init R_LIBS");{NSString *prefStr = [Preferences stringForKey:kAddUserLibPath withDefault:nil];BOOL flag = !isAdmin(); // the default is YES for users and NO for adminsif (prefStr)flag=[prefStr isEqualToString: @"YES"];if (flag) {char *cRLIBS = getenv("R_LIBS");NSString *addPath = [[NSString stringWithFormat:@"~/Library/R/%@/library", Rapp_R_version_short] stringByExpandingTildeInPath];if (![[NSFileManager defaultManager] fileExistsAtPath:addPath]) { // make sure the directory exists[[NSFileManager defaultManager] createDirectoryAtPath:[@"~/Library/R" stringByExpandingTildeInPath] attributes:nil];[[NSFileManager defaultManager] createDirectoryAtPath:[[NSString stringWithFormat:@"~/Library/R/%@", Rapp_R_version_short] stringByExpandingTildeInPath] attributes:nil];[[NSFileManager defaultManager] createDirectoryAtPath:addPath attributes:nil];}if (cRLIBS && *cRLIBS)addPath = [NSString stringWithFormat: @"%s:%@", cRLIBS, addPath];setenv("R_LIBS", [addPath UTF8String], 1);SLog(@" - setting R_LIBS=%s", [addPath UTF8String]);}}SLog(@" - starting REngine");{char *args[5]={ "R", "--no-save", "--no-restore-data", "--gui=cocoa", 0 };SLog(@" - initialize REngine");engine = [[REngine alloc] initWithHandler:self arguments:args];[engine setCocoaHandler:self];SLog(@" - activate R");if (![[REngine mainEngine] activate]) {NSRunAlertPanel(NLS(@"Cannot start R"),[NSString stringWithFormat:NLS(@"Unable to start R: %@"), [[REngine mainEngine] lastError]],NLS(@"OK"),nil,nil);exit(-1);}// start R on a separate threadSLog(@" - start REPL thread");[NSThread detachNewThreadSelector:@selector(run:) toTarget:engine withObject:self];}}- (void)windowDidLoad{SLog(@"RConsoleWinCtrl: windowDidLoad, name=%@, window=%@", [self windowNibName], [self window]);SLog(@" - font = %@", textFont);[textView setFont:textFont];//[textView setTypingAttributes:[NSDictionary dictionaryWithObject:textFont forKey:@"NSFont"]];[textView setString:@"\n"];[self initEngine];}/* console input - the string passed here is handled as if it was typed on the console */- (void) consoleInput: (NSString*) cmd interactive: (BOOL) inter{//@synchronized(textViewSync) {if (!inter) {int textLength = [[textView textStorage] length];if (textLength>committedLength)[textView replaceCharactersInRange:NSMakeRange(committedLength,textLength-committedLength) withString:@""];[textView setSelectedRange:NSMakeRange(committedLength,0)];[textView insertText: cmd];textLength = [[textView textStorage] length];[textView setTextColor:inputColor range:NSMakeRange(committedLength,textLength-committedLength)];}if (inter) {if ([cmd characterAtIndex:[cmd length]-1]!='\n') cmd=[cmd stringByAppendingString: @"\n"];[consoleInputQueue addObject:[[NSString alloc] initWithString:cmd]];[self handleShowInfo:@""]; // FIXME: possibly don't use handleShowInfo}//}}- (BOOL) processSingleEventBlocking: (BOOL) blocking{NSEvent *event = [NSApp nextEventMatchingMask:NSAnyEventMaskuntilDate:blocking?[NSDate distantFuture]:nilinMode:NSDefaultRunLoopModedequeue:YES];if (event)[NSApp sendEvent:event];return event?YES:NO;}/* this writes R output to the Console window directly, i.e. without using a buffer. Use handleWriteConsole: for the regular way. */- (void) writeConsoleDirectly: (NSString*) txt withColor: (NSColor*) color {//@synchronized(textViewSync) {NSTextStorage *textStorage = [textView textStorage];NSRange origSel = [textView selectedRange];unsigned tl = [txt length];if (tl>0) {unsigned oldCL=committedLength;/* NSLog(@"original: %d:%d, insertion: %d, length: %d, prompt: %d, commit: %d", origSel.location,origSel.length, outputPosition, tl, promptPosition, committedLength); */SLog(@"RConsoleWinCtrl writeConsoleDirectly, beginEditing");[textStorage beginEditing];committedLength=0;[textStorage replaceCharactersInRange: NSMakeRange(outputPosition,0) withString: txt];[textStorage addAttribute:@"NSColor" value:color range: NSMakeRange(outputPosition, tl)];//[textStorage addAttribute:@"NSFont" value:[[RController sharedController] currentFont] range: NSMakeRange(index, [text length])];if (outputPosition<=promptPosition) promptPosition+=tl;committedLength=oldCL;if (outputPosition<=committedLength) committedLength+=tl;if (outputPosition<=origSel.location) origSel.location+=tl;outputPosition+=tl;[textStorage endEditing];SLog(@"RConsoleWinCtrl writeConsoleDirectly, endEditing");[textView setSelectedRange:origSel];[textView scrollRangeToVisible:origSel];}//}}#pragma mark --- REngine callbacks ---/* --- REngine callbacks --- */- (int) handleChooseFile:(char *)buf len:(int)len isNew:(int)isNew{const char *fn;int answer;NSSavePanel *sp;NSOpenPanel *op;*buf = 0;if(isNew==1){sp = [NSSavePanel savePanel];[sp setTitle:NLS(@"Choose New File Name")];answer = [sp runModalForDirectory:nil file:nil];if(answer == NSOKButton) {if([sp filename] != nil){fn = [[sp filename] UTF8String];if (strlen(fn)>=len) {SLog(@"** handleChooseFile: bufer too small, truncating");memcpy(buf, fn, len-1);buf[len-1]=0;} elsestrcpy(buf, fn);}}} else {op = [NSOpenPanel openPanel];[op setTitle:NLS(@"Choose File")];answer = [op runModalForDirectory:nil file:nil];if(answer == NSOKButton) {if([op filename] != nil){fn = [[op filename] UTF8String];if (strlen(fn)>=len) {SLog(@"** handleChooseFile: bufer too small, truncating");memcpy(buf, fn, len-1);buf[len-1]=0;} elsestrcpy(buf, fn);}}}[[self window] makeKeyWindow];return strlen(buf); // is is used? it's potentially incorrect...}- (void) flushConsole {if (writeBuffer!=writeBufferPos) {[self writeConsoleDirectly:[NSString stringWithUTF8String:writeBuffer] withColor:outputColor];writeBufferPos=writeBuffer;}}- (int) handleEdit: (char*) file{//NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];NSString *fn = [NSString stringWithUTF8String:file];if (fn) fn = [fn stringByExpandingTildeInPath];if (!fn) Rf_error("Invalid file name.");SLog(@"RController.handleEdit: %s", file);if (![[NSFileManager defaultManager] isReadableFileAtPath:fn]) {//[pool release];return 0;}NSURL *url = [[NSURL alloc] initFileURLWithPath:fn];NSError *theError;RDocument *document = [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:url display:YES error:&theError];[document setREditFlag: YES];NSArray *wcs = [document windowControllers];if ([wcs count]<1) {SLog(@"handleEdit: WARNING, no window controllers for newly created document!");} else {NSWindowController *wc = (NSWindowController*)[wcs objectAtIndex:0];NSWindow *win = [wc window];if (win) [NSApp runModalForWindow:win];else { SLog(@"handleEdit: WARNING, window is null!"); };if ([wcs count]>1) {SLog(@"handleEdit: WARNING, there is more than one window controller, ignoring all but the first one.");}}//[pool release];return(0);}/* FIXME: the filename is not set for newly created files */- (int) handleEditFiles: (int) nfile withNames: (char**) file titles: (char**) wtitle pager: (char*) pager{int i;SLog(@"RController.handleEditFiles (%d of them, pager %s)", nfile, pager);if (nfile <=0) return 1;for (i = 0; i < nfile; i++) {NSString *fn = [NSString stringWithUTF8String:file[i]];if (fn) fn = [fn stringByExpandingTildeInPath];if (!fn) {if([[NSFileManager defaultManager] fileExistsAtPath:fn]) {NSURL *url = [[NSURL alloc] initFileURLWithPath:fn];NSError *theError;[[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:url display:YES error:&theError];} else[[NSDocumentController sharedDocumentController] newDocument: self];NSDocument *document = [[NSDocumentController sharedDocumentController] currentDocument];if(wtitle[i]!=nil)[(RDocument*)document changeTitle: [NSString stringWithUTF8String:wtitle[i]]];}}return 1;}- (void) handleFlushConsole {[self flushConsole];//[self flushStdConsole];}/* this writes R output to the Console window, but indirectly by using a buffer */- (void) handleWriteConsole: (NSString*) txt {if (!txt) return;const char *s = [txt UTF8String];int sl = strlen(s);int fits = writeBufferLen-(writeBufferPos-writeBuffer)-1;// let's flush the buffer if the new string is large and it would, but the buffer should be occupiedif (fits<sl && fits>writeBufferHighWaterMark) {// for efficiency we're not using handleFlushConsole, because that would trigger stdxx flush, too[self writeConsoleDirectly:[NSString stringWithUTF8String:writeBuffer] withColor:outputColor];writeBufferPos=writeBuffer;fits = writeBufferLen-1;}while (fits<sl) { // ok, we're in a situation where we must split the stringmemcpy(writeBufferPos, s, fits);writeBufferPos[writeBufferLen-1]=0;[self writeConsoleDirectly:[NSString stringWithUTF8String:writeBuffer] withColor:outputColor];sl-=fits; s+=fits;writeBufferPos=writeBuffer;fits=writeBufferLen-1;}strcpy(writeBufferPos, s);writeBufferPos+=sl;// flush the buffer if the low watermark is reachedif (fits-sl<writeBufferLowWaterMark) {[self writeConsoleDirectly:[NSString stringWithUTF8String:writeBuffer] withColor:outputColor];writeBufferPos=writeBuffer;}}/* Just writes the prompt in a different color */- (void)handleWritePrompt: (NSString*) prompt {[self handleFlushConsole];//@synchronized(textViewSync) {NSTextStorage *textStorage = [textView textStorage];unsigned textLength = [textStorage length];int promptLength=[prompt length];// NSLog(@"Prompt: %@", prompt);NSRange lr = [[textStorage string] lineRangeForRange:NSMakeRange(textLength,0)];SLog(@"RController handleWritePrompt: '%@', beginEditing", prompt);[textStorage beginEditing];promptPosition=textLength;if (lr.location!=textLength) { // the prompt must be on the beginning of the line//[textStorage insertText: @"\n" atIndex: textLength withColor:[consoleColors objectAtIndex:iPromptColor]];[textStorage replaceCharactersInRange: NSMakeRange(promptPosition,0) withString: @"\n"];[textStorage addAttribute:@"NSColor" value:promptColor range: NSMakeRange(promptPosition, 1)];textLength = [textStorage length];promptLength++;}if (promptLength>0) {//[textStorage insertText:prompt atIndex: textLength withColor:[consoleColors objectAtIndex:iPromptColor]];[textStorage replaceCharactersInRange: NSMakeRange(promptPosition,0) withString: prompt];[textStorage addAttribute:@"NSColor" value:promptColor range: NSMakeRange(promptPosition, promptLength)];if (promptLength>1) // this is a trick to make sure that the insertion color doesn't change at the prompt[textStorage addAttribute:@"NSColor" value:inputColor range:NSMakeRange(promptPosition+promptLength-1, 1)];committedLength=promptPosition+promptLength;}committedLength=promptPosition+promptLength;[textStorage endEditing];SLog(@"RController handleWritePrompt: '%@', endEditing", prompt);{NSRange targetRange = NSMakeRange(committedLength,0);[textView setSelectedRange:targetRange];[textView scrollRangeToVisible:targetRange];}//}}- (void) handleProcessEvents{while ([self processSingleEventBlocking:NO]) {};}- (void) handleProcessingInput: (char*) cmd{NSString *s = [[NSString alloc] initWithUTF8String:cmd];//@synchronized(textViewSync) {unsigned textLength = [[textView textStorage] length];[textView setSelectedRange:NSMakeRange(committedLength, textLength-committedLength)];[textView insertText:s];textLength = [[textView textStorage] length];[textView setTextColor:inputColor range:NSMakeRange(committedLength, textLength-committedLength)];outputPosition=committedLength=textLength;// remove undo actions to prevent undo across prompts[[textView undoManager] removeAllActions];//}[s release];}- (char*) handleReadConsole: (int) addtohist{if (currentConsoleInput) {[currentConsoleInput release];currentConsoleInput=nil;}while ([consoleInputQueue count]==0)[self processSingleEventBlocking: YES];currentConsoleInput = [consoleInputQueue objectAtIndex:0];[consoleInputQueue removeObjectAtIndex:0];if (addtohist) {// Figure out how to get hold of ParseStatus here![hist commit:currentConsoleInput];// FIXME: add historyView support//[historyView reloadData];}{const char *c = [currentConsoleInput UTF8String];if (!c) return 0;if (strlen(c)>readConsTransBufferSize-1) { // grow as necessaryfree(readConsTransBuffer);readConsTransBufferSize = (strlen(c)+2048)&0xfffffc00;readConsTransBuffer = (char*) malloc(readConsTransBufferSize);} // we don't shrink the buffer if gets too large - we may want to think about that ...strcpy(readConsTransBuffer, c);}return readConsTransBuffer;}- (int) handleSystemCommand: (char*) cmd{int cstat=-1;pid_t pid;// FIXME: enable root system commands again/*if ([self getRootFlag]) {FILE *f;char *argv[3] = { "-c", cmd, 0 };int res;NSBundle *b = [NSBundle mainBundle];char *sushPath=0;if (b) {NSString *sush=[[b resourcePath] stringByAppendingString:@"/sush"];sushPath = (char*) malloc([sush cStringLength]+1);[sush getCString:sushPath maxLength:[sush cStringLength]];}res = runRootScript(sushPath?sushPath:"/bin/sh",argv,&f,1);if (!res && f) {int fd = fileno(f);if (fd != -1) {struct timespec peSleep = { 0, 50000000 }; // 50ms sleep[self setRootFD:fileno(f)];while (rootFD!=-1) { // readThread will reset rootFD to -1 when reaching EOFnanosleep(&peSleep, 0); // sleep at least 50ms between PE calls (they're expensive)Re_ProcessEvents();}}}if (sushPath) free(sushPath);return res;}*/pid=fork();if (pid==0) {// int sr;// reset signal handlerssignal(SIGINT, SIG_DFL);signal(SIGTERM, SIG_DFL);signal(SIGQUIT, SIG_DFL);signal(SIGALRM, SIG_DFL);signal(SIGCHLD, SIG_DFL);execl("/bin/sh", "/bin/sh", "-c", cmd, NULL);exit(-1);//sr=system(cmd);//exit(WEXITSTATUS(sr));}if (pid==-1) return -1;{struct timespec peSleep = { 0, 100000000 }; // 100ms sleepwhile (1) {pid_t w = waitpid(pid, &cstat, WNOHANG);if (w!=0) break;nanosleep(&peSleep, 0); // sleep at least 50ms between PE calls (they're expensive)Re_ProcessEvents();}}// FIXME: [[RController sharedController] rmChildProcess: pid];return cstat;}- (int) handleShowFiles: (int) nfile withNames: (char**) file headers: (char**) headers windowTitle: (char*) wtitle pager: (char*) pages andDelete: (BOOL) del{int i;if (nfile <=0) return 1;SLog(@"RController.handleShowFiles (%d of them, title %s, pager %s)", nfile, wtitle, pages);for (i = 0; i < nfile; i++){NSString *fn = [NSString stringWithUTF8String:file[i]];if (fn) fn =[fn stringByExpandingTildeInPath];if (fn) {NSURL *url = [[NSURL alloc] initFileURLWithPath:fn];NSError *theError;RDocument *document = [[NSDocumentController sharedDocumentController] openDocumentWithContentsOfURL:url display:YES error:&theError];// don't display - we need to prevent the window controller from using highlightingif (document) {NSArray *wcs = [document windowControllers];if (wcs && [wcs count]>0) {SLog(@" - Disabling syntax highlighting for this document");// FIXME: add setPlain//[(RDocumentWinCtrl*) [wcs objectAtIndex:0] setPlain:YES];}if (wtitle)[document changeTitle: [NSString stringWithUTF8String:wtitle]];[document setEditable: NO];SLog(@" - finally show the document window");[document showWindows];}}}return 1;}- (void) handleBusy: (BOOL) isBusy {/*if (isBusy)[progressWheel startAnimation:self];else[progressWheel stopAnimation:self];busyRFlag = isBusy;if (toolbarStopItem) {if (isBusy || childPID>0)[toolbarStopItem setEnabled:YES];else[toolbarStopItem setEnabled:NO];}*/}- (void) handleShowMessage: (char*) msg{NSRunAlertPanel(NLS(@"R Message"),[NSString stringWithUTF8String:msg],NLS(@"OK"),nil,nil);}- (void) handleShowInfo: (NSString*) msg{[statusText setStringValue:msg?msg:@""];}#pragma mark --- Console text view delegate methods ---/* Allow changes only for uncommitted text */- (BOOL)textView:(NSTextView *)aTextView shouldChangeTextInRange:(NSRange)affectedCharRange replacementString:(NSString *)replacementString {if (aTextView != textView) return YES; /* this should never happen */if (replacementString && /* on font change we get nil replacementString which is ok to pass through */affectedCharRange.location < committedLength) { /* if the insertion is outside editable scope, append at the end */[textView setSelectedRange:NSMakeRange([[textView textStorage] length],0)];[textView insertText:replacementString];return NO;}return YES;}- (BOOL)textView:(NSTextView *)aTextView doCommandBySelector:(SEL)commandSelector {BOOL retval = NO;if (aTextView != textView) return NO; /* this should never happen */SLog(@"RConsoleWinCtrl textView: doCommandBySelector: %@\n", NSStringFromSelector(commandSelector));if (@selector(insertNewline:) == commandSelector) {unsigned textLength = [[textView textStorage] length];[textView setSelectedRange:NSMakeRange(textLength,0)];if (textLength >= committedLength) {[textView insertText:@"\n"];textLength = [[textView textStorage] length];[self consoleInput: [[textView attributedSubstringFromRange:NSMakeRange(committedLength, textLength - committedLength)] string] interactive: YES];return(YES);}retval = YES;}// ---- history browsing ----if (@selector(moveUp:) == commandSelector) {unsigned textLength = [[textView textStorage] length];NSRange sr=[textView selectedRange];if (sr.location==committedLength || sr.location==textLength) {NSRange rr=NSMakeRange(committedLength, textLength-committedLength);NSString *text = [[textView attributedSubstringFromRange:rr] string];if ([hist isDirty]) {[hist updateDirty: text];}NSString *news = [hist prev];if (news!=nil) {[news retain];sr.length=0; sr.location=committedLength;[textView setSelectedRange:sr];[textView replaceCharactersInRange:rr withString:news];[textView insertText:@""];[news release];}retval = YES;}}if (@selector(moveDown:) == commandSelector) {unsigned textLength = [[textView textStorage] length];NSRange sr=[textView selectedRange];if ((sr.location==committedLength || sr.location==textLength) && ![hist isDirty]) {NSRange rr=NSMakeRange(committedLength, textLength-committedLength);NSString *news = [hist next];if (news==nil) news=@""; else [news retain];sr.length=0; sr.location=committedLength;[textView setSelectedRange:sr];[textView replaceCharactersInRange:rr withString:news];[textView insertText:@""];[news release];retval = YES;}}// ---- make sure the user won't accidentally get out of the input line ----if (@selector(moveToBeginningOfParagraph:) == commandSelector || @selector(moveToBeginningOfLine:) == commandSelector) {[textView setSelectedRange: NSMakeRange(committedLength,0)];retval = YES;}if (@selector(moveToBeginningOfParagraphAndModifySelection:) == commandSelector || @selector(moveToBeginningOfLineAndModifySelection:) == commandSelector) {// FIXME: this kills the selection - we should retain it ...[textView setSelectedRange: NSMakeRange(committedLength,0)];retval = YES;}if (@selector(moveWordLeft:) == commandSelector || @selector(moveLeft:) == commandSelector ||@selector(moveWordLeftAndModifySelection:) == commandSelector || @selector(moveLeftAndModifySelection:) == commandSelector) {NSRange sr=[textView selectedRange];if (sr.location==committedLength) return YES;}// ---- code/file completion ----if (@selector(insertTab:) == commandSelector) {[textView complete:self];retval = YES;}// ---- cancel ---if (@selector(cancel:) == commandSelector || @selector(cancelOperation:) == commandSelector) {[self breakR:self];retval = YES;}return retval;}@end