From 91195d34e1f71246cadc41705004d66ab647087e Mon Sep 17 00:00:00 2001 From: cflip Date: Sun, 8 Jan 2023 14:15:01 -0700 Subject: Initial import of existing source code This is based off of snaptoken's "Build Your Own Text Editor" tutorial at https://viewsourcecode.org/snaptoken/kilo/. --- Makefile | 14 +++ TODO | 2 + buffer.c | 21 ++++ buffer.h | 11 ++ editor.c | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ editor.h | 35 ++++++ file.c | 94 ++++++++++++++++ file.h | 6 + input.c | 169 +++++++++++++++++++++++++++++ input.h | 22 ++++ main.c | 25 +++++ row.c | 149 +++++++++++++++++++++++++ row.h | 27 +++++ syntax.c | 197 +++++++++++++++++++++++++++++++++ syntax.h | 36 ++++++ terminal.c | 84 ++++++++++++++ terminal.h | 7 ++ 17 files changed, 1260 insertions(+) create mode 100644 Makefile create mode 100644 TODO create mode 100644 buffer.c create mode 100644 buffer.h create mode 100644 editor.c create mode 100644 editor.h create mode 100644 file.c create mode 100644 file.h create mode 100644 input.c create mode 100644 input.h create mode 100644 main.c create mode 100644 row.c create mode 100644 row.h create mode 100644 syntax.c create mode 100644 syntax.h create mode 100644 terminal.c create mode 100644 terminal.h diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2f785c9 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +CFLAGS=-std=c99 + +OUT=editor +SRC=main.c \ + buffer.c \ + editor.c \ + file.c \ + input.c \ + row.c \ + syntax.c \ + terminal.c \ + +${OUT}: ${SRC} + ${CC} ${CFLAGS} ${SRC} -o ${OUT} diff --git a/TODO b/TODO new file mode 100644 index 0000000..460325d --- /dev/null +++ b/TODO @@ -0,0 +1,2 @@ +- Write comments in some of the messy code +- Try and keep editor_state out of row.c diff --git a/buffer.c b/buffer.c new file mode 100644 index 0000000..596d5d2 --- /dev/null +++ b/buffer.c @@ -0,0 +1,21 @@ +#include "buffer.h" + +#include +#include + +void ab_append(struct append_buffer* ab, const char* string, int length) +{ + char* new = realloc(ab->buffer, ab->length + length); + + if (new == NULL) + return; + + memcpy(&new[ab->length], string, length); + ab->buffer = new; + ab->length += length; +} + +void ab_free(struct append_buffer* ab) +{ + free(ab->buffer); +} diff --git a/buffer.h b/buffer.h new file mode 100644 index 0000000..474202e --- /dev/null +++ b/buffer.h @@ -0,0 +1,11 @@ +#pragma once + +struct append_buffer { + char* buffer; + int length; +}; + +#define ABUF_INIT { NULL, 0 } + +void ab_append(struct append_buffer* ab, const char* string, int length); +void ab_free(struct append_buffer* ab); diff --git a/editor.c b/editor.c new file mode 100644 index 0000000..14eada7 --- /dev/null +++ b/editor.c @@ -0,0 +1,361 @@ +#include "editor.h" + +#include +#include +#include +#include +#include +#include + +#include "buffer.h" +#include "input.h" +#include "syntax.h" +#include "terminal.h" +#include "row.h" + +void init_editor(struct editor_state* editor) +{ + editor->cursor_x = 0; + editor->cursor_y = 0; + editor->cursor_display_x = 0; + editor->row_offset = 0; + editor->col_offset = 0; + editor->row_count = 0; + editor->rows = NULL; + editor->dirty = 0; + editor->filename = NULL; + editor->status_message[0] = '\0'; + editor->status_message_time = 0; + editor->syntax = NULL; + + if (get_window_size(&editor->screen_rows, &editor->screen_cols) == -1) + die("get_window_size"); + + editor->screen_rows -= 2; +} + +void editor_set_status_message(struct editor_state* editor, const char* format, ...) +{ + va_list args; + va_start(args, format); + vsnprintf(editor->status_message, sizeof(editor->status_message), format, args); + va_end(args); + editor->status_message_time = time(NULL); +} + +void editor_refresh_screen(struct editor_state* editor) +{ + editor_scroll(editor); + + struct append_buffer buffer = ABUF_INIT; + + ab_append(&buffer, "\x1b[?25l", 6); + ab_append(&buffer, "\x1b[H", 3); + + editor_draw_rows(editor, &buffer); + editor_draw_status_bar(editor, &buffer); + editor_draw_message_bar(editor, &buffer); + + char buf[32]; + snprintf(buf, sizeof(buf), "\x1b[%d;%dH", (editor->cursor_y - editor->row_offset) + 1, (editor->cursor_display_x - editor->col_offset) + 1); + ab_append(&buffer, buf, strlen(buf)); + + ab_append(&buffer, "\x1b[?25h", 6); + + write(STDOUT_FILENO, buffer.buffer, buffer.length); + ab_free(&buffer); +} + +char* editor_prompt(struct editor_state* editor, char* prompt, void (*callback)(struct editor_state*, char*, int)) +{ + size_t buffer_size = 128; + char* buffer = malloc(buffer_size); + + size_t buffer_length = 0; + buffer[0] = '\0'; + + while (1) { + editor_set_status_message(editor, prompt, buffer); + // TODO: This should refresh the active screen + //editor_refresh_screen(); + + int c = editor_read_key(); + if (c == DELETE_KEY || c == BACKSPACE) { + if (buffer_length != 0) buffer[--buffer_length] = '\0'; + } else if (c == '\x1b') { + editor_set_status_message(editor, ""); + if (callback) callback(editor, buffer, c); + free(buffer); + return NULL; + } else if (c == '\r') { + if (buffer_length != 0) { + editor_set_status_message(editor, ""); + if (callback) callback(editor, buffer, c); + return buffer; + } + } else if (!iscntrl(c) && c < 128) { + if (buffer_length == buffer_size - 1) { + buffer_size *= 2; + buffer = realloc(buffer, buffer_size); + } + buffer[buffer_length++] = c; + buffer[buffer_length] = '\0'; + } + if (callback) callback(editor, buffer, c); + } +} + +void editor_insert_char(struct editor_state* editor, int c) +{ + if (editor->cursor_y == editor->row_count) + insert_row(editor, editor->row_count, "", 0); + + row_insert_char(editor, &editor->rows[editor->cursor_y], editor->cursor_x, c); + editor->cursor_x++; +} + +void editor_insert_newline(struct editor_state* editor) +{ + if (editor->cursor_x == 0) { + insert_row(editor, editor->cursor_y, "", 0); + } else { + struct editor_row* row = &editor->rows[editor->cursor_y]; + insert_row(editor, editor->cursor_y + 1, &row->chars[editor->cursor_x], row->size - editor->cursor_x); + row = &editor->rows[editor->cursor_y]; + row->size = editor->cursor_x; + row->chars[row->size] = '\0'; + update_row(editor, row); + } + editor->cursor_y++; + editor->cursor_x = 0; +} + +void editor_delete_char(struct editor_state* editor) +{ + if (editor->cursor_y == editor->row_count) + return; + + if (editor->cursor_x == 0 && editor->cursor_y == 0) + return; + + struct editor_row* row = &editor->rows[editor->cursor_y]; + if (editor->cursor_x > 0) { + row_delete_char(editor, row, editor->cursor_x - 1); + editor->cursor_x--; + } else { + editor->cursor_x = editor->rows[editor->cursor_y - 1].size; + row_append_string(editor, &editor->rows[editor->cursor_y - 1], row->chars, row->size); + delete_row(editor, editor->cursor_y); + editor->cursor_y--; + } +} + +static void editor_find_callback(struct editor_state* editor, char* query, int key) +{ + static int last_match = -1; + static int direction = 1; + + static int saved_highlight_line; + static char* saved_highlight; + + if (saved_highlight) { + memset(editor->rows[saved_highlight_line].highlight, (size_t)saved_highlight, editor->rows[saved_highlight_line].render_size); + free(saved_highlight); + saved_highlight = 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 < editor->row_count; i++) { + current += direction; + if (current == -1) { + current = editor->row_count - 1; + } else if (current == editor->row_count) { + current = 0; + } + + struct editor_row* row = &editor->rows[current]; + char* match = strstr(row->render, query); + + if (match) { + last_match = current; + editor->cursor_y = current; + editor->cursor_x = row_display_x_to_x(row, match - row->render); + editor->row_offset = editor->row_count; + + saved_highlight_line = current; + saved_highlight = malloc(row->render_size); + memcpy(saved_highlight, row->highlight, row->render_size); + memset(&row->highlight[match - row->render], HIGHLIGHT_MATCH, strlen(query)); + break; + } + } +} + +void editor_find(struct editor_state* editor) +{ + int saved_cursor_x = editor->cursor_x; + int saved_cursor_y = editor->cursor_y; + int saved_col_offset = editor->col_offset; + int saved_row_offset = editor->row_offset; + + char* query = editor_prompt(editor, "Search: %s (Use Esc/Arrows/Enter)", editor_find_callback); + if (query) { + free(query); + } else { + editor->cursor_x = saved_cursor_x; + editor->cursor_y = saved_cursor_y; + editor->col_offset = saved_col_offset; + editor->row_offset = saved_row_offset; + } +} + +void editor_scroll(struct editor_state* editor) +{ + editor->cursor_display_x = 0; + if (editor->cursor_y < editor->row_count) + editor->cursor_display_x = row_x_to_display_x(&editor->rows[editor->cursor_y], editor->cursor_x); + + if (editor->cursor_y < editor->row_offset) + editor->row_offset = editor->cursor_y; + + if (editor->cursor_y >= editor->row_offset + editor->screen_rows) + editor->row_offset = editor->cursor_y - editor->screen_rows + 1; + + if (editor->cursor_display_x < editor->col_offset) + editor->col_offset = editor->cursor_display_x; + + if (editor->cursor_display_x >= editor->col_offset + editor->screen_cols) + editor->col_offset = editor->cursor_display_x - editor->screen_cols + 1; +} + +void editor_draw_rows(struct editor_state* editor, struct append_buffer* buffer) +{ + int y; + for (y = 0; y < editor->screen_rows; y++) { + int file_row = y + editor->row_offset; + if (file_row >= editor->row_count) { + if (editor->row_count == 0 && y == editor->screen_rows / 3) { + char welcome[80]; + int welcome_length = snprintf(welcome, sizeof(welcome), "Welcome to cflip text editor"); + + if (welcome_length > editor->screen_cols) + welcome_length = editor->screen_cols; + + int padding = (editor->screen_cols - welcome_length) / 2; + if (padding) { + ab_append(buffer, "~", 1); + padding--; + } + + while (padding--) + ab_append(buffer, " ", 1); + + ab_append(buffer, welcome, welcome_length); + } else { + ab_append(buffer, "~", 1); + } + } else { + int length = editor->rows[file_row].render_size - editor->col_offset; + + if (length < 0) length = 0; + if (length > editor->screen_cols) length = editor->screen_cols; + + char* c = &editor->rows[file_row].render[editor->col_offset]; + unsigned char* highlight = &editor->rows[file_row].highlight[editor->col_offset]; + int current_colour = -1; + int j; + + for (j = 0; j < length; j++) { + if (iscntrl(c[j])) { + char symbol = (c[j] <= 26) ? '@' + c[j] : '?'; + ab_append(buffer, "\x1b[7m", 4); + ab_append(buffer, &symbol, 1); + ab_append(buffer, "\x1b[m", 3); + + if (current_colour != -1) { + char buf[16]; + int col_length = snprintf(buf, sizeof(buf), "\x1b[%dm", current_colour); + ab_append(buffer, buf, col_length); + } + } else if (highlight[j] == HIGHLIGHT_NORMAL) { + if (current_colour != -1) { + ab_append(buffer, "\x1b[39m", 5); + current_colour = -1; + } + ab_append(buffer, &c[j], 1); + } else { + int colour = editor_syntax_to_colour(highlight[j]); + if (colour != current_colour) { + current_colour = colour; + char colour_buffer[16]; + int colour_buffer_length = snprintf(colour_buffer, sizeof(colour_buffer), "\x1b[%dm", colour); + + ab_append(buffer, colour_buffer, colour_buffer_length); + } + ab_append(buffer, &c[j], 1); + } + } + ab_append(buffer, "\x1b[39m", 5); + } + + ab_append(buffer, "\x1b[K", 3); + ab_append(buffer, "\r\n", 2); + } +} + +void editor_draw_status_bar(struct editor_state* editor, struct append_buffer* buffer) +{ + ab_append(buffer, "\x1b[7m", 4); + + char status[80], right_status[80]; + int length = snprintf(status, sizeof(status), "%.20s - %d lines %s", editor->filename ? editor->filename : "[New File]", editor->row_count, editor->dirty ? "(modified)" : ""); + int right_length = snprintf(right_status, sizeof(right_status), "%s | %d/%d", editor->syntax ? editor->syntax->filetype : "plaintext", editor->cursor_y + 1, editor->row_count); + + if (length > editor->screen_cols) + length = editor->screen_cols; + + ab_append(buffer, status, length); + + while (length < editor->screen_cols) { + if (editor->screen_cols - length == right_length) { + ab_append(buffer, right_status, right_length); + break; + } else { + ab_append(buffer, " ", 1); + length++; + } + } + + ab_append(buffer, "\x1b[m", 3); + ab_append(buffer, "\r\n", 2); +} + +void editor_draw_message_bar(struct editor_state* editor, struct append_buffer* buffer) +{ + ab_append(buffer, "\x1b[K", 3); + + int message_length = strlen(editor->status_message); + + if (message_length > editor->screen_cols) + message_length = editor->screen_cols; + + if (message_length && time(NULL) - editor->status_message_time < 5) + ab_append(buffer, editor->status_message, message_length); +} diff --git a/editor.h b/editor.h new file mode 100644 index 0000000..578a91e --- /dev/null +++ b/editor.h @@ -0,0 +1,35 @@ +#pragma once + +#include +#include "buffer.h" + +struct editor_state { + int cursor_x, cursor_y; + int cursor_display_x; + int row_offset; + int col_offset; + int screen_rows; + int screen_cols; + int row_count; + struct editor_row* rows; + int dirty; + char* filename; + char status_message[80]; + time_t status_message_time; + struct editor_syntax* syntax; +}; + +void init_editor(struct editor_state* editor); + +void editor_set_status_message(struct editor_state* editor, const char* format, ...); +void editor_refresh_screen(struct editor_state* editor); +char* editor_prompt(struct editor_state* editor, char* prompt, void (*callback)(struct editor_state*, char*, int)); + +void editor_insert_char(struct editor_state* editor, int c); +void editor_insert_newline(struct editor_state* editor); +void editor_delete_char(struct editor_state* editor); +void editor_find(struct editor_state* editor); +void editor_scroll(struct editor_state* editor); +void editor_draw_rows(struct editor_state* editor, struct append_buffer* buffer); +void editor_draw_status_bar(struct editor_state* editor, struct append_buffer* buffer); +void editor_draw_message_bar(struct editor_state* editor, struct append_buffer* buffer); diff --git a/file.c b/file.c new file mode 100644 index 0000000..78505df --- /dev/null +++ b/file.c @@ -0,0 +1,94 @@ +#include "file.h" + +#include +#include +#include +#include +#include +#include + +#include "row.h" +#include "syntax.h" +#include "terminal.h" + +char* editor_rows_to_string(struct editor_state* editor, int* buffer_length) +{ + int total_length = 0; + int j; + + for (j = 0; j < editor->row_count; j++) + total_length += editor->rows[j].size + 1; + + *buffer_length = total_length; + + char* buffer = malloc(total_length); + char* p = buffer; + + for (j = 0; j < editor->row_count; j++) { + memcpy(p, editor->rows[j].chars, editor->rows[j].size); + p += editor->rows[j].size; + *p = '\n'; + p++; + } + + return buffer; +} + +void editor_open(struct editor_state* editor, char* filename) +{ + free(editor->filename); + editor->filename = strdup(filename); + + editor_select_syntax_highlight(editor); + + FILE* fp = fopen(filename, "r"); + if (!fp) + die("fopen"); + + char* line = NULL; + size_t line_capacity = 0; + ssize_t line_length; + + while ((line_length = getline(&line, &line_capacity, fp)) != -1) { + while (line_length > 0 && (line[line_length - 1] == '\n' || line[line_length - 1] == '\r')) + line_length--; + + insert_row(editor, editor->row_count, line, line_length); + } + + free(line); + fclose(fp); + + editor->dirty = 0; +} + +void editor_save(struct editor_state* editor) +{ + if (editor->filename == NULL) { + editor->filename = editor_prompt(editor, "Save as: %s", NULL); + + if (editor->filename == NULL) + return; + + editor_select_syntax_highlight(editor); + } + + int length; + char* buffer = editor_rows_to_string(editor, &length); + + int fd = open(editor->filename, O_RDWR | O_CREAT, 0644); + if (fd != -1) { + if (ftruncate(fd, length) != -1) { + if (write(fd, buffer, length) == length) { + close(fd); + free(buffer); + editor_set_status_message(editor, "%d bytes written to disk", length); + editor->dirty = 0; + return; + } + } + close(fd); + } + free(buffer); + editor_set_status_message(editor, "Failed to write to disk: %s", strerror(errno)); +} diff --git a/file.h b/file.h new file mode 100644 index 0000000..831d193 --- /dev/null +++ b/file.h @@ -0,0 +1,6 @@ +#pragma once + +#include "editor.h" + +void editor_open(struct editor_state* editor, char* filename); +void editor_save(struct editor_state* editor); diff --git a/input.c b/input.c new file mode 100644 index 0000000..17a37f6 --- /dev/null +++ b/input.c @@ -0,0 +1,169 @@ +#include "input.h" + +#include +#include +#include + +#include "file.h" +#include "terminal.h" +#include "row.h" + +int editor_read_key() +{ + int read_count; + char c; + + while ((read_count = read(STDIN_FILENO, &c, 1)) != 1) { + if (read_count == -1 && errno != EAGAIN) + die("read"); + } + + 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 DELETE_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'; + } else { + return c; + } +} + +void editor_move_cursor(struct editor_state* editor, int key) +{ + struct editor_row* row = (editor->cursor_y >= editor->row_count) ? NULL : &editor->rows[editor->cursor_y]; + + switch (key) { + case ARROW_LEFT: + if (editor->cursor_x != 0) { + editor->cursor_x--; + } else if (editor->cursor_y > 0) { + editor->cursor_y--; + editor->cursor_x = editor->rows[editor->cursor_y].size; + } + break; + case ARROW_RIGHT: + if (row && editor->cursor_x < row->size) { + editor->cursor_x++; + } else if (row && editor->cursor_x == row->size) { + editor->cursor_y++; + editor->cursor_x = 0; + } + break; + case ARROW_UP: + if (editor->cursor_y != 0) editor->cursor_y--; + break; + case ARROW_DOWN: + if (editor->cursor_y != editor->row_count - 1) editor->cursor_y++; + break; + } + + row = (editor->cursor_y >= editor->row_count) ? NULL : &editor->rows[editor->cursor_y]; + int row_length = row ? row->size : 0; + if (editor->cursor_x > row_length) + editor->cursor_x = row_length; +} + +void editor_process_keypress(struct editor_state* editor) +{ + static int quit_message = 1; + int c = editor_read_key(); + + switch (c) { + case '\r': + editor_insert_newline(editor); + break; + case CTRL_KEY('q'): + if (editor->dirty && quit_message) { + editor_set_status_message(editor, "This file has unsaved changes. Press Ctrl+Q again to quit"); + quit_message = 0; + return; + } + write(STDOUT_FILENO, "\x1b[2J", 4); + write(STDOUT_FILENO, "\x1b[H", 3); + exit(0); + break; + case CTRL_KEY('s'): + editor_save(editor); + break; + case HOME_KEY: + editor->cursor_x = 0; + break; + case END_KEY: + if (editor->cursor_y < editor->row_count) + editor->cursor_x = editor->rows[editor->cursor_y].size; + break; + case CTRL_KEY('f'): + editor_find(editor); + break; + case BACKSPACE: + case DELETE_KEY: + if (c == DELETE_KEY) + editor_move_cursor(editor, ARROW_RIGHT); + editor_delete_char(editor); + break; + case PAGE_UP: + case PAGE_DOWN: + { + if (c == PAGE_UP) { + editor->cursor_y = editor->row_offset; + } else { + editor->cursor_y = editor->row_offset + editor->screen_rows -1; + if (editor->cursor_y > editor->row_count) + editor->cursor_y = editor->row_count; + } + + int times = editor->screen_rows; + while (times--) + editor_move_cursor(editor, c == PAGE_UP ? ARROW_UP : ARROW_DOWN); + } + break; + case ARROW_UP: + case ARROW_DOWN: + case ARROW_LEFT: + case ARROW_RIGHT: + editor_move_cursor(editor, c); + break; + case CTRL_KEY('l'): + case '\x1b': + break; + default: + editor_insert_char(editor, c); + break; + } + + quit_message = 1; +} diff --git a/input.h b/input.h new file mode 100644 index 0000000..5e63904 --- /dev/null +++ b/input.h @@ -0,0 +1,22 @@ +#pragma once + +#include "editor.h" + +enum editor_key { + BACKSPACE = 127, + ARROW_LEFT = 1000, + ARROW_RIGHT, + ARROW_UP, + ARROW_DOWN, + DELETE_KEY, + HOME_KEY, + END_KEY, + PAGE_UP, + PAGE_DOWN +}; + +#define CTRL_KEY(k) ((k) & 0x1f) + +int editor_read_key(); +void editor_move_cursor(struct editor_state* editor, int key); +void editor_process_keypress(struct editor_state* editor); diff --git a/main.c b/main.c new file mode 100644 index 0000000..1d17f79 --- /dev/null +++ b/main.c @@ -0,0 +1,25 @@ +#include "input.h" +#include "file.h" +#include "editor.h" +#include "terminal.h" + +int main(int argc, char** argv) +{ + enable_raw_mode(); + + struct editor_state editor; + init_editor(&editor); + + if (argc >= 2) { + editor_open(&editor, argv[1]); + } + + editor_set_status_message(&editor, "HELP: Ctrl+Q = quit, Ctrl+S = save, Ctrl+F = find"); + + while (1) { + editor_refresh_screen(&editor); + editor_process_keypress(&editor); + } + + return 0; +} diff --git a/row.c b/row.c new file mode 100644 index 0000000..c009cfc --- /dev/null +++ b/row.c @@ -0,0 +1,149 @@ +#include "row.h" + +#include +#include + +#include "syntax.h" + +int row_x_to_display_x(struct editor_row* row, int x) +{ + int display_x = 0; + int j; + + for (j = 0; j < x; j++) { + if (row->chars[j] == '\t') + display_x += (TAB_WIDTH - 1) - (display_x % TAB_WIDTH); + display_x++; + } + + return display_x; +} + +int row_display_x_to_x(struct editor_row* row, int display_x) +{ + int current_display_x = 0; + int result_x; + + for (result_x = 0; result_x < row->size; result_x++) { + if (row->chars[result_x] == '\t') + result_x += (TAB_WIDTH - 1) - (current_display_x % TAB_WIDTH); + result_x++; + + if (current_display_x > display_x) + return result_x; + } + return result_x; +} + +void update_row(struct editor_state* editor, struct editor_row* 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 * (TAB_WIDTH - 1) + 1); + + int index = 0; + for (j = 0; j < row->size; j++) { + if (row->chars[j] == '\t') { + row->render[index++] = ' '; + while (index % TAB_WIDTH != 0) row->render[index++] = ' '; + } else { + row->render[index++] = row->chars[j]; + } + } + + row->render[index] = '\0'; + row->render_size = index; + + editor_update_syntax(editor, row); +} + +void insert_row(struct editor_state* editor, int at, char* string, size_t length) +{ + if (at < 0 || at > editor->row_count) + return; + + editor->rows = realloc(editor->rows, sizeof(struct editor_row) * (editor->row_count + 1)); + memmove(&editor->rows[at + 1], &editor->rows[at], sizeof(struct editor_row) * (editor->row_count - at)); + + for (int j = at + 1; j <= editor->row_count; j++) + editor->rows[at].index++; + + editor->rows[at].index = at; + + editor->rows[at].size = length; + editor->rows[at].chars = malloc(length + 1); + memcpy(editor->rows[at].chars, string, length); + editor->rows[at].chars[length] = '\0'; + + editor->rows[at].render_size = 0; + editor->rows[at].render = NULL; + editor->rows[at].highlight = NULL; + editor->rows[at].highlight_open_comment = 0; + update_row(editor, &editor->rows[at]); + + editor->row_count++; + editor->dirty = 1; +} + +void free_row(struct editor_row* row) +{ + free(row->render); + free(row->chars); + free(row->highlight); +} + +void delete_row(struct editor_state* editor, int at) +{ + if (at < 0 || at >= editor->row_count) + return; + + free_row(&editor->rows[at]); + memmove(&editor->rows[at], &editor->rows[at + 1], sizeof(struct editor_row) * (editor->row_count - at - 1)); + + for (int j = at; j < editor->row_count; j++) + editor->rows[j].index--; + + editor->row_count--; + editor->dirty = 1; +} + +void row_insert_char(struct editor_state* editor, struct editor_row* 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; + + update_row(editor, row); + + editor->dirty = 1; +} + +void row_append_string(struct editor_state* editor, struct editor_row* row, char* string, size_t length) +{ + row->chars = realloc(row->chars, row->size + length + 1); + memcpy(&row->chars[row->size], string, length); + row->size += length; + row->chars[row->size] = '\0'; + + update_row(editor, row); + editor->dirty = 1; +} + +void row_delete_char(struct editor_state* editor, struct editor_row* row, int at) +{ + if (at < 0 || at >= row->size) + return; + + memmove(&row->chars[at], &row->chars[at + 1], row->size - at); + row->size--; + update_row(editor, row); + editor->dirty = 1; +} diff --git a/row.h b/row.h new file mode 100644 index 0000000..f3c9708 --- /dev/null +++ b/row.h @@ -0,0 +1,27 @@ +#pragma once + +#include + +#include "editor.h" + +#define TAB_WIDTH 4 + +struct editor_row { + int index; + int size; + char* chars; + int render_size; + char* render; + unsigned char* highlight; + int highlight_open_comment; +}; + +int row_x_to_display_x(struct editor_row* row, int x); +int row_display_x_to_x(struct editor_row* row, int display_x); +void update_row(struct editor_state* editor, struct editor_row* row); +void insert_row(struct editor_state* editor, int at, char* string, size_t length); +void free_row(struct editor_row* row); +void delete_row(struct editor_state* editor, int at); +void row_insert_char(struct editor_state* editor, struct editor_row* row, int at, int c); +void row_append_string(struct editor_state* editor, struct editor_row* row, char* string, size_t length); +void row_delete_char(struct editor_state* editor, struct editor_row* row, int at); diff --git a/syntax.c b/syntax.c new file mode 100644 index 0000000..ffaae4a --- /dev/null +++ b/syntax.c @@ -0,0 +1,197 @@ +#include "syntax.h" + +#include +#include "editor.h" + +char* c_highlight_extensions[] = { ".c", ".h", ".cpp", ".cc", NULL }; + +char* c_highlight_keywords[] = { + "switch", "if", "while", "for", "break", "continue", "return", "else", + "struct", "union", "typedef", "static", "enum", "class", "case", + "#include", "#define", "#ifdef", "#ifndef", + + "int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|", + "void|", NULL +}; + +struct editor_syntax highlight_database[] = { + { + "c", + c_highlight_extensions, + c_highlight_keywords, + "//", "/*", "*/", + HIGHLIGHT_FLAG_NUMBERS | HIGHLIGHT_FLAG_STRINGS + } +}; + +int is_separator(int c) +{ + return isspace(c) || c == '\0' || strchr(",.()+-/*=~%<>[];", c) != NULL; +} + +void editor_update_syntax(struct editor_state* editor, struct editor_row* row) +{ + row->highlight = realloc(row->highlight, row->render_size); + memset(row->highlight, HIGHLIGHT_NORMAL, row->render_size); + + if (editor->syntax == NULL) + return; + + char** keywords = editor->syntax->keywords; + + // TODO: Remove + char* single_line_comment_start = editor->syntax->single_line_comment_start; + char* multi_line_comment_start = editor->syntax->multi_line_comment_start; + char* multi_line_comment_end = editor->syntax->multi_line_comment_end; + + int single_line_comment_start_length = single_line_comment_start ? strlen(single_line_comment_start) : 0; + int multi_line_comment_start_length = multi_line_comment_start ? strlen(multi_line_comment_start) : 0; + int multi_line_comment_end_length = multi_line_comment_end ? strlen(multi_line_comment_end) : 0; + + int previous_separator = 1; + int in_string = 0; + int in_comment = (row->index > 0 && editor->rows[row->index - 1].highlight_open_comment); + + int i = 0; + while (i < row->render_size) { + char c = row->render[i]; + unsigned char previous_highlight = (i > 0) ? row->highlight[i - 1] : HIGHLIGHT_NORMAL; + + if (single_line_comment_start_length && !in_string && !in_comment) { + if (!strncmp(&row->render[i], single_line_comment_start, single_line_comment_start_length)) { + memset(&row->highlight[i], HIGHLIGHT_COMMENT, row->render_size - i); + break; + } + } + + if (multi_line_comment_start_length && multi_line_comment_end_length && !in_string) { + if (in_comment) { + row->highlight[i] = HIGHLIGHT_MULTILINE_COMMENT; + if (!strncmp(&row->render[i], multi_line_comment_end, multi_line_comment_end_length)) { + memset(&row->highlight[i], HIGHLIGHT_MULTILINE_COMMENT, multi_line_comment_end_length); + + i += multi_line_comment_end_length; + in_comment = 0; + previous_separator = 1; + continue; + } else { + i++; + continue; + } + } else if (!strncmp(&row->render[i], multi_line_comment_start, multi_line_comment_start_length)) { + memset(&row->highlight[i], HIGHLIGHT_MULTILINE_COMMENT, multi_line_comment_start_length); + i += multi_line_comment_start_length; + in_comment = 1; + continue; + } + } + + if (editor->syntax->flags & HIGHLIGHT_FLAG_STRINGS) { + if (in_string) { + row->highlight[i] = HIGHLIGHT_STRING; + + if (c == '\\' && i + 1 < row->render_size) { + row->highlight[i + 1] = HIGHLIGHT_STRING; + i += 2; + continue; + } + + if (c == in_string) + in_string = 0; + + i++; + previous_separator = 1; + continue; + } else { + if (c == '"' || c == '\'') { + in_string = c; + row->highlight[i] = HIGHLIGHT_STRING; + i++; + continue; + } + } + } + + if (editor->syntax->flags & HIGHLIGHT_FLAG_NUMBERS) { + if ((isdigit(c) && (previous_separator || previous_highlight == HIGHLIGHT_NUMBER)) || (c == '.' && previous_highlight == HIGHLIGHT_NUMBER)) { + row->highlight[i] = HIGHLIGHT_NUMBER; + i++; + previous_separator = 0; + continue; + } + } + + if (previous_separator) { + int j; + for (j = 0; keywords[j]; j++) { + int keyword_length = strlen(keywords[j]); + int is_secondary = keywords[j][keyword_length - 1] == '|'; + + if (is_secondary) + keyword_length--; + + if (!strncmp(&row->render[i], keywords[j], keyword_length) && is_separator(row->render[i + keyword_length])) { + memset(&row->highlight[i], is_secondary ? HIGHLIGHT_KEYWORD2 : HIGHLIGHT_KEYWORD1, keyword_length); + i += keyword_length; + break; + } + } + if (keywords[j] != NULL) { + previous_separator = 0; + continue; + } + } + + previous_separator = is_separator(c); + i++; + } + + int changed = (row->highlight_open_comment != in_comment); + row->highlight_open_comment = in_comment; + if (changed && row->index + 1 < editor->row_count) + editor_update_syntax(editor, &editor->rows[row->index + 1]); +} + +int editor_syntax_to_colour(int highlight) +{ + switch (highlight) { + case HIGHLIGHT_MULTILINE_COMMENT: + case HIGHLIGHT_COMMENT: return 36; + case HIGHLIGHT_KEYWORD1: return 33; + case HIGHLIGHT_KEYWORD2: return 32; + case HIGHLIGHT_STRING: return 35; + case HIGHLIGHT_NUMBER: return 31; + case HIGHLIGHT_MATCH: return 34; + default: return 37; + } +} + +void editor_select_syntax_highlight(struct editor_state* editor) +{ + editor->syntax = NULL; + + if (editor->filename == NULL) + return; + + char* extension = strrchr(editor->filename, '.'); + + for (unsigned int j = 0; j < HIGHLIGHT_DATABASE_ENTRY_COUNT; j++) { + struct editor_syntax* syntax = &highlight_database[j]; + unsigned int i = 0; + + while (syntax->filetype_match[i]) { + int is_extension = (syntax->filetype_match[i][0] == '.'); + if ((is_extension && extension && !strcmp(extension, syntax->filetype_match[i])) || (!is_extension && strstr(editor->filename, syntax->filetype_match[i]))) { + editor->syntax = syntax; + + int file_row; + for (file_row = 0; file_row < editor->row_count; file_row++) { + editor_update_syntax(editor, &editor->rows[file_row]); + } + + return; + } + i++; + } + } +} diff --git a/syntax.h b/syntax.h new file mode 100644 index 0000000..0cb673e --- /dev/null +++ b/syntax.h @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "editor.h" +#include "row.h" + +#define HIGHLIGHT_FLAG_NUMBERS (1 << 0) +#define HIGHLIGHT_FLAG_STRINGS (1 << 1) + +struct editor_syntax { + char* filetype; + char** filetype_match; + char** keywords; + char* single_line_comment_start; + char* multi_line_comment_start; + char* multi_line_comment_end; + int flags; +}; + +enum editor_highlight { + HIGHLIGHT_NORMAL = 0, + HIGHLIGHT_COMMENT, + HIGHLIGHT_MULTILINE_COMMENT, + HIGHLIGHT_KEYWORD1, + HIGHLIGHT_KEYWORD2, + HIGHLIGHT_STRING, + HIGHLIGHT_NUMBER, + HIGHLIGHT_MATCH +}; + +#define HIGHLIGHT_DATABASE_ENTRY_COUNT (sizeof(highlight_database) / sizeof(highlight_database[0])) + +void editor_update_syntax(struct editor_state* editor, struct editor_row* row); +int editor_syntax_to_colour(int highlight); +void editor_select_syntax_highlight(struct editor_state* editor); diff --git a/terminal.c b/terminal.c new file mode 100644 index 0000000..c24f46d --- /dev/null +++ b/terminal.c @@ -0,0 +1,84 @@ +#include "terminal.h" + +#include +#include +#include +#include +#include + +static struct termios original_termios; + +void die(const char* message) +{ + /* Clear the screen */ + write(STDOUT_FILENO, "\x1b[2J", 4); + write(STDOUT_FILENO, "\x1b[H", 3); + + perror(message); + exit(1); +} + +void disable_raw_mode() +{ + if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios) == -1) + die("tcsetattr"); +} + +/* Disables any input and output processing from the terminal */ +void enable_raw_mode() +{ + if (tcgetattr(STDIN_FILENO, &original_termios) == -1) + die("tcgetattr"); + + atexit(disable_raw_mode); + + struct termios raw = original_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 get_cursor_position(int* rows, int* cols) +{ + char buffer[32]; + unsigned int i = 0; + + if (write(STDOUT_FILENO, "\x1b[6n", 4) != 4) + return -1; + + while (i < sizeof(buffer) - 1) { + if (read(STDIN_FILENO, &buffer[i], 1) != 1) break; + if (buffer[i] == 'R') break; + i++; + } + buffer[i] = '\0'; + + printf("\r\n&buffer[1]: '%s'\r\n", &buffer[1]); + + if (buffer[0] != '\x1b' || buffer[1] != '[') return -1; + if (sscanf(&buffer[2], "%d;%d", rows, cols) != 2) return -1; + + return 0; +} + +int get_window_size(int* rows, int* cols) +{ + struct winsize size; + + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &size) == -1 || size.ws_col == 0) { + if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) + return -1; + return get_cursor_position(rows, cols); + } else { + *cols = size.ws_col; + *rows = size.ws_row; + return 0; + } +} diff --git a/terminal.h b/terminal.h new file mode 100644 index 0000000..529ba74 --- /dev/null +++ b/terminal.h @@ -0,0 +1,7 @@ +#pragma once + +void die(const char* message); +void disable_raw_mode(); +void enable_raw_mode(); +int get_cursor_position(int* rows, int* cols); +int get_window_size(int* rows, int* cols); -- cgit v1.2.3