Goto sanos source index

//
// edit.c
//
// Text editor
//
// Copyright (C) 2002 Michael Ringgaard. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
// 
// 1. Redistributions of source code must retain the above copyright 
//    notice, this list of conditions and the following disclaimer.  
// 2. Redistributions in binary form must reproduce the above copyright
//    notice, this list of conditions and the following disclaimer in the
//    documentation and/or other materials provided with the distribution.  
// 3. Neither the name of the project nor the names of its contributors
//    may be used to endorse or promote products derived from this software
//    without specific prior written permission. 
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
// OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 
// SUCH DAMAGE.
// 

#include <os.h>
#include <stdio.h>
#include <string.h>

#define MINEXTEND 32768

//
// Key codes
//

#define KEY_BACKSPACE   0x101
#define KEY_ESC         0x102
#define KEY_INS         0x103
#define KEY_DEL         0x104
#define KEY_LEFT        0x105
#define KEY_RIGHT       0x106
#define KEY_UP          0x107
#define KEY_DOWN        0x108
#define KEY_CTRL_LEFT   0x109
#define KEY_CTRL_RIGHT  0x10A
#define KEY_HOME        0x10B
#define KEY_END         0x10C
#define KEY_ENTER       0x10D
#define KEY_TAB         0x10E
#define KEY_PGUP        0x10F
#define KEY_PGDN        0x110

#define KEY_UNKNOWN     0xFFF

#define CTRL(c) ((c) - 0x60)

//
// Editor data block
//
// Structure of split buffer:
//
//    +------------------+------------------+------------------+
//    | text before gap  |        gap       |  text after gap  |
//    +------------------+------------------+------------------+
//    ^                  ^                  ^                  ^     
//    |                  |                  |                  |
//  start               gap                rest               end
//

struct editor
{
  unsigned char *start;     // Start of text buffer
  unsigned char *gap;       // Start of gap
  unsigned char *rest;      // End of gap
  unsigned char *end;       // End of text buffer

  int toppos;               // Text position for current top screen line
  int topline;              // Line number for top of screen
  int margin;               // Position for leftmost column on screen

  int linepos;              // Text position for current line
  int line;                 // Current document line
  int col;                  // Current document column
  int lastcol;              // Remembered column from last horizontal navigation

  int refresh;              // Flag to trigger screen redraw
  int lineupdate;           // Flag to trigger redraw of current line
  int dirty;                // Dirty flag is set when the editor buffer has been changed

  int minihelp;             // Flag to control mini help in status line
  int newfile;              // File is a new file

  int cols;
  int lines;
  char *linebuf;
  char filename[MAXPATH];
};

//
// Editor buffer functions
//

int new_file(struct editor *ed, char *filename)
{
  memset(ed, 0, sizeof(struct editor));
  strcpy(ed->filename, filename);

  ed->start = (unsigned char *) malloc(MINEXTEND);
  if (!ed->start) return -1;
#ifdef DEBUG
  memset(ed->start, 0, MINEXTEND);
#endif

  ed->gap = ed->start;
  ed->rest = ed->end = ed->gap + MINEXTEND;
  ed->newfile = 1;
  return 0;
}

int load_file(struct editor *ed, char *filename)
{
  struct stat statbuf;
  int length;

  int f = open(filename, O_RDONLY | O_BINARY);
  if (f < 0) return -1;

  memset(ed, 0, sizeof(struct editor));
  strcpy(ed->filename, filename);

  if (fstat(f, &statbuf) < 0) goto err;
  length = statbuf.st_size;

  ed->start = (unsigned char *) malloc(length + MINEXTEND);
  if (!ed->start) goto err;
#ifdef DEBUG
  memset(ed->start, 0, length + MINEXTEND);
#endif
  if (read(f, ed->start, length) != length) goto err;

  ed->gap = ed->start + length;
  ed->rest = ed->end = ed->gap + MINEXTEND;

  close(f);
  return 0;

err:
  close(f);
  if (ed->start) free(ed->start);
  return -1;
}

