This is EmacsText.m in view mode; [Download] [Up]
/* 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
*
* supplements by: Heinrich Giesen
* and reformatted:Jan/96
*/
#import "EmacsText.h"
@implementation EmacsText
/** This is the charCode offset for Control keys from Encoding Vectors Tech Doc **/
#define CONTROL_OFFSET (unsigned short)0x40
#define LBRACE '{'
#define RBRACE '}'
#define LPARA '('
#define RPARA ')'
#define LBRACKET '['
#define RBRACKET ']'
/** 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;
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;
}
}
if( (charCode==RBRACE) || (charCode==RBRACKET) || (charCode==RPARA) ){
[super keyDown:event];
[self findMatching:charCode andLit:1];
return YES;
}
return NO;
}
- keyDown:(NXEvent *)event
{
if ([self emacsEvent:event]) return self;
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;
}
//=============== Giesen ==========================
- (void) findBackward:(char)xChar andLit:(int)lit
{
int brackets = 1;
int charPos = -1;
char cc;
char closing;
NXStream *stream = [self stream];
int origStart, origEnd;
switch( xChar ){
case LBRACE : closing = RBRACE; break;
case LBRACKET : closing = RBRACKET; break;
case LPARA : closing = RPARA; break;
default : return;
}
if( sp0.cp<=0 ){
NXBeep();
return;
}
origStart = sp0.cp;
origEnd = spN.cp;
charPos = sp0.cp;
if( origStart==origEnd ) charPos--; // it's the cursor
NXSeek(stream, charPos, NX_FROMSTART );
while( brackets && (charPos != -1) ) {
if (spN.cp > 1) {
charPos--;
cc = GetPrevious(stream);
if( cc == xChar ) brackets--;
if( cc == closing ) brackets++;
}
else charPos = -1;
}
if (charPos != -1) {
if( lit==0 ){ // do nothing
return;
}
if( lit==1 ){ // show short
[self setSel:charPos :charPos+1];
NXPing();
usleep( 500000 );
[self setSel:origStart :origEnd];
NXPing();
}
if( lit==2 ){ // show all
[self setSel:charPos :origEnd];
NXPing();
//sleep(1);
}
}
else {
NXBeep();
}
}
- (void) findForward:(char)xChar andLit:(int)lit
{
int brackets = 1;
int charPos = -1;
char cc;
char opening;
NXStream *stream = [self stream];
NXSelPt origStart, origEnd;
switch( xChar ){
case RBRACE : opening = LBRACE; break;
case RBRACKET : opening = LBRACKET; break;
case RPARA : opening = LPARA; break;
default : return;
}
[self getSel: &origStart :&origEnd];
charPos = origStart.cp;
cc = xChar;
while(brackets != 0 && cc != EOF) {
charPos++;
cc = NXGetc(stream);
if (cc == opening) brackets++;
if (cc == xChar) brackets--;
}
if ( (charPos > origStart.cp) && (cc != EOF) ) {
if( lit==0 ){ // do nothing
return;
}
if( lit==1 ){ // show short
return;
}
if( lit==2 ){
[self setSel:origStart.cp :charPos+1];
NXPing();
//sleep(1);
}
}
else {
NXBeep();
}
}
- (void) findMatching:(char)xChar andLit:(int)lit
{
switch (xChar) {
case LBRACKET:
[self findForward:RBRACKET andLit:lit];
break;
case LBRACE:
[self findForward:RBRACE andLit:lit];
break;
case LPARA:
[self findForward:RPARA andLit:lit];
break;
case RBRACKET:
[self findBackward:LBRACKET andLit:lit];
break;
case RBRACE:
[self findBackward:LBRACE andLit:lit];
break;
case RPARA:
[self findBackward:LPARA andLit:lit];
break;
}
}
- mouseDown:(NXEvent *)theEvent
{
NXSelPt origStart, origEnd;
NXStream *stream = [self stream];
int charPos = -1;
[super mouseDown:theEvent];
if( theEvent->data.mouse.click!=2 ) return self;
// it is a double click
[self getSel: &origStart :&origEnd];
charPos = origStart.cp;
if(stream && (charPos >=0)) {
NXSeek(stream, charPos, NX_FROMSTART);
[self findMatching:NXGetc(stream) andLit:2];
}
return self;
}
@end
These are the contents of the former NiCE NeXT User Group NeXTSTEP/OpenStep software archive, currently hosted by Netfuture.ch.