/* EmacsText.m
 *
 *  EmacsText is a subclass of Text which adds support for
 * keyboard bindings commonly used by the emacs editor.
 *
 * You may freely copy, distribute, and reuse the code in this example.
 * NeXT disclaims any warranty of any kind, expressed or  implied, as to its
 * fitness for any particular use.
 *
 * Written by:  Julie Zelenski
 * Created:  Sept/91
 */

#import "EmacsText.h"

@implementation EmacsText

/** This is the charCode offset for Control keys from Encoding Vectors Tech Doc **/

#define CONTROL_OFFSET (unsigned short)0x40


/** Cursor Movement Commands **/

#define CTRL_A ('A'  - CONTROL_OFFSET)
#define CTRL_B ('B'  - CONTROL_OFFSET)
#define CTRL_E ('E'  - CONTROL_OFFSET)
#define CTRL_F ('F'  - CONTROL_OFFSET)
#define CTRL_N ('N'  - CONTROL_OFFSET)
#define CTRL_P ('P'  - CONTROL_OFFSET)
#define ALT_LESS ((unsigned short)0xa3)
#define ALT_GREATER ((unsigned short) 0xb3)
#define ALT_B ((unsigned short) 0xe5)
#define ALT_F ((unsigned short) 0xa6)

/** Delete Commands  **/

#define CTRL_D ('D'  - CONTROL_OFFSET)
#define CTRL_K ('K'  - CONTROL_OFFSET)
#define CTRL_O ('O'  - CONTROL_OFFSET)
#define CTRL_Y ('Y'  - CONTROL_OFFSET)
#define ALT_D ((unsigned short) 0x44)
#define ALT_H ((unsigned short) 0xe3)



typedef struct _sel
{
    unsigned short charCode;
    SEL selector;
    SEL positionSelector;
    char *selectorString;
    char *positionSelectorString;
} SelectorItem;

static SelectorItem emacsMetaKeys[] = 
{
{ALT_B, 0, 0, "moveToPosition:", "positionForWordBegin"},
{ALT_F, 0, 0, "moveToPosition:", "positionForWordEnd"},
{ALT_LESS, 0, 0, "moveToPosition:", "positionForDocumentBegin"},
{ALT_GREATER, 0, 0, "moveToPosition:", "positionForDocumentEnd"},
{ALT_D, 0, 0, "deleteToPosition:", "positionForWordEnd"},
{ALT_H, 0, 0, "deleteToPosition:", "positionForWordBegin"},
{0}
};

static SelectorItem emacsControlKeys[] = 
{
{CTRL_A, 0, 0, "moveToPosition:", "positionForLineBegin"},
{CTRL_E, 0, 0, "moveToPosition:", "positionForLineEnd"},
{CTRL_K, 0, 0, "deleteToLineEnd", 0},
{CTRL_D, 0, 0, "deleteToPosition:", "nextPositionIfEmpty"},
{CTRL_Y, 0, 0, "yank", 0},
{0}
};

unsigned short emacsFilter (unsigned short
    charCode, int flags, unsigned short charSet)
{
    if (flags & NX_CONTROLMASK) 
    {         
	switch(charCode) {
	    case CTRL_F:
		return NX_RIGHT;
	    case CTRL_B:
	    	return NX_LEFT;
	    case CTRL_N:
	    	return NX_DOWN;
	    case CTRL_P:
	    	return NX_UP;
	    default: break;
	}
    } 
    return NXEditorFilter(charCode, flags, charSet);
}


int GetPrevious(NXStream *s)
{
     int pos;
     int ch;
     
     pos = NXTell(s);
     if (pos <= 0) return EOF;
     NXSeek(s, --pos, NX_FROMSTART);
     ch = NXGetc(s);
     NXUngetc(s);
     return ch;
}

// Complete the build of the selector tables
+initialize
{
    SelectorItem *cur;

    for (cur = emacsMetaKeys; cur->charCode; cur++)
    {
	cur->selector = sel_getUid(cur->selectorString);
	cur->positionSelector = sel_getUid(cur->positionSelectorString);
    }

    for (cur = emacsControlKeys; cur->charCode; cur++)
    {
	cur->selector = sel_getUid(cur->selectorString);
	cur->positionSelector = sel_getUid(cur->positionSelectorString);
    }
    return self;
}