int save_file(struct editor *ed, char *filename)
{
  int f;

  if (!filename) filename = ed->filename;

  f = open(filename, O_CREAT | O_TRUNC, 0644);
  if (f < 0) return -1;

  if (write(f, ed->start, ed->gap - ed->start) != ed->gap - ed->start) goto err;
  if (write(f, ed->rest, ed->end - ed->rest) != ed->end - ed->rest) goto err;

  close(f);
  ed->dirty = 0;
  return 0;

err:
  close(f);
  return -1;
}

void delete_editor(struct editor *ed)
{
  if (ed->start) free(ed->start);
  if (ed->linebuf) free(ed->linebuf);
}

void move_gap(struct editor *ed, int pos, int minsize)
{
  int gapsize = ed->rest - ed->gap;
  unsigned char *p = ed->start + pos;
  if (p >= ed->gap) p += (ed->rest - ed->gap);
  if (minsize < 0) minsize = 0;

  if (minsize <= gapsize)
  {
    if (p != ed->rest)
    {
      if (p < ed->gap)
        memmove(p + gapsize, p, ed->gap - p);
      else
        memmove(ed->gap, ed->rest, p - ed->rest);

      ed->gap = ed->start + pos;
      ed->rest = ed->gap + gapsize;
    }
  }
  else
  {
    int newsize;
    unsigned char *start;
    unsigned char *gap;
    unsigned char *rest;
    unsigned char *end;

    if (gapsize + MINEXTEND > minsize) minsize = gapsize + MINEXTEND;
    newsize = (ed->end - ed->start) - gapsize + minsize;
    start = (unsigned char *) malloc(newsize); // TODO check for out of memory
    gap = start + pos;
    rest = gap + minsize;
    end = start + newsize;

    if (p < ed->gap)
    {
      memcpy(start, ed->start, pos);
      memcpy(rest, p, ed->gap - p);
      memcpy(end - (ed->end - ed->rest), ed->rest, ed->end - ed->rest);
    }
    else
    {
      memcpy(start, ed->start, ed->gap - ed->start);
      memcpy(start + (ed->gap - ed->start), ed->rest, p - ed->rest);
      memcpy(rest, p, ed->end - p);
    }

    free(ed->start);
    ed->start = start;
    ed->gap = gap;
    ed->rest = rest;
    ed->end = end;
  }

#ifdef DEBUG
  memset(ed->gap, 0, ed->rest - ed->gap);
#endif
}

int copy(struct editor *ed, unsigned char *buf, int pos, int len)
{
  unsigned char *bufptr = buf;
  unsigned char *p = ed->start + pos;
  if (p >= ed->gap) p += (ed->rest - ed->gap);

  while (len > 0)
  {
    if (p == ed->end) break;

    *bufptr++ = *p;
    len--;

    if (++p == ed->gap) p = ed->rest;
  }

  return bufptr - buf;
}

void replace(struct editor *ed, int pos, int len, unsigned char *buf, int bufsize)
{
  unsigned char *p = ed->start + pos;

  // Handle deletions at the edges of the gap
  if (bufsize == 0 && p <= ed->gap && p + len >= ed->gap)
  {
    ed->rest += len - (ed->gap - p);
    ed->gap = p;
    return;
  }

  // Move the gap
  move_gap(ed, pos + len, bufsize - len);

  // Replace contents
  memcpy(ed->start + pos, buf, bufsize);
  ed->gap = ed->start + pos + bufsize;

  // Mark buffer as dirty
  ed->dirty = 1;
}

void insert(struct editor *ed, int pos, unsigned char *buf, int bufsize)
{
  replace(ed, pos, 0, buf, bufsize);
}

void erase(struct editor *ed, int pos, int len)
{
  replace(ed, pos, len, NULL, 0);
}

