Rev 5995 | Rev 6021 | 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 5/11/05.* $Id: RTextView.m 5998 2012-01-12 12:51:06Z bibiko $*/#import "RTextView.h"#import "HelpManager.h"#import "RGUI.h"#import "RegexKitLite.h"#import "RController.h"#import "NSTextView_RAdditions.h"#import "RDocumentWinCtrl.h"// linked character attributes#define kTALinked @"link"#define kTAVal @"x"// context menu tags#define kShowHelpContextMenuItemTag 10001// declared externalBOOL RTextView_autoCloseBrackets = YES;#pragma mark -#pragma mark Private API@interface RTextView (Private)- (void)selectMatchingPairAt:(int)position;- (NSString*)functionNameForCurrentScope;@end#pragma mark -@implementation RTextView- (id) initWithCoder: (NSCoder*) coder{self = [super initWithCoder:coder];if (self) {separatingTokensSet = [[NSCharacterSet characterSetWithCharactersInString: @"()'\"+-=/* ,\t]{}^|&!;<>?`\n\\"] retain];undoBreakTokensSet = [[NSCharacterSet characterSetWithCharactersInString: @"+- .,|&*/:!?<>=\n"] retain];}return self;}- (void)awakeFromNib{SLog(@"RTextView: awakeFromNib %@", self);// commentTokensSet = [[NSCharacterSet characterSetWithCharactersInString: @"#"] retain];console = NO;RTextView_autoCloseBrackets = YES;SLog(@" - delegate: %@", [self delegate]);isRdDocument = NO;if([[self window] windowController] && [[[self window] windowController] respondsToSelector:@selector(isRdDocument)])isRdDocument = ([[[self window] windowController] isRdDocument]);// work-arounds for brain-dead "features" in Lionif ([self respondsToSelector:@selector(setAutomaticQuoteSubstitutionEnabled:)])[self setAutomaticQuoteSubstitutionEnabled:NO];if ([self respondsToSelector:@selector(setAutomaticTextReplacementEnabled:)])[self setAutomaticTextReplacementEnabled:NO];if ([self respondsToSelector:@selector(setAutomaticSpellingCorrectionEnabled:)])[self setAutomaticSpellingCorrectionEnabled:NO];if ([self respondsToSelector:@selector(setAutomaticLinkDetectionEnabled:)])[self setAutomaticLinkDetectionEnabled:NO];if ([self respondsToSelector:@selector(setAutomaticDataDetectionEnabled:)])[self setAutomaticDataDetectionEnabled:NO];if ([self respondsToSelector:@selector(setAutomaticDashSubstitutionEnabled:)])[self setAutomaticDashSubstitutionEnabled:NO];}- (void)dealloc{if(separatingTokensSet) [separatingTokensSet release];if(undoBreakTokensSet) [undoBreakTokensSet release];// if(commentTokensSet) [commentTokensSet release];[super dealloc];}- (NSMenu *)menuForEvent:(NSEvent *)event{NSMenu *menu = [[self class] defaultMenu];int insertionIndex = 0;NSArray* items = [menu itemArray];// Check if context menu additions were added alreadyfor(insertionIndex = 0; insertionIndex < [items count]; insertionIndex++) {if([[items objectAtIndex:insertionIndex] tag] == kShowHelpContextMenuItemTag)return menu;}// Add additional menu items// Look for insertion index (after the first separator)for(insertionIndex = 0; insertionIndex < [items count]; insertionIndex++) {if([[items objectAtIndex:insertionIndex] isSeparatorItem])break;}insertionIndex++;SLog(@"RTextView: add additional menu items at postion %d to context menu", insertionIndex);NSMenuItem *anItem;anItem = [[NSMenuItem alloc] initWithTitle:NLS(@"Show Help for current Function") action:@selector(showHelpForCurrentFunction) keyEquivalent:@"h"];[anItem setKeyEquivalentModifierMask:NSControlKeyMask];[anItem setTag:kShowHelpContextMenuItemTag];[menu insertItem:anItem atIndex:insertionIndex++];[anItem release];[menu insertItem:[NSMenuItem separatorItem] atIndex:insertionIndex++];return menu;}- (void)keyDown:(NSEvent *)theEvent{if(![self isEditable]) {[super keyDown:theEvent];return;}NSString *rc = [theEvent charactersIgnoringModifiers];NSString *cc = [theEvent characters];unsigned int modFlags = [theEvent modifierFlags];long allFlags = (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask);BOOL hilite = NO;SLog(@"RTextView: keyDown: %@ *** \"%@\" %d", theEvent, rc, modFlags);if([rc length] && [undoBreakTokensSet characterIsMember:[rc characterAtIndex:0]]) [self breakUndoCoalescing];if ([rc isEqual:@"."] && (modFlags&allFlags)==NSControlKeyMask) {SLog(@" - send complete: to self");[self complete:self];return;}if ([rc isEqual:@"="]) {int mf = modFlags&allFlags;if ( mf ==NSControlKeyMask) {[self breakUndoCoalescing];[self insertText:@"<-"];return;}if ( mf == NSAlternateKeyMask ) {[self breakUndoCoalescing];[self insertText:@"!="];return;}}if ([rc isEqual:@"-"] && (modFlags&allFlags)==NSAlternateKeyMask) {[self breakUndoCoalescing];[self insertText:@" <- "];return;}if ([rc isEqual:@"h"] && (modFlags&allFlags)==NSControlKeyMask) {SLog(@" - send showHelpForCurrentFunction to self");[self showHelpForCurrentFunction];return;}// Detect if matching bracket should be highlightedif(cc && [cc length]==1 && [[[NSUserDefaults standardUserDefaults] objectForKey:showBraceHighlightingKey] isEqualToString:@"YES"]) {switch([cc characterAtIndex:0]) {case '(':case '[':case '{':case ')':case ']':case '}':hilite = YES;}}if (cc && [cc length]==1 && [[[NSUserDefaults standardUserDefaults] objectForKey:kAutoCloseBrackets] isEqualToString:@"YES"]) {unichar ck = [cc characterAtIndex:0];NSString *complement = nil;NSRange r = [self selectedRange];BOOL acCheck = NO;switch (ck) {case '{':complement = @"}";case '(':if (!complement) complement = @")";case '[':if (!complement) complement = @"]";case '"':if (!complement) {complement = @"\"";acCheck = YES;if ([self parserContextForPosition:r.location] != pcExpression) break;}case '`':if (!complement) {complement = @"`";acCheck = YES;if ([self parserContextForPosition:r.location] != pcExpression) break;}case '\'':if (!complement) {complement = @"\'";acCheck = YES;if ([self parserContextForPosition:r.location] != pcExpression) break;}// Check if something is selected and wrap it into matching pair characters and preserve the selection// - in RConsole only if selection is in the last lineif(((console && [[self string] lineRangeForRange:NSMakeRange([[self string] length]-1,0)].location+1 < r.location) || !console)&& [self wrapSelectionWithPrefix:[NSString stringWithFormat:@"%c", ck] suffix:complement]) {SLog(@"RTextView: selection was wrapped with auto-pairs");return;}// Try to suppress unnecessary auto-pairingif( !isRdDocument && [self isCursorAdjacentToAlphanumCharWithInsertionOf:ck] && ![self isNextCharMarkedBy:kTALinked withValue:kTAVal] && ![self selectedRange].length ){SLog(@"RTextView: suppressed auto-pairing");[super keyDown:theEvent];if(hilite && [[self delegate] respondsToSelector:@selector(highlightBracesWithShift:andWarn:)])[[self delegate] highlightBracesWithShift:-1 andWarn:YES];return;}SLog(@"RTextView: open bracket chracter %c", ck);[super keyDown:theEvent];{r = [self selectedRange];if (r.location != NSNotFound) {// NSAttributedString *as = [[NSAttributedString alloc] initWithString:complement attributes:// [NSDictionary dictionaryWithObject:TAVal forKey:kTALinked]];NSTextStorage *ts = [self textStorage];// Register the auto-pairing for undo and insert the complement[self shouldChangeTextInRange:r replacementString:complement];[self replaceCharactersInRange:r withString:complement];r.length=1;[ts addAttribute:kTALinked value:kTAVal range:r];r.length=0;[self setSelectedRange:r];}return;}case '}':case ')':case ']':acCheck = YES;}if (acCheck) {NSRange r = [self selectedRange];if (r.location != NSNotFound && r.length == 0) {NSTextStorage *ts = [self textStorage];id attr = nil;@try {attr = [ts attribute:kTALinked atIndex:r.location effectiveRange:0];}@catch (id ue) {}if (attr) {unsigned int cuc = [[ts string] characterAtIndex:r.location];SLog(@"RTextView: encountered linked character '%c', while writing '%c'", cuc, ck);if (cuc == ck) {r.length = 1;SLog(@"RTextView: selecting linked character for removal on type");[self setSelectedRange:r];}}}SLog(@"RTextView: closing bracket chracter %c", ck);}}// if ((modFlags&(NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask))==NSControlKeyMask) {// if ([rc isEqual:@"{"]) {//// } else if ([rc isEqual:@"}"]) {//// }// }[super keyDown:theEvent];if(hilite && [[self delegate] respondsToSelector:@selector(highlightBracesWithShift:andWarn:)])[[self delegate] highlightBracesWithShift:-1 andWarn:YES];}- (void)deleteBackward:(id)sender{NSRange r = [self selectedRange];if (r.length == 0 && r.location > 0)[self selectMatchingPairAt:r.location];[super deleteBackward:sender];}- (void)deleteForward:(id)sender{NSRange r = [self selectedRange];if (r.length == 0)[self selectMatchingPairAt:r.location + 1];[super deleteForward:sender];}/*** If the textview has a selection, wrap it with the supplied prefix and suffix strings;* return whether or not any wrap was performed.*/- (BOOL) wrapSelectionWithPrefix:(NSString *)prefix suffix:(NSString *)suffix{NSRange currentRange = [self selectedRange];// Only proceed if a selection is activeif (currentRange.length == 0 || ![self isEditable])return NO;NSString *selString = [[self string] substringWithRange:currentRange];// Replace the current selection with the selected string wrapped in prefix and suffix[self insertText:[NSString stringWithFormat:@"%@%@%@", prefix, selString, suffix]];// Re-select original selectionNSRange innerSelectionRange = NSMakeRange(currentRange.location+1, [selString length]);[self setSelectedRange:innerSelectionRange];// Mark last autopair character as autopair-linked[[self textStorage] addAttribute:kTALinked value:kTAVal range:NSMakeRange(NSMaxRange(innerSelectionRange), 1)];return YES;}/*** Returns the parser context for the passed cursor position** @param position The cursor position to test*/- (int)parserContextForPosition:(int)position{int context = pcExpression;if (position < 1)return context;NSString *string = [self string];if (position > [string length])position = [string length];NSRange thisLine = [string lineRangeForRange:NSMakeRange(position, 0)];// we do NOT support multi-line strings, so the line always starts as an expressionif (thisLine.location == position)return context;SLog(@"RTextView: parserContextForPosition: %d, line span=%d:%d", position, thisLine.location, thisLine.length);int i = thisLine.location;BOOL skip = NO;unichar c;while (i < position && i >= 0) {c = CFStringGetCharacterAtIndex((CFStringRef)string, i);if (skip) {skip = NO;} else {if (c == '\\' && (context == pcStringDQ || context == pcStringSQ || context == pcStringBQ)) {skip = YES;} else if (c == '"') {if (context == pcStringDQ)context = pcExpression;else if (context == pcExpression)context = pcStringDQ;} else if (c == '\'') {if (context == pcStringSQ)context = pcExpression;else if (context == pcExpression)context = pcStringSQ;} else if (c == '`') {if (context == pcStringBQ)context = pcExpression;else if (context == pcExpression)context = pcStringBQ;}else if(context == pcExpression) {if(isRdDocument && c == '%')return pcComment;else if(c == '#')return pcComment;}}i++;}return context;}/*** Returns the range for user completion*/- (NSRange)rangeForUserCompletion{NSRange userRange = NSMakeRange(NSNotFound, 0);NSRange selection = [self selectedRange];NSString *string = [self string];int cursor = NSMaxRange(selection); // we complete at the end of the selectionint context = [self parserContextForPosition:cursor];SLog(@"RTextView: rangeForUserCompletion: parser context: %d", context);if (context == pcComment) return NSMakeRange(NSNotFound,0); // no completion in commentsif (context == pcStringDQ || context == pcStringSQ) // we're in a string, hence file completion// the beginning of the range doesn't matter, because we're guaranteed to find a string separator on the same lineuserRange = [string rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:(context == pcStringDQ) ? @"\" /" : @"' /"]options:NSBackwardsSearch|NSLiteralSearchrange:NSMakeRange(0, selection.location)];if (context == pcExpression || context == pcStringBQ) // we're in an expression or back-quote, so use R separating tokens (we could be smarter about the BQ but well..)userRange = [string rangeOfCharacterFromSet:separatingTokensSetoptions:NSBackwardsSearch|NSLiteralSearchrange:NSMakeRange(0, selection.location)];if( userRange.location == NSNotFound )// everything is one expression - we're guaranteed to be in the first line (because \n would match)return NSMakeRange(0, cursor);if( userRange.length < 1 ) // nothing to completereturn NSMakeRange(NSNotFound, 0);if( userRange.location == selection.location - 1 ) { // just before cursor means empty completionuserRange.location++;userRange.length = 0;} else { // normal completionuserRange.location++; // skip past first bad oneuserRange.length = selection.location - userRange.location;SLog(@" - returned range: %d:%d", userRange.location, userRange.length);// FIXME: do we really need to change it? Cocoa should be doing it .. (and does in Lion)if (os_version < 11.0)[self setSelectedRange:userRange];}return userRange;}/*** Checks if the char after the current caret position/selection matches a supplied attribute*/- (BOOL) isNextCharMarkedBy:(id)attribute withValue:(id)aValue{NSUInteger caretPosition = [self selectedRange].location;// Perform bounds checkingif (caretPosition >= [[self string] length]) return NO;// Perform the checkif ([[[self textStorage] attribute:attribute atIndex:caretPosition effectiveRange:nil] isEqualToString:aValue])return YES;return NO;}/*** Checks if the caret adjoins to an alphanumeric char |word or word| or wo|rd* Exception for word| and char is a “(” or “[” to allow e.g. auto-pairing () for functions*/- (BOOL) isCursorAdjacentToAlphanumCharWithInsertionOf:(unichar)aChar{NSUInteger caretPosition = [self selectedRange].location;NSCharacterSet *alphanum = [NSCharacterSet alphanumericCharacterSet];BOOL leftIsAlphanum = NO;BOOL rightIsAlphanum = NO;BOOL charIsOpenBracket = (aChar == '(' || aChar == '[');NSUInteger bufferLength = [[self string] length];if(!bufferLength) return NO;// Check previous/next character for being alphanum// @try block for bounds checking@try{if(caretPosition==0)leftIsAlphanum = NO;elseleftIsAlphanum = [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition-1]] && !charIsOpenBracket;} @catch(id ae) { }@try {if(caretPosition >= bufferLength)rightIsAlphanum = NO;elserightIsAlphanum= [alphanum characterIsMember:[[self string] characterAtIndex:caretPosition]];} @catch(id ae) { }return (leftIsAlphanum ^ rightIsAlphanum || (leftIsAlphanum && rightIsAlphanum));}/*** Returns the range of the current word relative the current cursor position* finds: [| := caret] |word wo|rd word|* if | is in between whitespaces range length is zero.*//*** Sets the console mode** @param isConsole If self is in console mode (YES) or not (NO)*/- (void)setConsoleMode:(BOOL)isConsole{console = isConsole;SLog(@"RTextView: set console flag to %@ (%@)", isConsole?@"yes":@"no", self);}/*** Shows the Help page for the current function relative to the current cursor position or* if something is selected for the selection in the HelpManager** Notes:* - if the cursor is in between or adjacent to an alphanumeric word take this one if it not a pure numeric value* - if nothing found try to parse backwards from cursor position to find the active function name according to opened and closed parentheses* examples | := cursor* a(b(1,2|,3)) -> b* a(b(1,2,3)|) -> a* - if nothing found set the input focus to the Help search field either in RConsole or in R script editor*/- (void) showHelpForCurrentFunction{NSString *helpString = [self functionNameForCurrentScope];if(helpString && [helpString length]) {int oldSearchType = [[HelpManager sharedController] searchType];[[HelpManager sharedController] setSearchType:kExactMatch];[[HelpManager sharedController] showHelpFor:helpString];[[HelpManager sharedController] setSearchType:oldSearchType];return;}id aSearchField = nil;NSWindow *keyWin = [NSApp keyWindow];if(![[keyWin toolbar] isVisible])[keyWin toggleToolbarShown:nil];if([[self delegate] respondsToSelector:@selector(searchToolbarView)])aSearchField = [[self delegate] searchToolbarView];if(aSearchField == nil || ![aSearchField isKindOfClass:[NSSearchField class]]) return;[aSearchField setStringValue:[[self string] substringWithRange:[self getRangeForCurrentWord]]];if([[aSearchField stringValue] length])[[HelpManager sharedController] showHelpFor:[aSearchField stringValue]];else[[NSApp keyWindow] makeFirstResponder:aSearchField];}- (void)currentFunctionHint{NSString *helpString = [self functionNameForCurrentScope];if(helpString && ![helpString isMatchedByRegex:@"(?s)[\\s\\[\\]\\(\\)\\{\\};\\?!]"] && [[self delegate] respondsToSelector:@selector(hintForFunction:)]) {SLog(@"RTextView: currentFunctionHint for '%@'", helpString);[(RController*)[self delegate] hintForFunction:helpString];}}/*** Shifts the selection, if any, rightwards by indenting any selected lines with one tab.* If the caret is within a line, the selection is not changed after the index; if the selection* has length, all lines crossed by the length are indented and fully selected.* Returns whether or not an indentation was performed.*/- (BOOL) shiftSelectionRight{NSString *textViewString = [[self textStorage] string];NSRange currentLineRange;NSRange selectedRange = [self selectedRange];if (selectedRange.location == NSNotFound || ![self isEditable]) return NO;NSString *indentString = @"\t";// if ([prefs soft]) {// NSUInteger numberOfSpaces = [prefs soft width];// if(numberOfSpaces < 1) numberOfSpaces = 1;// if(numberOfSpaces > 32) numberOfSpaces = 32;// NSMutableString *spaces = [NSMutableString string];// for(NSUInteger i = 0; i < numberOfSpaces; i++)// [spaces appendString:@" "];// indentString = [NSString stringWithString:spaces];// }// Indent the currently selected line if the caret is within a single lineif (selectedRange.length == 0) {// Extract the current line range based on the text caretcurrentLineRange = [textViewString lineRangeForRange:selectedRange];// Register the indent for undo[self shouldChangeTextInRange:NSMakeRange(currentLineRange.location, 0) replacementString:indentString];// Insert the new tab[self replaceCharactersInRange:NSMakeRange(currentLineRange.location, 0) withString:indentString];return YES;}// Otherwise, something is selectedNSRange firstLineRange = [textViewString lineRangeForRange:NSMakeRange(selectedRange.location,0)];NSUInteger lastLineMaxRange = NSMaxRange([textViewString lineRangeForRange:NSMakeRange(NSMaxRange(selectedRange)-1,0)]);// Expand selection for first and last line to begin and end resp. but not the last line endingNSRange blockRange = NSMakeRange(firstLineRange.location, lastLineMaxRange - firstLineRange.location);if([textViewString characterAtIndex:NSMaxRange(blockRange)-1] == '\n' || [textViewString characterAtIndex:NSMaxRange(blockRange)-1] == '\r')blockRange.length--;// Replace \n by \n\t of all lines in blockRangeNSString *newString;// check for line endingif([textViewString characterAtIndex:NSMaxRange(firstLineRange)-1] == '\r')newString = [indentString stringByAppendingString:[[textViewString substringWithRange:blockRange]stringByReplacingOccurrencesOfString:@"\r" withString:[NSString stringWithFormat:@"\r%@", indentString]]];elsenewString = [indentString stringByAppendingString:[[textViewString substringWithRange:blockRange]stringByReplacingOccurrencesOfString:@"\n" withString:[NSString stringWithFormat:@"\n%@", indentString]]];// Register the indent for undo[self shouldChangeTextInRange:blockRange replacementString:newString];[self replaceCharactersInRange:blockRange withString:newString];[self setSelectedRange:NSMakeRange(blockRange.location, [newString length])];if(blockRange.length == [newString length])return NO;elsereturn YES;}/*** Shifts the selection, if any, leftwards by un-indenting any selected lines by one tab if possible.* If the caret is within a line, the selection is not changed after the undent; if the selection has* length, all lines crossed by the length are un-indented and fully selected.* Returns whether or not an indentation was performed.*/- (BOOL) shiftSelectionLeft{NSString *textViewString = [[self textStorage] string];NSRange currentLineRange;if ([self selectedRange].location == NSNotFound || ![self isEditable]) return NO;// Undent the currently selected line if the caret is within a single lineif ([self selectedRange].length == 0) {// Extract the current line range based on the text caretcurrentLineRange = [textViewString lineRangeForRange:[self selectedRange]];// Ensure that the line has length and that the first character is a tabif (currentLineRange.length < 1|| ([textViewString characterAtIndex:currentLineRange.location] != '\t' && [textViewString characterAtIndex:currentLineRange.location] != ' '))return NO;NSRange replaceRange;// Check for soft indentionNSUInteger indentStringLength = 1;// if ([prefs soft]) {// NSUInteger numberOfSpaces = [prefs soft width];// if(numberOfSpaces < 1) numberOfSpaces = 1;// if(numberOfSpaces > 32) numberOfSpaces = 32;// indentStringLength = numberOfSpaces;// replaceRange = NSIntersectionRange(NSMakeRange(currentLineRange.location, indentStringLength), NSMakeRange(0,[[self string] length]));// // Correct length for only white spaces// NSString *possibleIndentString = [[[self textStorage] string] substringWithRange:replaceRange];// NSUInteger numberOfLeadingWhiteSpaces = [possibleIndentString rangeOfRegex:@"^(\\s*)" capture:1L].length;// if(numberOfLeadingWhiteSpaces == NSNotFound) numberOfLeadingWhiteSpaces = 0;// replaceRange = NSMakeRange(currentLineRange.location, numberOfLeadingWhiteSpaces);// } else {replaceRange = NSMakeRange(currentLineRange.location, indentStringLength);// }// Register the undent for undo[self shouldChangeTextInRange:replaceRange replacementString:@""];// Remove the tab[self replaceCharactersInRange:replaceRange withString:@""];return YES;}// Otherwise, something is selectedNSRange firstLineRange = [textViewString lineRangeForRange:NSMakeRange([self selectedRange].location,0)];NSUInteger lastLineMaxRange = NSMaxRange([textViewString lineRangeForRange:NSMakeRange(NSMaxRange([self selectedRange])-1,0)]);// Expand selection for first and last line to begin and end resp. but the last line endingNSRange blockRange = NSMakeRange(firstLineRange.location, lastLineMaxRange - firstLineRange.location);if([textViewString characterAtIndex:NSMaxRange(blockRange)-1] == '\n' || [textViewString characterAtIndex:NSMaxRange(blockRange)-1] == '\r')blockRange.length--;// Check for soft or hard indentionNSString *indentString = @"\t";NSUInteger indentStringLength = 1;// if ([prefs soft]) {// indentStringLength = [prefs soft width];// if(indentStringLength < 1) indentStringLength = 1;// if(indentStringLength > 32) indentStringLength = 32;// NSMutableString *spaces = [NSMutableString string];// for(NSUInteger i = 0; i < indentStringLength; i++)// [spaces appendString:@" "];// indentString = [NSString stringWithString:spaces];// }// Check if blockRange starts with SPACE or TAB// (this also catches the first line of the entire text buffer or// if only one line is selected)NSInteger leading = 0;if([textViewString characterAtIndex:blockRange.location] == ' '|| [textViewString characterAtIndex:blockRange.location] == '\t')leading += indentStringLength;// Replace \n[ \t] by \n of all lines in blockRangeNSString *newString;// check for line endingif([textViewString characterAtIndex:NSMaxRange(firstLineRange)-1] == '\r')newString = [[textViewString substringWithRange:NSMakeRange(blockRange.location+leading, blockRange.length-leading)]stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\r%@", indentString] withString:@"\r"];elsenewString = [[textViewString substringWithRange:NSMakeRange(blockRange.location+leading, blockRange.length-leading)]stringByReplacingOccurrencesOfString:[NSString stringWithFormat:@"\n%@", indentString] withString:@"\n"];// Register the unindent for undo[self shouldChangeTextInRange:blockRange replacementString:newString];[self replaceCharactersInRange:blockRange withString:newString];[self setSelectedRange:NSMakeRange(blockRange.location, [newString length])];if(blockRange.length == [newString length])return NO;elsereturn YES;}#pragma mark -/*** Selects matching pairs if the character before position and at position are linked** @param position The cursor position to test*/- (void)selectMatchingPairAt:(int)position{if(position < 1 || position >= [[self string] length])return;unichar c = [[self string] characterAtIndex:position - 1];unichar cc = 0;switch (c) {case '(': cc=')'; break;case '{': cc='}'; break;case '[': cc=']'; break;case '"':case '`':case '\'':cc=c; break;}if (cc) {unichar cs = [[self string] characterAtIndex:position];if (cs == cc) {id attr = [[self textStorage] attribute:kTALinked atIndex:position effectiveRange:0];if (attr) {[self setSelectedRange:NSMakeRange(position - 1, 2)];SLog(@"RTextView: selected matching pair");}}}}- (NSString*)functionNameForCurrentScope{NSString *helpString;NSString *parseString = [self string];NSRange selectedRange = [self selectedRange];NSRange parseRange;int parentheses = 0;int index = 0;SLog(@"RTextView: functionNameForCurrentScope");// if user selected something take the selection only; otherwise take the current wordif (selectedRange.length) {helpString = [parseString substringWithRange:selectedRange];SLog(@" - return selection");return helpString;} else {helpString = [parseString substringWithRange:[self getRangeForCurrentWord]];}SLog(@" - current word “%@”", helpString);// if a word was found and the word doesn't represent a numeric value// and a ( follows then return wordif([helpString length] && ![[[NSNumber numberWithFloat:[helpString floatValue]] stringValue] isEqualToString:helpString]) {int start = NSMaxRange(selectedRange);if(start < [parseString length]) {BOOL found = NO;int i = 0;int end = ([parseString length] > 100) ? 100 : [parseString length];unichar c;for(i = start; i < end; i++) {if ((c = CFStringGetCharacterAtIndex((CFStringRef)parseString, i)) == '(') {found = YES;break;}if (c != ' ' || c != '\t' || c != '\n' || c != '\r') break;}if(found) {SLog(@" - caret was inside function name; return it");return helpString;}}}SLog(@" - invalid current word -> start parsing for current function scope");// if we're in the RConsole do parse the current line onlyif(console) {parseRange = [parseString lineRangeForRange:NSMakeRange(selectedRange.location, 0)];// ignore any prompt signsparseRange.location += 1;parseRange.length -= 1;} else {parseRange = NSMakeRange(0, [parseString length]);}// sanety check; if it fails bailif(selectedRange.location - parseRange.location <= 0) {SLog(@" - parse range invalid - bail");return nil;}// set the to be parsed rangeparseRange.length = selectedRange.location - parseRange.location;// go back according opened/closed parenthesesBOOL opened, closed;BOOL found = NO;for(index = NSMaxRange(parseRange) - 1; index > parseRange.location; index--) {unichar c = CFStringGetCharacterAtIndex((CFStringRef)parseString, index);closed = (c == ')');opened = (c == '(');// Check if we're not inside of quotes or commentsif( ( closed || opened)&& (index > parseRange.location)&& [self parserContextForPosition:(index)] != pcExpression) {continue;}if(closed) parentheses--;if(opened) parentheses++;if(parentheses > 0) {found = YES;break;}}if(!found) {SLog(@" - parsing unsuccessfull; bail");return nil;}SLog(@" - first not closed ( found at index: %d", index);// check if we still are in the parse range; otherwise bailif(parseRange.location > index) {SLog(@" - found index invalid - bail");return nil;}// check if char in front of ( is not a white space; if so go backif(index > 0) {while(index > 0 && index >= parseRange.location) {unichar c = CFStringGetCharacterAtIndex((CFStringRef)parseString, --index);if(c == ' ' || c == '\t' || c == '\n' || c == '\r') {;} else {break;}}}SLog(@" - function name found at index: %d", index);// check if we still are in the parse range; otherwise bailif(parseRange.location > index) {SLog(@" - found index invalid - bail");return nil;}// get the adjacent word accordinghelpString = [parseString substringWithRange:[self getRangeForCurrentWordOfRange:NSMakeRange(index, 0)]];SLog(@" - found function name: “%@”", helpString);// if a word was found and the word doesn't represent a numeric value return itif([helpString length] && ![[[NSNumber numberWithFloat:[helpString floatValue]] stringValue] isEqualToString:helpString]) {SLog(@" - return found function name");return helpString;}SLog(@" - found function name wasn't valid, i.e. empty or represents a numeric value; return nil");return nil;}- (BOOL) isRConsole{return console;}#pragma mark -- (void)changeFont:(id)sender{NSFont *font= [[NSFontPanel sharedFontPanel] panelConvertFont:[NSUnarchiver unarchiveObjectWithData:[[NSUserDefaults standardUserDefaults] dataForKey:console ? RConsoleDefaultFont : RScriptEditorDefaultFont]]];if(!font) return;// If user selected something change the selection's font onlyif(!console && ([[[[self window] windowController] document] isRTF] || [self selectedRange].length)) {// register font change for undoNSRange r = [self selectedRange];[self shouldChangeTextInRange:r replacementString:[[self string] substringWithRange:r]];[[self textStorage] addAttribute:NSFontAttributeName value:font range:r];[self setAllowsUndo:NO];[self setSelectedRange:NSMakeRange(r.location, 0)];[self insertText:@""];[self setSelectedRange:r];[self setAllowsUndo:YES];[self setNeedsDisplay:YES];// otherwise update view and save new font in Preferences} else {[[RController sharedController] fontSizeChangedBy:0.0f withSender:nil];}}@end