/**
 ** EvalText.m
 **
 ** A text subclass which provides emacs key bindings
 ** and "colored" program text. 
 **/

#import "EvalText.h"
#import "Eval.h"
#import "CodeBrowser.h"
#import "Finder.h"
#import <stdlib.h>
#import <strings.h>
#import <appkit/appkit.h>
#import <defaults/defaults.h>
#import <libc.h>
#import <ctype.h>


#define ESC    (27)
#define CTRL_SPACE (0)
#define CTRL_A (1)
#define CTRL_B (2)
#define CTRL_C (3)
#define CTRL_D (4)
#define CTRL_E (5)
#define CTRL_F (6)
#define CTRL_K (11)
#define CTRL_L (12)
#define CTRL_N (14)
#define CTRL_O (15)
#define CTRL_P (16)
#define CTRL_S (19)
#define CTRL_V (22)
#define CTRL_W (23)
#define CTRL_X (24)
#define CTRL_Y (25)

/**
 ** keyWords[] is a structure used by format: sender to
 ** identify keyWords in transcript text
 **/
static struct 
{ char * word ;
   int len ;
} keyWords[] = 
  { {"int",3},
    {"char",4},
    {"float",5},
    {"double",6},
    {"struct",6},
    {"union",5},
    {"long",4},
    {"short",5},
    {"unsigned",8},
    {"auto",4},
    {"extern",6},
    {"register",8},
    {"typedef",7},
    {"static",6},
    {"goto",4},
    {"return",6},
    {"sizeof",6},
    {"break",5},
    {"continue",8},
    {"if",2},
    {"else",4},
    {"for",3},
    {"do",2},
    {"while",5},
    {"switch",6},
    {"case",4},
    {"default",7},
    {"entry",5},  /* is this a keyword in ansi C ? */
    {"const",5},
    {"id",2},
    {"void",4},
    {NULL,0}
  } ;


/**
 ** Fragments of the following procedures were borrowed from
 ** Lee boynton's LispExample program, then hacked to bits.
 **/

static char killedText[10000] ; // buffer for "killed" text...
// since it is statically allocated, this thing could blow up
// if > 10000 chars are written to the kill buffer.  If I had
// I'd make this grow as needed....

static BOOL useEmacsBindings = 1 ; 

typedef struct {
    id text;
    NXTextBlock *block;
} textInfo;

static char getPrevious(NXStream *s)
{ // NXBGetc - a text stream macro to get the previous character.
  textInfo *info = (textInfo *) s->info;
  NXTextBlock *block = info->block->prior;
  if (!block)
  { return EOF;
  }
  s->buf_base = block->text;
  s->buf_ptr = s->buf_base + block->chars;
  s->offset -= block->chars;
  info->block = block;
  return *(--s->buf_ptr);
}

#define NXBGetc(s) \
    (((s)->buf_base == (s)->buf_ptr) ? getPrevious(s) : \
	*(--((s)->buf_ptr)) )


int findMatchingOpenParen(id text, char thisChar, char thatChar, int curPos)
{ int count = 1;
  char ch;
  NXStream *s = [text stream];
  NXSeek(s, curPos, NX_FROMSTART);
  while ((ch = NXBGetc(s)) != EOF)
  { if(ch == thatChar && !(--count))
      return NXTell(s);
    else if (ch == thisChar)
      count++;
  }
  return -1;
}


int
findMatchingCloseParen(id text, char thisChar, char thatChar, int curPos)
{ int count = 1;
  char ch;
  NXStream *s = [text stream];
  NXSeek(s, curPos, NX_FROMSTART);
  while ((ch = NXGetc(s)) != EOF)
  { if(ch == thatChar && !(--count))
      return NXTell(s);
    else if (ch == thisChar)
      count++;
  }
  return -1;
}

void highlightChar(id text, int charPosition)
{ NXSelPt selStart, selEnd;
  [text getSel:&selStart :&selEnd];
  [text setSel:charPosition :charPosition+1];
  [[text window] flushWindow];
  NXPing();
  [[text window] disableFlushWindow];
  [text setSel:selStart.cp :selEnd.cp];
  [[text window] reenableFlushWindow];
}


@implementation EvalText:Text