__inline int get(struct editor *ed, int pos)
{
  unsigned char *p = ed->start + pos;
  if (p >= ed->gap) p += (ed->rest - ed->gap);
  if (p >= ed->end) return -1;
  return *p;
}

int copy_line(struct editor *ed, unsigned char *buf, int pos, int margin, int len)
{
  unsigned char *bufptr = buf;
  unsigned char *p = ed->start + pos;
  if (p >= ed->gap) p += (ed->rest - ed->gap);

  while (len > 0)
  {
    if (p == ed->end) break;
    if (*p == '\r' || *p == '\n') break;

    if (margin > 0)
      margin--;
    else
    {
      *bufptr++ = *p;
      len--;
    }

    if (++p == ed->gap) p = ed->rest;
  }

  return bufptr - buf;
}

//
// Navigation functions
//

int line_length(struct editor *ed, int linepos)
{
  int pos = linepos;
  while (1)
  {
    int ch = get(ed, pos);
    if (ch < 0 || ch == '\n' || ch == '\r') break;
    pos++;
  }

  return pos - linepos;
}

int line_start(struct editor *ed, int pos)
{
  while (1)
  {
    if (pos == 0) break;
    if (get(ed, pos - 1) == '\n') break;
    pos--;
  }

  return pos;
}

int next_line(struct editor *ed, int pos)
{
  while (1)
  {
    int ch = get(ed, pos);
    if (ch < 0) return -1;
    pos++;
    if (ch == '\n') return pos;
  }
}

int prev_line(struct editor *ed, int pos)
{
  if (pos == 0) return -1;

  while (pos > 0)
  {
    int ch = get(ed, --pos);
    if (ch == '\n') break;
  }

  while (pos > 0)
  {
    int ch = get(ed, --pos);
    if (ch == '\n') return pos + 1;
  }

  return 0;
}

//
// Keyboard functions
//

int getkey()
{
  int ch;

  ch = getchar();
  if (ch < 0) return ch;

  switch (ch)
  {
    case 0x08: return KEY_BACKSPACE;
    case 0x09: return KEY_TAB;
#ifdef SANOS
    case 0x0D: return gettib()->job->term->type == TERM_CONSOLE ? KEY_ENTER : KEY_UNKNOWN;
    case 0x0A: return gettib()->job->term->type != TERM_CONSOLE ? KEY_ENTER : KEY_UNKNOWN;
#else
    case 0x0D: return KEY_ENTER;
#endif
    case 0x1B:
      ch = getchar();
      switch (ch)
      {
        case 0x1B: return KEY_ESC;
        case 0x5B:
          ch = getchar();

          switch (ch)
          {
            case 0x31: return getchar() == 0x7E ? KEY_HOME : KEY_UNKNOWN;
            case 0x32: return getchar() == 0x7E ? KEY_INS : KEY_UNKNOWN;
            case 0x33: return getchar() == 0x7E ? KEY_DEL : KEY_UNKNOWN;
            case 0x34: return getchar() == 0x7E ? KEY_END : KEY_UNKNOWN;
            case 0x35: return getchar() == 0x7E ? KEY_PGUP : KEY_UNKNOWN;
            case 0x36: return getchar() == 0x7E ? KEY_PGDN : KEY_UNKNOWN;

            case 0x41: return KEY_UP;
            case 0x42: return KEY_DOWN;
            case 0x43: return KEY_RIGHT;
            case 0x44: return KEY_LEFT;

            default: return KEY_UNKNOWN;
          }
          break;

        default: return KEY_UNKNOWN;
      }
      break;

    case 0x00:
    case 0xE0:
      ch = getchar();
      switch (ch)
      {
        case 0x47: return KEY_HOME;
        case 0x48: return KEY_UP;
        case 0x49: return KEY_PGUP;
        case 0x4B: return KEY_LEFT;
        case 0x4D: return KEY_RIGHT;
        case 0x4F: return KEY_END;
        case 0x50: return KEY_DOWN;
        case 0x51: return KEY_PGDN;
        case 0x52: return KEY_INS;
        case 0x53: return KEY_DEL;
        case 0x73: return KEY_CTRL_LEFT;
        case 0x74: return KEY_CTRL_RIGHT;
        default: return KEY_UNKNOWN;
      }
      break;

    case 0x7F: return KEY_BACKSPACE;

    default: return ch;
  }
}