- (int)positionForLineBeginActual
/* Not currently in use.  Looks for newline to find actual paragraph begin.
 */
{
    NXStream *s = [self stream];
    int pos;
    int ch;
    
    if (spN.cp < 0) return 0; // Is this the right thing to do here?

    NXSeek(s, sp0.cp, NX_FROMSTART);
    while (((ch = GetPrevious(s)) != EOF) && (ch != '\n'));
    pos = NXTell(s);
    if (ch != EOF) pos++;
    return pos;
}

- (int)positionForLineEndActual
/* Not currently in use.  Looks for newline to find actual paragraph end.
 */
{
    NXStream *s = [self stream];

    int pos;
    int ch;
    int max = [self textLength];
    
    if (spN.cp < 0) return 0; 
    if (spN.cp > max) return max;

    NXSeek(s, spN.cp, NX_FROMSTART);
    while (((ch = NXGetc(s)) != EOF) && (ch != '\n'));
    pos = NXTell(s);
    if (ch != EOF) pos--;
    return pos;
}

- (int)positionForLineEndVisual
/* This uses the break array to find the visual line end.  
 * However, it subtracts one from the position because of that behavior 
 * of the Text object that makes the position at the end of one line 
 * the same character position a the beginning of next line.  Seems to
 * be no way to position the insertion point at the end of the line.
 * Bummer.
 */
{
    int lineLength;
    int line;
    
    line = (spN.line /sizeof(NXLineDesc));
    lineLength = theBreaks->breaks[line] & 0x3fff;
    lineLength--; // Notice the hack....
    return (spN.c1st + lineLength);
}

- (int)positionForLineBeginVisual
{
    return (sp0.c1st);
}

/** BIG FAT HAIRY NOTE
 * This is how to change CTRL-A, CTRL-E, CTRL-K to use paragraph ends
 * (actual newlines) instead of visual line breaks.  Have the position
 * for line end methods call to the position for actual rather than
 * visual.
 */

- (int)positionForLineBegin
{
    return [self positionForLineBeginVisual];
}

- (int)positionForLineEnd
{
    return [self positionForLineEndVisual];

}

- (int)nextPositionIfEmpty
{
     if (sp0.cp == spN.cp) 
	return spN.cp + 1;
    else
	return spN.cp;
}

/* This is my quick decision on what characters count as a word, and which
 * don't.  The correct way to do this is to parse the ClickTable, but the
 * documentation is so incredibly sparse on this one....
 */
 
#define NORMAL_CHAR(ch) (((ch >= 'a') && (ch <= 'z')) || ((ch >= 'A') && (ch <= 'Z')) ||((ch >= '0') && (ch <= '9')) || (ch == '\'')|| (ch == '_'))


- (int)positionForWordEnd
{
    NXStream *s = [self stream];

    int pos;
    int ch;
    int max = [self textLength];
    
    if (spN.cp < 0) return 0; 	// boundary conditions?  Is this right idea?
    if (spN.cp > max) return max;

    NXSeek(s, spN.cp, NX_FROMSTART);
    while ((ch = NXGetc(s)) != EOF && !NORMAL_CHAR(ch)); // skip white space
    while ((ch = NXGetc(s)) != EOF && NORMAL_CHAR(ch));	// jump normal chars
    pos = NXTell(s);
    if (ch != EOF) pos--;
    return pos;
}

- (int)positionForWordBegin
{
    NXStream *s = [self stream];

    int pos;
    int ch;
    int max = [self textLength];
    
    if (spN.cp < 0) return 0; 	// boundary conditions?  Is this right idea?
    if (spN.cp > max) return max;

    NXSeek(s, sp0.cp, NX_FROMSTART);
    while ((ch = GetPrevious(s)) != EOF && !NORMAL_CHAR(ch)); // skip white space
    while ((ch = GetPrevious(s)) != EOF && NORMAL_CHAR(ch)); // jump normal chars
    pos = NXTell(s);
    if (ch != EOF) pos++;
    return pos;
}

