tMerge source into single file - ve - a minimal text editor (work in progress) (HTM) git clone git://src.adamsgaard.dk/ve (DIR) Log (DIR) Files (DIR) Refs (DIR) README (DIR) LICENSE --- (DIR) commit 7a8cfc0fc10085e622932f958235613b9ca8e149 (DIR) parent 0c629a2638b242c02e9dfce571b6738d64727b34 (HTM) Author: Anders Damsgaard <anders@adamsgaard.dk> Date: Wed, 7 Aug 2019 21:34:42 +0200 Merge source into single file Diffstat: M Makefile | 14 +++++++------- D src/edit.c | 60 ------------------------------- D src/edit.h | 9 --------- D src/find.c | 113 ------------------------------- D src/find.h | 9 --------- D src/input.c | 254 ------------------------------- D src/input.h | 7 ------- D src/io.c | 103 ------------------------------- D src/io.h | 7 ------- D src/main.c | 27 --------------------------- D src/output.c | 215 ------------------------------- D src/output.h | 14 -------------- D src/row.c | 143 ------------------------------- D src/row.h | 16 ---------------- D src/terminal.c | 107 ------------------------------- D src/terminal.h | 11 ----------- D src/ve.c | 39 ------------------------------- D src/ve.h | 39 ------------------------------- A ve.c | 1062 +++++++++++++++++++++++++++++++ 19 files changed, 1069 insertions(+), 1180 deletions(-) --- (DIR) diff --git a/Makefile b/Makefile t@@ -1,9 +1,9 @@ +.POSIX: + CFLAGS = -g -std=c99 -pedantic -Wall -Wextra -#LDFLAGS = -lm -SRCDIR = src -SRC = $(wildcard $(SRCDIR)/*.c) -OBJ = $(patsubst %.c,%.o,$(SRC)) -HDR = $(wildcard *.h) +LDFLAGS = +SRC = ve.c +OBJ = $(SRC:.c=.o) BIN = ve PREFIX ?= /usr/local t@@ -12,7 +12,7 @@ STRIP ?= strip default: $(BIN) -$(BIN): $(OBJ) $(HDR) +$(BIN): $(OBJ) $(CC) $(LDFLAGS) $(OBJ) -o $@ install: $(BIN) t@@ -31,7 +31,7 @@ memtest: $(BIN) valgrind --error-exitcode=1 --leak-check=full $(BIN) -v clean: - $(RM) $(SRCDIR)/*.o + $(RM) *.o $(RM) $(BIN) .PHONY: default install uninstall test memtest clean (DIR) diff --git a/src/edit.c b/src/edit.c t@@ -1,60 +0,0 @@ -#include "row.h" -#include "ve.h" - -void -editor_insert_char(int c) -{ - if (E.cursor_y == E.num_rows) - editor_row_insert(E.num_rows, "", 0); - editor_row_insert_char(&E.row[E.cursor_y], E.cursor_x, c); - E.cursor_x++; -} - -void -editor_insert_new_line() -{ - eRow *row; - if (E.cursor_x == 0) { - editor_row_insert(E.cursor_y, "", 0); - } else { - row = &E.row[E.cursor_y]; - editor_row_insert(E.cursor_y + 1, &row->chars[E.cursor_x], - row->size - E.cursor_x); - row = &E.row[E.cursor_y]; - row->size = E.cursor_x; - row->chars[row->size] = '\0'; - editor_row_update(row); - } - E.cursor_y++; - E.cursor_x = 0; -} - -/* delete a character before the cursor, and allow for deletion across rows */ -void -editor_delete_char_left() -{ - eRow *row; - if (E.cursor_y == E.num_rows || (E.cursor_x == 0 && E.cursor_y == 0)) - return; - - row = &E.row[E.cursor_y]; - if (E.cursor_x > 0) { - editor_row_delete_char(row, E.cursor_x - 1); - E.cursor_x--; - } else { - E.cursor_x = E.row[E.cursor_y - 1].size; - editor_row_append_string(&E.row[E.cursor_y - 1], - row->chars, row->size); - editor_row_delete(E.cursor_y); - E.cursor_y--; - } -} - -void -editor_delete_char_right() -{ - if (E.cursor_y == E.num_rows) - return; - eRow *row = &E.row[E.cursor_y]; - editor_row_delete_char(row, E.cursor_x); -} (DIR) diff --git a/src/edit.h b/src/edit.h t@@ -1,9 +0,0 @@ -#ifndef EDIT_H_ -#define EDIT_H_ - -void editor_insert_char(int c); -void editor_insert_new_line(); -void editor_delete_char_left(); -void editor_delete_char_right(); - -#endif (DIR) diff --git a/src/find.c b/src/find.c t@@ -1,113 +0,0 @@ -/* add feature test macro for strdup compatibility */ -#define _DEFAULT_SOURCE -#define _BSD_SOURCE -#define _GNU_SOURCE - -#include <stdlib.h> -#include <string.h> -#include "input.h" -#include "ve.h" -#include "row.h" -#include "output.h" - -/* reverse of strstr (3) */ -char* -strrstr(char *haystack, char *needle, - size_t haystack_length, size_t needle_length) -{ - char *cp; - for (cp = haystack + haystack_length - needle_length; - cp >= haystack; - cp--) { - if (strncmp(cp, needle, needle_length) == 0) - return cp; - } - return NULL; -} - -/* find E.find_query from current cursor position moving in E.direction - * if opposite_direction = 0, and opposite E.direction if - * opposite_direction = 1 */ -void -editor_find_next_occurence(int opposite_direction) -{ - int y, y_inc, x_offset, query_len; - eRow *row; - char *match; - - if (!E.find_query) - return; - - if ((E.find_direction && !opposite_direction) || - (!E.find_direction && opposite_direction)) - y_inc = +1; - else - y_inc = -1; - - x_offset = 0; - query_len = strlen(E.find_query); - /* from cursor until end of document */ - for (y = E.cursor_y; - y != ((y_inc == +1) ? E.num_rows : -1); - y += y_inc) { - row = &E.row[y]; - - if (y == E.cursor_y) - x_offset = y_inc + E.cursor_x; - else - x_offset = 0; - - if (y_inc == +1) { - match = strstr(row->chars + x_offset, E.find_query); - if (match) - break; - } else { - match = strrstr(row->chars, E.find_query, - (y == E.cursor_y) ? E.cursor_x : row->size, - query_len); - if (match) - break; - } - } - - if (match) { - E.cursor_y = y; - E.cursor_x = match - row->chars; - /* E.row_offset = E.num_rows; */ /* put line to top of screen */ - - } else { - /* from other end of file until cursor */ - for (y = (y_inc == +1) ? 0 : E.num_rows - 1; - y != E.cursor_y + y_inc; - y += y_inc) { - row = &E.row[y]; - match = strstr(row->chars, E.find_query); - if (match) - break; - } - if (match) { - E.cursor_y = y; - E.cursor_x = editor_row_cursor_rx_to_x(row, match - row->chars); - } - } -} - -/* direction = 1 is forward, 0 is backward */ -void -editor_find(int direction) -{ - char *query; - - if (direction) - query = editor_prompt("/%s"); - else - query = editor_prompt("?%s"); - if (query == NULL) - return; - - E.find_direction = direction; - free(E.find_query); - E.find_query = strdup(query); - editor_find_next_occurence(0); - free(query); -} (DIR) diff --git a/src/find.h b/src/find.h t@@ -1,9 +0,0 @@ -#ifndef FIND_H_ -#define FIND_H_ - -char* strrstr(char *haystack, char *needle, - size_t haystack_length, size_t needle_length); -void editor_find_next_occurence(int opposite_direction); -void editor_find(int direction); - -#endif (DIR) diff --git a/src/input.c b/src/input.c t@@ -1,254 +0,0 @@ -#include <unistd.h> -#include <stdlib.h> -#include <ctype.h> -#include <string.h> -#include "ve.h" -#include "terminal.h" -#include "edit.h" -#include "output.h" -#include "io.h" -#include "row.h" -#include "find.h" - -#define CTRL_KEY(k) ((k) & 0x1f) - -/* prompt is expected to be a format string containing a %s */ -char* -editor_prompt(char *prompt) -{ - size_t bufsize, buflen; - char *buf; - int c; - - bufsize = 128; - buflen = 0; - buf = malloc(bufsize); - buf[0] = '\0'; - - while (1) { - editor_set_status_message(prompt, buf); - editor_refresh_screen(); - editor_place_cursor(strlen(prompt) - 1 + strlen(buf), - E.screen_rows + 2); - - c = editor_read_key(); - if (c == CTRL_KEY('h') || c == 127) { /* detect backspace */ - if (buflen != 0) - buf[--buflen] = '\0'; - } else if (c == '\x1b' || (c == '\r' && !buflen)) { /* detect escape */ - editor_set_status_message(""); - free(buf); - return NULL; - } else if (c == '\r') { - if (buflen != 0) { - editor_set_status_message(""); - return buf; - } - } else if (!iscntrl(c) && c < 128) { - if (buflen >= bufsize - 1) { - bufsize *= 2; - buf = realloc(buf, bufsize); - } - buf[buflen++] = c; - buf[buflen] = '\0'; - } - } -} - -/* move cursor according to screen, file, and line limits */ -void -editor_move_cursor(char key) -{ - int row_len; - eRow *row; - - switch(key) { - case 'h': - if (E.cursor_x != 0) { - E.cursor_x--; - } else if (E.cursor_y > 0) { - E.cursor_y--; - E.cursor_x = E.row[E.cursor_y].size; - } - break; - case 'j': - if (E.cursor_y < E.num_rows - 1) - E.cursor_y++; - break; - case 'k': - if (E.cursor_y != 0) - E.cursor_y--; - break; - case 'l': - row = (E.cursor_y >= E.num_rows) ? NULL : &E.row[E.cursor_y]; - if (row && E.cursor_x < row->size - 1) { - E.cursor_x++; - } else if (row && E.cursor_x == row->size && - E.cursor_y < E.num_rows - 1) { - E.cursor_y++; - E.cursor_x = 0; - } - break; - } - - /* do not allow navigation past EOL by vertical navigation */ - row = (E.cursor_y >= E.num_rows) ? NULL : &E.row[E.cursor_y]; - row_len = row ? row->size : 0; - if (E.cursor_x > row_len) - E.cursor_x = row_len; -} - -void -editor_process_keypress() -{ - char c; - int i; - - c = editor_read_key(); - - if (E.mode == 0) { /* normal mode */ - switch (c) { - case 'i': - E.mode = 1; - break; - case 'a': - editor_move_cursor('l'); - E.mode = 1; - break; - - case CTRL_KEY('q'): - if (E.file_changed) { - editor_set_status_message("error: " - "file has unsaved changes. " - "Press C-x to confirm quit"); - break; - } else { - write(STDOUT_FILENO, "\x1b[2J", 4); /* clear screen */ - write(STDOUT_FILENO, "\x1b[H", 3); - exit(0); - break; - } - case CTRL_KEY('x'): - write(STDOUT_FILENO, "\x1b[2J", 4); /* clear screen */ - write(STDOUT_FILENO, "\x1b[H", 3); - exit(0); - break; - case 'h': - case 'j': - case 'k': - case 'l': - editor_move_cursor(c); - break; - - case CTRL_KEY('w'): - file_save(E.filename); - break; - - case CTRL_KEY('f'): - i = E.screen_rows; - while (i--) - editor_move_cursor('j'); - break; - case CTRL_KEY('b'): - i = E.screen_rows; - while (i--) - editor_move_cursor('k'); - break; - - case CTRL_KEY('d'): - i = E.screen_rows/2; - while (i--) - editor_move_cursor('j'); - break; - case CTRL_KEY('u'): - i = E.screen_rows/2; - while (i--) - editor_move_cursor('k'); - break; - - case '0': - E.cursor_x = 0; - break; - case '$': - if (E.cursor_y < E.num_rows) - E.cursor_x = E.row[E.cursor_y].size - 1; - break; - - case 'g': - E.cursor_x = 0; - E.cursor_y = 0; - break; - case 'G': - E.cursor_x = 0; - E.cursor_y = E.num_rows - 1; - break; - - case 'x': - editor_delete_char_right(); - break; - case 'd': - editor_row_delete(E.cursor_y); - editor_move_cursor('h'); - break; - - case 'o': - if (E.cursor_y < E.num_rows) - E.cursor_x = E.row[E.cursor_y].size; - editor_insert_new_line(); - E.mode = 1; - break; - case 'O': - E.cursor_x = 0; - editor_insert_new_line(); - editor_move_cursor('k'); - E.mode = 1; - break; - - case 'I': - E.cursor_x = 0; - E.mode = 1; - break; - case 'A': - if (E.cursor_y < E.num_rows) - E.cursor_x = E.row[E.cursor_y].size; - E.mode = 1; - break; - - case '/': - editor_find(1); - break; - case '?': - editor_find(0); - break; - case 'n': - editor_find_next_occurence(0); - break; - case 'N': - editor_find_next_occurence(1); - break; - } - } else if (E.mode == 1) { /* insert mode */ - switch (c) { - case CTRL_KEY('c'): - case '\x1b': /* escape */ - E.mode = 0; - break; - - case CTRL_KEY('\r'): /* enter */ - editor_insert_new_line(); - break; - - case 127: /* backspace */ - case CTRL_KEY('h'): - editor_delete_char_left(); - break; - - case CTRL_KEY('l'): - break; - - default: - editor_insert_char(c); - break; - } - } -} (DIR) diff --git a/src/input.h b/src/input.h t@@ -1,7 +0,0 @@ -#ifndef INPUT_H_ -#define INPUT_H_ - -char* editor_prompt(char *prompt); -void editor_process_keypress(); - -#endif (DIR) diff --git a/src/io.c b/src/io.c t@@ -1,103 +0,0 @@ -/* add feature test macro for getline compatibility */ -#define _DEFAULT_SOURCE -#define _BSD_SOURCE -#define _GNU_SOURCE - -#include <stdlib.h> -#include <stdio.h> -#include <sys/types.h> -#include <string.h> -#include <fcntl.h> -#include <unistd.h> -#include <errno.h> -#include "ve.h" -#include "terminal.h" -#include "row.h" -#include "output.h" -#include "input.h" - -void -file_open(char *filename) -{ - free(E.filename); - E.filename = strdup(filename); - - FILE *fp; - char *line; - size_t linecap; - ssize_t linelen; - - fp = fopen(filename, "r"); - if (!fp) - die("fopen in file_open"); - - line = NULL; - linecap = 0; - while ((linelen = getline(&line, &linecap, fp)) != -1) { - while (linelen > 0 && (line[linelen - 1] == '\n' || - line[linelen - 1] == '\r')) - linelen--; - editor_row_insert(E.num_rows, line, linelen); - } - free(line); - fclose(fp); - E.file_changed = 0; -} - -/* convert rows to one long char array of length buflen */ -char* -editor_concatenate_rows(int *buflen) -{ - int totlen, j; - char *buf, *p; - - totlen = 0; - for (j=0; j<E.num_rows; ++j) - totlen += E.row[j].size + 1; /* add space for newline char */ - *buflen = totlen; - - buf = malloc(totlen); - p = buf; - for (j=0; j<E.num_rows; ++j) { - memcpy(p, E.row[j].chars, E.row[j].size); - p += E.row[j].size; - *p = '\n'; - p++; - } - return buf; -} - -void -file_save(char *filename) -{ - int len, fd; - char *buf; - - if (filename == NULL) { - filename = editor_prompt("save as: %s"); - if (filename == NULL) { - editor_set_status_message("save aborted"); - return; - } - } - - buf = editor_concatenate_rows(&len); - - fd = open(filename, O_RDWR | O_CREAT, 0644); - if (fd != -1) { - if (ftruncate(fd, len) != -1) { - if (write(fd, buf, len) == len) { - close(fd); - free(buf); - E.filename = filename; - E.file_changed = 0; - editor_set_status_message("%d bytes written to disk", len); - return; - } - } - close(fd); - } - free(buf); - editor_set_status_message("error: can't save! I/O error %s", - strerror(errno)); -} (DIR) diff --git a/src/io.h b/src/io.h t@@ -1,7 +0,0 @@ -#ifndef IO_H_ -#define IO_H_ - -void file_open(char *filename); -void file_save(char *filename); - -#endif (DIR) diff --git a/src/main.c b/src/main.c t@@ -1,27 +0,0 @@ -#include <stdlib.h> -#include "terminal.h" -#include "output.h" -#include "input.h" -#include "io.h" -#include "output.h" -#include "ve.h" - -int -main(int argc, char* argv[]) -{ - enable_raw_mode(); - init_editor(); - - /* TODO: proper argument handling */ - if (argc >= 2) { - file_open(argv[1]); - } - - editor_set_status_message("%s v%s", PROGNAME, VERSION); - - while (1) { - editor_refresh_screen(); - editor_process_keypress(); - } - return 0; -} (DIR) diff --git a/src/output.c b/src/output.c t@@ -1,215 +0,0 @@ -#include <unistd.h> -#include <string.h> -#include <stdlib.h> -#include <stdio.h> -#include <stdarg.h> -#include <time.h> -#include "terminal.h" -#include "output.h" -#include "row.h" -#include "ve.h" - -/* reallocate append buffer to hold string s */ -void -ab_append(struct abuf *ab, const char *s, int len) -{ - char *new; - new = realloc(ab->b, ab->len + len); - - if (new == NULL) - return; - memcpy(&new[ab->len], s, len); - ab->b = new; - ab->len += len; -} - -void -ab_free(struct abuf *ab) { - free(ab->b); -} - -int -show_status_message() -{ - if (E.show_status && - strlen(E.status_msg) && - time(NULL) - E.status_msg_time < STATUS_MESSAGE_TIMEOUT) { - return 1; - } else { - if (E.show_status) { - E.show_status = 0; - E.screen_rows++; - } - return 0; - } -} - -/* draw status line consisting of left and right components. - * if the screen is narrower than both parts, just show the left status, - * truncated, if necessary. */ -void -editor_draw_status(struct abuf *ab) -{ - char left_status[512], right_status[512]; - int left_status_len, right_status_len, padding; - int percentage; - - left_status_len = 0; - switch (E.mode) { - case 1: - left_status_len = snprintf(left_status, sizeof(left_status), - "INSERT "); - break; - case 2: - left_status_len = snprintf(left_status, sizeof(left_status), - "VISUAL "); - break; - } - left_status_len += snprintf(left_status + left_status_len, - sizeof(left_status), - "%.20s %s", - E.filename ? E.filename : "[unnamed]", - E.file_changed ? "[+] " : ""); - - percentage = (int)((float)(E.cursor_y)/(E.num_rows-1)*100); - if (percentage < 0) percentage = 0; - right_status_len = snprintf(right_status, sizeof(right_status), - "%d%% < %d, %d", - percentage, - E.cursor_x+1, E.cursor_y+1); - - if (left_status_len > E.screen_columns) - left_status_len = E.screen_columns; - - padding = E.screen_columns - left_status_len - right_status_len; - if (padding < 0) { - if (left_status_len < E.screen_columns) - ab_append(ab, left_status, left_status_len); - else - ab_append(ab, left_status, E.screen_columns); - } else { - ab_append(ab, left_status, left_status_len); - while (padding--) - ab_append(ab, " ", 1); - ab_append(ab, right_status, right_status_len); - } - - if (show_status_message()) { - ab_append(ab, "\x1b[m", 3); - ab_append(ab, "\r\n", 2); - } -} - -/* draw status message if as long as it is specified and it is not too old */ -void -editor_draw_status_message(struct abuf *ab) -{ - int msglen; - ab_append(ab, "\x1b[K", 3); - msglen = strlen(E.status_msg); - if (msglen > E.screen_columns) - msglen = E.screen_columns; - if (show_status_message()) - ab_append(ab, E.status_msg, msglen); -} - -/* set vertical offset between file and screen when hitting the boundaries */ -void -editor_scroll() -{ - E.cursor_rx = 0; - if (E.cursor_y < E.num_rows) - E.cursor_rx = editor_row_cursor_x_to_rx(&E.row[E.cursor_y], - E.cursor_x); - - if (E.cursor_y < E.row_offset) - E.row_offset = E.cursor_y; - else if (E.cursor_y >= E.row_offset + E.screen_rows) - E.row_offset = E.cursor_y - E.screen_rows + 1; - - if (E.cursor_rx < E.column_offset) - E.column_offset = E.cursor_rx; - else if (E.cursor_rx >= E.column_offset + E.screen_columns) - E.column_offset = E.cursor_rx - E.screen_columns + 1; -} - -/* draw editor screen. - * show tilde characters after EOF, and show status on last line */ -void -editor_draw_rows(struct abuf *ab) -{ - int y, len, file_row; - for (y = 0; y < E.screen_rows; ++y) { - file_row = y + E.row_offset; - if (file_row < E.num_rows) { - len = E.row[file_row].rsize - E.column_offset; - if (len < 0) - len = 0; - if (len > E.screen_columns) - len = E.screen_columns; - ab_append(ab, &E.row[file_row].rchars[E.column_offset], len); - } else { - ab_append(ab, "~", 1); - } - - ab_append(ab, "\x1b[K", 3); /* erase to end of line */ - ab_append(ab, "\r\n", 2); - } -} - -/* fill output append buffer and write to terminal. - * move cursor to left before repaint, and to cursor position after. - * hide the cursor when repainting. - * VT100 escape sequences: - * - http://vt100.net/docs/vt100-ug/chapter3.html - * - http://vt100.net/docs/vt100-ug/chapter3.html#CUP */ -void -editor_refresh_screen() -{ - char buf[32]; - struct abuf ab = ABUF_INIT; - - show_status_message(); - editor_scroll(); - - ab_append(&ab, "\x1b[?25l", 6); /* hide cursor */ - ab_append(&ab, "\x1b[H", 3); /* cursor to home */ - - editor_draw_rows(&ab); - editor_draw_status(&ab); - editor_draw_status_message(&ab); - - snprintf(buf, sizeof(buf), "\x1b[%d;%dH", - (E.cursor_y - E.row_offset)+1, - (E.cursor_rx - E.column_offset)+1); - ab_append(&ab, buf, strlen(buf)); - - ab_append(&ab, "\x1b[?25h", 6); /* show cursor */ - - write(STDOUT_FILENO, ab.b, ab.len); - ab_free(&ab); -} - -void -editor_place_cursor(int cx, int cy) -{ - char buf[128]; - snprintf(buf, sizeof(buf), "\x1b[%d;%dH", cy, cx); - write(STDOUT_FILENO, buf, strlen(buf)); -} - -/* set status message text, uses same format as printf */ -void -editor_set_status_message(const char *fmt, ...) -{ - va_list ap; - va_start(ap, fmt); - vsnprintf(E.status_msg, sizeof(E.status_msg), fmt, ap); - va_end(ap); - E.status_msg_time = time(NULL); - - if (!E.show_status) { - E.screen_rows--; - E.show_status = 1; - } -} (DIR) diff --git a/src/output.h b/src/output.h t@@ -1,14 +0,0 @@ -#ifndef OUTPUT_H_ -#define OUTPUT_H_ - -struct abuf { - char *b; - int len; -}; -#define ABUF_INIT {NULL, 0} - -void editor_refresh_screen(); -void editor_set_status_message(const char *fmt, ...); -void editor_place_cursor(int cx, int cy); - -#endif (DIR) diff --git a/src/row.c b/src/row.c t@@ -1,143 +0,0 @@ -#include <stdlib.h> -#include <string.h> -#include "ve.h" - -/* navigate over tab-representative string as one character */ -int -editor_row_cursor_x_to_rx(eRow *row, int cursor_x) -{ - int rx, j; - rx = 0; - for (j=0; j<cursor_x; ++j) { - if (row->chars[j] == '\t') - rx += (TAB_WIDTH - 1) - (rx % TAB_WIDTH); - rx++; - } - return rx; -} - -/* translate on-screen position to data position */ -int -editor_row_cursor_rx_to_x(eRow *row, int cursor_rx) -{ - int cur_rx, cx; - cur_rx = 0; - for (cx=0; cx<row->size; ++cx) { - if (row->chars[cx] == '\t') - cur_rx += (TAB_WIDTH - 1) - (cur_rx % TAB_WIDTH); - cur_rx++; - - if (cur_rx > cursor_rx) - return cx; - } - return cx; -} - -/* translate tabs before display */ -void -editor_row_update(eRow* row) -{ - int j, idx, tabs; - - free(row->rchars); - row->rchars = malloc(row->size + 1); - - tabs = 0; - for (j=0; j<row->size; ++j) - if (row->chars[j] == '\t') - tabs++; - - free(row->rchars); - row->rchars = malloc(row->size + tabs*(TAB_WIDTH - 1) + 1); - - idx = 0; - for (j=0; j<row->size; ++j) { - if (row->chars[j] == '\t') { - row->rchars[idx++] = '>'; - while (idx % TAB_WIDTH != 0) - row->rchars[idx++] = ' '; - } else { - row->rchars[idx++] = row->chars[j]; - } - } - row->rchars[idx] = '\0'; - row->rsize = idx; -} - -/* add row to buffer */ -void -editor_row_insert(int i, char *s, size_t len) -{ - if (i<0 || i>E.num_rows) - return; - - E.row = realloc(E.row, sizeof(eRow) * (E.num_rows + 1)); - memmove(&E.row[i+1], &E.row[i], sizeof(eRow) * (E.num_rows - i)); - - E.row[i].size = len; - E.row[i].chars = malloc(len + 1); - memcpy(E.row[i].chars, s, len); - E.row[i].chars[len] = '\0'; - - E.row[i].rsize = 0; - E.row[i].rchars = NULL; - editor_row_update(&E.row[i]); - - ++E.num_rows; - E.file_changed = 1; -} - -/* insert character to row, making sure to allocate memory accordingly */ -void -editor_row_insert_char(eRow *row, int i, int c) -{ - if (i<0 || i>row->size) - i = row->size; - row->chars = realloc(row->chars, row->size + 2); - memmove(&row->chars[i+1], &row->chars[i], row->size - i+1); - row->size++; - row->chars[i] = c; - editor_row_update(row); - E.file_changed = 1; -} - -/* append a string to the end of a row */ -void -editor_row_append_string(eRow *row, char *s, size_t len) -{ - row->chars = realloc(row->chars, row->size + len + 1); - memcpy(&row->chars[row->size], s, len); - row->size += len; - row->chars[row->size] = '\0'; - editor_row_update(row); - E.file_changed = 1; -} - -void -editor_row_delete_char(eRow *row, int i) -{ - if (i<0 || i>=row->size) - return; - memmove(&row->chars[i], &row->chars[i+1], row->size - i); - row->size--; - editor_row_update(row); - E.file_changed = 1; -} - -void -editor_row_free(eRow *row) -{ - free(row->rchars); - free(row->chars); -} - -void -editor_row_delete(int i) -{ - if (i<0 || i>=E.num_rows) - return; - editor_row_free(&E.row[i]); - memmove(&E.row[i], &E.row[i+1], sizeof(eRow)*(E.num_rows - i - 1)); - E.num_rows--; - E.file_changed = 1; -} (DIR) diff --git a/src/row.h b/src/row.h t@@ -1,16 +0,0 @@ -#ifndef ROW_H_ -#define ROW_H_ - -#include "ve.h" - -int editor_row_cursor_x_to_rx(eRow *row, int cursor_x); -int editor_row_cursor_rx_to_x(eRow *row, int cursor_rx); -void editor_row_update(eRow* row); -void editor_row_append_string(eRow *row, char *s, size_t len); -void editor_row_insert(int i, char *s, size_t len); -void editor_row_insert_char(eRow *row, int i, int c); -void editor_row_delete_char(eRow *row, int i); -void editor_row_delete(int i); -void editor_row_free(eRow *row); - -#endif (DIR) diff --git a/src/terminal.c b/src/terminal.c t@@ -1,107 +0,0 @@ -#include <ctype.h> -#include <errno.h> -#include <stdio.h> -#include <stdlib.h> -#include <termios.h> -#include <unistd.h> -#include <sys/ioctl.h> -#include "ve.h" - -struct editor_config E; - -void -die(const char *s) -{ - /* clear screen on exit */ - write(STDOUT_FILENO, "\x1b[2J", 4); - write(STDOUT_FILENO, "\x1b[H", 3); - perror(s); - exit(1); -} - -void -disable_raw_mode() -{ - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1) - die("tcsetattr in disable_raw_mode()"); -} - -void -enable_raw_mode() -{ - struct termios raw; - - if (tcgetattr(STDIN_FILENO, &E.orig_termios) == -1) - die("tcgetattr in enable_raw_mode()"); - atexit(disable_raw_mode); - - /* fix modifier keys, set 8 bits per char, ignore interrupts */ - raw = E.orig_termios; - raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); - raw.c_oflag &= ~(OPOST); - raw.c_cflag |= (CS8); - raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); - - /* set read() timeout in tenths of seconds */ - raw.c_cc[VMIN] = 0; - raw.c_cc[VTIME] = 1; - - /* apply terminal settings */ - if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) - die("tcsetattr in enable_raw_mode()"); -} - -char -editor_read_key() -{ - int nread; - char c; - while ((nread = read(STDIN_FILENO, &c, 1)) != 1) { - if (nread == -1 && errno != EAGAIN) - die("read in editor_read_key()"); - } - return c; -} - -/* get screen size by moving cursor and get current position */ -int -get_cursor_position(int *rows, int *cols) { - char buf[32]; - unsigned int i; - - if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) - return -1; - - i = 0; - while (i < sizeof(buf) - 1) { - if (read(STDIN_FILENO, &buf[i], 1) != 1) - break; - if (buf[i] == 'R') - break; - ++i; - } - buf[i] = '\0'; - - if (buf[0] != '\x1b' || buf[1] != '[') - return -1; - if (scanf(&buf[2], "%d;%d", rows, cols) != 2) - return -1; - return 0; -} - -int -get_window_size(int *rows, int *cols) -{ - struct winsize ws; - - if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { - /* fallback screen size detection */ - if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) - return -1; - return get_cursor_position(rows, cols); - } else { - *cols = ws.ws_col; - *rows = ws.ws_row; - return 0; - } -} (DIR) diff --git a/src/terminal.h b/src/terminal.h t@@ -1,11 +0,0 @@ -#ifndef TERMINAL_H_ -#define TERMINAL_H_ - -void die(const char *s); -void disable_raw_mode(); -void enable_raw_mode(); -char editor_read_key(); -int get_window_size(int *rows, int *cols); -void init_editor(); - -#endif (DIR) diff --git a/src/ve.c b/src/ve.c t@@ -1,39 +0,0 @@ -#include <stdlib.h> -#include "terminal.h" -#include "row.h" -#include "ve.h" - -struct editor_config E; - -void -deinit_editor() { - int i; - free(E.filename); - free(E.find_query); - for (i=0; i<E.num_rows; ++i) - editor_row_free(&E.row[i]); -} - -/* set editor state variables, make room for status */ -void -init_editor() { - E.cursor_x = 0; - E.cursor_y = 0; - E.cursor_rx = 0; - E.mode = 0; - E.num_rows = 0; - atexit(deinit_editor); - E.row = NULL; - E.row_offset = 0; - E.column_offset = 0; - E.filename = NULL; - E.status_msg[0] = '\0'; - E.status_msg_time = 0; - E.show_status = 0; - E.file_changed = 0; - E.find_query = NULL; - - if (get_window_size(&E.screen_rows, &E.screen_columns) == -1) - die("get_window_size"); - E.screen_rows -= 1; -} (DIR) diff --git a/src/ve.h b/src/ve.h t@@ -1,39 +0,0 @@ -#ifndef BYOTE_H_ -#define BYOTE_H_ - -#include <termios.h> -#include <time.h> - -#define PROGNAME "ve" -#define VERSION "0.0.1" - -#define TAB_WIDTH 4 -#define STATUS_MESSAGE_TIMEOUT 3 - -/* editor row: stores a row of text */ -typedef struct eRow { - char *chars; /* text read from file */ - char *rchars; /* text to render */ - int size; /* length of chars */ - int rsize; /* length of rchars */ -} eRow; - -struct editor_config { - int cursor_x, cursor_y, cursor_rx; - int screen_rows, screen_columns; - int num_rows; - eRow *row; - int row_offset, column_offset; - char *filename; - struct termios orig_termios; - int mode; /* 0: normal, 1: insert, 2: visual */ - int show_status; - char status_msg[80]; - time_t status_msg_time; - int file_changed; - char *find_query; - int find_direction; -}; - -extern struct editor_config E; -#endif (DIR) diff --git a/ve.c b/ve.c t@@ -0,0 +1,1062 @@ +/* see LICENSE for license details */ + +/* add feature test macro for getline and strdup compatibility */ +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#define _GNU_SOURCE + +#include <ctype.h> +#include <errno.h> +#include <fcntl.h> +#include <stdarg.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> +#include <sys/ioctl.h> +#include <termios.h> +#include <time.h> +#include <unistd.h> + + +/* macros */ +#define PROGNAME "ve" +#define VERSION "0.0.1" +#define TAB_WIDTH 4 +#define STATUS_MESSAGE_TIMEOUT 3 +#define ABUF_INIT {NULL, 0} +#define CTRL_KEY(k) ((k) & 0x1f) + + + +/* types */ + +struct abuf { + char *b; + int len; +}; + +/* editor row: stores a row of text */ +typedef struct eRow { + char *chars; /* text read from file */ + char *rchars; /* text to render */ + int size; /* length of chars */ + int rsize; /* length of rchars */ +} eRow; + +struct editor_config { + int cursor_x, cursor_y, cursor_rx; + int screen_rows, screen_columns; + int num_rows; + eRow *row; + int row_offset, column_offset; + char *filename; + struct termios orig_termios; + int mode; /* 0: normal, 1: insert, 2: visual */ + int show_status; + char status_msg[80]; + time_t status_msg_time; + int file_changed; + char *find_query; + int find_direction; +}; + + +/* function declarations */ +char* editor_prompt(char *prompt); +void editor_set_status_message(const char *fmt, ...); + + +/* global variables */ +struct editor_config E; + + +/* function definitions */ + +void +die(const char *s) +{ + /* clear screen on exit */ + write(STDOUT_FILENO, "\x1b[2J", 4); + write(STDOUT_FILENO, "\x1b[H", 3); + perror(s); + exit(1); +} + +void +disable_raw_mode() +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1) + die("tcsetattr in disable_raw_mode()"); +} + +void +enable_raw_mode() +{ + struct termios raw; + + if (tcgetattr(STDIN_FILENO, &E.orig_termios) == -1) + die("tcgetattr in enable_raw_mode()"); + atexit(disable_raw_mode); + + /* fix modifier keys, set 8 bits per char, ignore interrupts */ + raw = E.orig_termios; + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + raw.c_oflag &= ~(OPOST); + raw.c_cflag |= (CS8); + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + + /* set read() timeout in tenths of seconds */ + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + /* apply terminal settings */ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) + die("tcsetattr in enable_raw_mode()"); +} + +char +editor_read_key() +{ + int nread; + char c; + while ((nread = read(STDIN_FILENO, &c, 1)) != 1) { + if (nread == -1 && errno != EAGAIN) + die("read in editor_read_key()"); + } + return c; +} + +/* get screen size by moving cursor and get current position */ +int +get_cursor_position(int *rows, int *cols) { + char buf[32]; + unsigned int i; + + if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) + return -1; + + i = 0; + while (i < sizeof(buf) - 1) { + if (read(STDIN_FILENO, &buf[i], 1) != 1) + break; + if (buf[i] == 'R') + break; + ++i; + } + buf[i] = '\0'; + + if (buf[0] != '\x1b' || buf[1] != '[') + return -1; + if (scanf(&buf[2], "%d;%d", rows, cols) != 2) + return -1; + return 0; +} + +int +get_window_size(int *rows, int *cols) +{ + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* fallback screen size detection */ + if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) + return -1; + return get_cursor_position(rows, cols); + } else { + *cols = ws.ws_col; + *rows = ws.ws_row; + return 0; + } +} + + +/* navigate over tab-representative string as one character */ +int +editor_row_cursor_x_to_rx(eRow *row, int cursor_x) +{ + int rx, j; + rx = 0; + for (j=0; j<cursor_x; ++j) { + if (row->chars[j] == '\t') + rx += (TAB_WIDTH - 1) - (rx % TAB_WIDTH); + rx++; + } + return rx; +} + +/* translate on-screen position to data position */ +int +editor_row_cursor_rx_to_x(eRow *row, int cursor_rx) +{ + int cur_rx, cx; + cur_rx = 0; + for (cx=0; cx<row->size; ++cx) { + if (row->chars[cx] == '\t') + cur_rx += (TAB_WIDTH - 1) - (cur_rx % TAB_WIDTH); + cur_rx++; + + if (cur_rx > cursor_rx) + return cx; + } + return cx; +} + +/* translate tabs before display */ +void +editor_row_update(eRow* row) +{ + int j, idx, tabs; + + free(row->rchars); + row->rchars = malloc(row->size + 1); + + tabs = 0; + for (j=0; j<row->size; ++j) + if (row->chars[j] == '\t') + tabs++; + + free(row->rchars); + row->rchars = malloc(row->size + tabs*(TAB_WIDTH - 1) + 1); + + idx = 0; + for (j=0; j<row->size; ++j) { + if (row->chars[j] == '\t') { + row->rchars[idx++] = '>'; + while (idx % TAB_WIDTH != 0) + row->rchars[idx++] = ' '; + } else { + row->rchars[idx++] = row->chars[j]; + } + } + row->rchars[idx] = '\0'; + row->rsize = idx; +} + +/* add row to buffer */ +void +editor_row_insert(int i, char *s, size_t len) +{ + if (i<0 || i>E.num_rows) + return; + + E.row = realloc(E.row, sizeof(eRow) * (E.num_rows + 1)); + memmove(&E.row[i+1], &E.row[i], sizeof(eRow) * (E.num_rows - i)); + + E.row[i].size = len; + E.row[i].chars = malloc(len + 1); + memcpy(E.row[i].chars, s, len); + E.row[i].chars[len] = '\0'; + + E.row[i].rsize = 0; + E.row[i].rchars = NULL; + editor_row_update(&E.row[i]); + + ++E.num_rows; + E.file_changed = 1; +} + +/* insert character to row, making sure to allocate memory accordingly */ +void +editor_row_insert_char(eRow *row, int i, int c) +{ + if (i<0 || i>row->size) + i = row->size; + row->chars = realloc(row->chars, row->size + 2); + memmove(&row->chars[i+1], &row->chars[i], row->size - i+1); + row->size++; + row->chars[i] = c; + editor_row_update(row); + E.file_changed = 1; +} + +/* append a string to the end of a row */ +void +editor_row_append_string(eRow *row, char *s, size_t len) +{ + row->chars = realloc(row->chars, row->size + len + 1); + memcpy(&row->chars[row->size], s, len); + row->size += len; + row->chars[row->size] = '\0'; + editor_row_update(row); + E.file_changed = 1; +} + +void +editor_row_delete_char(eRow *row, int i) +{ + if (i<0 || i>=row->size) + return; + memmove(&row->chars[i], &row->chars[i+1], row->size - i); + row->size--; + editor_row_update(row); + E.file_changed = 1; +} + +void +editor_row_free(eRow *row) +{ + free(row->rchars); + free(row->chars); +} + +void +editor_row_delete(int i) +{ + if (i<0 || i>=E.num_rows) + return; + editor_row_free(&E.row[i]); + memmove(&E.row[i], &E.row[i+1], sizeof(eRow)*(E.num_rows - i - 1)); + E.num_rows--; + E.file_changed = 1; +} + +void +editor_insert_char(int c) +{ + if (E.cursor_y == E.num_rows) + editor_row_insert(E.num_rows, "", 0); + editor_row_insert_char(&E.row[E.cursor_y], E.cursor_x, c); + E.cursor_x++; +} + +void +editor_insert_new_line() +{ + eRow *row; + if (E.cursor_x == 0) { + editor_row_insert(E.cursor_y, "", 0); + } else { + row = &E.row[E.cursor_y]; + editor_row_insert(E.cursor_y + 1, &row->chars[E.cursor_x], + row->size - E.cursor_x); + row = &E.row[E.cursor_y]; + row->size = E.cursor_x; + row->chars[row->size] = '\0'; + editor_row_update(row); + } + E.cursor_y++; + E.cursor_x = 0; +} + +/* delete a character before the cursor, and allow for deletion across rows */ +void +editor_delete_char_left() +{ + eRow *row; + if (E.cursor_y == E.num_rows || (E.cursor_x == 0 && E.cursor_y == 0)) + return; + + row = &E.row[E.cursor_y]; + if (E.cursor_x > 0) { + editor_row_delete_char(row, E.cursor_x - 1); + E.cursor_x--; + } else { + E.cursor_x = E.row[E.cursor_y - 1].size; + editor_row_append_string(&E.row[E.cursor_y - 1], + row->chars, row->size); + editor_row_delete(E.cursor_y); + E.cursor_y--; + } +} + +void +editor_delete_char_right() +{ + if (E.cursor_y == E.num_rows) + return; + eRow *row = &E.row[E.cursor_y]; + editor_row_delete_char(row, E.cursor_x); +} + +void +file_open(char *filename) +{ + free(E.filename); + E.filename = strdup(filename); + + FILE *fp; + char *line; + size_t linecap; + ssize_t linelen; + + fp = fopen(filename, "r"); + if (!fp) + die("fopen in file_open"); + + line = NULL; + linecap = 0; + while ((linelen = getline(&line, &linecap, fp)) != -1) { + while (linelen > 0 && (line[linelen - 1] == '\n' || + line[linelen - 1] == '\r')) + linelen--; + editor_row_insert(E.num_rows, line, linelen); + } + free(line); + fclose(fp); + E.file_changed = 0; +} + +/* convert rows to one long char array of length buflen */ +char* +editor_concatenate_rows(int *buflen) +{ + int totlen, j; + char *buf, *p; + + totlen = 0; + for (j=0; j<E.num_rows; ++j) + totlen += E.row[j].size + 1; /* add space for newline char */ + *buflen = totlen; + + buf = malloc(totlen); + p = buf; + for (j=0; j<E.num_rows; ++j) { + memcpy(p, E.row[j].chars, E.row[j].size); + p += E.row[j].size; + *p = '\n'; + p++; + } + return buf; +} + +void +file_save(char *filename) +{ + int len, fd; + char *buf; + + if (filename == NULL) { + filename = editor_prompt("save as: %s"); + if (filename == NULL) { + editor_set_status_message("save aborted"); + return; + } + } + + buf = editor_concatenate_rows(&len); + + fd = open(filename, O_RDWR | O_CREAT, 0644); + if (fd != -1) { + if (ftruncate(fd, len) != -1) { + if (write(fd, buf, len) == len) { + close(fd); + free(buf); + E.filename = filename; + E.file_changed = 0; + editor_set_status_message("%d bytes written to disk", len); + return; + } + } + close(fd); + } + free(buf); + editor_set_status_message("error: can't save! I/O error %s", + strerror(errno)); +} + +/* reverse of strstr (3) */ +char* +strrstr(char *haystack, char *needle, + size_t haystack_length, size_t needle_length) +{ + char *cp; + for (cp = haystack + haystack_length - needle_length; + cp >= haystack; + cp--) { + if (strncmp(cp, needle, needle_length) == 0) + return cp; + } + return NULL; +} + +/* find E.find_query from current cursor position moving in E.direction + * if opposite_direction = 0, and opposite E.direction if + * opposite_direction = 1 */ +void +editor_find_next_occurence(int opposite_direction) +{ + int y, y_inc, x_offset, query_len; + eRow *row; + char *match; + + if (!E.find_query) + return; + + if ((E.find_direction && !opposite_direction) || + (!E.find_direction && opposite_direction)) + y_inc = +1; + else + y_inc = -1; + + x_offset = 0; + query_len = strlen(E.find_query); + /* from cursor until end of document */ + for (y = E.cursor_y; + y != ((y_inc == +1) ? E.num_rows : -1); + y += y_inc) { + row = &E.row[y]; + + if (y == E.cursor_y) + x_offset = y_inc + E.cursor_x; + else + x_offset = 0; + + if (y_inc == +1) { + match = strstr(row->chars + x_offset, E.find_query); + if (match) + break; + } else { + match = strrstr(row->chars, E.find_query, + (y == E.cursor_y) ? E.cursor_x : row->size, + query_len); + if (match) + break; + } + } + + if (match) { + E.cursor_y = y; + E.cursor_x = match - row->chars; + /* E.row_offset = E.num_rows; */ /* put line to top of screen */ + + } else { + /* from other end of file until cursor */ + for (y = (y_inc == +1) ? 0 : E.num_rows - 1; + y != E.cursor_y + y_inc; + y += y_inc) { + row = &E.row[y]; + match = strstr(row->chars, E.find_query); + if (match) + break; + } + if (match) { + E.cursor_y = y; + E.cursor_x = editor_row_cursor_rx_to_x(row, match - row->chars); + } + } +} + +/* direction = 1 is forward, 0 is backward */ +void +editor_find(int direction) +{ + char *query; + + if (direction) + query = editor_prompt("/%s"); + else + query = editor_prompt("?%s"); + if (query == NULL) + return; + + E.find_direction = direction; + free(E.find_query); + E.find_query = strdup(query); + editor_find_next_occurence(0); + free(query); +} + + +/* reallocate append buffer to hold string s */ +void +ab_append(struct abuf *ab, const char *s, int len) +{ + char *new; + new = realloc(ab->b, ab->len + len); + + if (new == NULL) + return; + memcpy(&new[ab->len], s, len); + ab->b = new; + ab->len += len; +} + +void +ab_free(struct abuf *ab) { + free(ab->b); +} + +int +show_status_message() +{ + if (E.show_status && + strlen(E.status_msg) && + time(NULL) - E.status_msg_time < STATUS_MESSAGE_TIMEOUT) { + return 1; + } else { + if (E.show_status) { + E.show_status = 0; + E.screen_rows++; + } + return 0; + } +} + +/* draw status line consisting of left and right components. + * if the screen is narrower than both parts, just show the left status, + * truncated, if necessary. */ +void +editor_draw_status(struct abuf *ab) +{ + char left_status[512], right_status[512]; + int left_status_len, right_status_len, padding; + int percentage; + + left_status_len = 0; + switch (E.mode) { + case 1: + left_status_len = snprintf(left_status, sizeof(left_status), + "INSERT "); + break; + case 2: + left_status_len = snprintf(left_status, sizeof(left_status), + "VISUAL "); + break; + } + left_status_len += snprintf(left_status + left_status_len, + sizeof(left_status), + "%.20s %s", + E.filename ? E.filename : "[unnamed]", + E.file_changed ? "[+] " : ""); + + percentage = (int)((float)(E.cursor_y)/(E.num_rows-1)*100); + if (percentage < 0) percentage = 0; + right_status_len = snprintf(right_status, sizeof(right_status), + "%d%% < %d, %d", + percentage, + E.cursor_x+1, E.cursor_y+1); + + if (left_status_len > E.screen_columns) + left_status_len = E.screen_columns; + + padding = E.screen_columns - left_status_len - right_status_len; + if (padding < 0) { + if (left_status_len < E.screen_columns) + ab_append(ab, left_status, left_status_len); + else + ab_append(ab, left_status, E.screen_columns); + } else { + ab_append(ab, left_status, left_status_len); + while (padding--) + ab_append(ab, " ", 1); + ab_append(ab, right_status, right_status_len); + } + + if (show_status_message()) { + ab_append(ab, "\x1b[m", 3); + ab_append(ab, "\r\n", 2); + } +} + +/* draw status message if as long as it is specified and it is not too old */ +void +editor_draw_status_message(struct abuf *ab) +{ + int msglen; + ab_append(ab, "\x1b[K", 3); + msglen = strlen(E.status_msg); + if (msglen > E.screen_columns) + msglen = E.screen_columns; + if (show_status_message()) + ab_append(ab, E.status_msg, msglen); +} + +/* set vertical offset between file and screen when hitting the boundaries */ +void +editor_scroll() +{ + E.cursor_rx = 0; + if (E.cursor_y < E.num_rows) + E.cursor_rx = editor_row_cursor_x_to_rx(&E.row[E.cursor_y], + E.cursor_x); + + if (E.cursor_y < E.row_offset) + E.row_offset = E.cursor_y; + else if (E.cursor_y >= E.row_offset + E.screen_rows) + E.row_offset = E.cursor_y - E.screen_rows + 1; + + if (E.cursor_rx < E.column_offset) + E.column_offset = E.cursor_rx; + else if (E.cursor_rx >= E.column_offset + E.screen_columns) + E.column_offset = E.cursor_rx - E.screen_columns + 1; +} + +/* draw editor screen. + * show tilde characters after EOF, and show status on last line */ +void +editor_draw_rows(struct abuf *ab) +{ + int y, len, file_row; + for (y = 0; y < E.screen_rows; ++y) { + file_row = y + E.row_offset; + if (file_row < E.num_rows) { + len = E.row[file_row].rsize - E.column_offset; + if (len < 0) + len = 0; + if (len > E.screen_columns) + len = E.screen_columns; + ab_append(ab, &E.row[file_row].rchars[E.column_offset], len); + } else { + ab_append(ab, "~", 1); + } + + ab_append(ab, "\x1b[K", 3); /* erase to end of line */ + ab_append(ab, "\r\n", 2); + } +} + +/* fill output append buffer and write to terminal. + * move cursor to left before repaint, and to cursor position after. + * hide the cursor when repainting. + * VT100 escape sequences: + * - http://vt100.net/docs/vt100-ug/chapter3.html + * - http://vt100.net/docs/vt100-ug/chapter3.html#CUP */ +void +editor_refresh_screen() +{ + char buf[32]; + struct abuf ab = ABUF_INIT; + + show_status_message(); + editor_scroll(); + + ab_append(&ab, "\x1b[?25l", 6); /* hide cursor */ + ab_append(&ab, "\x1b[H", 3); /* cursor to home */ + + editor_draw_rows(&ab); + editor_draw_status(&ab); + editor_draw_status_message(&ab); + + snprintf(buf, sizeof(buf), "\x1b[%d;%dH", + (E.cursor_y - E.row_offset)+1, + (E.cursor_rx - E.column_offset)+1); + ab_append(&ab, buf, strlen(buf)); + + ab_append(&ab, "\x1b[?25h", 6); /* show cursor */ + + write(STDOUT_FILENO, ab.b, ab.len); + ab_free(&ab); +} + +void +editor_place_cursor(int cx, int cy) +{ + char buf[128]; + snprintf(buf, sizeof(buf), "\x1b[%d;%dH", cy, cx); + write(STDOUT_FILENO, buf, strlen(buf)); +} + +/* set status message text, uses same format as printf */ +void +editor_set_status_message(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vsnprintf(E.status_msg, sizeof(E.status_msg), fmt, ap); + va_end(ap); + E.status_msg_time = time(NULL); + + if (!E.show_status) { + E.screen_rows--; + E.show_status = 1; + } +} + +/* prompt is expected to be a format string containing a %s */ +char* +editor_prompt(char *prompt) +{ + size_t bufsize, buflen; + char *buf; + int c; + + bufsize = 128; + buflen = 0; + buf = malloc(bufsize); + buf[0] = '\0'; + + while (1) { + editor_set_status_message(prompt, buf); + editor_refresh_screen(); + editor_place_cursor(strlen(prompt) - 1 + strlen(buf), + E.screen_rows + 2); + + c = editor_read_key(); + if (c == CTRL_KEY('h') || c == 127) { /* detect backspace */ + if (buflen != 0) + buf[--buflen] = '\0'; + } else if (c == '\x1b' || (c == '\r' && !buflen)) { /* detect escape */ + editor_set_status_message(""); + free(buf); + return NULL; + } else if (c == '\r') { + if (buflen != 0) { + editor_set_status_message(""); + return buf; + } + } else if (!iscntrl(c) && c < 128) { + if (buflen >= bufsize - 1) { + bufsize *= 2; + buf = realloc(buf, bufsize); + } + buf[buflen++] = c; + buf[buflen] = '\0'; + } + } +} + +/* move cursor according to screen, file, and line limits */ +void +editor_move_cursor(char key) +{ + int row_len; + eRow *row; + + switch(key) { + case 'h': + if (E.cursor_x != 0) { + E.cursor_x--; + } else if (E.cursor_y > 0) { + E.cursor_y--; + E.cursor_x = E.row[E.cursor_y].size; + } + break; + case 'j': + if (E.cursor_y < E.num_rows - 1) + E.cursor_y++; + break; + case 'k': + if (E.cursor_y != 0) + E.cursor_y--; + break; + case 'l': + row = (E.cursor_y >= E.num_rows) ? NULL : &E.row[E.cursor_y]; + if (row && E.cursor_x < row->size - 1) { + E.cursor_x++; + } else if (row && E.cursor_x == row->size && + E.cursor_y < E.num_rows - 1) { + E.cursor_y++; + E.cursor_x = 0; + } + break; + } + + /* do not allow navigation past EOL by vertical navigation */ + row = (E.cursor_y >= E.num_rows) ? NULL : &E.row[E.cursor_y]; + row_len = row ? row->size : 0; + if (E.cursor_x > row_len) + E.cursor_x = row_len; +} + +void +editor_process_keypress() +{ + char c; + int i; + + c = editor_read_key(); + + if (E.mode == 0) { /* normal mode */ + switch (c) { + case 'i': + E.mode = 1; + break; + case 'a': + editor_move_cursor('l'); + E.mode = 1; + break; + + case CTRL_KEY('q'): + if (E.file_changed) { + editor_set_status_message("error: " + "file has unsaved changes. " + "Press C-x to confirm quit"); + break; + } else { + write(STDOUT_FILENO, "\x1b[2J", 4); /* clear screen */ + write(STDOUT_FILENO, "\x1b[H", 3); + exit(0); + break; + } + case CTRL_KEY('x'): + write(STDOUT_FILENO, "\x1b[2J", 4); /* clear screen */ + write(STDOUT_FILENO, "\x1b[H", 3); + exit(0); + break; + case 'h': + case 'j': + case 'k': + case 'l': + editor_move_cursor(c); + break; + + case CTRL_KEY('w'): + file_save(E.filename); + break; + + case CTRL_KEY('f'): + i = E.screen_rows; + while (i--) + editor_move_cursor('j'); + break; + case CTRL_KEY('b'): + i = E.screen_rows; + while (i--) + editor_move_cursor('k'); + break; + + case CTRL_KEY('d'): + i = E.screen_rows/2; + while (i--) + editor_move_cursor('j'); + break; + case CTRL_KEY('u'): + i = E.screen_rows/2; + while (i--) + editor_move_cursor('k'); + break; + + case '0': + E.cursor_x = 0; + break; + case '$': + if (E.cursor_y < E.num_rows) + E.cursor_x = E.row[E.cursor_y].size - 1; + break; + + case 'g': + E.cursor_x = 0; + E.cursor_y = 0; + break; + case 'G': + E.cursor_x = 0; + E.cursor_y = E.num_rows - 1; + break; + + case 'x': + editor_delete_char_right(); + break; + case 'd': + editor_row_delete(E.cursor_y); + editor_move_cursor('h'); + break; + + case 'o': + if (E.cursor_y < E.num_rows) + E.cursor_x = E.row[E.cursor_y].size; + editor_insert_new_line(); + E.mode = 1; + break; + case 'O': + E.cursor_x = 0; + editor_insert_new_line(); + editor_move_cursor('k'); + E.mode = 1; + break; + + case 'I': + E.cursor_x = 0; + E.mode = 1; + break; + case 'A': + if (E.cursor_y < E.num_rows) + E.cursor_x = E.row[E.cursor_y].size; + E.mode = 1; + break; + + case '/': + editor_find(1); + break; + case '?': + editor_find(0); + break; + case 'n': + editor_find_next_occurence(0); + break; + case 'N': + editor_find_next_occurence(1); + break; + } + } else if (E.mode == 1) { /* insert mode */ + switch (c) { + case CTRL_KEY('c'): + case '\x1b': /* escape */ + E.mode = 0; + break; + + case CTRL_KEY('\r'): /* enter */ + editor_insert_new_line(); + break; + + case 127: /* backspace */ + case CTRL_KEY('h'): + editor_delete_char_left(); + break; + + case CTRL_KEY('l'): + break; + + default: + editor_insert_char(c); + break; + } + } +} + +void +deinit_editor() { + int i; + free(E.filename); + free(E.find_query); + for (i=0; i<E.num_rows; ++i) + editor_row_free(&E.row[i]); +} + +/* set editor state variables, make room for status */ +void +init_editor() { + E.cursor_x = 0; + E.cursor_y = 0; + E.cursor_rx = 0; + E.mode = 0; + E.num_rows = 0; + atexit(deinit_editor); + E.row = NULL; + E.row_offset = 0; + E.column_offset = 0; + E.filename = NULL; + E.status_msg[0] = '\0'; + E.status_msg_time = 0; + E.show_status = 0; + E.file_changed = 0; + E.find_query = NULL; + + if (get_window_size(&E.screen_rows, &E.screen_columns) == -1) + die("get_window_size"); + E.screen_rows -= 1; +} + + +/* main */ +int +main(int argc, char* argv[]) +{ + enable_raw_mode(); + init_editor(); + + /* TODO: proper argument handling */ + if (argc >= 2) { + file_open(argv[1]); + } + + editor_set_status_message("%s v%s", PROGNAME, VERSION); + + while (1) { + editor_refresh_screen(); + editor_process_keypress(); + } + return 0; +} +