//
// Screen functions
//

void outch(char c)
{
  putchar(c);
}

void outbuf(char *buf, int len)
{
  fwrite(buf, 1, len, stdout);
}

void outstr(char *str)
{
  fputs(str, stdout);
}

void clear_screen()
{
  outstr("\033[0J");
}

void gotoxy(int col, int line)
{
  char buf[32];

  sprintf(buf, "\033[%d;%dH", line + 1, col + 1);
  outstr(buf);
}

//
// Display functions
//

void display_message(struct editor *ed, char *msg)
{
  gotoxy(0, ed->lines);
  outstr("\033[1m");
  outstr(msg);
  outstr("\033[K\033[0m");
  fflush(stdout);
}

void draw_full_statusline(struct editor *ed)
{
  char buf[128];

  gotoxy(0, ed->lines);
  if (ed->minihelp)
  {
    sprintf(buf, "\033[1m%-44.44s Ctrl-S=Save Ctrl-Q=Quit %s\033[K\033[0m", ed->filename, ed->newfile ? "(NEW)" : "");
    ed->minihelp = 0;
  }
  else
    sprintf(buf, "\033[1m%-44.44s%c Ln %-6d Col %-4d Pos %-10d\033[K\033[0m", ed->filename, ed->dirty ? '*' : ' ', ed->line + 1, ed->col + 1, ed->linepos + ed->col + 1);
  outstr(buf);
}

void draw_statusline(struct editor *ed)
{
  char buf[128];

  gotoxy(44, ed->lines);
  sprintf(buf, "\033[1m%c Ln %-6d Col %-4d Pos %-10d\033[K\033[0m", ed->dirty ? '*' : ' ', ed->line + 1, ed->col + 1, ed->linepos + ed->col + 1);
  outstr(buf);
}

void update_line(struct editor *ed, int col)
{
  int len;

  len = copy_line(ed, ed->linebuf, ed->linepos, ed->margin + col, ed->cols - col);
  gotoxy(col, ed->line - ed->topline);
  outbuf(ed->linebuf, len);
  if (len + col < ed->cols) outstr("\033[K");
}

void draw_screen(struct editor *ed)
{
  int pos;
  int i;

  gotoxy(0, 0);
  pos = ed->toppos;
  for (i = 0; i < ed->lines; i++)
  {
    int len;

    if (pos < 0) 
    {
      outstr("\033[K\r\n");
    }
    else
    {
      len = copy_line(ed, ed->linebuf, pos, ed->margin, ed->cols);
      outbuf(ed->linebuf, len);
      if (len < ed->cols) outstr("\033[K\r\n");
      pos = next_line(ed, pos);
    }
  }
}

void position_cursor(struct editor *ed)
{
  gotoxy(ed->col - ed->margin, ed->line - ed->topline);
}

//
// Cursor movement
//

void adjust(struct editor *ed)
{
  int ll = line_length(ed, ed->linepos);
  ed->col = ed->lastcol;
  if (ed->col > ll) ed->col = ll;

  while (ed->col < ed->margin)
  {
    ed->margin -= 4;
    if (ed->margin < 0) ed->margin = 0;
    ed->refresh = 1;
  }

  while (ed->col - ed->margin >= ed->cols) 
  {
    ed->margin += 4;
    ed->refresh = 1;
  }
}

void up(struct editor *ed)
{
  int newpos = prev_line(ed, ed->linepos);
  if (newpos < 0) return;

  ed->linepos = newpos;
  ed->line--;
  if (ed->line < ed->topline)
  {
    ed->toppos = ed->linepos;
    ed->topline = ed->line;
    ed->refresh = 1;
  }

  adjust(ed);
}