- (int) positionForDocumentEnd
{
     return [self textLength];
}

- (int) positionForDocumentBegin
{
     return 0;
}

- moveToPosition:(SEL)command
{
    int pos;
    
    pos = (int)[self perform:command];
    [self setSel:pos :pos];
    [self scrollSelToVisible];
    return self;
}

- deleteToPosition:(SEL)command
/* Entry point for delete forward/backward word
 */
{
    int pos;
    int start,end;
    
    pos = (int)[self perform:command];
    if (pos > spN.cp) 
    { 	// if position extends to the right
    	start = sp0.cp;
	end = pos;
    } 
    else 
    {		// else position extends to the left
    	start = pos;
	end = spN.cp;
    }
    [self delete:start :end];
    return self;
}

- delete:(int)start :(int)end
/* Entry point for all deletes done for Emacs bindings.  Turns off 
 * autodisplay to avoid flicker and other unwanted drawing artifacts.
 * Calls cut and uses the Pasteboard to implement yank.  It is possible
 * to implement separate Emacs kill buffer, but it would be a bit of
 * hassle, because you need a Change object to keep both the runs and
 * the text that is yanked.  You can do it quick by storing only ASCII
 * text, which is not a good idea.  (Actually, to be correct, this is
 * all that Edit does, but who wants to use Edit for a role model?)
 */
{
    if (end - start) 
    {
	[self setAutodisplay:NO];
	[self setSel:start :end];
	[self cut:self];
	[[self setAutodisplay:YES] display];
    }
    return self;
}


- deleteToLineEnd
/* Somewhat icky hack has to handle the special case for deleting at end 
 * of line.  If in middle of line, don't delete the new line.  If at the 
 * very end of the line, do delete the new line.
 */
{
    int pos;
    int start,end;
    
    pos = [self positionForLineEnd];
    start = sp0.cp;
    end = pos;
    if (start == end) {// If already at end of line
	int line;
	int endsWithNewLine;
	
	line = (spN.line /sizeof(NXLineDesc));
	endsWithNewLine = theBreaks->breaks[line] & 0x4000;

	if (endsWithNewLine) 
	    end++;
	else  // Bail on case where at visual end of line, but no newline
	    return self;
    }
    [self delete:start :end];
    return self;
}


- yank
{
    [self paste:self];
    return self;
}


- (BOOL) emacsEvent:(NXEvent *)event
{
    SelectorItem *cur;
    unsigned charCode = event->data.key.charCode;
    
    if (event->flags & NX_CONTROLMASK) 
    {  
    	cur = emacsControlKeys;
			
	while (cur->charCode && (cur->charCode != charCode)) cur++;
	if (cur->charCode) 
	{
	    [self perform:cur->selector 
	    	withSel:(cur->positionSelector? cur->positionSelector : 0)];
	    return YES;
	}
    }
    if (event->flags & NX_ALTERNATEMASK) 
    {  
    	cur = emacsMetaKeys;
			
	while (cur->charCode && (cur->charCode != charCode)) cur++;
	if (cur->charCode) 
	{
	    [self perform:cur->selector 
	    	withSel:(cur->positionSelector? cur->positionSelector : 0)];
	    return YES;
	}
    }
    return NO;
}

- keyDown:(NXEvent *)event
{
    if ([self emacsEvent:event]) 
	return self;
    else
	return [super keyDown:event];
}

- (int)perform:(SEL)selector withSel:(SEL)helper 
{
    int   (*func)(id,SEL,SEL); 
	
    func = (int (*)(id,SEL,SEL))[self methodFor:selector];
    return (* func)(self, selector, helper);
}

- initFrame:(const NXRect *)fRect
{
    NXRect r = *fRect;
    [super initFrame:fRect];
    [self setMonoFont:NO];
    [self setBackgroundGray:NX_WHITE];
    [self setOpaque:YES];
    [self setCharFilter:(NXCharFilterFunc)emacsFilter];
    [self notifyAncestorWhenFrameChanged: YES];
    [self setVertResizable:YES];
    [self setMinSize:&r.size];
    r.size.height = 1.0e30;
    [self setMaxSize:&r.size];
    return self;
}


@end
