/* SourceEditorDocument.m Implementation of the SourceEditorDocument class for the ProjectManager application. Copyright (C) 2005, 2006 Saso Kiselkov 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. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ #import "SourceEditorDocument.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import "CommandQueryPanel.h" #import "LineQueryPanel.h" #import "TextFinder.h" #import "EditorRulerView.h" #import "EditorTextView.h" /** * Checks whether a character is a delimiter. * * This function checks whether `character' is a delimiter character, * (i.e. one of "(", ")", "[", "]", "{", "}") and returns YES if it * is and NO if it isn't. Additionaly, if `character' is a delimiter, * `oppositeDelimiter' is set to a string denoting it's opposite * delimiter and `searchBackwards' is set to YES if the opposite * delimiter is located before the checked delimiter character, or * to NO if it is located after the delimiter character. */ static inline BOOL CheckDelimiter(unichar character, unichar * oppositeDelimiter, BOOL * searchBackwards) { if (character == '(') { *oppositeDelimiter = ')'; *searchBackwards = NO; return YES; } else if (character == ')') { *oppositeDelimiter = '('; *searchBackwards = YES; return YES; } else if (character == '[') { *oppositeDelimiter = ']'; *searchBackwards = NO; return YES; } else if (character == ']') { *oppositeDelimiter = '['; *searchBackwards = YES; return YES; } else if (character == '{') { *oppositeDelimiter = '}'; *searchBackwards = NO; return YES; } else if (character == '}') { *oppositeDelimiter = '{'; *searchBackwards = YES; return YES; } else { return NO; } } /** * Attempts to find a delimiter in a certain string around a certain location. * * Attempts to locate `delimiter' in `string', starting at * location `startLocation' a searching forwards (backwards if * searchBackwards = YES) at most 1000 characters. The argument * `oppositeDelimiter' denotes what is considered to be the opposite * delimiter of the one being search for, so that nested delimiters * are ignored correctly. * * @return The location of the delimiter if it is found, or NSNotFound * if it isn't. */ unsigned int FindDelimiterInString(NSString * string, unichar delimiter, unichar oppositeDelimiter, unsigned int startLocation, BOOL searchBackwards) { unsigned int i; unsigned int length; unichar (*charAtIndex)(id, SEL, unsigned int); SEL sel = @selector(characterAtIndex:); int nesting = 1; charAtIndex = (unichar (*)(id, SEL, unsigned int)) [string methodForSelector: sel]; if (searchBackwards) { if (startLocation < 1000) length = startLocation; else length = 1000; for (i=1; i <= length; i++) { unichar c; c = charAtIndex(string, sel, startLocation - i); if (c == delimiter) nesting--; else if (c == oppositeDelimiter) nesting++; if (nesting == 0) break; } if (i > length) return NSNotFound; else return startLocation - i; } else { if ([string length] < startLocation + 1000) length = [string length] - startLocation; else length = 1000; for (i=1; i < length; i++) { unichar c; c = charAtIndex(string, sel, startLocation + i); if (c == delimiter) nesting--; else if (c == oppositeDelimiter) nesting++; if (nesting == 0) break; } if (i == length) return NSNotFound; else return startLocation + i; } } @interface SourceEditorDocument (Private) - (void) pipeOutputOfCommand: (NSString *) command; - (void) updateMiniwindowIconToEdited: (BOOL) flag; - (void) computeNewParenthesisNesting; @end @implementation SourceEditorDocument (Private) - (void) pipeOutputOfCommand: (NSString *) command { NSTask * task; NSPipe * inPipe, * outPipe; NSString * inString, * outString; NSFileHandle * inputHandle; BOOL hadTrailingNewlineInInput; inString = [[textView string] substringWithRange: [textView selectedRange]]; hadTrailingNewlineInInput = ([inString characterAtIndex: [inString length] - 1] == '\n'); inPipe = [NSPipe pipe]; outPipe = [NSPipe pipe]; task = [[NSTask new] autorelease]; [task setLaunchPath: @"/bin/sh"]; [task setArguments: [NSArray arrayWithObjects: @"-c", command, nil]]; [task setStandardInput: inPipe]; [task setStandardOutput: outPipe]; [task setStandardError: outPipe]; inputHandle = [inPipe fileHandleForWriting]; [task launch]; [inputHandle writeData: [inString dataUsingEncoding: NSUTF8StringEncoding]]; [inputHandle closeFile]; [task waitUntilExit]; outString = [[[NSString alloc] initWithData: [[outPipe fileHandleForReading] availableData] encoding: NSUTF8StringEncoding] autorelease]; // strip a trailing newline character if it was not present in the input if ([outString characterAtIndex: [outString length] - 1] == '\n' && hadTrailingNewlineInInput == NO) { outString = [outString substringToIndex: [outString length] - 1]; } if ([task terminationStatus] != 0) { if (NSRunAlertPanel(_(@"Error running command"), _(@"The command returned with a non-zero exit status" @" -- aborting pipe.\n" @"Do you want to see the command's output?\n"), _(@"No"), _(@"Yes"), nil) == NSAlertAlternateReturn) { NSRunAlertPanel(_(@"The command's output"), outString, nil, nil, nil); } } else { [textView replaceCharactersInRange: [textView selectedRange] withString: outString]; [self textDidChange: nil]; } } /** * This method updates the document's miniwindow icon to either an edited * or unedited version. The names of the icons which to use are searched * for using -[NSImage imageNamed:]. The icon names are: * * - "File_" for the unedited version * - "File__mod" for the edited (dirty) version. * * In case there is no dirty version of the icon, the normal icon is left * in place. * * @param flag YES if the window's miniwindow icon should be set to the * edited version, NO if it should be reset to the un-edited version. */ - (void) updateMiniwindowIconToEdited: (BOOL) flag { NSImage * icon; if (flag) { icon = [NSImage imageNamed: [NSString stringWithFormat: @"File_%@_mod", [self fileType]]]; } else { icon = [NSImage imageNamed: [NSString stringWithFormat: @"File_%@", [self fileType]]]; } if (icon != nil) { [myWindow setMiniwindowImage: icon]; } } - (void) computeNewParenthesisNesting { NSRange selectedRange; NSString * myString; if ([[NSUserDefaults standardUserDefaults] boolForKey: @"DontTrackNesting"]) { return; } selectedRange = [textView selectedRange]; if (parenthesesHighlighted) { [textView setNeedsDisplayInHighlightedCharacters: YES]; parenthesesHighlighted = NO; } // if we have a character at the selected location, check // to see if it is a delimiter character myString = [textView string]; if (selectedRange.length <= 1 && [myString length] > selectedRange.location) { unichar c; // we must initialize these explicitly in order to make // gcc shut up about flow control unichar oppositeDelimiter = 0; BOOL searchBackwards = NO; c = [myString characterAtIndex: selectedRange.location]; // if it is, search for the opposite delimiter in a range // of at most 1000 characters around it in either forward // or backward direction (depends on the kind of delimiter // we're searching for). if (CheckDelimiter(c, &oppositeDelimiter, &searchBackwards)) { unsigned int result; result = FindDelimiterInString(myString, oppositeDelimiter, c, selectedRange.location, searchBackwards); // and in case a delimiter is found, highlight it if (result != NSNotFound) { parenthesisA = selectedRange.location; parenthesisB = result; parenthesesHighlighted = YES; [textView setNeedsDisplayInHighlightedCharacters: YES]; } } } } @end @implementation SourceEditorDocument - (void) dealloc { TEST_RELEASE(string); TEST_RELEASE(defaultFont); TEST_RELEASE(textColor); TEST_RELEASE(highlightColor); TEST_RELEASE(backgroundColor); TEST_RELEASE(insertionPointColor); [super dealloc]; } - init { if ((self = [super init]) != nil) { NSUserDefaults * df = [NSUserDefaults standardUserDefaults]; NSData * data; ASSIGN(defaultFont, [HKSyntaxHighlighter defaultFont]); data = [df dataForKey: @"EditorHighlightColor"]; if (data != nil) { ASSIGN (highlightColor, [NSKeyedUnarchiver unarchiveObjectWithData: data]); } else { ASSIGN (highlightColor, [NSColor redColor]); } data = [df dataForKey: @"EditorTextColor"]; if (data != nil) { ASSIGN (textColor, [NSKeyedUnarchiver unarchiveObjectWithData: data]); } data = [df dataForKey: @"EditorBackgroundColor"]; if (data != nil) { ASSIGN (backgroundColor, [NSKeyedUnarchiver unarchiveObjectWithData: data]); } data = [df dataForKey: @"EditorInsertionPointColor"]; if (data != nil) { ASSIGN (insertionPointColor, [NSKeyedUnarchiver unarchiveObjectWithData: data]); } return self; } else { return nil; } } - (BOOL) readFromFile: (NSString *) fileName ofType: (NSString *) fileType { NSString * aString = [NSString stringWithContentsOfFile: fileName]; if (aString != nil) { ASSIGN (string, aString); return YES; } else { return NO; } } - (BOOL) writeToFile: (NSString *) fileName ofType: (NSString *) fileType { BOOL result; result = [[textView string] writeToFile: fileName atomically: NO]; if (result == YES) [self updateMiniwindowIconToEdited: NO]; return result; } // TODO - clean this method up - (void) awakeFromNib { NSScrollView * enclosingScrollView; NSRulerView * ruler; NSRect frame; NSMutableDictionary * typingAttrs; NSUserDefaults * df; if (textColor != nil) { [textView setTextColor: textColor]; } if (backgroundColor != nil) { [textView setBackgroundColor: backgroundColor]; [textView setDrawsBackground: YES]; } if (insertionPointColor != nil) { [textView setInsertionPointColor: insertionPointColor]; } // set a wide width so that lines don't wrap at the view boundary frame = [textView frame]; frame.size.width = 4096; [textView setFrame: frame]; // don't allow users to change the font with the font panel [textView setFont: defaultFont]; /* turn off ligatures - they're useless for a code editor */ typingAttrs = [[[textView typingAttributes] mutableCopy] autorelease]; [typingAttrs setObject: [NSNumber numberWithInt: 0] forKey: NSLigatureAttributeName]; [textView setTypingAttributes: typingAttrs]; enclosingScrollView = [textView enclosingScrollView]; // add a horizontal scroller [enclosingScrollView setHasHorizontalScroller: YES]; // configure the vertical ruler [enclosingScrollView setHasVerticalRuler: YES]; { NSMutableParagraphStyle * paraStyle; NSMutableDictionary * typingAttributes; EditorRulerView * erv = [[[EditorRulerView alloc] initWithScrollView: enclosingScrollView orientation: NSVerticalRuler] autorelease]; float value = [defaultFont defaultLineHeightForFont]; [enclosingScrollView setHasVerticalRuler: YES]; [enclosingScrollView setVerticalRulerView: erv]; [erv setRuleThickness: [[NSFont systemFontOfSize: [NSFont smallSystemFontSize]] widthOfString: @"99999"]]; // set up a right margin so that the text view's contents are a // bit offset from the rule [erv setMargin: 2]; [erv setUnitSize: value]; // fixate the line height typingAttributes = [[[textView typingAttributes] mutableCopy] autorelease]; paraStyle = [[[typingAttributes objectForKey: NSParagraphStyleAttributeName] mutableCopy] autorelease]; [paraStyle setMinimumLineHeight: value]; [paraStyle setMaximumLineHeight: value]; [typingAttributes setObject: paraStyle forKey: NSParagraphStyleAttributeName]; [textView setTypingAttributes: typingAttributes]; } // required due to a GNUstep bug - we have to make the scroll view // forget about it's present horizontal ruler (which get's archived // incorrectly - why??) and then recreate it later on correctly. [enclosingScrollView setHasHorizontalRuler: NO]; // check to see if the font is fixed pitch. If yes, set up a // horizontal ruler as well // FIXME: this should use -isFixedPitch, but this method is broken // on GNUstep - fix it! if ([defaultFont widthOfString: @"a"] == [defaultFont widthOfString: @"i"]) { EditorRulerView * erv = [[[EditorRulerView alloc] initWithScrollView: enclosingScrollView orientation: NSHorizontalRuler] autorelease]; [enclosingScrollView setHasHorizontalRuler: YES]; [erv setOriginOffset: [[enclosingScrollView verticalRulerView] ruleThickness]]; [enclosingScrollView setHorizontalRulerView: erv]; [erv setReservedThicknessForMarkers: 0]; [erv setUnitSize: [defaultFont widthOfString: @"a"]]; [textView setDrawsColumnIndicationGuideline: YES]; } else { [enclosingScrollView setHasHorizontalRuler: NO]; [textView setDrawsColumnIndicationGuideline: NO]; } [enclosingScrollView setRulersVisible: YES]; [textView replaceCharactersInRange: NSMakeRange(0, 0) withString: string]; [textView setSelectedRange: NSMakeRange(0, 0)]; DESTROY(string); [self updateMiniwindowIconToEdited: NO]; df = [NSUserDefaults standardUserDefaults]; if (![df boolForKey: @"DisableSyntaxHighlighting"]) { [textView createSyntaxHighlighterForFileType: [self fileType]]; } } - (NSString *) windowNibName { return @"Editor"; } - (NSString *) displayName { if ([self fileName] != nil) { return [NSString stringWithFormat: @"%@ -- %@", [[self fileName] lastPathComponent], [[self fileName] stringByDeletingLastPathComponent]]; } else { return [super displayName]; } } - (void) customPipeOutput: sender { NSString * command; command = [(CommandQueryPanel *) [CommandQueryPanel shared] runModal]; if (command != nil) { [self pipeOutputOfCommand: command]; } } - (void) pipeOutput: sender { NSString * command; command = [[[[NSUserDefaults standardUserDefaults] objectForKey: @"UserPipes"] objectForKey: [sender title]] objectForKey: @"Command"]; if (command != nil) { [self pipeOutputOfCommand: command]; } else { NSRunAlertPanel(_(@"Associated command not found"), _(@"I couldn't find the command associated with this user-\n" @"defined pipe. Your user-defaults could be corrupt..."), nil, nil, nil); } } - (void) goToLine: sender { LineQueryPanel * lqp = [LineQueryPanel shared]; if ([lqp runModal] == NSOKButton) { [self goToLineNumber: (unsigned int) [lqp unsignedIntValue]]; } } - (void) goToLineNumber: (unsigned int) lineNumber { unsigned int offset; unsigned int i; NSString * line; NSEnumerator * e; NSArray * lines = [[textView string] componentsSeparatedByString: @"\n"]; e = [lines objectEnumerator]; NSRange r; for (offset = 0, i=1; (line = [e nextObject]) != nil && i < lineNumber; i++, offset += [line length] + 1); if (line != nil) { r = NSMakeRange(offset, [line length]); } else { r = NSMakeRange([[textView string] length], 0); } [textView setSelectedRange: r]; [textView scrollRangeToVisible: r]; } - (void) textViewDidChangeSelection: (NSNotification *) notification { if (editorTextViewIsPressingKey == NO) { [self computeNewParenthesisNesting]; } [(EditorRulerView *) [[textView enclosingScrollView] horizontalRulerView] refreshHighlightedArea]; [(EditorRulerView *) [[textView enclosingScrollView] verticalRulerView] refreshHighlightedArea]; } /** * Notification method invoked when the text view changes. This method * marks the window as edited and sets the window's miniwindow icon to * an edited (normally dimmed) version. */ - (void) textDidChange: (NSNotification *) notif { if (![self isDocumentEdited]) { [self updateMiniwindowIconToEdited: YES]; } [self updateChangeCount: NSChangeDone]; } - (void) editorTextViewWillPressKey: sender { editorTextViewIsPressingKey = YES; } - (void) editorTextViewDidPressKey: sender { [self computeNewParenthesisNesting]; editorTextViewIsPressingKey = NO; } - (void) findNext: sender { [[TextFinder sharedInstance] findNext: self]; } - (void) findPrevious: sender { [[TextFinder sharedInstance] findPrevious: self]; } - (void) jumpToSelection: sender { [textView scrollRangeToVisible: [textView selectedRange]]; } - (NSIndexSet *) highlightedCharacterIndexes { if (parenthesesHighlighted) { NSMutableIndexSet * indexes = [NSMutableIndexSet indexSet]; [indexes addIndex: parenthesisA]; [indexes addIndex: parenthesisB]; return indexes; } else { return nil; } } - (NSColor *) highlightColor { return highlightColor; } - (NSColor *) textColor { return textColor; } @end