// macros which write rtf to a stream called "cookedStream"
// for creating fancy text for various code categories
#define PREPROC NXPrintf(cookedStream,"\\f0%s\\cf0%s ",\
  fontStuff[0][0],fontStuff[0][2])    
#define  DIRECTIVE NXPrintf(cookedStream,"\\f1%s\\cf1%s ",\
  fontStuff[1][0],fontStuff[1][2])    
#define METHODDEF NXPrintf(cookedStream,"\\f2%s\\cf2%s ",\
  fontStuff[2][0],fontStuff[2][2])    
#define COMMENT NXPrintf(cookedStream,"\\f3%s\\cf3%s ",\
  fontStuff[3][0],fontStuff[3][2])    
#define STRING NXPrintf(cookedStream,"\\f4%s\\cf4%s ",\
  fontStuff[4][0],fontStuff[4][2])    
#define  KEYWORD NXPrintf(cookedStream,"\\f5%s\\cf5%s ",\
  fontStuff[5][0],fontStuff[5][2])    
#define NORMAL NXPrintf(cookedStream,"\\f6%s\\cf6%s ",\
  fontStuff[6][0],fontStuff[6][2])    

#define TEXTDEFAULTKNT 7 /* number of text categories */

- awakeFromNib ;
{ // Do some setup work...I am my own delegate
  [self setDelegate: self] ;
  return self ;
}

- changeFont: sender ;
{  return self ;
}

- clearMessage: sender ;
{ // Clear the message in the transcript.  Called by keydown
  // via a delayed message send.
  [window message: ""] ;
  return self ; 
}

