de.c raw

   1  /*
   2   * Copyright (c) 1993-1994 by Xerox Corporation.  All rights reserved.
   3   *
   4   * THIS MATERIAL IS PROVIDED AS IS, WITH ABSOLUTELY NO WARRANTY EXPRESSED
   5   * OR IMPLIED.  ANY USE IS AT YOUR OWN RISK.
   6   *
   7   * Permission is hereby granted to use or copy this program
   8   * for any purpose, provided the above notices are retained on all copies.
   9   * Permission to modify the code and to distribute modified code is granted,
  10   * provided the above notices are retained, and a notice that the code was
  11   * modified is included with the above copyright notice.
  12   */
  13  
  14  /*
  15   * A really simple-minded text editor based on cords.
  16   *
  17   * Things it does right:
  18   *   - No size bounds;
  19   *   - Unbounded undo;
  20   *   - Should not crash no matter what file you invoke it on;
  21   *   - Scrolls horizontally.
  22   *
  23   * Things it does wrong:
  24   *   - It does not handle tabs reasonably (use `expand` first);
  25   *   - The command set is *much* too small;
  26   *   - The redisplay algorithm does not let `curses` do the scrolling;
  27   *   - The rule for moving the window over the file is suboptimal.
  28   */
  29  
  30  #include <stdio.h>
  31  #include <stdlib.h> /*< for `exit` */
  32  
  33  #include "gc.h"
  34  #include "gc/cord.h"
  35  
  36  #include <ctype.h>
  37  
  38  #if (defined(__CYGWIN__) || defined(__MINGW32__)                  \
  39       || (defined(__NT__) && defined(__386__)) || defined(_WIN32)) \
  40      && !defined(WIN32)
  41  #  define WIN32
  42  #endif
  43  
  44  #if defined(WIN32)
  45  #  ifndef WIN32_LEAN_AND_MEAN
  46  #    define WIN32_LEAN_AND_MEAN 1
  47  #  endif
  48  #  define NOSERVICE
  49  #  include <windows.h>
  50  #else
  51  #  include <curses.h>
  52  #  include <unistd.h> /*< for `sleep` */
  53  #  define de_error(s)     \
  54      {                     \
  55        fprintf(stderr, s); \
  56        sleep(2);           \
  57      }
  58  #endif
  59  
  60  #ifdef WIN32
  61  #  include "de_win.h"
  62  #endif
  63  
  64  #include "de_cmds.h"
  65  
  66  #if defined(CPPCHECK)
  67  #  define MACRO_BLKSTMT_BEGIN {
  68  #  define MACRO_BLKSTMT_END }
  69  #else
  70  #  define MACRO_BLKSTMT_BEGIN do {
  71  #  define MACRO_BLKSTMT_END \
  72      }                       \
  73      while (0)
  74  #endif
  75  
  76  #define OUT_OF_MEMORY                 \
  77    MACRO_BLKSTMT_BEGIN                 \
  78    fprintf(stderr, "Out of memory\n"); \
  79    exit(3);                            \
  80    MACRO_BLKSTMT_END
  81  
  82  /*
  83   * List of line number to position mappings, in descending order.
  84   * There may be holes.
  85   */
  86  struct LineMapRep {
  87    int line;
  88    size_t pos;
  89    struct LineMapRep *previous;
  90  };
  91  typedef struct LineMapRep *line_map;
  92  
  93  /* List of file versions, one per edit operation. */
  94  struct HistoryRep {
  95    CORD file_contents;
  96    struct HistoryRep *previous;
  97    line_map map; /*< note: this is invalid for the first record `now` */
  98  };
  99  typedef struct HistoryRep *history;
 100  
 101  static history now = NULL;
 102  
 103  /* This is `now -> file_contents`. */
 104  static CORD current;
 105  
 106  /* The current file length. */
 107  static size_t current_len;
 108  
 109  /* Map of current line number to position. */
 110  static line_map current_map = NULL;
 111  
 112  /*
 113   * Number of `current_map` entries.  Not always accurate, but reset
 114   * by `prune_map`.
 115   */
 116  static size_t current_map_size = 0;
 117  
 118  #define MAX_MAP_SIZE 3000
 119  
 120  /* Current display position. */
 121  static int dis_line = 0;
 122  static int dis_col = 0;
 123  
 124  #define ALL -1
 125  #define NONE -2
 126  static int need_redisplay = 0; /*< line that needs to be redisplayed */
 127  
 128  /* Current cursor position. Always within file. */
 129  static int line = 0;
 130  static int col = 0;
 131  
 132  /* Character position corresponding to cursor. */
 133  static size_t file_pos = 0;
 134  
 135  /* Invalidate line map for lines greater than `i`. */
 136  static void
 137  invalidate_map(int i)
 138  {
 139    for (;;) {
 140      if (NULL == current_map)
 141        exit(4); /*< for CSA, should not happen */
 142      if (current_map->line <= i)
 143        break;
 144      current_map = current_map->previous;
 145      current_map_size--;
 146    }
 147  }
 148  
 149  /*
 150   * Reduce the number of map entries to save space for huge files.
 151   * This also affects maps in histories.
 152   */
 153  static void
 154  prune_map(void)
 155  {
 156    line_map map = current_map;
 157    int start_line = map->line;
 158  
 159    current_map_size = 0;
 160    do {
 161      current_map_size++;
 162      if (map->line < start_line - LINES && map->previous != 0) {
 163        line_map pred = map->previous->previous;
 164  
 165        GC_PTR_STORE_AND_DIRTY(&map->previous, pred);
 166      }
 167      map = map->previous;
 168    } while (map != 0);
 169  }
 170  
 171  /* Add mapping entry. */
 172  static void
 173  add_map(int line_arg, size_t pos)
 174  {
 175    line_map new_map = GC_NEW(struct LineMapRep);
 176    line_map cur_map;
 177  
 178    if (NULL == new_map)
 179      OUT_OF_MEMORY;
 180    if (current_map_size >= MAX_MAP_SIZE)
 181      prune_map();
 182    new_map->line = line_arg;
 183    new_map->pos = pos;
 184    cur_map = current_map;
 185    GC_PTR_STORE_AND_DIRTY(&new_map->previous, cur_map);
 186    current_map = new_map;
 187    current_map_size++;
 188  }
 189  
 190  /*
 191   * Return position of column `*c` of `i`-th line in the current file.
 192   * Adjust `*c` to be within the line.  A `NULL` pointer is taken as
 193   * column zero.  Returns `CORD_NOT_FOUND` if `i` is too big.
 194   * Assumes `i` is greater than `dis_line`.
 195   */
 196  static size_t
 197  line_pos(int i, int *c)
 198  {
 199    int j;
 200    size_t cur;
 201    line_map map = current_map;
 202  
 203    while (map->line > i)
 204      map = map->previous;
 205    if (map->line < i - 2) /*< rebuild */
 206      invalidate_map(i);
 207    for (j = map->line, cur = map->pos; j < i;) {
 208      cur = CORD_chr(current, cur, '\n');
 209      if (cur == current_len - 1)
 210        return CORD_NOT_FOUND;
 211      cur++;
 212      if (++j > current_map->line)
 213        add_map(j, cur);
 214    }
 215    if (c != 0) {
 216      size_t next = CORD_chr(current, cur, '\n');
 217  
 218      if (next == CORD_NOT_FOUND)
 219        next = current_len - 1;
 220      if (next < cur + *c) {
 221        *c = (int)(next - cur);
 222      }
 223      cur += *c;
 224    }
 225    return cur;
 226  }
 227  
 228  static void
 229  add_hist(CORD s)
 230  {
 231    history new_file = GC_NEW(struct HistoryRep);
 232  
 233    if (NULL == new_file)
 234      OUT_OF_MEMORY;
 235    new_file->file_contents = current = s;
 236    current_len = CORD_len(s);
 237    new_file->previous = now;
 238    GC_END_STUBBORN_CHANGE(new_file);
 239    if (now != NULL) {
 240      now->map = current_map;
 241      GC_END_STUBBORN_CHANGE(now);
 242    }
 243    now = new_file;
 244  }
 245  
 246  static void
 247  del_hist(void)
 248  {
 249    now = now->previous;
 250    current = now->file_contents;
 251    current_map = now->map;
 252    current_len = CORD_len(current);
 253  }
 254  
 255  #ifndef WIN32
 256  /* Current screen contents; a dynamically allocated array of cords. */
 257  static CORD *screen = NULL;
 258  static int screen_size = 0;
 259  
 260  /*
 261   * Replace a line in the `stdscr` of `curses` package.  All control
 262   * characters are displayed as upper case characters in standout mode.
 263   * This is not terribly appropriate for tabs.
 264   */
 265  static void
 266  replace_line(int i, CORD s)
 267  {
 268    size_t len = CORD_len(s);
 269  
 270    if (NULL == screen || LINES > screen_size) {
 271      screen_size = LINES;
 272      screen = (CORD *)GC_MALLOC(screen_size * sizeof(CORD));
 273      if (NULL == screen)
 274        OUT_OF_MEMORY;
 275    }
 276  
 277    /* A gross workaround for an apparent `curses` bug. */
 278    if (i == LINES - 1 && len == (unsigned)COLS) {
 279      s = CORD_substr(s, 0, len - 1);
 280    }
 281  
 282    if (CORD_cmp(screen[i], s) != 0) {
 283      CORD_pos p;
 284  
 285      move(i, 0);
 286      clrtoeol();
 287      move(i, 0);
 288  
 289  #  if defined(CPPCHECK)
 290      memset(p, '\0', sizeof(CORD_pos));
 291  #  endif
 292      CORD_FOR(p, s)
 293      {
 294        int c = CORD_pos_fetch(p) & 0x7f;
 295  
 296        if (iscntrl(c)) {
 297          standout();
 298          addch(c + 0x40);
 299          standend();
 300        } else {
 301          addch(c);
 302        }
 303      }
 304      GC_PTR_STORE_AND_DIRTY(screen + i, s);
 305    }
 306  }
 307  #else
 308  #  define replace_line(i, s) invalidate_line(i)
 309  #endif
 310  
 311  /*
 312   * Return up to `COLS` characters of the line of `s` starting at `pos`,
 313   * returning only characters after the given `column`.
 314   */
 315  static CORD
 316  retrieve_line(CORD s, size_t pos, unsigned column)
 317  {
 318    /* Avoid scanning very long lines. */
 319    CORD candidate = CORD_substr(s, pos, column + COLS);
 320    size_t eol = CORD_chr(candidate, 0, '\n');
 321    int len;
 322  
 323    if (eol == CORD_NOT_FOUND)
 324      eol = CORD_len(candidate);
 325    len = (int)eol - (int)column;
 326    if (len < 0)
 327      len = 0;
 328    return CORD_substr(s, pos + column, len);
 329  }
 330  
 331  #ifdef WIN32
 332  #  define refresh() (void)0
 333  
 334  const void *
 335  retrieve_screen_line(int i)
 336  {
 337    size_t pos;
 338  
 339    /* Prune the search. */
 340    invalidate_map(dis_line + LINES);
 341  
 342    pos = line_pos(dis_line + i, 0);
 343    if (pos == CORD_NOT_FOUND)
 344      return CORD_EMPTY;
 345    return retrieve_line(current, pos, dis_col);
 346  }
 347  #endif
 348  
 349  /* Display the visible section of the current file. */
 350  static void
 351  redisplay(void)
 352  {
 353    int i;
 354  
 355    /* Prune the search. */
 356    invalidate_map(dis_line + LINES);
 357  
 358    for (i = 0; i < LINES; i++) {
 359      if (need_redisplay == ALL || need_redisplay == i) {
 360        size_t pos = line_pos(dis_line + i, 0);
 361  
 362        if (pos == CORD_NOT_FOUND)
 363          break;
 364        replace_line(i, retrieve_line(current, pos, dis_col));
 365        if (need_redisplay == i)
 366          goto done;
 367      }
 368    }
 369    for (; i < LINES; i++)
 370      replace_line(i, CORD_EMPTY);
 371  done:
 372    refresh();
 373    need_redisplay = NONE;
 374  }
 375  
 376  static int dis_granularity;
 377  
 378  /*
 379   * Update `dis_line`, `dis_col` and `dis_pos` to make cursor visible.
 380   * Assumes `line`, `col`, `dis_line` and `dis_pos` are in bounds.
 381   */
 382  static void
 383  normalize_display(void)
 384  {
 385    int old_line = dis_line;
 386    int old_col = dis_col;
 387  
 388    dis_granularity = 1;
 389    if (LINES > 15 && COLS > 15)
 390      dis_granularity = 2;
 391    while (dis_line > line)
 392      dis_line -= dis_granularity;
 393    while (dis_col > col)
 394      dis_col -= dis_granularity;
 395    while (line >= dis_line + LINES)
 396      dis_line += dis_granularity;
 397    while (col >= dis_col + COLS)
 398      dis_col += dis_granularity;
 399    if (old_line != dis_line || old_col != dis_col) {
 400      need_redisplay = ALL;
 401    }
 402  }
 403  
 404  #if defined(WIN32)
 405  /* Defined in `de_win.c` file. */
 406  #else
 407  #  define move_cursor(x, y) move(y, x)
 408  #endif
 409  
 410  /*
 411   * Adjust display so that cursor is visible; move cursor into position.
 412   * Update `screen` if necessary.
 413   */
 414  static void
 415  fix_cursor(void)
 416  {
 417    normalize_display();
 418    if (need_redisplay != NONE)
 419      redisplay();
 420    move_cursor(col - dis_col, line - dis_line);
 421    refresh();
 422  #ifndef WIN32
 423    fflush(stdout);
 424  #endif
 425  }
 426  
 427  /*
 428   * Make sure `line`, `col` and `dis_pos` are somewhere inside file.
 429   * Recompute `file_pos`.  Assumes `dis_pos` is accurate or past the
 430   * end of file.
 431   */
 432  static void
 433  fix_pos(void)
 434  {
 435    int my_col = col;
 436  
 437    if ((size_t)line > current_len)
 438      line = (int)current_len;
 439    file_pos = line_pos(line, &my_col);
 440    if (file_pos == CORD_NOT_FOUND) {
 441      for (line = current_map->line, file_pos = current_map->pos;
 442           file_pos < current_len;
 443           line++, file_pos = CORD_chr(current, file_pos, '\n') + 1)
 444        ;
 445      line--;
 446      file_pos = line_pos(line, &col);
 447    } else {
 448      col = my_col;
 449    }
 450  }
 451  
 452  #if defined(WIN32)
 453  #  define beep() Beep(1000 /* Hz */, 300 /* ms */)
 454  #else
 455  /*
 456   * `beep()` is part of some `curses` packages and not others.
 457   * We try to match the type of the built-in one, if any.
 458   * Declared in the platform `curses.h` file.
 459   */
 460  int
 461  beep(void)
 462  {
 463    putc('\007', stderr);
 464    return 0;
 465  }
 466  #endif
 467  
 468  #define NO_PREFIX -1
 469  #define BARE_PREFIX -2
 470  static int repeat_count = NO_PREFIX; /*< the current command prefix */
 471  
 472  static int locate_mode = 0; /*< currently between 2 ^Ls */
 473  
 474  static CORD locate_string = CORD_EMPTY; /*< the current search string */
 475  
 476  char *arg_file_name;
 477  
 478  #ifdef WIN32
 479  void
 480  set_position(int c, int l)
 481  {
 482    line = l + dis_line;
 483    col = c + dis_col;
 484    fix_pos();
 485    move_cursor(col - dis_col, line - dis_line);
 486  }
 487  #endif
 488  
 489  void
 490  do_command(int c)
 491  {
 492    int i;
 493    int need_fix_pos;
 494    FILE *out;
 495  
 496    if (c == '\r')
 497      c = '\n';
 498    if (locate_mode) {
 499      size_t new_pos;
 500  
 501      if (c == LOCATE) {
 502        locate_mode = 0;
 503        locate_string = CORD_EMPTY;
 504        return;
 505      }
 506      locate_string = CORD_cat_char(locate_string, (char)c);
 507      new_pos = CORD_str(current, file_pos - CORD_len(locate_string) + 1,
 508                         locate_string);
 509      if (new_pos != CORD_NOT_FOUND) {
 510        need_redisplay = ALL;
 511        new_pos += CORD_len(locate_string);
 512        for (;;) {
 513          file_pos = line_pos(line + 1, 0);
 514          if (file_pos > new_pos)
 515            break;
 516          line++;
 517        }
 518        col = (int)(new_pos - line_pos(line, 0));
 519        file_pos = new_pos;
 520        fix_cursor();
 521      } else {
 522        locate_string
 523            = CORD_substr(locate_string, 0, CORD_len(locate_string) - 1);
 524        beep();
 525      }
 526      return;
 527    }
 528    if (c == REPEAT) {
 529      repeat_count = BARE_PREFIX;
 530      return;
 531    } else if (c < 0x100 && isdigit(c)) {
 532      if (repeat_count == BARE_PREFIX) {
 533        repeat_count = c - '0';
 534        return;
 535      } else if (repeat_count != NO_PREFIX) {
 536        repeat_count = 10 * repeat_count + c - '0';
 537        return;
 538      }
 539    }
 540    if (repeat_count == NO_PREFIX)
 541      repeat_count = 1;
 542    if (repeat_count == BARE_PREFIX && (c == UP || c == DOWN)) {
 543      repeat_count = LINES - dis_granularity;
 544    }
 545    if (repeat_count == BARE_PREFIX)
 546      repeat_count = 8;
 547    need_fix_pos = 0;
 548    for (i = 0; i < repeat_count; i++) {
 549      switch (c) {
 550      case LOCATE:
 551        locate_mode = 1;
 552        break;
 553      case TOP:
 554        line = col = 0;
 555        file_pos = 0;
 556        break;
 557      case UP:
 558        if (line != 0) {
 559          line--;
 560          need_fix_pos = 1;
 561        }
 562        break;
 563      case DOWN:
 564        line++;
 565        need_fix_pos = 1;
 566        break;
 567      case LEFT:
 568        if (col != 0) {
 569          col--;
 570          file_pos--;
 571        }
 572        break;
 573      case RIGHT:
 574        if (CORD_fetch(current, file_pos) == '\n')
 575          break;
 576        col++;
 577        file_pos++;
 578        break;
 579      case UNDO:
 580        del_hist();
 581        need_redisplay = ALL;
 582        need_fix_pos = 1;
 583        break;
 584      case BS:
 585        if (col == 0) {
 586          beep();
 587          break;
 588        }
 589        col--;
 590        file_pos--;
 591        /* FALLTHRU */
 592      case DEL:
 593        if (file_pos == current_len - 1)
 594          break;
 595        /* Cannot delete a trailing newline. */
 596        if (CORD_fetch(current, file_pos) == '\n') {
 597          need_redisplay = ALL;
 598          need_fix_pos = 1;
 599        } else {
 600          need_redisplay = line - dis_line;
 601        }
 602        add_hist(CORD_cat(CORD_substr(current, 0, file_pos),
 603                          CORD_substr(current, file_pos + 1, current_len)));
 604        invalidate_map(line);
 605        break;
 606      case WRITE:
 607        {
 608          CORD name = CORD_cat(CORD_from_char_star(arg_file_name), ".new");
 609  
 610          if ((out = fopen(CORD_to_const_char_star(name), "wb")) == NULL
 611              || CORD_put(current, out) == EOF) {
 612            de_error("Write failed\n");
 613            need_redisplay = ALL;
 614          } else {
 615            fclose(out);
 616          }
 617        }
 618        break;
 619      default:
 620        {
 621          CORD left_part = CORD_substr(current, 0, file_pos);
 622          CORD right_part = CORD_substr(current, file_pos, current_len);
 623  
 624          add_hist(CORD_cat(CORD_cat_char(left_part, (char)c), right_part));
 625          invalidate_map(line);
 626          if (c == '\n') {
 627            col = 0;
 628            line++;
 629            file_pos++;
 630            need_redisplay = ALL;
 631          } else {
 632            col++;
 633            file_pos++;
 634            need_redisplay = line - dis_line;
 635          }
 636          break;
 637        }
 638      }
 639    }
 640    if (need_fix_pos)
 641      fix_pos();
 642    fix_cursor();
 643    repeat_count = NO_PREFIX;
 644  }
 645  
 646  void
 647  generic_init(void)
 648  {
 649    FILE *f;
 650    CORD initial;
 651  
 652    if ((f = fopen(arg_file_name, "rb")) == NULL) {
 653      initial = "\n";
 654    } else {
 655      size_t len;
 656  
 657      initial = CORD_from_file(f);
 658      len = CORD_len(initial);
 659      if (0 == len || CORD_fetch(initial, len - 1) != '\n') {
 660        initial = CORD_cat(initial, "\n");
 661      }
 662    }
 663    add_map(0, 0);
 664    add_hist(initial);
 665    now->map = current_map;
 666    /* Cannot back up further: beginning of the world. */
 667    now->previous = now;
 668  
 669    GC_END_STUBBORN_CHANGE(now);
 670    need_redisplay = ALL;
 671    fix_cursor();
 672  }
 673  
 674  #ifndef WIN32
 675  int
 676  main(int argc, char **argv)
 677  {
 678    int c;
 679    void *buf;
 680  
 681    /* The application is not for testing leak detection mode. */
 682    GC_set_find_leak(0);
 683  
 684    GC_INIT();
 685  #  ifndef NO_INCREMENTAL
 686    GC_enable_incremental();
 687  #  endif
 688  
 689    if (argc != 2) {
 690      fprintf(stderr, "Usage: %s file\n", argv[0]);
 691      fprintf(stderr, "Cursor keys: ^B(left) ^F(right) ^P(up) ^N(down)\n");
 692      fprintf(stderr, "Undo: ^U    Write to <file>.new: ^W");
 693      fprintf(stderr, "Quit:^D     Repeat count: ^R[n]\n");
 694      fprintf(stderr, "Top: ^T     Locate (search, find): ^L text ^L\n");
 695      exit(1);
 696    }
 697    arg_file_name = argv[1];
 698    buf = GC_MALLOC_ATOMIC(8192);
 699    if (NULL == buf)
 700      OUT_OF_MEMORY;
 701    setvbuf(stdout, (char *)buf, _IOFBF, 8192);
 702    initscr();
 703    noecho();
 704    nonl();
 705    cbreak();
 706    generic_init();
 707    while ((c = getchar()) != QUIT) {
 708      if (c == EOF)
 709        break;
 710      do_command(c);
 711    }
 712    move(LINES - 1, 0);
 713    clrtoeol();
 714    refresh();
 715    nl();
 716    echo();
 717    endwin();
 718    return 0;
 719  }
 720  #endif
 721