From ed51e2c4906f6d830c4d8d3b2cc88fcdb9913b12 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 1 Oct 2024 07:17:05 +0200 Subject: [PATCH 1/4] feat: Added REPL based on Antirez's kilo --- build/init.mk | 2 +- build/linux.mk | 2 +- build/musl.mk | 2 +- src/cjit.c | 223 +++++++- src/kilo.c | 1349 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1570 insertions(+), 8 deletions(-) create mode 100644 src/kilo.c diff --git a/build/init.mk b/build/init.mk index 8a830d9..599c5f0 100644 --- a/build/init.mk +++ b/build/init.mk @@ -22,7 +22,7 @@ cflags := ${CFLAGS} -Isrc -Ilib/tinycc cflags_stack_protect := -fstack-protector-all -D_FORTIFY_SOURCE=2 -fno-strict-overflow SOURCES := src/io.o src/file.o src/cflag.o src/cjit.o \ - src/embed-libtcc1.o src/embed-headers.o + src/embed-libtcc1.o src/embed-headers.o src/kilo.o ldadd := lib/tinycc/libtcc.a diff --git a/build/linux.mk b/build/linux.mk index 7226162..d065e9c 100644 --- a/build/linux.mk +++ b/build/linux.mk @@ -1,7 +1,7 @@ include build/init.mk cc := gcc -cflags += -DLIBC_GNU -D_GNU_SOURCE +cflags += -DLIBC_GNU -D_GNU_SOURCE -DREPL_SUPPORTED ldadd+=lib/tinycc/libtcc.a all: deps cjit diff --git a/build/musl.mk b/build/musl.mk index 72fb494..f890845 100644 --- a/build/musl.mk +++ b/build/musl.mk @@ -2,7 +2,7 @@ include build/init.mk cc := musl-gcc cflags := -Wall -static -Os -cflags += -Isrc -Ilib/tinycc -DLIBC_MUSL -nostdlib +cflags += -Isrc -Ilib/tinycc -DLIBC_MUSL -nostdlib -DREPL_SUPPORTED ldadd := lib/tinycc/libtcc.a /usr/lib/x86_64-linux-musl/crt1.o /usr/lib/x86_64-linux-musl/libc.a diff --git a/src/cjit.c b/src/cjit.c index a78b711..a8a5c29 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -27,6 +27,7 @@ #include #include #include +#include #endif #include @@ -72,12 +73,205 @@ extern char *win32_mkdtemp(); // from io.c extern void _out(const char *fmt, ...); extern void _err(const char *fmt, ...); -// TCC callback + +static void error_callback(void *ctx, const char *msg); + +#ifdef REPL_SUPPORTED +static int cjit_compile_and_run(TCCState *TCC, const char *code, int argc, char **argv, int rn, char **err_msg) +{ + pid_t pid; + int res = 0; + int (*_ep)(int, char**); + const char main_fn[]="main"; + int err_fds[2]; + int err_r, err_w; + const char compile_errmsg[]= "Code compilation error in source\n"; + const char reloc_errmsg[]= "Code relocation error in source\n"; + const char nomain_errmsg[]= "Symbol 'main' was not found in source\n"; + + + *err_msg = NULL; + + if (pipe(err_fds) != 0) { + _err("Error creating pipe\n"); + return 1; + } + err_r = err_fds[0]; + err_w = err_fds[1]; + + pid = fork(); + if (pid == 0) { + close(err_r); + tcc_set_error_func(TCC, &err_w, error_callback); + if (tcc_compile_string(TCC, code) < 0) { + write(err_w, compile_errmsg, strlen(compile_errmsg)); + res = 1; + } + if ((res == 0) && (tcc_relocate(TCC) < 0)) { + write(err_w, reloc_errmsg, strlen(reloc_errmsg)); + res = 1; + } + if (res == 0) { + _ep = tcc_get_symbol(TCC, main_fn); + if (!_ep) { + write(err_w, nomain_errmsg, strlen(nomain_errmsg)); + res = 1; + } + } + + if ((res == 0) && (rn != 0)) { + res = _ep(argc, argv); + } + close(err_w); + exit(res); + } else { + int status; + int ret; + struct pollfd pfd; + close(err_w); + pfd.fd = err_r; + pfd.events = POLLIN; + + while (poll(&pfd, 1, 2000) > 0) { + ssize_t n = 0; + char buf[1024]; + memset(buf, 0, 1024); + n = read(err_r, buf, 1023); + if (n > 0) { + if (!*err_msg) + *err_msg = strdup(buf); + } else break; + } + close(err_r); + + ret = waitpid(pid, &status, WUNTRACED | WCONTINUED); + if (ret != pid){ + _err("Wait error in source: %s","main"); + return 1; + } + if (WIFEXITED(status)) { + res = WEXITSTATUS(status); + //_err("Process has returned %d", res); + } else if (WIFSIGNALED(status)) { + res = WTERMSIG(status); + _err("Process terminated with signal %d", WTERMSIG(status)); + } else if (WIFSTOPPED(status)) { + res = WSTOPSIG(status); + _err("Process has returned %d", WSTOPSIG(status)); + } else if (WIFSTOPPED(status)) { + res = WSTOPSIG(status); + _err("Process stopped with signal", WSTOPSIG(status)); + } else { + _err("wait: unknown status: %d", status); + } + } + return res; +} + +/* Called by the editor when the buffer is set to run */ +static int cjit_compile_buffer(void *tcs, char *code, int argc, char **argv) +{ + TCCState *TCC = (TCCState *)tcs; + int res = 0; + char *err_msg; + disableRawMode(STDIN_FILENO); + /* Clear the screen */ + write(STDOUT_FILENO, "\x1b[2J", 4); + // run the code from main + res = cjit_compile_and_run(TCC, code, argc, argv, 1, &err_msg); + if (err_msg) { + _err(err_msg); + free(err_msg); + } + enableRawMode(STDIN_FILENO); + sleep(3); + editorRefreshScreen(); + return res; +} + +static void error_callback(void *ctx, const char *msg) +{ + int wfd = *(int *)ctx; + if ((msg) && (wfd >=0)) { + write(wfd, msg, strlen(msg)); + } +} + +#define ERR_MAX 128 +static int cjit_check_buffer(void *tcs, char *code, char **err_msg) +{ + TCCState *TCC = (TCCState *)tcs; + int res = 0; + // run the code from main + // + disableRawMode(STDIN_FILENO); + res = cjit_compile_and_run(TCC, code, 0, NULL, 0, err_msg); + if (res != 0) { + if(err_msg) { + char *p = strchr(err_msg, '\n'); + if (p) *p = 0; + editorSetStatusMessage(*err_msg); + } + } else { + editorSetStatusMessage("No errors."); + } + enableRawMode(STDIN_FILENO); + return res; +} + +static int cjit_cli(TCCState *TCC) +{ + char *line = NULL; + size_t len = 0; + ssize_t rd; + int res = 0; + char *err_msg; + // don't add automatic main preamble if in a pipe + if (!isatty(fileno(stdin))) { + int rd; + char *code = NULL; + char *line = NULL; + _err("Not running from a terminal, executing source from STDIN\n"); + do { + rd = getline(&line, &len, stdin); + if (rd > 0) { + if (!code) + code = strdup(line); + else { + code = realloc(code, strlen(code) + rd + 1); + if (!code) { + _err("Memory error while executing from STDIN\n"); + return 2; + } + strcat(code, line); + } + } + } while (rd != -1); + cjit_compile_and_run(TCC, code, 0, NULL, 1, &err_msg); + if (err_msg) + _err(err_msg); + } else { + initEditor(); + enableRawMode(STDIN_FILENO); + editorSetStatusMessage( + "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find | Ctrl-R Run"); + while(1) { + editorSetCompilerCallback(cjit_compile_buffer); + editorSetCheckCallback(cjit_check_buffer); + editorSetCompilerContext(TCC); + editorRefreshScreen(); + editorProcessKeypress(STDIN_FILENO); + } + } + return res; +} +#else void handle_error(void *n, const char *m) { (void)n; _err("%s",m); } +#ifdef LIBC_MINGW32 static int cjit_exec_win(TCCState *TCC, const char *ep, int argc, char **argv) { int res = 1; int (*_ep)(int, char**); @@ -91,7 +285,7 @@ static int cjit_exec_win(TCCState *TCC, const char *ep, int argc, char **argv) { return(res); } -#ifndef LIBC_MINGW32 +#else static int cjit_exec_fork(TCCState *TCC, const char *ep, int argc, char **argv) { pid_t pid; @@ -218,6 +412,9 @@ static int cjit_cli(TCCState *TCC) return res; } +#endif + + #ifdef LIBC_MUSL #include #include @@ -634,8 +831,6 @@ int main(int argc, char **argv) { _err("Could not initialize tcc"); exit(1); } - // error handler callback for TCC - tcc_set_error_func(TCC, stderr, handle_error); // initialize the tmpdir for execution #ifndef LIBC_MINGW32 @@ -654,6 +849,11 @@ int main(int argc, char **argv) { if(! write_to_file(tmpdir,"libtcc1.a",(char*)&libtcc1,libtcc1_len) ) goto endgame; + + //// TCC DEFAULT PATHS + tcc_add_include_path(TCC,"/usr/include/x86_64-linux-musl"); + + #if defined(LIBC_MUSL) if(! write_to_file(tmpdir,"libc.so",(char*)&musl_libc,musl_libc_len) ) goto endgame; @@ -684,12 +884,24 @@ int main(int argc, char **argv) { } _err("Source to execute: %s",code_path); char *code = file_load(code_path); + char *err_msg = NULL; if(!code) { _err("File not found: %s",code_path); goto endgame; } + +#ifdef REPL_SUPPORTED + _err("Execution start\n---"); + res = cjit_compile_and_run(TCC, code, argc, argv, 1, &err_msg); + if (err_msg) { + _err(err_msg); + } +#else + // error handler callback for TCC + tcc_set_error_func(TCC, stderr, handle_error); if (tcc_compile_string(TCC, code) == -1) return 1; free(code); // safe: bytecode compiled is in TCC now + code = NULL; _err("Compilation successful"); // relocate the code @@ -703,9 +915,10 @@ int main(int argc, char **argv) { #else res = cjit_exec_win(TCC, "main", argc, argv); #endif - +#endif // REPL_SUPPORTED endgame: // free TCC + free(code); tcc_delete(TCC); if(tmpdir) { rm_recursive(tmpdir); diff --git a/src/kilo.c b/src/kilo.c new file mode 100644 index 0000000..83b8702 --- /dev/null +++ b/src/kilo.c @@ -0,0 +1,1349 @@ +/* This REPL interface is part of + * CJIT https://dyne.org/cjit + * + * Copyright (C) 2024 Dyne.org foundation + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * + * + * However, this interface has been derived from an existing project: + * + * Kilo -- A very simple editor in less than 1-kilo lines of code (as counted + * by "cloc"). Does not depend on libcurses, directly emits VT100 + * escapes on the terminal. + * + * ----------------------------------------------------------------------- + * + * Copyright (C) 2016 Salvatore Sanfilippo + * + * + */ + + +#ifdef REPL_SUPPORTED +#ifdef __linux__ +#define _POSIX_C_SOURCE 200809L +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* Syntax highlight types */ +#define HL_NORMAL 0 +#define HL_NONPRINT 1 +#define HL_COMMENT 2 /* Single line comment. */ +#define HL_MLCOMMENT 3 /* Multi-line comment. */ +#define HL_KEYWORD1 4 +#define HL_KEYWORD2 5 +#define HL_STRING 6 +#define HL_NUMBER 7 +#define HL_ERROR HL_NUMBER +#define HL_MATCH 8 /* Search match. */ + +#define HL_HIGHLIGHT_STRINGS (1<<0) +#define HL_HIGHLIGHT_NUMBERS (1<<1) + +struct editorSyntax { + char **filematch; + char **keywords; + char singleline_comment_start[2]; + char multiline_comment_start[3]; + char multiline_comment_end[3]; + int flags; +}; + +/* This structure represents a single line of the file we are editing. */ +typedef struct erow { + int idx; /* Row index in the file, zero-based. */ + int size; /* Size of the row, excluding the null term. */ + int rsize; /* Size of the rendered row. */ + char *chars; /* Row content. */ + char *render; /* Row content "rendered" for screen (for TABs). */ + unsigned char *hl; /* Syntax highlight type for each character in render.*/ + int hl_oc; /* Row had open comment at end in last syntax highlight + check. */ +} erow; + +typedef struct hlcolor { + int r,g,b; +} hlcolor; + +struct editorConfig { + int cx,cy; /* Cursor x and y position in characters */ + int rowoff; /* Offset of row displayed. */ + int coloff; /* Offset of column displayed. */ + int screenrows; /* Number of rows that we can show */ + int screencols; /* Number of cols that we can show */ + int numrows; /* Number of rows */ + int rawmode; /* Is terminal raw mode enabled? */ + erow *row; /* Rows */ + int dirty; /* File modified but not saved. */ + char *filename; /* Currently open filename */ + char statusmsg[80]; + time_t statusmsg_time; + struct editorSyntax *syntax; /* Current syntax highlight, or NULL. */ + int (*compiler_cb)(void *, const char *, int, char **); + int (*check_cb)(void *, const char *, char *); + void *compiler_cb_ctx; +}; + +static struct editorConfig E; + +enum KEY_ACTION{ + KEY_NULL = 0, /* NULL */ + CTRL_C = 3, /* Ctrl-c */ + CTRL_D = 4, /* Ctrl-d */ + CTRL_F = 6, /* Ctrl-f */ + CTRL_H = 8, /* Ctrl-h */ + TAB = 9, /* Tab */ + CTRL_L = 12, /* Ctrl+l */ + ENTER = 13, /* Enter */ + CTRL_Q = 17, /* Ctrl-q */ + CTRL_R = 18, /* Ctrl-r */ + CTRL_S = 19, /* Ctrl-s */ + CTRL_U = 21, /* Ctrl-u */ + ESC = 27, /* Escape */ + BACKSPACE = 127, /* Backspace */ + /* The following are just soft codes, not really reported by the + * terminal directly. */ + ARROW_LEFT = 1000, + ARROW_RIGHT, + ARROW_UP, + ARROW_DOWN, + DEL_KEY, + HOME_KEY, + END_KEY, + PAGE_UP, + PAGE_DOWN +}; + +void editorSetStatusMessage(const char *fmt, ...); + +/* =========================== Syntax highlights DB ========================= + * + * In order to add a new syntax, define two arrays with a list of file name + * matches and keywords. The file name matches are used in order to match + * a given syntax with a given file name: if a match pattern starts with a + * dot, it is matched as the last past of the filename, for example ".c". + * Otherwise the pattern is just searched inside the filenme, like "Makefile"). + * + * The list of keywords to highlight is just a list of words, however if they + * a trailing '|' character is added at the end, they are highlighted in + * a different color, so that you can have two different sets of keywords. + * + * Finally add a stanza in the HLDB global variable with two two arrays + * of strings, and a set of flags in order to enable highlighting of + * comments and numbers. + * + * The characters for single and multi line comments must be exactly two + * and must be provided as well (see the C language example). + * + * There is no support to highlight patterns currently. */ + +/* C / C++ */ +char *C_HL_extensions[] = {".c",".h",".cpp",".hpp",".cc",NULL}; +char *C_HL_keywords[] = { + /* C Keywords */ + "auto","break","case","continue","default","do","else","enum", + "extern","for","goto","if","register","return","sizeof","static", + "struct","switch","typedef","union","volatile","while","NULL", + + /* Preprocessor statements */ + "#include", "#define", "#undef", "#ifdef", "#ifndef", "#if", "#else", + "defined", "#endif", "#elif", "#error", "#pragma", "#line", "#undef", + + /* C types */ + "int|","long|","double|","float|","char|","unsigned|","signed|", + "void|","short|","auto|","const|","bool|",NULL +}; + +/* Here we define an array of syntax highlights by extensions, keywords, + * comments delimiters and flags. */ +struct editorSyntax HLDB[] = { + { + /* C / C++ */ + C_HL_extensions, + C_HL_keywords, + "//","/*","*/", + HL_HIGHLIGHT_STRINGS | HL_HIGHLIGHT_NUMBERS + } +}; + +#define HLDB_ENTRIES (sizeof(HLDB)/sizeof(HLDB[0])) + +/* ======================= Low level terminal handling ====================== */ + +static struct termios orig_termios; /* In order to restore at exit.*/ + +void disableRawMode(int fd) { + /* Don't even check the return value as it's too late. */ + if (E.rawmode) { + tcsetattr(fd,TCSAFLUSH,&orig_termios); + E.rawmode = 0; + } +} + +/* Called at exit to avoid remaining in raw mode. */ +void editorAtExit(void) { + disableRawMode(STDIN_FILENO); +} + +/* Raw mode: 1960 magic shit. */ +int enableRawMode(int fd) { + struct termios raw; + + if (E.rawmode) return 0; /* Already enabled. */ + if (!isatty(STDIN_FILENO)) goto fatal; + atexit(editorAtExit); + if (tcgetattr(fd,&orig_termios) == -1) goto fatal; + + raw = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); + /* output modes - disable post processing */ + raw.c_oflag &= ~(OPOST); + /* control modes - set 8 bit chars */ + raw.c_cflag |= (CS8); + /* local modes - choing off, canonical off, no extended functions, + * no signal chars (^Z,^C) */ + raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. */ + raw.c_cc[VMIN] = 0; /* Return each byte, or zero for timeout. */ + raw.c_cc[VTIME] = 1; /* 100 ms timeout (unit is tens of second). */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; + E.rawmode = 1; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +/* Read a key from the terminal put in raw mode, trying to handle + * escape sequences. */ +int editorReadKey(int fd) { + int nread; + char c, seq[3]; + while ((nread = read(fd,&c,1)) == 0); + if (nread == -1) exit(1); + + while(1) { + switch(c) { + case ESC: /* escape sequence */ + /* If this is just an ESC, we'll timeout here. */ + if (read(fd,seq,1) == 0) return ESC; + if (read(fd,seq+1,1) == 0) return ESC; + + /* ESC [ sequences. */ + if (seq[0] == '[') { + if (seq[1] >= '0' && seq[1] <= '9') { + /* Extended escape, read additional byte. */ + if (read(fd,seq+2,1) == 0) return ESC; + if (seq[2] == '~') { + switch(seq[1]) { + case '3': return DEL_KEY; + case '5': return PAGE_UP; + case '6': return PAGE_DOWN; + } + } + } 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; + } + } + } + + /* ESC O sequences. */ + else if (seq[0] == 'O') { + switch(seq[1]) { + case 'H': return HOME_KEY; + case 'F': return END_KEY; + } + } + break; + default: + return c; + } + } +} + +/* Use the ESC [6n escape sequence to query the horizontal cursor position + * and return it. On error -1 is returned, on success the position of the + * cursor is stored at *rows and *cols and 0 is returned. */ +int getCursorPosition(int ifd, int ofd, int *rows, int *cols) { + char buf[32]; + unsigned int i = 0; + + /* Report cursor location */ + if (write(ofd, "\x1b[6n", 4) != 4) return -1; + + /* Read the response: ESC [ rows ; cols R */ + while (i < sizeof(buf)-1) { + if (read(ifd,buf+i,1) != 1) break; + if (buf[i] == 'R') break; + i++; + } + buf[i] = '\0'; + + /* Parse it. */ + if (buf[0] != ESC || buf[1] != '[') return -1; + if (sscanf(buf+2,"%d;%d",rows,cols) != 2) return -1; + return 0; +} + +/* Try to get the number of columns in the current terminal. If the ioctl() + * call fails the function will try to query the terminal itself. + * Returns 0 on success, -1 on error. */ +int getWindowSize(int ifd, int ofd, int *rows, int *cols) { + struct winsize ws; + + if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { + /* ioctl() failed. Try to query the terminal itself. */ + int orig_row, orig_col, retval; + + /* Get the initial position so we can restore it later. */ + retval = getCursorPosition(ifd,ofd,&orig_row,&orig_col); + if (retval == -1) goto failed; + + /* Go to right/bottom margin and get position. */ + if (write(ofd,"\x1b[999C\x1b[999B",12) != 12) goto failed; + retval = getCursorPosition(ifd,ofd,rows,cols); + if (retval == -1) goto failed; + + /* Restore position. */ + char seq[32]; + snprintf(seq,32,"\x1b[%d;%dH",orig_row,orig_col); + if (write(ofd,seq,strlen(seq)) == -1) { + /* Can't recover... */ + } + return 0; + } else { + *cols = ws.ws_col; + *rows = ws.ws_row; + return 0; + } + +failed: + return -1; +} + +/* ====================== Syntax highlight color scheme ==================== */ + +int is_separator(int c) { + return c == '\0' || isspace(c) || strchr(",.()+-/*=~%[];",c) != NULL; +} + +/* Return true if the specified row last char is part of a multi line comment + * that starts at this row or at one before, and does not end at the end + * of the row but spawns to the next row. */ +int editorRowHasOpenComment(erow *row) { + if (row->hl && row->rsize && row->hl[row->rsize-1] == HL_MLCOMMENT && + (row->rsize < 2 || (row->render[row->rsize-2] != '*' || + row->render[row->rsize-1] != '/'))) return 1; + return 0; +} + +/* Set every byte of row->hl (that corresponds to every character in the line) + * to the right syntax highlight type (HL_* defines). */ +void editorUpdateSyntax(erow *row) { + if (!row) + return; + row->hl = realloc(row->hl,row->rsize); + memset(row->hl,HL_NORMAL,row->rsize); + + if (E.syntax == NULL) return; /* No syntax, everything is HL_NORMAL. */ + + int i, prev_sep, in_string, in_comment; + char *p; + 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; + + /* Point to the first non-space char. */ + p = row->render; + i = 0; /* Current char offset */ + while(*p && isspace(*p)) { + p++; + i++; + } + prev_sep = 1; /* Tell the parser if 'i' points to start of word. */ + in_string = 0; /* Are we inside "" or '' ? */ + in_comment = 0; /* Are we inside multi-line comment? */ + + /* If the previous line has an open comment, this line starts + * with an open comment state. */ + if (row->idx > 0 && editorRowHasOpenComment(&E.row[row->idx-1])) + in_comment = 1; + + while(*p) { + /* Handle // comments. */ + if (prev_sep && *p == scs[0] && *(p+1) == scs[1]) { + /* From here to end is a comment */ + memset(row->hl+i,HL_COMMENT,row->size-i); + return; + } + + /* Handle multi line comments. */ + if (in_comment) { + row->hl[i] = HL_MLCOMMENT; + if (*p == mce[0] && *(p+1) == mce[1]) { + row->hl[i+1] = HL_MLCOMMENT; + p += 2; i += 2; + in_comment = 0; + prev_sep = 1; + continue; + } else { + prev_sep = 0; + p++; i++; + continue; + } + } else if (*p == mcs[0] && *(p+1) == mcs[1]) { + row->hl[i] = HL_MLCOMMENT; + row->hl[i+1] = HL_MLCOMMENT; + p += 2; i += 2; + in_comment = 1; + prev_sep = 0; + continue; + } + + /* Handle "" and '' */ + if (in_string) { + row->hl[i] = HL_STRING; + if (*p == '\\') { + row->hl[i+1] = HL_STRING; + p += 2; i += 2; + prev_sep = 0; + continue; + } + if (*p == in_string) in_string = 0; + p++; i++; + continue; + } else { + if (*p == '"' || *p == '\'') { + in_string = *p; + row->hl[i] = HL_STRING; + p++; i++; + prev_sep = 0; + continue; + } + } + + /* Handle non printable chars. */ + if (!isprint(*p)) { + row->hl[i] = HL_NONPRINT; + p++; i++; + prev_sep = 0; + continue; + } + + /* Handle numbers */ + if ((isdigit(*p) && (prev_sep || row->hl[i-1] == HL_NUMBER)) || + (*p == '.' && i >0 && row->hl[i-1] == HL_NUMBER)) { + row->hl[i] = HL_NUMBER; + p++; i++; + prev_sep = 0; + continue; + } + + /* Handle keywords and lib calls */ + 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 (!memcmp(p,keywords[j],klen) && + is_separator(*(p+klen))) + { + /* Keyword */ + memset(row->hl+i,kw2 ? HL_KEYWORD2 : HL_KEYWORD1,klen); + p += klen; + i += klen; + break; + } + } + if (keywords[j] != NULL) { + prev_sep = 0; + continue; /* We had a keyword match */ + } + } + + /* Not special chars */ + prev_sep = is_separator(*p); + p++; i++; + } + + /* Propagate syntax change to the next row if the open commen + * state changed. This may recursively affect all the following rows + * in the file. */ + int oc = editorRowHasOpenComment(row); + if (row->hl_oc != oc && row->idx+1 < E.numrows) + editorUpdateSyntax(&E.row[row->idx+1]); + row->hl_oc = oc; +} + +/* Maps syntax highlight token types to terminal colors. */ +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; /* blu */ + default: return 37; /* white */ + } +} + +/* Select the syntax highlight scheme depending on the filename, + * setting it in the global state E.syntax. */ +void editorSelectSyntaxHighlight(char *filename) { + for (unsigned int j = 0; j < HLDB_ENTRIES; j++) { + struct editorSyntax *s = HLDB+j; + unsigned int i = 0; + while(s->filematch[i]) { + char *p; + int patlen = strlen(s->filematch[i]); + if ((p = strstr(filename,s->filematch[i])) != NULL) { + if (s->filematch[i][0] != '.' || p[patlen] == '\0') { + E.syntax = s; + return; + } + } + i++; + } + } +} + +/* ======================= Editor rows implementation ======================= */ + +/* Update the rendered version and the syntax highlight of a row. */ +void editorUpdateRow(erow *row) { + unsigned int tabs = 0, nonprint = 0; + int j, idx; + + /* Create a version of the row we can directly print on the screen, + * respecting tabs, substituting non printable characters with '?'. */ + free(row->render); + for (j = 0; j < row->size; j++) + if (row->chars[j] == TAB) tabs++; + + unsigned long long allocsize = + (unsigned long long) row->size + tabs*8 + nonprint*9 + 1; + if (allocsize > UINT32_MAX) { + printf("Some line of the edited file is too long for kilo\n"); + exit(1); + } + + row->render = malloc(row->size + tabs*8 + nonprint*9 + 1); + idx = 0; + for (j = 0; j < row->size; j++) { + if (row->chars[j] == TAB) { + row->render[idx++] = ' '; + while((idx+1) % 8 != 0) row->render[idx++] = ' '; + } else { + row->render[idx++] = row->chars[j]; + } + } + row->rsize = idx; + row->render[idx] = '\0'; + + /* Update the syntax highlighting attributes of the row. */ + editorUpdateSyntax(row); + + +} + +char *editorRowsToString(int *buflen); + +/* Insert a row at the specified position, shifting the other rows on the bottom + * if required. */ +void editorInsertRow(int at, char *s, size_t len) { + if (at > E.numrows) return; + E.row = realloc(E.row,sizeof(erow)*(E.numrows+1)); + if (at != E.numrows) { + memmove(E.row+at+1,E.row+at,sizeof(E.row[0])*(E.numrows-at)); + for (int j = at+1; j <= E.numrows; j++) E.row[j].idx++; + } + E.row[at].size = len; + E.row[at].chars = malloc(len+1); + memcpy(E.row[at].chars,s,len+1); + E.row[at].hl = NULL; + E.row[at].hl_oc = 0; + E.row[at].render = NULL; + E.row[at].rsize = 0; + E.row[at].idx = at; + editorUpdateRow(E.row+at); + E.numrows++; + E.dirty++; +} + +/* Free row's heap allocated stuff. */ +void editorFreeRow(erow *row) { + free(row->render); + free(row->chars); + free(row->hl); +} + +/* Remove the row at the specified position, shifting the remainign on the + * top. */ +void editorDelRow(int at) { + erow *row; + + if (at >= E.numrows) return; + row = E.row+at; + editorFreeRow(row); + memmove(E.row+at,E.row+at+1,sizeof(E.row[0])*(E.numrows-at-1)); + for (int j = at; j < E.numrows-1; j++) E.row[j].idx++; + E.numrows--; + E.dirty++; +} + +/* Turn the editor rows into a single heap-allocated string. + * Returns the pointer to the heap-allocated string and populate the + * integer pointed by 'buflen' with the size of the string, escluding + * the final nulterm. */ +char *editorRowsToString(int *buflen) { + char *buf = NULL, *p; + int totlen = 0; + int j; + + /* Compute count of bytes */ + for (j = 0; j < E.numrows; j++) + totlen += E.row[j].size+1; /* +1 is for "\n" at end of every row */ + *buflen = totlen; + totlen++; /* Also make space for nulterm */ + + p = buf = malloc(totlen); + 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++; + } + *p = '\0'; + return buf; +} + +/* Insert a character at the specified position in a row, moving the remaining + * chars on the right if needed. */ +void editorRowInsertChar(erow *row, int at, int c) { + if (at > row->size) { + /* Pad the string with spaces if the insert location is outside the + * current length by more than a single character. */ + int padlen = at-row->size; + /* In the next line +2 means: new char and null term. */ + row->chars = realloc(row->chars,row->size+padlen+2); + memset(row->chars+row->size,' ',padlen); + row->chars[row->size+padlen+1] = '\0'; + row->size += padlen+1; + } else { + /* If we are in the middle of the string just make space for 1 new + * char plus the (already existing) null term. */ + 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++; +} + +/* Append the string 's' at the end of a row */ +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++; +} + +/* Delete the character at offset 'at' from the specified row. */ +void editorRowDelChar(erow *row, int at) { + if (row->size <= at) return; + memmove(row->chars+at,row->chars+at+1,row->size-at); + editorUpdateRow(row); + row->size--; + E.dirty++; +} + +/* Insert the specified char at the current prompt position. */ +void editorInsertChar(int c) { + int filerow = E.rowoff+E.cy; + int filecol = E.coloff+E.cx; + erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + + /* If the row where the cursor is currently located does not exist in our + * logical representaion of the file, add enough empty rows as needed. */ + if (!row) { + while(E.numrows <= filerow) + editorInsertRow(E.numrows,"",0); + + } + row = &E.row[filerow]; + editorRowInsertChar(row,filecol,c); + if (E.cx == E.screencols-1) + E.coloff++; + else + E.cx++; + E.dirty++; +} + +/* Inserting a newline is slightly complex as we have to handle inserting a + * newline in the middle of a line, splitting the line as needed. */ +void editorInsertNewline(void) { + int filerow = E.rowoff+E.cy; + int filecol = E.coloff+E.cx; + erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + + if (!row) { + if (filerow == E.numrows) { + editorInsertRow(filerow,"",0); + goto fixcursor; + } + return; + } + /* If the cursor is over the current line size, we want to conceptually + * think it's just over the last character. */ + if (filecol >= row->size) filecol = row->size; + if (filecol == 0) { + editorInsertRow(filerow,"",0); + } else { + /* We are in the middle of a line. Split it between two rows. */ + editorInsertRow(filerow+1,row->chars+filecol,row->size-filecol); + row = &E.row[filerow]; + row->chars[filecol] = '\0'; + row->size = filecol; + editorUpdateRow(row); + } +fixcursor: + if (E.cy == E.screenrows-1) { + E.rowoff++; + } else { + E.cy++; + } + E.cx = 0; + E.coloff = 0; +} + +/* Delete the char at the current prompt position. */ +void editorDelChar() { + int filerow = E.rowoff+E.cy; + int filecol = E.coloff+E.cx; + erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + + if (!row || (filecol == 0 && filerow == 0)) return; + if (filecol == 0) { + /* Handle the case of column 0, we need to move the current line + * on the right of the previous one. */ + filecol = E.row[filerow-1].size; + editorRowAppendString(&E.row[filerow-1],row->chars,row->size); + editorDelRow(filerow); + row = NULL; + if (E.cy == 0) + E.rowoff--; + else + E.cy--; + E.cx = filecol; + if (E.cx >= E.screencols) { + int shift = (E.screencols-E.cx)+1; + E.cx -= shift; + E.coloff += shift; + } + } else { + editorRowDelChar(row,filecol-1); + if (E.cx == 0 && E.coloff) + E.coloff--; + else + E.cx--; + } + if (row) editorUpdateRow(row); + E.dirty++; +} + +/* Load the specified program in the editor memory and returns 0 on success + * or 1 on error. */ +int editorOpen(char *filename) { + FILE *fp; + + E.dirty = 0; + free(E.filename); + size_t fnlen = strlen(filename)+1; + E.filename = malloc(fnlen); + memcpy(E.filename,filename,fnlen); + + fp = fopen(filename,"r"); + if (!fp) { + if (errno != ENOENT) { + perror("Opening file"); + exit(1); + } + return 1; + } + + char *line = NULL; + size_t linecap = 0; + ssize_t linelen; + while((linelen = getline(&line,&linecap,fp)) != -1) { + if (linelen && (line[linelen-1] == '\n' || line[linelen-1] == '\r')) + line[--linelen] = '\0'; + editorInsertRow(E.numrows,line,linelen); + } + free(line); + fclose(fp); + E.dirty = 0; + return 0; +} + +int editorRun(void) +{ + int len; + char *buf = editorRowsToString(&len); + if (E.compiler_cb == NULL) { + return 0; + } + return E.compiler_cb(E.compiler_cb_ctx, buf, 0, NULL); +} + +/* Save the current file on disk. Return 0 on success, 1 on error. */ +int editorSave(void) { + int len; + char *buf = editorRowsToString(&len); + int fd = open(E.filename,O_RDWR|O_CREAT,0644); + if (fd == -1) goto writeerr; + + /* Use truncate + a single write(2) call in order to make saving + * a bit safer, under the limits of what we can do in a small editor. */ + if (ftruncate(fd,len) == -1) goto writeerr; + if (write(fd,buf,len) != len) goto writeerr; + + close(fd); + free(buf); + E.dirty = 0; + editorSetStatusMessage("%d bytes written on disk", len); + return 0; + +writeerr: + free(buf); + if (fd != -1) close(fd); + editorSetStatusMessage("Can't save! I/O error: %s",strerror(errno)); + return 1; +} + +/* ============================= Terminal update ============================ */ + +/* We define a very simple "append buffer" structure, that is an heap + * allocated string where we can append to. This is useful in order to + * write all the escape sequences in a buffer and flush them to the standard + * output in a single call, to avoid flickering effects. */ +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); +} + +/* This function writes the whole screen using VT100 escape characters + * starting from the logical state of the editor in the global state 'E'. */ +void editorRefreshScreen(void) { + int y; + erow *r; + char buf[32]; + struct abuf ab = ABUF_INIT; + + abAppend(&ab,"\x1b[?25l",6); /* Hide cursor. */ + abAppend(&ab,"\x1b[H",3); /* Go home. */ + for (y = 0; y < E.screenrows; y++) { + int filerow = E.rowoff+y; + + if (filerow >= E.numrows) { + if (E.numrows == 0 && y == E.screenrows/3) { + char welcome[80]; + int welcomelen = snprintf(welcome,sizeof(welcome), + "CJIT REPL\x1b[0K\r\n"); + int padding = (E.screencols-welcomelen)/2; + if (padding) { + abAppend(&ab,"~",1); + padding--; + } + while(padding--) abAppend(&ab," ",1); + abAppend(&ab,welcome,welcomelen); + } else { + abAppend(&ab,"~\x1b[0K\r\n",7); + } + continue; + } + + r = &E.row[filerow]; + + int len = r->rsize - E.coloff; + int current_color = -1; + if (len > 0) { + if (len > E.screencols) len = E.screencols; + char *c = r->render+E.coloff; + unsigned char *hl = r->hl+E.coloff; + int j; + for (j = 0; j < len; j++) { + if (hl[j] == HL_NONPRINT) { + char sym; + abAppend(&ab,"\x1b[7m",4); + if (c[j] <= 26) + sym = '@'+c[j]; + else + sym = '?'; + abAppend(&ab,&sym,1); + abAppend(&ab,"\x1b[0m",4); + } 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) { + char buf[16]; + int clen = snprintf(buf,sizeof(buf),"\x1b[%dm",color); + current_color = color; + abAppend(&ab,buf,clen); + } + abAppend(&ab,c+j,1); + } + } + } + abAppend(&ab,"\x1b[39m",5); + abAppend(&ab,"\x1b[0K",4); + abAppend(&ab,"\r\n",2); + } + + /* Create a two rows status. First row: */ + abAppend(&ab,"\x1b[0K",4); + abAppend(&ab,"\x1b[7m",4); + char status[80], rstatus[80]; + int len = snprintf(status, sizeof(status), "%.20s - %d lines %s", + E.filename, E.numrows, E.dirty ? "(modified)" : ""); + int rlen = snprintf(rstatus, sizeof(rstatus), + "%d/%d",E.rowoff+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[0m\r\n",6); + + /* Second row depends on E.statusmsg and the status message update time. */ + abAppend(&ab,"\x1b[0K",4); + int msglen = strlen(E.statusmsg); + if (msglen && time(NULL)-E.statusmsg_time < 5) + abAppend(&ab,E.statusmsg,msglen <= E.screencols ? msglen : E.screencols); + + /* Put cursor at its current position. Note that the horizontal position + * at which the cursor is displayed may be different compared to 'E.cx' + * because of TABs. */ + int j; + int cx = 1; + int filerow = E.rowoff+E.cy; + erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + if (row) { + for (j = E.coloff; j < (E.cx+E.coloff); j++) { + if (j < row->size && row->chars[j] == TAB) cx += 7-((cx)%8); + cx++; + } + } + snprintf(buf,sizeof(buf),"\x1b[%d;%dH",E.cy+1,cx); + abAppend(&ab,buf,strlen(buf)); + abAppend(&ab,"\x1b[?25h",6); /* Show cursor. */ + write(STDOUT_FILENO,ab.b,ab.len); + abFree(&ab); + /* Check for errors on new line */ + if (E.check_cb) { + int err_line = 0; + char *err_msg = NULL; + unsigned buflen; + char *buf = editorRowsToString(&buflen); + buf[buflen] = (char)0; + if ((E.check_cb(E.compiler_cb_ctx, buf, &err_msg) != 0) && (err_msg != NULL)) { + int err_line = 0; + /* extact the error line */ + char *line_ns = strchr(err_msg, ':'); + if (line_ns) { + err_line = atoi(line_ns + 1); + } + if (err_line > 0) { + int at = err_line - 1; + /* Mark the error line as red */ + E.row[at].hl = realloc(E.row[at].hl, E.row[at].rsize); + memset(E.row[at].hl, HL_ERROR, E.row[at].rsize); + } + } + free(buf); + } +} + +/* Set an editor status message for the second line of the status, at the + * end of the screen. */ +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); +} + +/* =============================== Find mode ================================ */ + +#define KILO_QUERY_LEN 256 + +void editorFind(int fd) { + char query[KILO_QUERY_LEN+1] = {0}; + int qlen = 0; + int last_match = -1; /* Last line where a match was found. -1 for none. */ + int find_next = 0; /* if 1 search next, if -1 search prev. */ + int saved_hl_line = -1; /* No saved HL */ + char *saved_hl = NULL; + +#define FIND_RESTORE_HL do { \ + if (saved_hl) { \ + memcpy(E.row[saved_hl_line].hl,saved_hl, E.row[saved_hl_line].rsize); \ + free(saved_hl); \ + saved_hl = NULL; \ + } \ +} while (0) + + /* Save the cursor position in order to restore it later. */ + int saved_cx = E.cx, saved_cy = E.cy; + int saved_coloff = E.coloff, saved_rowoff = E.rowoff; + + while(1) { + editorSetStatusMessage( + "Search: %s (Use ESC/Arrows/Enter)", query); + editorRefreshScreen(); + + int c = editorReadKey(fd); + if (c == DEL_KEY || c == CTRL_H || c == BACKSPACE) { + if (qlen != 0) query[--qlen] = '\0'; + last_match = -1; + } else if (c == ESC || c == ENTER) { + if (c == ESC) { + E.cx = saved_cx; E.cy = saved_cy; + E.coloff = saved_coloff; E.rowoff = saved_rowoff; + } + FIND_RESTORE_HL; + editorSetStatusMessage(""); + return; + } else if (c == ARROW_RIGHT || c == ARROW_DOWN) { + find_next = 1; + } else if (c == ARROW_LEFT || c == ARROW_UP) { + find_next = -1; + } else if (isprint(c)) { + if (qlen < KILO_QUERY_LEN) { + query[qlen++] = c; + query[qlen] = '\0'; + last_match = -1; + } + } + + /* Search occurrence. */ + if (last_match == -1) find_next = 1; + if (find_next) { + char *match = NULL; + int match_offset = 0; + int i, current = last_match; + + for (i = 0; i < E.numrows; i++) { + current += find_next; + if (current == -1) current = E.numrows-1; + else if (current == E.numrows) current = 0; + match = strstr(E.row[current].render,query); + if (match) { + match_offset = match-E.row[current].render; + break; + } + } + find_next = 0; + + /* Highlight */ + FIND_RESTORE_HL; + + if (match) { + erow *row = &E.row[current]; + last_match = current; + if (row->hl) { + saved_hl_line = current; + saved_hl = malloc(row->rsize); + memcpy(saved_hl,row->hl,row->rsize); + memset(row->hl+match_offset,HL_MATCH,qlen); + } + E.cy = 0; + E.cx = match_offset; + E.rowoff = current; + E.coloff = 0; + /* Scroll horizontally as needed. */ + if (E.cx > E.screencols) { + int diff = E.cx - E.screencols; + E.cx -= diff; + E.coloff += diff; + } + } + } + } +} + +/* ========================= Editor events handling ======================== */ + +/* Handle cursor position change because arrow keys were pressed. */ +void editorMoveCursor(int key) { + int filerow = E.rowoff+E.cy; + int filecol = E.coloff+E.cx; + int rowlen; + erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + + switch(key) { + case ARROW_LEFT: + if (E.cx == 0) { + if (E.coloff) { + E.coloff--; + } else { + if (filerow > 0) { + E.cy--; + E.cx = E.row[filerow-1].size; + if (E.cx > E.screencols-1) { + E.coloff = E.cx-E.screencols+1; + E.cx = E.screencols-1; + } + } + } + } else { + E.cx -= 1; + } + break; + case ARROW_RIGHT: + if (row && filecol < row->size) { + if (E.cx == E.screencols-1) { + E.coloff++; + } else { + E.cx += 1; + } + } else if (row && filecol == row->size) { + E.cx = 0; + E.coloff = 0; + if (E.cy == E.screenrows-1) { + E.rowoff++; + } else { + E.cy += 1; + } + } + break; + case ARROW_UP: + if (E.cy == 0) { + if (E.rowoff) E.rowoff--; + } else { + E.cy -= 1; + } + break; + case ARROW_DOWN: + if (filerow < E.numrows) { + if (E.cy == E.screenrows-1) { + E.rowoff++; + } else { + E.cy += 1; + } + } + break; + } + /* Fix cx if the current line has not enough chars. */ + filerow = E.rowoff+E.cy; + filecol = E.coloff+E.cx; + row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; + rowlen = row ? row->size : 0; + if (filecol > rowlen) { + E.cx -= filecol-rowlen; + if (E.cx < 0) { + E.coloff += E.cx; + E.cx = 0; + } + } +} + +/* Process events arriving from the standard input, which is, the user + * is typing stuff on the terminal. */ +#define KILO_QUIT_TIMES 3 +void editorProcessKeypress(int fd) { + /* When the file is modified, requires Ctrl-q to be pressed N times + * before actually quitting. */ + static int quit_times = KILO_QUIT_TIMES; + + int c = editorReadKey(fd); + switch(c) { + case ENTER: /* Enter */ + editorInsertNewline(); + break; + case CTRL_C: /* Ctrl-c */ + /* We ignore ctrl-c, it can't be so simple to lose the changes + * to the edited file. */ + break; + case CTRL_Q: /* Ctrl-q */ + /* Quit if the file was already saved. */ + if (E.dirty && quit_times) { + editorSetStatusMessage("WARNING!!! File has unsaved changes. " + "Press Ctrl-Q %d more times to quit.", quit_times); + quit_times--; + return; + } + exit(0); + break; + case CTRL_S: /* Ctrl-s */ + editorSave(); + break; + case CTRL_R: /* Ctrl-r */ + editorRun(); + break; + case CTRL_F: + editorFind(fd); + break; + case BACKSPACE: /* Backspace */ + case CTRL_H: /* Ctrl-h */ + case DEL_KEY: + editorDelChar(); + break; + case PAGE_UP: + case PAGE_DOWN: + if (c == PAGE_UP && E.cy != 0) + E.cy = 0; + else if (c == PAGE_DOWN && E.cy != E.screenrows-1) + E.cy = E.screenrows-1; + { + 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_L: /* ctrl+l, clear screen */ + /* Just refresht the line as side effect. */ + break; + case ESC: + /* Nothing to do for ESC in this mode. */ + break; + default: + editorInsertChar(c); + break; + } + + quit_times = KILO_QUIT_TIMES; /* Reset it to the original value. */ +} + +int editorFileWasModified(void) { + return E.dirty; +} + +void updateWindowSize(void) { + if (getWindowSize(STDIN_FILENO,STDOUT_FILENO, + &E.screenrows,&E.screencols) == -1) { + perror("Unable to query the screen for size (columns / rows)"); + exit(1); + } + E.screenrows -= 2; /* Get room for status bar. */ +} + +void handleSigWinCh(int unused __attribute__((unused))) { + updateWindowSize(); + if (E.cy > E.screenrows) E.cy = E.screenrows - 1; + if (E.cx > E.screencols) E.cx = E.screencols - 1; + editorRefreshScreen(); +} + +void initEditor(void) { + E.cx = 0; + E.cy = 0; + E.rowoff = 0; + E.coloff = 0; + E.numrows = 0; + E.row = NULL; + E.dirty = 0; + E.filename = ""; + E.syntax = &HLDB[0]; /* Hard coding to C. */ + E.compiler_cb = NULL; + E.check_cb = NULL; + E.compiler_cb_ctx = NULL; + updateWindowSize(); + signal(SIGWINCH, handleSigWinCh); +} + + +void editorSetCompilerCallback(int (*cb)(void *ctx, const char *code, int argc, char **argv)) { + E.compiler_cb = cb; +} + +void editorSetCheckCallback(int (*cb)(void *ctx, const char *code)) { + E.check_cb = cb; +} + +void editorSetCompilerContext(void *ctx) { + E.compiler_cb_ctx = ctx; +} + +#endif //REPL_SUPPORTED From b1e073ab2775d51eab075fbed9b416c0c93983ac Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 1 Oct 2024 07:28:30 +0200 Subject: [PATCH 2/4] Whitespaces (make linter happy) --- src/kilo.c | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/kilo.c b/src/kilo.c index 83b8702..2827256 100644 --- a/src/kilo.c +++ b/src/kilo.c @@ -563,14 +563,12 @@ void editorUpdateRow(erow *row) { free(row->render); for (j = 0; j < row->size; j++) if (row->chars[j] == TAB) tabs++; - unsigned long long allocsize = (unsigned long long) row->size + tabs*8 + nonprint*9 + 1; if (allocsize > UINT32_MAX) { printf("Some line of the edited file is too long for kilo\n"); exit(1); } - row->render = malloc(row->size + tabs*8 + nonprint*9 + 1); idx = 0; for (j = 0; j < row->size; j++) { @@ -583,11 +581,8 @@ void editorUpdateRow(erow *row) { } row->rsize = idx; row->render[idx] = '\0'; - /* Update the syntax highlighting attributes of the row. */ editorUpdateSyntax(row); - - } char *editorRowsToString(int *buflen); @@ -715,7 +710,6 @@ void editorInsertChar(int c) { if (!row) { while(E.numrows <= filerow) editorInsertRow(E.numrows,"",0); - } row = &E.row[filerow]; editorRowInsertChar(row,filecol,c); @@ -832,14 +826,14 @@ int editorOpen(char *filename) { return 0; } -int editorRun(void) +int editorRun(void) { int len; char *buf = editorRowsToString(&len); if (E.compiler_cb == NULL) { return 0; } - return E.compiler_cb(E.compiler_cb_ctx, buf, 0, NULL); + return E.compiler_cb(E.compiler_cb_ctx, buf, 0, NULL); } /* Save the current file on disk. Return 0 on success, 1 on error. */ From ef0af4c3ee039c168a90c451306540bc39b2cf60 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 1 Oct 2024 08:41:13 +0200 Subject: [PATCH 3/4] "Press any key" after CTRL+R --- src/cjit.c | 6 +++++- src/kilo.c | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/cjit.c b/src/cjit.c index a8a5c29..45c5f82 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -183,8 +183,12 @@ static int cjit_compile_buffer(void *tcs, char *code, int argc, char **argv) _err(err_msg); free(err_msg); } + enableGetCharMode(STDIN_FILENO); + _err("\n\n\n\nPress any key to continue....\n"); + getchar(); + disableGetCharMode(STDIN_FILENO); + enableRawMode(STDIN_FILENO); - sleep(3); editorRefreshScreen(); return res; } diff --git a/src/kilo.c b/src/kilo.c index 2827256..d4e8d5a 100644 --- a/src/kilo.c +++ b/src/kilo.c @@ -247,6 +247,38 @@ int enableRawMode(int fd) { return -1; } +/* For getchar() behavior */ +int enableGetCharMode(int fd) { + struct termios nocanon; + + if (!isatty(STDIN_FILENO)) + goto fatal; + disableRawMode(STDIN_FILENO); + if (tcgetattr(fd,&orig_termios) == -1) + goto fatal; + + nocanon = orig_termios; /* modify the original mode */ + /* input modes: no break, no CR to NL, no parity check, no strip char, + * no start/stop output control. */ + nocanon.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); + /* control chars - set return condition: min number of bytes and timer. */ + nocanon.c_cc[VMIN] = 1; /* Wait for one byte */ + nocanon.c_cc[VTIME] = 0; /* No timeout */ + + /* put terminal in raw mode after flushing */ + if (tcsetattr(fd,TCSAFLUSH,&nocanon) < 0) goto fatal; + return 0; + +fatal: + errno = ENOTTY; + return -1; +} + +/* Disable GetChar mode */ +void disableGetCharMode(int fd) { + tcsetattr(fd,TCSAFLUSH,&orig_termios); +} + /* Read a key from the terminal put in raw mode, trying to handle * escape sequences. */ int editorReadKey(int fd) { From d0351d35d4b62a74cf4459deccab2305298eee3c Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Tue, 1 Oct 2024 21:47:06 +0200 Subject: [PATCH 4/4] Improvements to kilo REPL --- src/cjit.c | 22 ++++++--- src/kilo.c | 143 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 129 insertions(+), 36 deletions(-) diff --git a/src/cjit.c b/src/cjit.c index 45c5f82..5254bdc 100644 --- a/src/cjit.c +++ b/src/cjit.c @@ -85,9 +85,9 @@ static int cjit_compile_and_run(TCCState *TCC, const char *code, int argc, char const char main_fn[]="main"; int err_fds[2]; int err_r, err_w; - const char compile_errmsg[]= "Code compilation error in source\n"; - const char reloc_errmsg[]= "Code relocation error in source\n"; - const char nomain_errmsg[]= "Symbol 'main' was not found in source\n"; + const char compile_errmsg[]= "Code compilation error in source"; + const char reloc_errmsg[]= "Code relocation error in source"; + const char nomain_errmsg[]= "Symbol 'main' was not found in source"; *err_msg = NULL; @@ -201,7 +201,7 @@ static void error_callback(void *ctx, const char *msg) } } -#define ERR_MAX 128 +#define ERR_MAX 80 static int cjit_check_buffer(void *tcs, char *code, char **err_msg) { TCCState *TCC = (TCCState *)tcs; @@ -212,6 +212,9 @@ static int cjit_check_buffer(void *tcs, char *code, char **err_msg) res = cjit_compile_and_run(TCC, code, 0, NULL, 0, err_msg); if (res != 0) { if(err_msg) { + if (strlen(err_msg) > ERR_MAX -1) { + err_msg[ERR_MAX - 1] = 0; + } char *p = strchr(err_msg, '\n'); if (p) *p = 0; editorSetStatusMessage(*err_msg); @@ -255,7 +258,15 @@ static int cjit_cli(TCCState *TCC) if (err_msg) _err(err_msg); } else { + int row = 0; initEditor(); + + editorInsertRow(row++, "#include ", 18); + editorInsertRow(row++, "#include ", 19); + editorInsertRow(row++, "", 0); + editorInsertRow(row++, "int main(int argc, char **argv) {", 33); + editorInsertRow(row++, "", 0); + editorInsertRow(row++, "}", 1); enableRawMode(STDIN_FILENO); editorSetStatusMessage( "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find | Ctrl-R Run"); @@ -854,9 +865,6 @@ int main(int argc, char **argv) { if(! write_to_file(tmpdir,"libtcc1.a",(char*)&libtcc1,libtcc1_len) ) goto endgame; - //// TCC DEFAULT PATHS - tcc_add_include_path(TCC,"/usr/include/x86_64-linux-musl"); - #if defined(LIBC_MUSL) if(! write_to_file(tmpdir,"libc.so",(char*)&musl_libc,musl_libc_len) ) diff --git a/src/kilo.c b/src/kilo.c index d4e8d5a..71faf57 100644 --- a/src/kilo.c +++ b/src/kilo.c @@ -62,12 +62,13 @@ #define HL_KEYWORD2 5 #define HL_STRING 6 #define HL_NUMBER 7 -#define HL_ERROR HL_NUMBER #define HL_MATCH 8 /* Search match. */ +#define HL_ERROR 9 #define HL_HIGHLIGHT_STRINGS (1<<0) #define HL_HIGHLIGHT_NUMBERS (1<<1) + struct editorSyntax { char **filematch; char **keywords; @@ -110,6 +111,7 @@ struct editorConfig { int (*compiler_cb)(void *, const char *, int, char **); int (*check_cb)(void *, const char *, char *); void *compiler_cb_ctx; + int keep_scratchpad; }; static struct editorConfig E; @@ -123,6 +125,7 @@ enum KEY_ACTION{ TAB = 9, /* Tab */ CTRL_L = 12, /* Ctrl+l */ ENTER = 13, /* Enter */ + CTRL_E = 5, /* Ctrl-e */ CTRL_Q = 17, /* Ctrl-q */ CTRL_R = 18, /* Ctrl-r */ CTRL_S = 19, /* Ctrl-s */ @@ -142,6 +145,19 @@ enum KEY_ACTION{ PAGE_DOWN }; +static void editorMakeFilename(void) { + const char scratchpad_fname[]="cjit-scratchpad.c"; + char dirname_tmpl[] = "/tmp/cjit-scratchpad.c.XXXXXX"; + char *dirname; + size_t filename_sz; + if (E.filename) + return; + dirname = mkdtemp(dirname_tmpl); + filename_sz = strlen(dirname) + 1 + strlen(scratchpad_fname) + 1; + E.filename = malloc(filename_sz); + snprintf(E.filename, filename_sz, "%s/%s", dirname, scratchpad_fname); +} + void editorSetStatusMessage(const char *fmt, ...); /* =========================== Syntax highlights DB ========================= @@ -211,6 +227,13 @@ void disableRawMode(int fd) { /* Called at exit to avoid remaining in raw mode. */ void editorAtExit(void) { disableRawMode(STDIN_FILENO); + if ((!E.keep_scratchpad) && (E.filename)) { + char *cp1, *dir_nm; + cp1 = strdup(E.filename); + dir_nm = dirname(cp1); + unlink(E.filename); + rmdir(dir_nm); + } } /* Raw mode: 1960 magic shit. */ @@ -219,7 +242,6 @@ int enableRawMode(int fd) { if (E.rawmode) return 0; /* Already enabled. */ if (!isatty(STDIN_FILENO)) goto fatal; - atexit(editorAtExit); if (tcgetattr(fd,&orig_termios) == -1) goto fatal; raw = orig_termios; /* modify the original mode */ @@ -377,7 +399,7 @@ int getWindowSize(int ifd, int ofd, int *rows, int *cols) { /* Restore position. */ char seq[32]; - snprintf(seq,32,"\x1b[%d;%dH",orig_row,orig_col); + snprintf(seq,sizeof(seq),"\x1b[%d;%dH",orig_row,orig_col); if (write(ofd,seq,strlen(seq)) == -1) { /* Can't recover... */ } @@ -472,7 +494,7 @@ void editorUpdateSyntax(erow *row) { continue; } - /* Handle "" and '' */ + if (in_string) { row->hl[i] = HL_STRING; if (*p == '\\') { @@ -484,6 +506,7 @@ void editorUpdateSyntax(erow *row) { if (*p == in_string) in_string = 0; p++; i++; continue; + /* Handle "" and '' */ } else { if (*p == '"' || *p == '\'') { in_string = *p; @@ -559,6 +582,7 @@ int editorSyntaxToColor(int hl) { case HL_STRING: return 35; /* magenta */ case HL_NUMBER: return 31; /* red */ case HL_MATCH: return 34; /* blu */ + case HL_ERROR: return 41; /* red bg */ default: return 37; /* white */ } } @@ -648,6 +672,7 @@ void editorFreeRow(erow *row) { free(row->hl); } + /* Remove the row at the specified position, shifting the remainign on the * top. */ void editorDelRow(int at) { @@ -657,11 +682,21 @@ void editorDelRow(int at) { row = E.row+at; editorFreeRow(row); memmove(E.row+at,E.row+at+1,sizeof(E.row[0])*(E.numrows-at-1)); - for (int j = at; j < E.numrows-1; j++) E.row[j].idx++; + for (int j = at; j < E.numrows-1; j++) E.row[j].idx--; E.numrows--; E.dirty++; } +/* Clear the entire buffer */ +void editorReset(void) { + int j; + for (j = E.numrows - 1; j > 0; j--) { + editorDelRow(E.row+j); + } + E.numrows = 0; + E.dirty = 0; +} + /* Turn the editor rows into a single heap-allocated string. * Returns the pointer to the heap-allocated string and populate the * integer pointed by 'buflen' with the size of the string, escluding @@ -708,7 +743,13 @@ void editorRowInsertChar(erow *row, int at, int c) { row->size++; } row->chars[at] = c; + if (c == '\\') { + row->size++; + editorUpdateRow(row); + row->size--; + } editorUpdateRow(row); + E.dirty++; } @@ -826,16 +867,14 @@ void editorDelChar() { /* Load the specified program in the editor memory and returns 0 on success * or 1 on error. */ -int editorOpen(char *filename) { +int editorOpen(void) { FILE *fp; - E.dirty = 0; - free(E.filename); - size_t fnlen = strlen(filename)+1; - E.filename = malloc(fnlen); - memcpy(E.filename,filename,fnlen); + if (!E.filename) + return 0; - fp = fopen(filename,"r"); + E.dirty = 0; + fp = fopen(E.filename,"r"); if (!fp) { if (errno != ENOENT) { perror("Opening file"); @@ -870,20 +909,20 @@ int editorRun(void) /* Save the current file on disk. Return 0 on success, 1 on error. */ int editorSave(void) { - int len; - char *buf = editorRowsToString(&len); - int fd = open(E.filename,O_RDWR|O_CREAT,0644); + int len, fd; + char *buf; + + editorMakeFilename(); + buf = editorRowsToString(&len); + fd = open(E.filename,O_RDWR|O_CREAT|O_TRUNC,0644); if (fd == -1) goto writeerr; - /* Use truncate + a single write(2) call in order to make saving - * a bit safer, under the limits of what we can do in a small editor. */ - if (ftruncate(fd,len) == -1) goto writeerr; if (write(fd,buf,len) != len) goto writeerr; close(fd); free(buf); E.dirty = 0; - editorSetStatusMessage("%d bytes written on disk", len); + editorSetStatusMessage("%d bytes written to %s", len, E.filename); return 0; writeerr: @@ -954,11 +993,15 @@ void editorRefreshScreen(void) { int len = r->rsize - E.coloff; int current_color = -1; + abAppend(&ab, "\x1b[40m", 5); if (len > 0) { if (len > E.screencols) len = E.screencols; char *c = r->render+E.coloff; unsigned char *hl = r->hl+E.coloff; int j; + if (hl[0] == HL_ERROR) { + abAppend(&ab, "\x1b[41m", 5); + } for (j = 0; j < len; j++) { if (hl[j] == HL_NONPRINT) { char sym; @@ -988,6 +1031,7 @@ void editorRefreshScreen(void) { } } abAppend(&ab,"\x1b[39m",5); + abAppend(&ab, "\x1b[40m", 5); abAppend(&ab,"\x1b[0K",4); abAppend(&ab,"\r\n",2); } @@ -1043,8 +1087,23 @@ void editorRefreshScreen(void) { char *err_msg = NULL; unsigned buflen; char *buf = editorRowsToString(&buflen); + int ret; buf[buflen] = (char)0; - if ((E.check_cb(E.compiler_cb_ctx, buf, &err_msg) != 0) && (err_msg != NULL)) { + if (E.dirty) { + int i,j; + /* Set highlights from HL_ERROR to HL_NORMAL */ + for (i = 0; i < E.numrows; i++) { + erow *row = &E.row[i]; + for (j = 0; j < row->rsize; j++) { + if (row->hl[j] == HL_ERROR) + row->hl[j] = HL_NORMAL; + } + } + E.dirty = 0; + } + editorUpdateSyntax(row); + ret = E.check_cb(E.compiler_cb_ctx, buf, &err_msg); + if ((ret != 0) && (err_msg != NULL)) { int err_line = 0; /* extact the error line */ char *line_ns = strchr(err_msg, ':'); @@ -1056,6 +1115,7 @@ void editorRefreshScreen(void) { /* Mark the error line as red */ E.row[at].hl = realloc(E.row[at].hl, E.row[at].rsize); memset(E.row[at].hl, HL_ERROR, E.row[at].rsize); + editorUpdateSyntax(row); } } free(buf); @@ -1232,6 +1292,18 @@ void editorMoveCursor(int key) { } } break; + case HOME_KEY: + E.cx = 0; + break; + case END_KEY: + if (row && filecol < row->size) { + E.cx = row->size; + if (E.cx > E.screencols-1) { + E.coloff = E.cx-E.screencols+1; + E.cx = E.screencols-1; + } + } + break; } /* Fix cx if the current line has not enough chars. */ filerow = E.rowoff+E.cy; @@ -1254,7 +1326,6 @@ void editorProcessKeypress(int fd) { /* When the file is modified, requires Ctrl-q to be pressed N times * before actually quitting. */ static int quit_times = KILO_QUIT_TIMES; - int c = editorReadKey(fd); switch(c) { case ENTER: /* Enter */ @@ -1265,19 +1336,29 @@ void editorProcessKeypress(int fd) { * to the edited file. */ break; case CTRL_Q: /* Ctrl-q */ - /* Quit if the file was already saved. */ - if (E.dirty && quit_times) { - editorSetStatusMessage("WARNING!!! File has unsaved changes. " - "Press Ctrl-Q %d more times to quit.", quit_times); - quit_times--; - return; - } exit(0); break; case CTRL_S: /* Ctrl-s */ + E.dirty++; + E.keep_scratchpad = 1; + editorSave(); + break; + case CTRL_E: /* Ctrl-e */ + char ed_cmd[200]="/usr/bin/editor "; + E.dirty++; editorSave(); + strcat(ed_cmd, E.filename); + disableRawMode(STDIN_FILENO); + editorReset(); + printf("Running %s\n", ed_cmd); + system(ed_cmd); + enableRawMode(STDIN_FILENO); + editorOpen(); break; case CTRL_R: /* Ctrl-r */ + E.cx = 0; + E.cy = 0; + editorRefreshScreen(); editorRun(); break; case CTRL_F: @@ -1306,6 +1387,8 @@ void editorProcessKeypress(int fd) { case ARROW_DOWN: case ARROW_LEFT: case ARROW_RIGHT: + case HOME_KEY: + case END_KEY: editorMoveCursor(c); break; case CTRL_L: /* ctrl+l, clear screen */ @@ -1350,12 +1433,14 @@ void initEditor(void) { E.numrows = 0; E.row = NULL; E.dirty = 0; - E.filename = ""; + E.filename = NULL; E.syntax = &HLDB[0]; /* Hard coding to C. */ E.compiler_cb = NULL; E.check_cb = NULL; E.compiler_cb_ctx = NULL; + E.keep_scratchpad = 0; updateWindowSize(); + atexit(editorAtExit); signal(SIGWINCH, handleSigWinCh); }