void down(struct editor *ed)
{
  int newpos = next_line(ed, ed->linepos);
  if (newpos < 0) return;

  ed->linepos = newpos;
  ed->line++;

  if (ed->line >= ed->topline + ed->lines)
  {
    ed->toppos = next_line(ed, ed->toppos);
    ed->topline++;
    ed->refresh = 1;
  }

  adjust(ed);
}

void left(struct editor *ed)
{
  if (ed->col > 0) 
    ed->col--;
  else
  {
    int newpos = prev_line(ed, ed->linepos);
    if (newpos < 0) return;

    ed->col = line_length(ed, newpos);
    ed->linepos = newpos;
    ed->line--;
    if (ed->line < ed->topline)
    {
      ed->toppos = ed->linepos;
      ed->topline = ed->line;
      ed->refresh = 1;
    }
  }

  ed->lastcol = ed->col;
  adjust(ed);
}

void right(struct editor *ed)
{
  if (ed->col < line_length(ed, ed->linepos))
    ed->col++;
  else
  {
    int newpos = next_line(ed, ed->linepos);
    if (newpos < 0) return;

    ed->col = 0;
    ed->linepos = newpos;
    ed->line++;

    if (ed->line >= ed->topline + ed->lines)
    {
      ed->toppos = next_line(ed, ed->toppos);
      ed->topline++;
      ed->refresh = 1;
    }
  }

  ed->lastcol = ed->col;
  adjust(ed);
}

void home(struct editor *ed)
{
  ed->col = ed->lastcol = 0;
  adjust(ed);
}

void end(struct editor *ed)
{
  ed->col = ed->lastcol = line_length(ed, ed->linepos);
  adjust(ed);
}

void pageup(struct editor *ed)
{
  int i;

  if (ed->line < ed->lines)
  {
    ed->linepos = ed->toppos = 0;
    ed->line = ed->topline = 0;
  }
  else
  {
    for (i = 0; i < ed->lines; i++)
    {
      int newpos = prev_line(ed, ed->linepos);
      if (newpos < 0) return;

      ed->linepos = newpos;
      ed->line--;

      if (ed->topline > 0)
      {
        ed->toppos = prev_line(ed, ed->toppos);
        ed->topline--;
      }
    }
  }

  ed->refresh = 1;
  adjust(ed);
}

void pagedown(struct editor *ed)
{
  int i;

  for (i = 0; i < ed->lines; i++)
  {
    int newpos = next_line(ed, ed->linepos);
    if (newpos < 0) break;

    ed->linepos = newpos;
    ed->line++;

    ed->toppos = next_line(ed, ed->toppos);
    ed->topline++;
  }

  ed->refresh = 1;
  adjust(ed);
}

//
// Text editing
//

void insert_char(struct editor *ed, unsigned char ch)
{
  insert(ed, ed->linepos + ed->col, &ch, 1);
  ed->col++;
  ed->lastcol = ed->col;
  adjust(ed);
  if (!ed->refresh) update_line(ed, ed->col - ed->margin - 1);
}

void newline(struct editor *ed)
{
  insert(ed, ed->linepos + ed->col, "\r\n", 2);
  ed->col = ed->lastcol = 0;
  ed->line++;
  ed->linepos = next_line(ed, ed->linepos);
  ed->lastcol = ed->col;
  ed->refresh = 1;

  if (ed->line >= ed->topline + ed->lines)
  {
    ed->toppos = next_line(ed, ed->toppos);
    ed->topline++;
    ed->refresh = 1;
  }

  adjust(ed);
}

