From cc99f08747e0553d90255a96f05ac4ad8b2d99f8 Mon Sep 17 00:00:00 2001 From: Timothy Warren Date: Tue, 3 Oct 2023 17:02:22 -0400 Subject: [PATCH] Update to chapter 7 step 161 in kilo tutorial --- _code/kilo.c | 1468 +++++++++++++++++++++ editor/{editor.go => Editor.go} | 20 +- editor/{editor_test.go => Editor_test.go} | 0 editor/document/document.go | 7 +- editor/document/row.go | 34 +- editor/draw.go | 20 +- editor/highlight/constants.go | 12 + editor/highlight/syntax.go | 7 + editor/input.go | 6 +- editor/input_test.go | 2 +- editor/search.go | 49 +- internal/gilo/point.go | 1 + 12 files changed, 1569 insertions(+), 57 deletions(-) create mode 100644 _code/kilo.c rename editor/{editor.go => Editor.go} (88%) rename editor/{editor_test.go => Editor_test.go} (100%) create mode 100644 editor/highlight/syntax.go diff --git a/_code/kilo.c b/_code/kilo.c new file mode 100644 index 0000000..17ad3fa --- /dev/null +++ b/_code/kilo.c @@ -0,0 +1,1468 @@ +/*** includes ***/ + +#define _DEFAULT_SOURCE +#define _BSD_SOURCE +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/*** defines ***/ + +#define KILO_VERSION "0.0.1" +#define KILO_TAB_STOP 4 +#define KILO_QUIT_TIMES 3 + +#define CTRL_KEY(k) ((k) & 0x1f) + +enum editorKey { + BACKSPACE = 127, + ARROW_LEFT = 1000, + ARROW_RIGHT, + ARROW_UP, + ARROW_DOWN, + DEL_KEY, + HOME_KEY, + END_KEY, + PAGE_UP, + PAGE_DOWN, +}; + +enum editorHighlight { + HL_NORMAL = 0, + HL_COMMENT, + HL_MLCOMMENT, + HL_KEYWORD1, + HL_KEYWORD2, + HL_STRING, + HL_NUMBER, + HL_MATCH +}; + +#define HL_HIGHLIGHT_NUMBERS (1<<0) +#define HL_HIGHLIGHT_STRINGS (1<<1) + +/*** data ***/ + +/** + * Struct representing parsing parameters for a file type + */ +struct editorSyntax { + char *filetype; + char **filematch; + char **keywords; + char *singleline_comment_start; + char *multiline_comment_start; + char *multiline_comment_end; + int flags; +}; + +/** + * Struct representing a row in the editor + */ +typedef struct erow { + int idx; // Row number in the file + int size; // Number of characters + int rsize; // Number of characters rendered to screen + char *chars; // Input characters + char *render; // Display characters + unsigned char *hl; // Highlighted representation of characters + int hl_open_comment; // Is this row part of a multiline comment? +} erow; + +/** + * Global editor state + * + * // Nested comment to check for double highlight + */ +struct editorConfig { + int cx, cy; // Cursor position + int rx; // Cursor render position + int rowoff; // Vertical scroll offset + int coloff; // Horizontal scroll offset + int screenrows; // Number of rows visible in the current terminal + int screencols; // Number of columns visible in the current terminal + int numrows; + erow *row; // Current row + int dirty; // File modification check flag + char *filename; // Name of the current file + char statusmsg[80]; // Message for the status bar + time_t statusmsg_time; + struct editorSyntax *syntax; // Type of syntax for current file + struct termios orig_termios; +}; + +struct editorConfig E; + +/*** filetypes ***/ + +char *C_HL_extensions[] = { ".c", ".h", ".cpp", NULL }; +char *C_HL_keywords[] = { + // Keywords + "switch", "if", "while", "for", "break", "continue", "return", "else", + "struct", "union", "typedef", "static", "enum", "class", "case", + + // Types + "int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|", + "void|", + + // Preprocessor Directives + "#define|", "#endif|", "#error|", "#if|", "#ifdef|", "#ifndef|", "#include|", + "#undef|", NULL +}; + +struct editorSyntax HLDB[] = { + { + "c", + C_HL_extensions, + C_HL_keywords, + "//", "/*", "*/", + HL_HIGHLIGHT_NUMBERS | HL_HIGHLIGHT_STRINGS + }, +}; + +#define HLDB_ENTRIES (sizeof(HLDB) / sizeof(HLDB[0])) + +/*** prototypes ***/ + +void editorSetStatusMessage(const char *fmt, ...); +void editorRefreshScreen(); +char *editorPrompt(char *prompt, void (*callback)(char *, int)); + +/*** terminal ***/ + +void die(const char *s) +{ + write(STDOUT_FILENO, "\x1b[2J", 4); + write(STDOUT_FILENO, "\x1b[H", 3); + + perror(s); + exit(1); +} + +void disableRawMode() +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.orig_termios) == -1) + { + die("tcsetattr"); + } +} + +void enableRawMode() +{ + if (tcgetattr(STDIN_FILENO, &E.orig_termios) == -1) { + die("tcgetattr"); + } + atexit(disableRawMode); + + struct termios 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); + raw.c_cc[VMIN] = 0; + raw.c_cc[VTIME] = 1; + + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1) + { + die("tcsetattr"); + } +} + +int editorReadKey() +{ + int nread; + char c; + while ((nread = read(STDIN_FILENO, &c, 1)) != 1) + { + if (nread == -1 && errno != EAGAIN) + { + die("read"); + } + } + + // The character code starts with the escape sequence (\x1b) + if (c == '\x1b') + { + char seq[3]; + + if (read(STDIN_FILENO, &seq[0], 1) != 1) + { + return '\x1b'; + } + + if (read(STDIN_FILENO, &seq[1], 1) != 1) + { + return '\x1b'; + } + + if (seq[0] == '[') + { + if (seq[1] >= '0' && seq[1] <= 9) + { + if (read(STDIN_FILENO, &seq[2], 1) != 1) + { + return '\x1b'; + } + + if (seq[2] == '~') + { + switch (seq[1]) + { + case '1': return HOME_KEY; + case '3': return DEL_KEY; + case '4': return END_KEY; + case '5': return PAGE_UP; + case '6': return PAGE_DOWN; + case '7': return HOME_KEY; + case '8': return END_KEY; + } + } + } + else + { + switch (seq[1]) + { + case 'A': return ARROW_UP; + case 'B': return ARROW_DOWN; + case 'C': return ARROW_RIGHT; + case 'D': return ARROW_LEFT; + case 'H': return HOME_KEY; + case 'F': return END_KEY; + } + } + + } + else if (seq[0] == 'O') + { + switch (seq[1]) + { + case 'H': return HOME_KEY; + case 'F': return END_KEY; + } + } + + return '\x1b'; + } + + return c; +} + +int getCursorPosition(int *rows, int *cols) +{ + char buf[32]; + unsigned int i = 0; + + if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) + { + return -1; + } + + 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 (sscanf(&buf[2], "%d;%d", rows, cols) != 2) + { + return -1; + } + + return 0; +} + +int getWindowSize(int *rows, int *cols) +{ + struct winsize ws; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) + { + if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) + { + return -1; + } + else + { + return getCursorPosition(rows, cols); + } + } + + *cols = ws.ws_col; + *rows = ws.ws_row; + + return 0; +} + +/*** syntax highlighting ***/ + +int is_separator(int c) +{ + return isspace(c) || c == '\0' || strchr(",.()+-/*=~%<>[];", c) != NULL; +} + +void editorUpdateSyntax(erow *row) +{ + row->hl = realloc(row->hl, row->rsize); + memset(row->hl, HL_NORMAL, row->rsize); + + if (E.syntax == NULL) + { + return; + } + + char **keywords = E.syntax->keywords; + + char *scs = E.syntax->singleline_comment_start; + char *mcs = E.syntax->multiline_comment_start; + char *mce = E.syntax->multiline_comment_end; + + int scs_len = scs ? strlen(scs) : 0; + int mcs_len = mcs ? strlen(mcs): 0; + int mce_len = mce ? strlen(mce): 0; + + int prev_sep = 1; + int in_string = 0; + int in_comment = (row->idx > 0 && E.row[row->idx -1].hl_open_comment); + + int i = 0; + while (i < row->rsize) + { + char c = row->render[i]; + unsigned char prev_hl = (i > 0) ? row->hl[i - 1] : HL_NORMAL; + + // Single line comments + if (scs_len && ! in_string && ! in_comment) + { + if ( ! strncmp(&row->render[i], scs, scs_len)) + { + memset(&row->hl[i], HL_COMMENT, row->rsize - i); + break; + } + } + + // Multi-line comments + if (mcs_len && mce_len && ! in_string) + { + if (in_comment) + { + row->hl[i] = HL_MLCOMMENT; + if ( ! strncmp(&row->render[i], mce, mce_len)) + { + memset(&row->hl[i], HL_MLCOMMENT, mce_len); + i += mce_len; + in_comment = 0; + prev_sep = 1; + continue; + } + else + { + i++; + continue; + } + } + else if ( ! strncmp(&row->render[i], mcs, mcs_len)) + { + memset(&row->hl[i], HL_MLCOMMENT, mcs_len); + i += mcs_len; + in_comment = 1; + continue; + } + } + + // Strings + if (E.syntax->flags & HL_HIGHLIGHT_STRINGS) + { + if (in_string) + { + row->hl[i] = HL_STRING; + if (c == '\\' && i+1 < row->rsize) + { + row->hl[i + 1] = HL_STRING; + i += 2; + continue; + } + + if (c == in_string) + { + in_string = 0; + } + i++; + prev_sep = 1; + continue; + } + else + { + if (c == '"' || c == '\'') + { + in_string = c; + row->hl[i] = HL_STRING; + i++; + continue; + } + } + } + + // Numbers + if (E.syntax->flags & HL_HIGHLIGHT_NUMBERS) + { + if ((isdigit(c) && (prev_sep || prev_hl == HL_NUMBER)) || + (c == '.' && prev_hl == HL_NUMBER)) + { + row->hl[i] = HL_NUMBER; + i++; + prev_sep = 0; + continue; + } + } + + // Keywords + if (prev_sep) + { + int j; + for (j = 0; keywords[j]; j++) + { + int klen = strlen(keywords[j]); + int kw2 = keywords[j][klen -1] == '|'; + if (kw2) + { + klen--; + } + + if ( ! strncmp(&row->render[i], keywords[j], klen) && + is_separator(row->render[i + klen])) + { + memset(&row->hl[i], kw2 ? HL_KEYWORD2 : HL_KEYWORD1, klen); + i += klen; + break; + } + } + if (keywords[j] != NULL) + { + prev_sep = 0; + continue; + } + } + + prev_sep = is_separator(c); + i++; + } + + int changed = (row->hl_open_comment != in_comment); + row->hl_open_comment = in_comment; + if (changed && row->idx + 1 < E.numrows) + { + editorUpdateSyntax(&E.row[row->idx + 1]); + } +} + +int editorSyntaxToColor(int hl) +{ + switch (hl) + { + case HL_COMMENT: + case HL_MLCOMMENT: + return 36; // cyan + + case HL_KEYWORD1: + return 33; // yellow + + case HL_KEYWORD2: + return 32; // green + + case HL_STRING: + return 35; // magenta + + case HL_NUMBER: + return 31; // red + + case HL_MATCH: + return 34; // blue + + default: + return 37; + } +} + +void editorSelectSyntaxHighlight() +{ + E.syntax = NULL; + if (E.filename == NULL) + { + return; + } + + char *ext = strrchr(E.filename, '.'); + + for (unsigned int j = 0; j < HLDB_ENTRIES; j++) + { + struct editorSyntax *s = &HLDB[j]; + unsigned int i = 0; + while (s->filematch[i]) + { + int is_ext = (s->filematch[i][0] == '.'); + if ((is_ext && ext && !strcmp(ext, s->filematch[i])) || + ( ! is_ext && strstr(E.filename, s->filematch[i]))) + { + E.syntax = s; + + int filerow; + for (filerow = 0; filerow < E.numrows; filerow++) + { + editorUpdateSyntax(&E.row[filerow]); + } + + return; + } + i++; + } + } +} + +/*** row operations ***/ + +int editorRowCxToRx(erow *row, int cx) +{ + int rx = 0; + int j; + + for (j = 0; j < cx; j++) + { + if (row->chars[j] == '\t') + { + rx += (KILO_TAB_STOP -1) - (rx % KILO_TAB_STOP); + } + rx++; + } + + return rx; +} + +int editorRowRxToCx(erow *row, int rx) +{ + int cur_rx = 0; + int cx; + for (cx = 0; cx < row->size; cx++) + { + if (row->chars[cx] == '\t') + { + cur_rx += (KILO_TAB_STOP - 1) - (cur_rx % KILO_TAB_STOP); + } + cur_rx++; + + if (cur_rx > rx) + { + return rx; + } + } + + return cx; +} + +void editorUpdateRow(erow *row) +{ + int tabs = 0; + int j; + + for (j = 0; j < row->size; j++) + { + if (row->chars[j] == '\t') + { + tabs++; + } + } + + free(row->render); + row->render = malloc(row->size + tabs *(KILO_TAB_STOP - 1) + 1); + + int idx = 0; + for (j = 0; j < row->size; j++) + { + if (row->chars[j] == '\t') + { + row->render[idx++] = ' '; + while (idx % KILO_TAB_STOP != 0) + { + row->render[idx++] = ' '; + } + } + else + { + row->render[idx++] = row->chars[j]; + } + } + row->render[idx] = '\0'; + row->rsize = idx; + + editorUpdateSyntax(row); +} + +void editorInsertRow(int at, char *s, size_t len) +{ + if (at < 0 || at > E.numrows) + { + return; + } + + E.row = realloc(E.row, sizeof(erow) * (E.numrows + 1)); + memmove(&E.row[at + 1], &E.row[at], sizeof(erow) * (E.numrows - at)); + // Update index of following rows on insert + for (int j = at + 1; j <= E.numrows; j++) + { + E.row[j].idx++; + } + + E.row[at].idx = at; + + E.row[at].size = len; + E.row[at].chars = malloc(len + 1); + memcpy(E.row[at].chars, s, len); + E.row[at].chars[len] = '\0'; + + E.row[at].rsize = 0; + E.row[at].render = NULL; + E.row[at].hl = NULL; + E.row[at].hl_open_comment = 0; + editorUpdateRow(&E.row[at]); + + E.numrows++; + E.dirty++; +} + +void editorFreeRow(erow *row) +{ + free(row->render); + free(row->chars); + free(row->hl); +} + +void editorDelRow(int at) +{ + if (at < 0 || at >= E.numrows) + { + return; + } + editorFreeRow(&E.row[at]); + memmove(&E.row[at], &E.row[at + 1], sizeof(erow) * (E.numrows - at - 1)); + // Update index of following rows on delete + for (int j = at; j < E.numrows - 1; j++) + { + E.row[j].idx--; + } + E.numrows--; + E.dirty++; +} + +void editorRowInsertChar(erow *row, int at, int c) +{ + if (at < 0 || at > row->size) + { + at = row->size; + } + + row->chars = realloc(row->chars, row->size + 2); + memmove(&row->chars[at + 1], &row->chars[at], row->size - at + 1); + row->size++; + row->chars[at] = c; + editorUpdateRow(row); + E.dirty++; +} + +void editorRowAppendString(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'; + editorUpdateRow(row); + E.dirty++; +} + +void editorRowDelChar(erow *row, int at) +{ + if (at < 0 || at >= row->size) + { + return; + } + + memmove(&row->chars[at], &row->chars[at + 1], row->size - at); + row->size--; + editorUpdateRow(row); + E.dirty++; +} + +/*** editor operations ***/ + +void editorInsertChar(int c) +{ + if (E.cy == E.numrows) + { + editorInsertRow(E.numrows, "", 0); + } + editorRowInsertChar(&E.row[E.cy], E.cx, c); + E.cx++; +} + +void editorInsertNewline() +{ + if (E.cx == 0) + { + editorInsertRow(E.cy, "", 0); + } + else + { + erow *row = &E.row[E.cy]; + editorInsertRow(E.cy + 1, &row->chars[E.cx], row->size - E.cx); + row = &E.row[E.cy]; + row->size = E.cx; + row->chars[row->size] = '\0'; + editorUpdateRow(row); + } + E.cy++; + E.cx = 0; +} + +void editorDelChar() +{ + if (E.cy == E.numrows) + { + return; + } + + if (E.cx == 0 && E.cy == 0) + { + return; + } + + erow *row = &E.row[E.cy]; + if (E.cx > 0) + { + editorRowDelChar(row, E.cx - 1); + E.cx--; + } + else + { + E.cx = E.row[E.cy - 1].size; + editorRowAppendString(&E.row[E.cy - 1], row->chars, row->size); + editorDelRow(E.cy); + E.cy--; + } +} + +/*** file i/o ***/ + +char *editorRowsToString(int *buflen) +{ + int totlen = 0; + int j; + for (j = 0; j < E.numrows; j++) + { + totlen += E.row[j].size + 1; + } + *buflen = totlen; + + char *buf = malloc(totlen); + char *p = buf; + for (j = 0; j < E.numrows; j++) + { + memcpy(p, E.row[j].chars, E.row[j].size); + p += E.row[j].size; + *p = '\n'; + p++; + } + + return buf; +} + +void editorOpen(char *filename) +{ + free(E.filename); + E.filename = strdup(filename); + + editorSelectSyntaxHighlight(); + + FILE *fp = fopen(filename, "r"); + if ( ! fp) + { + die("fopen"); + } + + char *line = NULL; + size_t linecap = 0; + ssize_t linelen; + while ((linelen = getline(&line, &linecap, fp)) != -1) + { + while (linelen > 0 && ( + line[linelen - 1] == '\n' || + line[linelen - 1] == '\r')) + { + linelen--; + } + editorInsertRow(E.numrows, line, linelen); + } + + free(line); + fclose(fp); + E.dirty = 0; +} + +void editorSave() +{ + if (E.filename == NULL) + { + E.filename = editorPrompt("Save as: %s (ESC to cancel)", NULL); + if (E.filename == NULL) + { + editorSetStatusMessage("Save aborted"); + return; + } + editorSelectSyntaxHighlight(); + } + + int len; + char *buf = editorRowsToString(&len); + + int fd = open(E.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.dirty = 0; + editorSetStatusMessage("%d bytes written to disk", len); + return; + } + } + close(fd); + } + free(buf); + editorSetStatusMessage("Can't save! I/O error: %s", strerror(errno)); +} + +/*** find ***/ + +void editorFindCallback(char *query, int key) +{ + static int last_match = -1; + static int direction = 1; + + static int saved_hl_line; + static char *saved_hl = NULL; + + if (saved_hl) + { + memcpy(E.row[saved_hl_line].hl, saved_hl, E.row[saved_hl_line].rsize); + free(saved_hl); + saved_hl = NULL; + } + + if (key == '\r' || key == '\x1b') + { + last_match = -1; + direction = 1; + return; + } + else if (key == ARROW_RIGHT || key == ARROW_DOWN) + { + direction = 1; + } + else if (key == ARROW_LEFT || key == ARROW_UP) + { + direction = -1; + } + else + { + last_match = -1; + direction = 1; + } + + if (last_match == -1) + { + direction = 1; + } + int current = last_match; + int i; + for (i = 0; i < E.numrows; i++) + { + current += direction; + if (current == -1) + { + current = E.numrows -1; + } + else if (current == E.numrows) + { + current = 0; + } + + erow *row = &E.row[current]; + char *match = strstr(row->render, query); + if (match) + { + last_match = current; + E.cy = current; + E.cx = editorRowRxToCx(row, match - row->render); + E.rowoff = E.numrows; + + saved_hl_line = current; + saved_hl = malloc(row->rsize); + memcpy(saved_hl, row->hl, row->rsize); + memset(&row->hl[match - row->render], HL_MATCH, strlen(query)); + break; + } + } +} + +void editorFind() +{ + int saved_cx = E.cx; + int saved_cy = E.cy; + int saved_coloff = E.coloff; + int saved_rowoff = E.rowoff; + + char *query = editorPrompt("Search: %s (Use ESC/Arrows/Enter)", + editorFindCallback); + + if (query) + { + free(query); + } + else + { + E.cx = saved_cx; + E.cy = saved_cy; + E.coloff = saved_coloff; + E.rowoff = saved_rowoff; + } +} + +/*** append buffer ***/ + +struct abuf { + char *b; + int len; +}; + +#define ABUF_INIT {NULL, 0}; + +void abAppend(struct abuf *ab, const char *s, int len) +{ + char *new = realloc(ab->b, ab->len + len); + + if (new == NULL) + { + return; + } + + memcpy(&new[ab->len], s, len); + ab->b = new; + ab->len += len; +} + +void abFree(struct abuf *ab) +{ + free(ab->b); +} + +/*** output ***/ + +void editorScroll() +{ + E.rx = 0; + if (E.cy < E.numrows) + { + E.rx = editorRowCxToRx(&E.row[E.cy], E.cx); + } + + if (E.cy < E.rowoff) + { + E.rowoff = E.cy; + } + + if (E.cy >= E.rowoff + E.screenrows) + { + E.rowoff = E.cy - E.screenrows + 1; + } + + if (E.cx < E.coloff) + { + E.coloff = E.rx; + } + + if (E.cx >= E.coloff + E.screencols) + { + E.coloff = E.rx - E.screencols + 1; + } +} + +void editorDrawRows(struct abuf *ab) +{ + int y; + for (y = 0; y < E.screenrows; y++) + { + int filerow = y + E.rowoff; + if (filerow >= E.numrows) + { + if (E.numrows == 0 && y == E.screenrows / 3) + { + char welcome[80]; + int welcomelen = snprintf(welcome, sizeof(welcome), + "Kilo editor -- version %s", KILO_VERSION); + + if (welcomelen > E.screencols) + { + welcomelen = E.screencols; + } + + int padding = (E.screencols - welcomelen) / 2; + if (padding) + { + abAppend(ab, "~", 1); + padding--; + } + + while (padding--) + { + abAppend(ab, " ", 1); + } + + abAppend(ab, welcome, welcomelen); + } + else + { + abAppend(ab, "~", 1); + } + } + else + { + int len = E.row[filerow].rsize - E.coloff; + if (len < 0) + { + len = 0; + } + + if (len > E.screencols) + { + len = E.screencols; + } + + char *c = &E.row[filerow].render[E.coloff]; + unsigned char *hl = &E.row[filerow].hl[E.coloff]; + int current_color = -1; + int j; + for (j = 0; j < len; j++) + { + // Make control characters "readable" + if (iscntrl(c[j])) + { + char sym = (c[j] <= 26) ? '@' + c[j] : '?'; + abAppend(ab, "\x1b[7m", 4); + abAppend(ab, &sym, 1); + abAppend(ab, "\x1b[m", 3); + // Restore the previous highlighting color + if (current_color != -1) + { + char buf[16]; + int clen = snprintf(buf, sizeof(buf), "\x1b[%dm", current_color); + abAppend(ab, buf, clen); + } + } + else if (hl[j] == HL_NORMAL) + { + if (current_color != -1) + { + abAppend(ab, "\x1b[39m", 5); + current_color = -1; + } + + abAppend(ab, &c[j], 1); + } + else + { + int color = editorSyntaxToColor(hl[j]); + if (color != current_color) + { + current_color = color; + char buf[16]; + int clen = snprintf(buf, sizeof(buf), "\x1b[%dm", color); + abAppend(ab, buf, clen); + } + abAppend(ab, &c[j], 1); + } + } + abAppend(ab, "\x1b[39m", 5); + } + + abAppend(ab, "\x1b[K", 3); + abAppend(ab, "\r\n", 2); + } +} + +void editorDrawStatusBar(struct abuf *ab) +{ + abAppend(ab, "\x1b[7m", 4); + char status[80], rstatus[80]; + int len = snprintf(status, sizeof(status), "%.20s - %d lines %s", + E.filename ? E.filename : "[No Name]", E.numrows, + E.dirty ? "(modified)" : ""); + int rlen = snprintf(rstatus, sizeof(rstatus), "%s | %d/%d", + E.syntax ? E.syntax->filetype : "no ft", E.cy + 1, E.numrows); + + if (len > E.screencols) + { + len = E.screencols; + } + abAppend(ab, status, len); + + while (len < E.screencols) + { + if (E.screencols - len == rlen) + { + abAppend(ab, rstatus, rlen); + break; + } + else + { + abAppend(ab, " ", 1); + len++; + } + } + + abAppend(ab, "\x1b[m", 3); + abAppend(ab, "\r\n", 2); +} + +void editorDrawMessageBar(struct abuf *ab) +{ + abAppend(ab, "\x1b[K", 3); + int msglen = strlen(E.statusmsg); + + if (msglen > E.screencols) + { + msglen = E.screencols; + } + + if (msglen && time(NULL) - E.statusmsg_time < 5) + { + abAppend(ab, E.statusmsg, msglen); + } +} + +void editorRefreshScreen() +{ + editorScroll(); + + struct abuf ab = ABUF_INIT; + + abAppend(&ab, "\x1b[?25l", 6); + abAppend(&ab, "\x1b[H", 3); + + editorDrawRows(&ab); + editorDrawStatusBar(&ab); + editorDrawMessageBar(&ab); + + char buf[32]; + snprintf(buf, sizeof(buf), "\x1b[%d;%dH", + (E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1); + abAppend(&ab, buf, strlen(buf)); + + abAppend(&ab, "\x1b[?25h", 6); + + write(STDOUT_FILENO, ab.b, ab.len); + abFree(&ab); +} + +void editorSetStatusMessage(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vsnprintf(E.statusmsg, sizeof(E.statusmsg), fmt, ap); + va_end(ap); + E.statusmsg_time = time(NULL); +} + +/*** input ***/ + +char *editorPrompt(char *prompt, void (*callback)(char *, int)) +{ + size_t bufsize = 128; + char *buf = malloc(bufsize); + + size_t buflen = 0; + buf[0] = '\0'; + + while (1) + { + editorSetStatusMessage(prompt, buf); + editorRefreshScreen(); + + int c = editorReadKey(); + if (c == DEL_KEY || c == CTRL_KEY('h') || c == BACKSPACE) + { + if (buflen != 0) + { + buf[buflen--] = '\0'; + } + } + else if (c == '\x1b') + { + editorSetStatusMessage(""); + if (callback) + { + callback(buf, c); + } + free(buf); + return NULL; + } + else if (c == '\r') + { + if (buflen != 0) + { + editorSetStatusMessage(""); + if (callback) + { + callback(buf, c); + } + return buf; + } + } + else if (!iscntrl(c) && c < 128) + { + if (buflen == bufsize -1) + { + bufsize *= 2; + buf = realloc(buf, bufsize); + } + buf[buflen++] = c; + buf[buflen] = '\0'; + } + + if (callback) + { + callback(buf, c); + } + } +} + +void editorMoveCursor(int key) +{ + erow *row = (E.cy >= E.numrows) ? NULL : &E.row[E.cy]; + + switch (key) + { + case ARROW_LEFT: + if (E.cx != 0) + { + E.cx--; + } + else if (E.cy > 0) + { + E.cy--; + E.cx = E.row[E.cy].size; + } + break; + + case ARROW_RIGHT: + if (row && E.cx < row->size) + { + E.cx++; + } + else if (row && E.cx == row->size) + { + E.cy++; + E.cx = 0; + } + break; + + case ARROW_UP: + if (E.cy != 0) + { + E.cy--; + } + break; + + case ARROW_DOWN: + if (E.cy < E.numrows) + { + E.cy++; + } + break; + } + + row = (E.cy >= E.numrows) ? NULL : &E.row[E.cy]; + int rowlen = row ? row->size : 0; + if (E.cx > rowlen) + { + E.cx = rowlen; + } +} + +void editorProcessKeypress() +{ + static int quit_times = KILO_QUIT_TIMES; + + int c = editorReadKey(); + + switch (c) + { + case '\r': + editorInsertNewline(); + break; + + case CTRL_KEY('q'): + if (E.dirty && quit_times > 0) + { + editorSetStatusMessage("WARNING!!! File has unsaved changes ." + "Press Ctrl-Q %d more time(s) to quit.", quit_times); + quit_times--; + return; + } + write(STDOUT_FILENO, "\x1b[2J", 4); + write(STDOUT_FILENO, "\x1b[H", 3); + exit(0); + break; + + case CTRL_KEY('s'): + editorSave(); + break; + + case HOME_KEY: + E.cx = 0; + break; + + case END_KEY: + if (E.cy < E.numrows) + { + E.cx = E.row[E.cy].size; + } + break; + + case CTRL_KEY('f'): + editorFind(); + break; + + case BACKSPACE: + case CTRL_KEY('h'): + case DEL_KEY: + if (c == DEL_KEY) + { + editorMoveCursor(ARROW_RIGHT); + } + editorDelChar(); + break; + + case PAGE_UP: + case PAGE_DOWN: + { + if (c == PAGE_UP) + { + E.cy = E.rowoff; + } + else if (c == PAGE_DOWN) + { + E.cy = E.rowoff + E.screenrows - 1; + if (E.cy > E.numrows) + { + E.cy = E.numrows; + } + } + + int times = E.screenrows; + while (times--) + { + editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN); + } + } + break; + + case ARROW_UP: + case ARROW_DOWN: + case ARROW_LEFT: + case ARROW_RIGHT: + editorMoveCursor(c); + break; + + case CTRL_KEY('l'): + case '\x1b': + break; + + default: + editorInsertChar(c); + break; + } + + quit_times = KILO_QUIT_TIMES; +} + +/*** init ***/ + +void initEditor() +{ + E.cx = 0; + E.cy = 0; + E.rx = 0; + E.rowoff = 0; + E.coloff = 0; + E.numrows = 0; + E.row = NULL; + E.dirty = 0; + E.filename = NULL; + E.statusmsg[0] = '\0'; + E.statusmsg_time = 0; + E.syntax = NULL; + + if (getWindowSize(&E.screenrows, &E.screencols) == -1) + { + die("getWindowSize"); + } + + E.screenrows -= 2; +} + +int main(int argc, char *argv[]) +{ + enableRawMode(); + initEditor(); + if (argc >= 2) + { + editorOpen(argv[1]); + } + + editorSetStatusMessage( + "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find"); + + while (1) + { + editorRefreshScreen(); + editorProcessKeypress(); + } + return 0; +} + diff --git a/editor/editor.go b/editor/Editor.go similarity index 88% rename from editor/editor.go rename to editor/Editor.go index 8bf9952..7131b06 100644 --- a/editor/editor.go +++ b/editor/Editor.go @@ -19,7 +19,7 @@ type statusMsg struct { created time.Time } -type editor struct { +type Editor struct { screen *terminal.Screen cursor *gilo.Point offset *gilo.Point @@ -30,7 +30,7 @@ type editor struct { renderX int } -func NewEditor() *editor { +func NewEditor() *Editor { // Subtract rows for status bar and message bar/prompt screen := terminal.Size() screen.Rows -= 2 @@ -40,7 +40,7 @@ func NewEditor() *editor { time.Now(), } - return &editor{ + return &Editor{ screen, gilo.DefaultPoint(), gilo.DefaultPoint(), @@ -52,24 +52,24 @@ func NewEditor() *editor { } } -func (e *editor) Open(filename string) { +func (e *Editor) Open(filename string) { e.doc.Open(filename) } -func (e *editor) SetStatusMessage(template string, a ...interface{}) { +func (e *Editor) SetStatusMessage(template string, a ...interface{}) { e.status = &statusMsg{ fmt.Sprintf(template, a...), time.Now(), } } -func (e *editor) ProcessKeypress() bool { +func (e *Editor) ProcessKeypress() bool { ch, _ := terminal.ReadKey() return e.processKeypressChar(ch) } -func (e *editor) save() { +func (e *Editor) save() { if e.doc.Filename == "" { e.doc.Filename = e.prompt("Save as: %s (ESC to cancel)", nil) if e.doc.Filename == "" { @@ -86,7 +86,7 @@ func (e *editor) save() { } } -func (e *editor) prompt(prompt string, callback func(string, string)) string { +func (e *Editor) prompt(prompt string, callback func(string, string)) string { buf := gilo.NewBuffer() // Show the prompt message @@ -136,7 +136,7 @@ func (e *editor) prompt(prompt string, callback func(string, string)) string { } } -func (e *editor) insertChar(ch rune) { +func (e *Editor) insertChar(ch rune) { if e.cursor.Y == e.doc.RowCount() { e.doc.AppendRow("") } @@ -145,7 +145,7 @@ func (e *editor) insertChar(ch rune) { e.cursor.X += 1 } -func (e *editor) delChar() { +func (e *Editor) delChar() { if e.cursor.Y == e.doc.RowCount() { return } diff --git a/editor/editor_test.go b/editor/Editor_test.go similarity index 100% rename from editor/editor_test.go rename to editor/Editor_test.go diff --git a/editor/document/document.go b/editor/document/document.go index 94fd28d..8c9b15a 100644 --- a/editor/document/document.go +++ b/editor/document/document.go @@ -4,12 +4,14 @@ import ( "bufio" "log" "os" + "timshome.page/gilo/editor/highlight" "timshome.page/gilo/internal/gilo" ) type Document struct { dirty bool Filename string + Syntax *highlight.Syntax rows []*Row } @@ -19,6 +21,7 @@ func NewDocument() *Document { return &Document{ false, "", + nil, rows, } } @@ -80,7 +83,7 @@ func (d *Document) GetRow(at int) *Row { } func (d *Document) AppendRow(s string) { - newRow := newRow(s) + newRow := newRow(d, s) newRow.update() d.rows = append(d.rows, newRow) @@ -100,7 +103,7 @@ func (d *Document) InsertRow(at int, s string) { // Splice it back together newRows = append(newRows, start...) - newRows = append(newRows, newRow(s)) + newRows = append(newRows, newRow(d, s)) newRows = append(newRows, end...) d.rows = newRows diff --git a/editor/document/row.go b/editor/document/row.go index 92aea05..1381359 100644 --- a/editor/document/row.go +++ b/editor/document/row.go @@ -9,12 +9,13 @@ import ( ) type Row struct { + parent *Document chars []rune render []rune Hl []int } -func newRow(s string) *Row { +func newRow(parent *Document, s string) *Row { var chars []rune var render []rune @@ -23,13 +24,14 @@ func newRow(s string) *Row { render = append(render, ch) } - return &Row{chars, render, []int{}} + return &Row{parent, chars, render, []int{}} } func (r *Row) Size() int { return len(r.chars) } +// RenderSize is a convenient equivalent of row->rsize in kilo func (r *Row) RenderSize() int { return len(r.render) } @@ -106,11 +108,21 @@ func (r *Row) update() { r.updateSyntax() } +// updateSyntax is the equivalent of editorUpdateSyntax in kilo func (r *Row) updateSyntax() { i := 0 + s := r.parent.Syntax prevSep := true r.Hl = make([]int, r.RenderSize()) + for x := range r.Hl { + r.Hl[x] = highlight.Normal + } + + // Don't bother updating the syntax if there isn't any + if s == nil { + return + } for i < r.RenderSize() { ch := r.render[i] @@ -120,14 +132,14 @@ func (r *Row) updateSyntax() { prevHl = r.Hl[i-1] } - if (unicode.IsDigit(ch) && (prevSep || prevHl == highlight.Number)) || - (ch == '.' && prevHl == highlight.Number) { - r.Hl[i] = highlight.Number - i += 1 - prevSep = false - continue - } else { - r.Hl[i] = highlight.Normal + if s.Flags&highlight.HighlightNumbers == 1 { + if (unicode.IsDigit(ch) && (prevSep || prevHl == highlight.Number)) || + (ch == '.' && prevHl == highlight.Number) { + r.Hl[i] = highlight.Number + i += 1 + prevSep = false + continue + } } prevSep = key.IsSeparator(ch) @@ -139,6 +151,7 @@ func (r *Row) toString() string { return string(r.chars) } +// CursorXToRenderX is the equivalent of editorRowCxToRx in kilo func (r *Row) CursorXToRenderX(cursorX int) (renderX int) { renderX = 0 @@ -153,6 +166,7 @@ func (r *Row) CursorXToRenderX(cursorX int) (renderX int) { return renderX } +// RenderXtoCursorX is the equivalent of editorRowRxToCx in kilo func (r *Row) RenderXtoCursorX(renderX int) (cursorX int) { currentRenderX := 0 cursorX = 0 diff --git a/editor/draw.go b/editor/draw.go index 0749b1a..464aa81 100644 --- a/editor/draw.go +++ b/editor/draw.go @@ -14,7 +14,7 @@ import ( // !Editor Methods // ---------------------------------------------------------------------------- -func (e *editor) RefreshScreen() { +func (e *Editor) RefreshScreen() { e.scroll() ab := gilo.NewBuffer() @@ -33,7 +33,7 @@ func (e *editor) RefreshScreen() { terminal.Write(ab.ToString()) } -func (e *editor) scroll() { +func (e *Editor) scroll() { e.renderX = 0 if e.cursor.Y < e.doc.RowCount() { @@ -57,7 +57,7 @@ func (e *editor) scroll() { } } -func (e *editor) drawRows(ab *gilo.Buffer) { +func (e *Editor) drawRows(ab *gilo.Buffer) { for y := 0; y < e.screen.Rows; y++ { fileRow := y + e.offset.Y @@ -81,7 +81,7 @@ func (e *editor) drawRows(ab *gilo.Buffer) { } } -func (e *editor) drawFileRow(fileRow int, ab *gilo.Buffer) { +func (e *Editor) drawFileRow(fileRow int, ab *gilo.Buffer) { currentColor := terminal.DefaultFGColor row := e.doc.GetRow(fileRow) @@ -107,7 +107,7 @@ func (e *editor) drawFileRow(fileRow int, ab *gilo.Buffer) { ab.Append(terminal.DefaultFGColor) } -func (e *editor) drawPlaceholderRow(y int, ab *gilo.Buffer) { +func (e *Editor) drawPlaceholderRow(y int, ab *gilo.Buffer) { if e.doc.RowCount() == 0 && y == e.screen.Rows/3 { welcome := fmt.Sprintf("Gilo editor -- version %s", gilo.Version) if len(welcome) > e.screen.Cols { @@ -131,7 +131,7 @@ func (e *editor) drawPlaceholderRow(y int, ab *gilo.Buffer) { } } -func (e *editor) drawStatusBar(ab *gilo.Buffer) { +func (e *Editor) drawStatusBar(ab *gilo.Buffer) { cols := e.screen.Cols ab.Append(terminal.InvertColor) @@ -156,7 +156,11 @@ func (e *editor) drawStatusBar(ab *gilo.Buffer) { return } - rightStatus := fmt.Sprintf("%d/%d", e.cursor.Y+1, e.doc.RowCount()) + syntaxName := "no filetype" + if e.doc.Syntax != nil { + syntaxName = e.doc.Syntax.FileType + } + rightStatus := fmt.Sprintf("%s | %d/%d", syntaxName, e.cursor.Y+1, e.doc.RowCount()) rlength := len(rightStatus) statusLength := length + rlength @@ -182,7 +186,7 @@ func (e *editor) drawStatusBar(ab *gilo.Buffer) { ab.Append(terminal.ResetColor) } -func (e *editor) drawMessageBar(ab *gilo.Buffer) { +func (e *Editor) drawMessageBar(ab *gilo.Buffer) { ab.Append("\r\n") ab.Append(terminal.ClearLine) diff --git a/editor/highlight/constants.go b/editor/highlight/constants.go index b09f4c5..78256e8 100644 --- a/editor/highlight/constants.go +++ b/editor/highlight/constants.go @@ -8,3 +8,15 @@ const ( Number Match ) + +const HighlightNumbers = (1 << 0) + +var HLDB = []*Syntax{{ + "c", + []string{".c", ".h", ".cpp"}, + HighlightNumbers, +}, { + "go", + []string{".go"}, + HighlightNumbers, +}} diff --git a/editor/highlight/syntax.go b/editor/highlight/syntax.go new file mode 100644 index 0000000..a95f059 --- /dev/null +++ b/editor/highlight/syntax.go @@ -0,0 +1,7 @@ +package highlight + +type Syntax struct { + FileType string + FileMatch []string + Flags int +} diff --git a/editor/input.go b/editor/input.go index ce3b187..9653be7 100644 --- a/editor/input.go +++ b/editor/input.go @@ -26,7 +26,7 @@ const ( /** * Determine what to do with an individual character of input */ -func (e *editor) processKeypressChar(ch rune) bool { +func (e *Editor) processKeypressChar(ch rune) bool { switch ch { case key.Ctrl('q'): if e.doc.IsDirty() && e.quitTimes > 0 { @@ -85,7 +85,7 @@ func (e *editor) processKeypressChar(ch rune) bool { /** * Determine what do do with a parsed ANSI escape sequence */ -func (e *editor) processKeypressStr(key string) { +func (e *Editor) processKeypressStr(key string) { switch key { case keyUp, keyDown, @@ -104,7 +104,7 @@ func (e *editor) processKeypressStr(key string) { } } -func (e *editor) moveCursor(key string) { +func (e *Editor) moveCursor(key string) { var row *document.Row if e.cursor.Y >= e.doc.RowCount() { row = nil diff --git a/editor/input_test.go b/editor/input_test.go index 04211ee..bd5435a 100644 --- a/editor/input_test.go +++ b/editor/input_test.go @@ -27,7 +27,7 @@ func TestMoveCursor(t *testing.T) { e := NewEditor() if test.withFile { - e.Open("editor.go") + e.Open("Editor.go") } for _, k := range test.keys { diff --git a/editor/search.go b/editor/search.go index 285a8ce..67ff534 100644 --- a/editor/search.go +++ b/editor/search.go @@ -7,26 +7,26 @@ import ( ) type search struct { - cursor *gilo.Point - offset *gilo.Point - hlLine int - hl []int - direction int - lastMatch int + cursor *gilo.Point + offset *gilo.Point + savedhlLine int + savedhl []int + direction int + lastMatch int } func newSearch() *search { return &search{ - cursor: gilo.DefaultPoint(), - offset: gilo.DefaultPoint(), - hlLine: -1, - hl: []int{}, - lastMatch: -1, - direction: 1, + cursor: gilo.DefaultPoint(), + offset: gilo.DefaultPoint(), + savedhlLine: -1, + savedhl: []int{}, + lastMatch: -1, + direction: 1, } } -func (e *editor) find() { +func (e *Editor) find() { e.search.cursor.X = e.cursor.X e.search.cursor.Y = e.cursor.Y e.search.offset.X = e.offset.X @@ -44,14 +44,15 @@ func (e *editor) find() { } } -func (e *editor) findCallback(query string, ch string) { - if e.search.hlLine != -1 && e.search.hl != nil { - staleRow := e.doc.GetRow(e.search.hlLine) - for i, val := range e.search.hl { +func (e *Editor) findCallback(query string, ch string) { + // If we highlighted a previous match, restore the original highlighting + if e.search.savedhlLine != -1 && e.search.savedhl != nil { + staleRow := e.doc.GetRow(e.search.savedhlLine) + for i, val := range e.search.savedhl { staleRow.Hl[i] = val } - e.search.hl = nil - e.search.hlLine = -1 + e.search.savedhl = nil + e.search.savedhlLine = -1 } if ch == string(key.Enter) || ch == string(key.Esc) { @@ -97,12 +98,14 @@ func (e *editor) findCallback(query string, ch string) { e.cursor.X = row.RenderXtoCursorX(matchIndex) e.offset.Y = e.doc.RowCount() - // Update highlighting of search result - e.search.hlLine = current - e.search.hl = make([]int, row.RenderSize()) + // Save the current highlighting of where the search result is + e.search.savedhlLine = current + e.search.savedhl = make([]int, row.RenderSize()) for i, val := range row.Hl { - e.search.hl[i] = val + e.search.savedhl[i] = val } + + // Update highlighting of search result for x := matchIndex; x < matchIndex+len(query); x++ { row.Hl[x] = highlight.Match } diff --git a/internal/gilo/point.go b/internal/gilo/point.go index b99b42c..2261767 100644 --- a/internal/gilo/point.go +++ b/internal/gilo/point.go @@ -6,6 +6,7 @@ type Point struct { X int Y int } + // DefaultPoint creates a Point at 0,0 func DefaultPoint() *Point { return &Point{0, 0}