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