void backspace(struct editor *ed)
{
  if (ed->linepos + ed->col == 0) return;
  if (ed->col == 0)
  {
    int pos = ed->linepos;
    erase(ed, --pos, 1);
    if (get(ed, pos - 1) == '\r') erase(ed, --pos, 1);

    ed->line--;
    ed->linepos = line_start(ed, pos);
    ed->col = pos - ed->linepos;
    ed->refresh = 1;

    if (ed->line < ed->topline)
    {
      ed->toppos = ed->linepos;
      ed->topline = ed->line;
    }
  }
  else
  {
    ed->col--;
    erase(ed, ed->linepos + ed->col, 1);
    ed->lineupdate = 1;
  }

  ed->lastcol = ed->col;
  adjust(ed);
}

void del(struct editor *ed)
{
  int pos = ed->linepos + ed->col;
  int ch = get(ed, pos);
  if (ch < 0) return;

  erase(ed, pos, 1);
  if (ch == '\r')
  {
    ch = get(ed, pos);
    if (ch == '\n') erase(ed, pos, 1);
  }

  if (ch == '\n')
    ed->refresh = 1;
  else
    ed->lineupdate = 1;
}

//
// Editor Commands
//

void save(struct editor *ed)
{
  int rc;

  if (!ed->dirty && !ed->newfile) return;
  rc = save_file(ed, NULL);
  if (rc < 0)
  {
    char msg[128];

    sprintf(msg, "Error %d saving document (%s)", errno, strerror(errno));
    display_message(ed, msg);
    sleep(5);
  }

  ed->refresh = 1;
}

int quit(struct editor *ed)
{
  int ch;

  if (!ed->dirty) return 1;
  display_message(ed, "Quit without saving changes (y/n)? ");
  ch = getchar();
  if (ch == 'y' || ch == 'Y') return 1;
  ed->refresh = 1;
  return 0;
}

//
// main
//

int main(int argc, char *argv[])
{
  struct editor editbuf;
  struct editor *ed = &editbuf;
  int rc;
  int key;
  int done;
  struct term *term;

  if (argc != 2)
  {
    printf("usage: edit <filename>\n");
    return 0;
  }
  
  rc = load_file(ed, argv[1]);
  if (rc < 0 && errno == ENOENT) rc = new_file(ed, argv[1]);
  if (rc < 0) 
  {
    perror(argv[1]);
    return 0;
  }

  setvbuf(stdout, NULL, 0, 8192);

  term = gettib()->job->term;
  ed->cols = term->cols;
  ed->lines = term->lines - 1;
  ed->linebuf = malloc(ed->cols);

  ed->refresh = 1;
  ed->minihelp = 1;
  done = 0;
  while (!done)
  {
    if (ed->refresh)
    {
      draw_screen(ed);
      draw_full_statusline(ed);
      ed->refresh = 0;
      ed->lineupdate = 0;
    }
    else if (ed->lineupdate)
    {
      update_line(ed, 0);
      ed->lineupdate = 0;
      draw_statusline(ed);
    }
    else
      draw_statusline(ed);

    position_cursor(ed);
    fflush(stdout);
    key = getkey();

    switch (key)
    {
      case KEY_UP: up(ed); break;
      case KEY_DOWN: down(ed); break;
      case KEY_LEFT: left(ed); break;
      case KEY_RIGHT: right(ed); break;
      case KEY_HOME: home(ed); break;
      case KEY_END: end(ed); break;
      case KEY_PGUP: pageup(ed); break;
      case KEY_PGDN: pagedown(ed); break;
      case KEY_ENTER: newline(ed); break;
      case KEY_BACKSPACE: backspace(ed); break;
      case KEY_DEL: del(ed); break;
      case CTRL('s'): save(ed); break;
      case CTRL('q'): if (quit(ed)) done = 1; break;
      case CTRL('l'): 
        ed->cols = term->cols;
        ed->lines = term->lines - 1;
        ed->linebuf = realloc(ed->linebuf, ed->cols);
        draw_screen(ed);
        break;

      default: 
        if (key >= ' ' && key <= 0xFF) insert_char(ed, (unsigned char) key);
    }
  }

  delete_editor(ed);

  gotoxy(0, term->lines);
  outstr("\033[K");
  setbuf(stdout, NULL);
  return 0;
}