- format: sender ;
{ // Color and set font attributes for text in the
  // transcript.  If sender responds to
  // to the methodList: method, (which must should return a List),
  // then allocates and adds an EvalString with the first line
  // of each method name to this methodlist.
  char textBuf[1024] ;
  char fontStuff[TEXTDEFAULTKNT][3][40], defName[12] ;
  const char *defaults ;
  int i, j, keyWordLen, charNum, braceLevel = 0 ; 
  BOOL inString = NO, inComment = NO, inSlashSlashComment = NO,
       inMethodDef = NO, inPreProc = NO, newline;
  NXStream *rawStream, *cookedStream ;
  id methodList = nil ;
  if([sender respondsTo: @selector(methodList)])
  { methodList = [sender methodList] ;
  }    
  rawStream = [self stream] ;
  cookedStream = NXOpenMemory(NULL,0,NX_READWRITE) ;
  NXPrintf(cookedStream,"{\\rtf0\\ansi{\\fonttbl") ;
  // get the font information from defaults database
  for(i = 0 ; i < TEXTDEFAULTKNT ; i++)
  { // Manufacture default name 
    sprintf(defName,"text%1d",i) ;
    defaults = NXGetDefaultValue([NXApp appName],defName) ;
    // parse the default value
    sscanf(defaults,"%s %s %s %s",
       textBuf, fontStuff[i][0], fontStuff[i][1],fontStuff[i][2]) ;
    NXPrintf(cookedStream,"\\f%1d\\fnil %s;", i,textBuf) ;
   }
   NXPrintf(cookedStream,"}\n") ;
   NXPrintf(cookedStream,"{\\colortbl") ;
   for(i = 0 ; i < TEXTDEFAULTKNT ; i++)
   { NXPrintf(cookedStream,fontStuff[i][1]) ;
     NXPrintf(cookedStream,";") ;
   }
   NXPrintf(cookedStream,"}\n") ;
   charNum = 0 ;
   NORMAL ;
   do
   { NXSeek(rawStream, charNum, NX_FROMSTART) ;
     newline = EvalGets(rawStream,textBuf) ;
     inString = NO ; // ANSI C forbids newline in string
     // now examine the string, adding rtf directives as required
     for(i = 0 ; textBuf[i] ;) 
     { switch(textBuf[i])
       { case '/':
           if(inString)
              NXPutc(cookedStream,textBuf[i++]) ;
           else if(textBuf[i + 1] == '*' || textBuf[i+1] == '/')
           { if(!inComment)
             { COMMENT ;
               inComment = YES ;
               if(textBuf[i+1] == '/') // behaves specially!
                 inSlashSlashComment = YES ;
             }
             NXPutc(cookedStream,textBuf[i++]) ;
           } 
           else
             NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '*':
           if(inComment && textBuf[i+1] == '/' && !inSlashSlashComment)
           { NXPutc(cookedStream,textBuf[i++]) ;
             NXPutc(cookedStream,textBuf[i++]) ;
             if(inMethodDef)
               METHODDEF ;
             else
               NORMAL ;
             inComment = NO ;
           }
           else
             NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '"':
            if(inComment || inPreProc)
              NXPutc(cookedStream,textBuf[i++]) ;
            else if(inString) 
            { NXPutc(cookedStream,textBuf[i++]) ;
              if(textBuf[i - 2] != '\\')
              { inString = NO ;
                NORMAL ;
              }
            }
            else
            { if(i == 0 || textBuf[i - 1] != '\\')
              { inString = YES ;
                STRING ;
              }
              NXPutc(cookedStream,textBuf[i++]) ;
            }
            break ;
         case '}' :
           if(!inComment && !inString && !inPreProc)
           { if(braceLevel > 0) // don't allow this to go negative
               braceLevel-- ;
           }  
           // must be escaped for rtf
           NXPutc(cookedStream,'\\') ;
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '\\':
           // must be escaped for rtf
           NXPutc(cookedStream,'\\') ;
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '{' :
           if(!inComment && !inString && !inPreProc)
             braceLevel++ ;
           if(braceLevel == 1 && inMethodDef)
           { inMethodDef = NO ;
             NORMAL ; 
           }
           NXPutc(cookedStream,'\\') ;
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '#':
           if(!inComment && i == 0)
           { // a hash in column 0 means a preprocessor line
             PREPROC ;
             inPreProc = YES ;
           }
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '@':
           if(!inString && !inComment && !inPreProc) 
           { // then this is a compiler directive
             DIRECTIVE ;
             if((i == 0) && methodList) // if a compiler directive is in
             // column 0, put it into the methodList 
               [methodList addObject: [[EvalString alloc] init: textBuf]] ;
             NXPutc(cookedStream,textBuf[i++]) ;
             while(isalnum(textBuf[i]))
               NXPutc(cookedStream,textBuf[i++]) ;
             NORMAL ;
           }
           else
             NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case '-':
         case '+':
           if(!inComment && !inString && !inPreProc && braceLevel == 0)
           { for(j = 0 ; j < i ; j++)
               if(!isspace(textBuf[j]) && textBuf[j] != '\\') break ;
             if(j == i)
             { // this is not a truly accurate way of finding the beginning of a
               // method def, but it will work "most" of the time...
               inMethodDef = YES ;
               METHODDEF ;
               if(methodList)
                 [methodList addObject: [[EvalString alloc] init: textBuf]] ;
             }
           }
           if(textBuf[i] == '-') // must be escaped
             NXPutc(cookedStream,'\\') ;
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         case ' ':
         case '\t':
           NXPutc(cookedStream,textBuf[i++]) ;
           break ;
         default:
           if(!inComment && !inString && !inMethodDef && !inPreProc &&
              (i == 0 || !(isalnum(textBuf[i-1]) || textBuf[i-1] == '_')))
           { // check for a keyword
             for(j = 0 ; keyWordLen = keyWords[j].len ; j++)
             { if(!strncmp(&textBuf[i],keyWords[j].word,keyWordLen))    
               { int followChar = i + keyWordLen  ; 
                 int isKeyWord = !(isalnum(textBuf[followChar])
                                   || textBuf[followChar]=='_') ;
                 if(isKeyWord)
                   KEYWORD ;
                 NXPrintf(cookedStream,"%s\n",keyWords[j].word) ;
                 if(isKeyWord)
                   NORMAL ;
                 i += keyWordLen  ;
                 break ;
               }
             }
             if(!keyWordLen) // no keyWord match found
               NXPutc(cookedStream,textBuf[i++]) ;
           }
          else
            NXPutc(cookedStream,textBuf[i++]) ;
          break ;
       }
    }
    // check for a preprocessor continuation line
    if(inPreProc && textBuf[i - 1] != '\\') 
    { inPreProc = NO ;
       NORMAL ;
    }
    // terminate a slash-slash style comment
    if(inSlashSlashComment)
    { inSlashSlashComment = inComment = NO ;
      if(inMethodDef)
        METHODDEF ;
      else
        NORMAL ;
    }
    if(newline) // output a quoted newline
    { NXPutc(cookedStream,'\\') ;
      NXPutc(cookedStream,'\n') ;
    }
    charNum += strlen(textBuf) + 1;
  } while(!NXAtEOS(rawStream)) ;
  NXSeek(cookedStream, 0, NX_FROMSTART) ;
  [self readRichText: cookedStream] ;
  NXCloseMemory(cookedStream,NX_FREEBUFFER) ;
  return self ;
}


