// // ICeCoffEETerminal.m // ICeCoffEE // // Created by Nicholas Riley on Mon Jan 28 2002. // Copyright (c) 2002 Nicholas Riley. All rights reserved. // #import "ICeCoffEETerminal.h" #import "ICeCoffEE.h" #import "ICeCoffEEParser.h" #import #include static NSRange ICCF_zeroRange = { NSNotFound, 5 }; #define min(a,b) (a < b ? a : b) #define max(a,b) (a > b ? a : b) @interface TermStorage:NSObject - (struct _FSelPt)selPt0; - (struct _FSelPt)selPt1; - (BOOL)hasSelection; - (void)startSelectionAtLine:(unsigned int)row column:(unsigned short)column; - (void)startSelectionAtLine:(unsigned int)row offset:(unsigned short)offset; - (void)endSelectionAtLine:(unsigned int)row offset:(unsigned short)offset; - (void)selectWhitespaceDelimitedTextAtLine:(unsigned int)line offset:(unsigned short)offset; - (void)selectWordAtLine:(unsigned int)line offset:(unsigned short)offset; - (NSString *)selectedString; - (unsigned int)numberOfLines; - (unsigned int)effectiveColumnsForLine:(unsigned int)line; - (void)clearSelection; - (BOOL)isSelected:(unsigned int)line :(unsigned int)column; @end @interface TermController:NSObject - (TermStorage *)storage; @end @implementation ICeCoffEETermSubviewSuper // NSTextInput implementation - (void)insertText:(id)aString {} - (void)doCommandBySelector:(SEL)aSelector {} - (void)setMarkedText:(id)aString selectedRange:(NSRange)selRange {} - (void)unmarkText {} - (BOOL)hasMarkedText { return NO; } - (long)conversationIdentifier { return 0; } - (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange { return nil; } - (NSRange)markedRange { return ICCF_zeroRange; } - (NSRange)selectedRange { return ICCF_zeroRange; } - (NSRect)firstRectForCharacterRange:(NSRange)theRange { return NSZeroRect; } - (unsigned int)characterIndexForPoint:(NSPoint)thePoint { return 0; } - (NSArray*)validAttributesForMarkedText { return nil; } // NSDraggingDestination - (NSDragOperation)draggingEntered:(id )sender { return NSDragOperationNone; } // misc. other stuff - (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column {} - (void)setNeedsDisplay {} // NSView methods (needed to avoid crash in super invocation) - (void)mouseUp:(NSEvent *)e { [super mouseUp: e]; } - (void)mouseDown:(NSEvent *)e { [super mouseDown: e]; } @end @interface ICeCoffEETerminalRange : NSObject { TermStorage *storage; struct _FSelPt pt0; struct _FSelPt pt1; unsigned int width; unsigned int height; } + (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal; + (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1; - (id)initWithTerminal:(ICeCoffEETerminal *)terminal; - (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1; - (struct _FSelPt)pt0; - (struct _FSelPt)pt1; - (NSString *)stringFromRange; - (void)growBackwardByLength:(unsigned long)length; - (void)growForwardByLength:(unsigned long)length; - (void)shrinkBackByLength:(unsigned long)length; - (void)shrinkFrontByLength:(unsigned long)length; - (BOOL)rangeIsEmpty; - (void)select; @end @implementation ICeCoffEETerminalRange + (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal; { return [[[self alloc] initWithTerminal: terminal] autorelease]; } + (ICeCoffEETerminalRange *)rangeWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1; { return [[[self alloc] initWithTerminal: terminal pt0: selPt0 pt1: selPt1] autorelease]; } - (id)initWithTerminal:(ICeCoffEETerminal *)terminal; { if ( (self = [self init]) != nil) { storage = [(TermController *)[terminal valueForKey: @"controller"] storage]; pt0 = [storage selPt0]; pt1 = [storage selPt1]; width = [storage effectiveColumnsForLine: pt0.line]; height = [storage numberOfLines]; } return self; } - (id)initWithTerminal:(ICeCoffEETerminal *)terminal pt0:(struct _FSelPt)selPt0 pt1:(struct _FSelPt)selPt1; { if ( (self = [self initWithTerminal: terminal]) != nil) { pt0 = selPt0; pt1 = selPt1; } return self; } - (struct _FSelPt)pt0; { return pt0; } - (struct _FSelPt)pt1; { return pt1; } - (NSString *)stringFromRange; { struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1]; NSString *str; [storage startSelectionAtLine: pt0.line offset: pt0.col]; [storage endSelectionAtLine: pt1.line offset: pt1.col]; str = [storage selectedString]; [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col]; [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col]; return str; } - (struct _FSelPt)_moveBack:(struct _FSelPt)pt byLength:(unsigned long)length; { unsigned int extraLines = length / width; if ((long)(pt.line - extraLines) < 0) { pt.line = 0; pt.col = 0; } else if (pt.line - extraLines == 0) { pt.line = 0; pt.col += width - (length % height); if (pt.col < width) pt.col = 0; else pt.col -= width; } else { pt.line -= extraLines; pt.col += width - (length % height); if (pt.col < width) pt.line--; else pt.col -= width; } return pt; } - (struct _FSelPt)_moveForward:(struct _FSelPt)pt byLength:(unsigned long)length; { unsigned int extraLines = length / width; if (pt.line + extraLines >= height) { pt.line = height - 1; pt.col = width - 1; } else if (pt.line + extraLines == height - 1) { pt.line = height - 1; pt.col += length % height; if (pt.col >= width) pt.col = width - 1; } else { pt.line += extraLines; pt.col = pt.col + (length % height); if (pt.col > width) { pt.line++; pt.col -= width; } } return pt; } - (BOOL)rangeIsEmpty; { return (pt1.line < pt0.line) || (pt1.line == pt0.line && pt1.col <= pt0.col); } - (void)growBackwardByLength:(unsigned long)length; { pt0 = [self _moveBack: pt0 byLength: length]; } - (void)growForwardByLength:(unsigned long)length; { pt1 = [self _moveForward: pt1 byLength: length]; } - (void)shrinkFrontByLength:(unsigned long)length; { pt0 = [self _moveForward: pt0 byLength: length]; } - (void)shrinkBackByLength:(unsigned long)length; { pt1 = [self _moveBack: pt1 byLength: length]; } - (void)select; { [storage startSelectionAtLine: pt0.line offset: pt0.col]; [storage endSelectionAtLine: pt1.line offset: pt1.col]; } - (NSString *)description; { return [NSString stringWithFormat: @"L%uC%hu - L%uC%hu: |%@|", pt0.line, pt0.col, pt1.line, pt1.col, [[self stringFromRange] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]]]; } @end // TermSubview's NSTextInput implementation is essentially a noop: make one that does something @implementation ICeCoffEETerminal // XXX normally in category (NSTextInput), but APE doesnŐt work if I do that - (NSRange)selectedRange { TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage]; struct _FSelPt selPt0 = [storage selPt0], selPt1 = [storage selPt1]; unsigned width = [storage effectiveColumnsForLine: selPt0.line]; unsigned pt0 = selPt0.line * width + selPt0.col; ICLog(@"selPt0 %d x %d selPt1 %d x %d", selPt0.line, selPt0.col, selPt1.line, selPt1.col); return NSMakeRange(pt0, selPt1.line * width + selPt1.col - pt0); } // XXX string isn't padded: what we need is something like the ICeCoffEE 1.2.x method, adapted to Terminal 1.2, but we've chosen another way - (NSAttributedString *)attributedSubstringFromRange:(NSRange)theRange; { TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage]; struct _FSelPt oldPt0 = [storage selPt0], oldPt1 = [storage selPt1], realPt0, realPt1; unsigned pt1 = theRange.location + theRange.length; unsigned width = [storage effectiveColumnsForLine: 0]; NSAttributedString *str; realPt0.line = theRange.location / width; realPt0.col = theRange.location % width; realPt1.line = pt1 / width; realPt1.col = pt1 % width; [storage startSelectionAtLine: realPt0.line offset: realPt0.col]; [storage endSelectionAtLine: realPt1.line offset: realPt1.col]; str = [[NSAttributedString alloc] initWithString: [storage selectedString]]; NSAssert2([str length] == theRange.length, @"Substring has length %lu when we expected %lu", [str length], theRange.length); [storage startSelectionAtLine: oldPt0.line offset: oldPt0.col]; [storage endSelectionAtLine: oldPt1.line offset: oldPt1.col]; return [str autorelease]; } static NSEvent *ICCF_downEvent; static unsigned int ICCF_line; static unsigned short ICCF_col; static BOOL ICCF_optionClickRegistered; - (void)setSelectedRange:(NSRange)charRange affinity:(NSSelectionAffinity)affinity stillSelecting:(BOOL)stillSelectingFlag; { TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage]; unsigned width = [storage effectiveColumnsForLine: 0]; unsigned pt1 = charRange.location + charRange.length; [storage startSelectionAtLine: charRange.location / width offset: charRange.location % width]; [storage endSelectionAtLine: pt1 / width offset: pt1 % width]; // [self refresh]; } void ICCF_LaunchURLFromTerminal(ICeCoffEETerminal *self) { NSCharacterSet *urlLeftDelimiters = nil, *urlRightDelimiters = nil; ICeCoffEETerminalRange *termRange = nil, *selRange = nil; NSString *s; NSRange range, delimiterRange; NS_DURING TermStorage *storage = [(TermController *)[self valueForKey: @"controller"] storage]; if ([storage hasSelection] && [storage isSelected: ICCF_line : ICCF_col]) { selRange = [ICeCoffEETerminalRange rangeWithTerminal: self]; } else { // select something // XXX test this next line, it may be what's causing a Terminal bug to exhibit itself [storage selectWordAtLine: ICCF_line offset: ICCF_col]; selRange = [ICeCoffEETerminalRange rangeWithTerminal: self]; NSCAssert(![selRange rangeIsEmpty], ICCF_LocalizedString(@"Sorry, ICeCoffEE was unable to find anything to select")); } // However, word selection does not capture even the approximate boundaries of a URL // (text to a space/line ending character); it'll stop at a period in the middle of a hostname. // So, we expand it as follows: ICCF_Delimiters(&urlLeftDelimiters, &urlRightDelimiters); termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt0] pt1: [selRange pt0]]; [termRange growBackwardByLength: ICCF_MAX_URL_LEN]; // potentially too big expandFront: s = [termRange stringFromRange]; ICLog(@"front %@", termRange); delimiterRange = [s rangeOfCharacterFromSet: urlLeftDelimiters options: NSLiteralSearch | NSBackwardsSearch]; if (delimiterRange.location == NSNotFound) { // extend to beginning of string (as much as possible) [selRange growBackwardByLength: [s length]]; } else { NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1"); [selRange growBackwardByLength: [s length] - delimiterRange.location - 1]; // in url/(parens)stuff, handle clicking inside or after (parens). if ([s characterAtIndex: delimiterRange.location] == '(') { s = [selRange stringFromRange]; if ([s rangeOfString: @")"].location != NSNotFound || [s rangeOfCharacterFromSet: [NSCharacterSet characterSetWithCharactersInString: @"/."]].location == NSNotFound) { [selRange growBackwardByLength: 1]; ICLog(@"expanding past (, now |%@|", selRange); [termRange shrinkBackByLength: [[termRange stringFromRange] length] - delimiterRange.location]; goto expandFront; } } } ICLog(@"parsed front %@", selRange); termRange = [ICeCoffEETerminalRange rangeWithTerminal: self pt0: [selRange pt1] pt1: [selRange pt1]]; [termRange growForwardByLength: ICCF_MAX_URL_LEN]; // potentially too big expandBack: s = [termRange stringFromRange]; ICLog(@"back %@", termRange); delimiterRange = [s rangeOfCharacterFromSet: urlRightDelimiters options: NSLiteralSearch]; if (delimiterRange.location == NSNotFound) { // extend to end of string [selRange growForwardByLength: [s length]]; } else { NSCAssert(delimiterRange.length == 1, @"Internal error: delimiter matched range is not of length 1"); [selRange growForwardByLength: delimiterRange.location]; // grow URL past closing paren if we've seen an open paren if ([s characterAtIndex: delimiterRange.location] == ')' && [[selRange stringFromRange] rangeOfString: @"("].location != NSNotFound) { [selRange growForwardByLength: 1]; ICLog(@"expanding past ), now |%@|", selRange); [termRange shrinkFrontByLength: delimiterRange.location + 1]; goto expandBack; } } ICCF_StartIC(); s = [selRange stringFromRange]; range = NSMakeRange(0, [s length]); ICCF_CheckRange(range); ICLog(@"parsed back %@", selRange); ICLog(@"range of string %@", NSStringFromRange(range)); ICCF_ParseURL(s, &range); ICLog(@"parsed range %@ |%@|", NSStringFromRange(range), [s substringWithRange: range]); [selRange shrinkFrontByLength: range.location]; [selRange shrinkBackByLength: [s length] - range.length - range.location]; s = [selRange stringFromRange]; ICLog(@"reconstituted URL %@", selRange); [selRange select]; [self setNeedsDisplay]; [[self superview] display]; if (ICCF_LaunchURL(s, ICCF_KeyboardAction([NSApp currentEvent])) && ICCF_prefs.textBlinkEnabled) { int i; // Terminal flashes the selection one more time, so blink one fewer for (i = 1 ; i < ICCF_prefs.textBlinkCount ; i++) { [storage clearSelection]; [self setNeedsDisplay]; [[self superview] display]; usleep(kICBlinkDelayUsecs); [selRange select]; [self setNeedsDisplay]; [[self superview] display]; usleep(kICBlinkDelayUsecs); } } NS_HANDLER ICCF_HandleException(localException, ICCF_downEvent); NS_ENDHANDLER ICCF_StopIC(); } - (void)_optionClickEvent:(NSEvent *)event:(unsigned int)row:(unsigned short)column; { if (ICCF_downEvent != nil) { ICCF_line = row; // XXX are these lines or rows? check with wrapping ICCF_col = column; ICCF_optionClickRegistered = YES; } else { [super _optionClickEvent: event :row :column]; } } - (void)mouseUp:(NSEvent *)upEvent; { ICLog(@"ICeCoffEE Terminal up: %@", upEvent); [super mouseUp: upEvent]; if (ICCF_downEvent != nil) { NSPoint downPt = [ICCF_downEvent locationInWindow]; NSPoint upPt = [upEvent locationInWindow]; if (abs(downPt.x - upPt.x) <= kICHysteresisPixels && abs(downPt.y - upPt.y) <= kICHysteresisPixels) { if (ICCF_optionClickRegistered) { ICCF_optionClickRegistered = NO; ICLog(@"========= launching... %d x %d", ICCF_line, ICCF_col); ICCF_LaunchURLFromTerminal(self); } } [ICCF_downEvent release]; ICCF_downEvent = nil; } } - (void)mouseDown:(NSEvent *)downEvent; { if (ICCF_enabled && ICCF_prefs.commandClickEnabled && ICCF_EventIsCommandMouseDown(downEvent)) { NSEvent *optionClickEvent = [NSEvent mouseEventWithType: NSLeftMouseDown location: [downEvent locationInWindow] modifierFlags: NSAlternateKeyMask timestamp: [downEvent timestamp] windowNumber: [downEvent windowNumber] context: [downEvent context] eventNumber: [downEvent eventNumber] clickCount: 1 pressure: 0]; [ICCF_downEvent release]; ICCF_downEvent = [downEvent retain]; ICLog(@"ICeCoffEE Terminal constructed: %@", optionClickEvent); ICCF_optionClickRegistered = NO; [super mouseDown: optionClickEvent]; } else { [super mouseDown: downEvent]; } } // NSDraggingDestination // -[TermSubview draggingUpdated:] just invokes draggingEntered... // XXX Crashing on repeated drag snap-back can happen even without ICeCoffEE installed; don't bother to try to fix - (NSDragOperation)draggingEntered:(id )sender; { if (!ICCF_prefs.terminalRequireOptionForSelfDrag || [sender draggingSource] != self || ([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)) { [super draggingEntered: sender]; // When doing non-self drags, this works around one bug in Terminal wherein the option key acts as a toggle, and it shouldn't (see Aqua HIG). Unfortunately, this messes up drag feedback for self drags, but I don't know of any way to fix it. Not that most Cocoa apps get it remotely right, anyway. return NSDragOperationCopy; } return NSDragOperationNone; } @end