#define MARK_UNSET -1

- keyDown:(NXEvent *)theEvent
{ // provide some emacs-style key bindings. We make
  // the assumption that we are subview of a scrollview.
  int i ;
  static unsigned short val = '\0', lastChar = '\0', nextChar ;
  static BOOL firstKey = TRUE, definingMacro = NO,
      insertChar, replayingMacro = NO ;
  static long mark = MARK_UNSET ; 
  NXSelPt selStart, selEnd;
  static NXStream * macroStream ;
  if(!useEmacsBindings)
    return [super keyDown: theEvent] ;

  if(firstKey) // make sure killedText is empty
  { killedText[0] = '\0' ;
    firstKey = FALSE ;
    macroStream = NXOpenMemory(NULL, 0, NX_READWRITE) ;
  }
  [self getSel:&selStart :&selEnd];
  do
  { lastChar = val ; // val was set in "previous" call of keyDown..
    val = theEvent->data.key.charCode + (theEvent->data.key.charSet << 8);
    if(definingMacro)
      NXPutc(macroStream,theEvent->data.key.charCode) ;
    insertChar = YES ;
    switch ((char) val)
    { case ESC: // don't want ESC to cause a beep
        insertChar = NO ;
        break ;
      case '<':
        if(lastChar != ESC)
          break ;
      case 0xa3:  // ALT-< or ESC <
        { [self setSel: 0 :0] ;
          [self scrollSelToVisible] ;
          insertChar = NO ;
        }
        break ;
      case '>':
       if(lastChar != ESC)
         break ;
      case 0xb3: // ALT-> or ESC >
       { i = [self textLength] ; --i ;
	      [self setSel: i :i] ;
         [self scrollSelToVisible] ;
         insertChar = NO ;   
       }
       break ;
     case '(':
       if((lastChar == CTRL_X) && !definingMacro)
       { // start defining macro
         [window message: "Defining kbd macro..."] ;
         definingMacro = YES ;
         NXSeek(macroStream, 0, NX_FROMSTART) ;
         insertChar = NO ;
       }
       break ;
     case ')':
       if((lastChar == CTRL_X) && definingMacro)
       {  [window message: "Keyboard macro defined."] ;
          [self perform: @selector(clearMessage:) with: self
           afterDelay: 1500 cancelPrevious: YES] ;
          definingMacro = NO ;
          NXSeek(macroStream, -2, NX_FROMCURRENT) ; // seek back 2 to remove ctrl_X )
          NXPutc(macroStream,'\0') ; // null-terminate the stream
          insertChar = NO ; 
       }
       else      
       { i = findMatchingOpenParen(self,')','(',selEnd.cp);
         [super keyDown:theEvent];
         if (i >= 0)
           highlightChar(self,i);
        insertChar = NO ;
        }
       break ;
     case '}':
       i = findMatchingOpenParen(self,'}','{',selEnd.cp);
       [super keyDown:theEvent];
       if(i >= 0)
         highlightChar(self,i);
       insertChar = NO ;
       break ;
     case ']':
       i = findMatchingOpenParen(self,']','[',selEnd.cp);
       [super keyDown:theEvent];
       if (i >= 0)
         highlightChar(self,i);
       insertChar = NO ;
       break ;
     case 'e':
       if(lastChar == CTRL_X)
       { if(!definingMacro)
         { // replay macro
           replayingMacro = YES ;
           NXSeek(macroStream,0L,NX_FROMSTART) ;
           insertChar = NO ;
         }
         else
           NXSeek(macroStream, -1, NX_FROMCURRENT) ; // seek back 1 to remove ctrl_X 
       }
       break ;
     case 'i':
       if(lastChar == CTRL_X)
       { if(!definingMacro)
         { // insert a file into the text
           int rval ;
           id openPanel = [OpenPanel new] ;
           [openPanel allowMultipleFiles: NO] ;
           rval = [openPanel runModalForTypes: NULL] ;
           if(rval == NX_OKTAG)
           { NXStream *fileStream ;
             char fName[2048] ;
             sprintf(fName,"%s/%s",[openPanel directory],*[openPanel filenames]) ;
             if((fileStream = NXMapFile(fName,NX_READONLY)) != NULL)
             { int len,unused ;
               char *buf ;
               [window message: "Inserting file..."] ;
               NXGetMemoryBuffer(fileStream,&buf,&len,&unused) ;
               [self replaceSel: buf length: len] ;
               [window message: ""] ;
             }
             else
               NXRunAlertPanel("Eval","Couldn't insert file: %s",NULL,NULL,NULL,fName) ;               
           }
         }
         insertChar = NO ;
       }
       break ;

     case 'k': // CTRL_X k = close window
       if(lastChar == CTRL_X)
       { [window performClose: self] ;
         insertChar = NO ;
       }
       break ;       
     case 's': 
       if(lastChar == CTRL_X)
       { [window save: self] ;
         insertChar = NO ;
       }
       break ;
     case 'v':   
       if(lastChar != ESC)
         break ;
     case 0xd6: // ALT-v or ESC v
       { // move up one screenful
          NXPoint scrollPoint = {0.0,0.0} ;
          NXRect  clipRect ;
          [self convertPoint: &scrollPoint fromView: [superview superview]] ;
          // get the clipview's bounds
          [superview getBounds: &clipRect] ;
          scrollPoint.y -= clipRect.size.height ; // scroll up
          [self scrollPoint: &scrollPoint] ;  
          insertChar = NO ;
        }
        break ;    
     case 'w':
       if(lastChar != ESC)
         break ;
     case 0xc8: // ALT-w or ESC w
       { int subStringLength ;
         if(mark == MARK_UNSET)
           break ; 
         [self getSel:&selStart :&selEnd];
         if(selStart.cp < mark)
           [self getSubstring: killedText
            start: selStart.cp length: subStringLength = mark - selStart.cp] ;
         else
           [self getSubstring: killedText
            start: mark length: subStringLength = selStart.cp - mark] ;
         killedText[subStringLength] = '\0' ;
         insertChar = NO ;
       }
       break ;
     case CTRL_SPACE: // set mark
       [self getSel:&selStart :&selEnd];
       mark = selStart.cp ;
       insertChar = NO ;
       break ;
     case CTRL_A: // beginning of line
       [self getSel:&selStart :&selEnd];
       i = [self lineFromPosition:selStart.cp] ;
       i = [self positionFromLine: i] ;
       [self setSel:i :i];
       insertChar = NO ;
       break ;
     case CTRL_C: 
       if(lastChar == CTRL_X)
       { [NXApp terminate: self] ;
         insertChar = NO ;
       }
       break ;
     case CTRL_E: // end of line
       [self getSel:&selStart :&selEnd];
       i = [self lineFromPosition: selStart.cp] ;
       i = [self positionFromLine: i + 1] ; // beginning of next line
       if(i == -1) // then there is no next line...
         i = [self textLength] + 1 ;
       [self setSel:i -1 :i - 1];
       insertChar = NO ;
       break ;
     case CTRL_B:// backwards one char
       [self getSel:&selStart :&selEnd];
       if(selStart.cp > 1)
         [self setSel:selStart.cp - 1 :selStart.cp -1];
       else
         NXBeep();
       insertChar = NO ;
       break ;
     case CTRL_F: // CTRL_X CTRL_F = open file, CTRL_F forwards one char
       if(lastChar == CTRL_X)
       { [NXApp open: self] ;
       }
       else
       { [self getSel:&selStart :&selEnd];
         if(selEnd.cp < [self textLength])
           [self setSel:selEnd.cp+1 :selEnd.cp+1];
         else
           NXBeep();
        }
        insertChar = NO ;
        break ;
     case CTRL_D: // delete char under cursor
       [self getSel:&selStart :&selEnd];
       [self setSel:selEnd.cp :selEnd.cp+1];
       [self replaceSel:""];
       [self setSel:selStart.cp :selStart.cp] ;
       insertChar = NO ;
       break ;
     case CTRL_K: // clear to end of line, place killed text in killedText 
     // if line is empty, then delete the newline (and place at end of killedText)
     { int currentEnd = 0 ;
       [self getSel:&selStart :&selEnd];
       i = [self lineFromPosition: selStart.cp] ;
       i = [self positionFromLine: i + 1] ; // i is index of beginning of next line
       if(i == -1) // then there is no next line...
       { i = [self textLength] ;
       }
       else // there is another line
       { if(i - selStart.cp > 1) // if line is not empty,
         i-- ;  // don't include the newline.
       }
       // successive CTRL_K's append to killedText ; any other char
       // interrupts the sequence
       if(lastChar == CTRL_K)
         currentEnd = strlen(killedText) ;
       [self getSubstring:  &killedText[currentEnd]
         start:selStart.cp length:i- selStart.cp] ;
       killedText[i - selStart.cp + currentEnd] = '\0' ;
       [self setSel:selStart.cp :i];
       [self replaceSel:""];
       [self setSel:selStart.cp :selStart.cp] ;
       insertChar = NO ;
       break ;
     }
     case CTRL_L: // center the current selection
     { // get the selection's location in the scrollview's coordinates
       NXPoint loc, scrollPoint = {0.0,0.0} ;
       NXRect  scrollRect ;
       float delta ;
       id sView = [[self superview] superview] ;
       [self getSel:&selStart :&selEnd];
       loc.x = selStart.x ;
       loc.y = selStart.y ;
       [self convertPoint: &loc toView: sView] ;
       // get the scrollview's bounds
       [sView getBounds: &scrollRect] ;
       // how far is loc.y from middle of scrollview?...reset delta to this value
       delta = scrollRect.size.height / 2.0 - loc.y ;
       [sView convertPoint: &scrollPoint toView: self] ;
       scrollPoint.y -= delta ;
       [self scrollPoint: &scrollPoint] ;  
       insertChar = NO ;
       break ;     
     }
     case CTRL_O: // "open" a line...i.e. add blank line before current
       [self getSel:&selStart :&selEnd];
       [self setSel: selStart.cp :selStart.cp] ;
       [self replaceSel: "\n"] ;            
       [self setSel: selStart.cp :selStart.cp] ;
       insertChar = NO ;
       break ;
     case CTRL_N: // move directly up or down one line
     case CTRL_P: // move directly up one line...fake up arrow
        { NXEvent fakeEvent ;
          bcopy((char *) theEvent,(char *) &fakeEvent,sizeof(NXEvent)) ;
          fakeEvent.data.key.charCode = val == CTRL_N ? 175 :173 ;
          fakeEvent.data.key.charSet = 1 ;
          fakeEvent.flags = NX_NUMERICPADMASK ;
          [super keyDown: &fakeEvent] ;
          insertChar = NO ;
         }
        break ;
     case CTRL_S:  // ctrl x ctrl s = save
       if(lastChar == CTRL_X)
       { [window save: self] ;
         insertChar = NO ;
       }
       else
       { extern id finderObject ; // declared in Finder.m
         [finderObject doFind: self] ; 
       }
       break ;
      case CTRL_V: // move down one screenful
       {  NXPoint scrollPoint = {0.0,0.0} ;
          NXRect  clipRect ;

          [self convertPoint: &scrollPoint fromView: [superview superview]] ;
          // get the clipview's bounds
          [superview getBounds: &clipRect] ;
          scrollPoint.y += clipRect.size.height ; // scroll down
          [self scrollPoint: &scrollPoint] ;  
          insertChar = NO ;
          break ;    
        }
      case CTRL_W: // delete text from point to mark ; add to killedText
        if(mark == MARK_UNSET)
          break ;
        [self getSel:&selStart :&selEnd];
        if(selStart.cp < mark)
        {  [self getSubstring: killedText
            start: selStart.cp length:mark - selStart.cp] ;
           [self setSel: selStart.cp :mark] ;
        }
        else
        { [self getSubstring: killedText
            start: mark length: selStart.cp - mark] ;
          [self setSel: mark :selStart.cp] ;
        } 
        [self replaceSel: ""] ;          
        insertChar = NO ;
        break ;
       case CTRL_X: // don't want CTRL_X to cause a beep
        insertChar = NO ;
        break ;
      case CTRL_Y: // yank killedText 
        [self getSel:&selStart :&selEnd];
        [self setSel:selStart.cp :selStart.cp] ;
        [self replaceSel: killedText] ;
        insertChar = NO ;
        break ;
      default:
        break;
    }
    if(insertChar)
      [super keyDown: theEvent] ;
    if(replayingMacro)
    { nextChar = NXGetc(macroStream) ;
      if(nextChar == '\0')
      { replayingMacro = NO ;
        NXPing() ;
      }
      else
        theEvent->data.key.charCode = nextChar ;
    }
  } while(replayingMacro) ;
  return self ;
}


- mouseDown: (NXEvent *) anEvent ;
{ // augment double-click behavior to
  // check for bracketed structures and quoted
  // items. augment command-click behaviour to
  // select between backquotes.
  NXSelPt start, end ;
  char openers[] = "{([<\"'/`" ;
  char closers[] = "})]>\"'/`" ;
  char c,d, *place ;
  int otherPos ;
  NXStream *aStream ;
  if(anEvent->data.mouse.click == 1)
    [super mouseDown: anEvent] ;
  else if(anEvent->data.mouse.click == 2)
  { aStream = [self stream] ;
    [self getSel: &start :&end] ;
    if(start.cp > 0)
    { NXSeek(aStream,start.cp - 1,NX_FROMSTART) ;
      c = NXGetc(aStream) ;
      if(place = index(closers,c)) // we have right side of a structure
      {  d = openers[(int) place - (int) closers] ;// grab matching char
         if( (otherPos = findMatchingOpenParen(self, c, d, start.cp -1))
           != -1)
        { if(c == '`') // put selection "inside" ` ... ` pair
            [self setSel: otherPos + 1 :start.cp - 1] ;
          else
            [self setSel: otherPos :start.cp] ;
          return self ;
        }
        else
          [super mouseDown: anEvent] ;
      }
      else
      { NXSeek(aStream,start.cp,NX_FROMSTART) ;
        c = NXGetc(aStream) ;
        if(place = index(openers,c)) // we have left side of a structure
        { d = closers[(int) place - (int) openers] ;
          if( (otherPos = findMatchingCloseParen(self, c, d, start.cp +1))
           != -1)
          { if(c == '`') // put selection "inside" ` ... ` pair
             [self setSel: start.cp +1 :otherPos-1] ;
           else
             [self setSel: start.cp :otherPos] ;
          }
        }
        else
          [super mouseDown: anEvent] ;
      }
    }
  }
  else // deal with triple, quadruple, etc.  clicks here...
    [super mouseDown: anEvent] ; 
  if((anEvent->data.mouse.click == 1) &&
     (anEvent->flags & NX_COMMANDMASK))
  { // command click == search back for backquote (or beginning of text)
    // forward for backquote (or end ot text), then select. 
    int from, to ;
    aStream = [self stream] ;
    [self getSel: &start :&end] ;
    if(start.cp > 0)
    { from = start.cp - 1 ;
      NXSeek(aStream,from,NX_FROMSTART) ;
      while(from >= 1 && ((c = NXBGetc(aStream)) != '`'))
        from-- ;
      to = start.cp ;
      NXSeek(aStream,to,NX_FROMSTART) ;
      while(!NXAtEOS(aStream) && ((c = NXGetc(aStream)) != '`'))
        to++ ;
      [self setSel: from :to] ;
    }
  }
  return self ;
}

- textDidGetKeys: sender isEmpty: (BOOL) flag
{ [window setDocEdited: YES] ;     
  return self ;
}

- useEmacsBindings: (BOOL) YESorNO ;
{ useEmacsBindings = YESorNO ;
  return self ;
}

@end