190075Sobrien/* Kilo -- A very simple editor in less than 1-kilo lines of code (as counted 290075Sobrien * by "cloc"). Does not depend on libcurses, directly emits VT100 390075Sobrien * escapes on the terminal. 490075Sobrien * 590075Sobrien * ----------------------------------------------------------------------- 690075Sobrien * 790075Sobrien * Copyright (C) 2016 Salvatore Sanfilippo <antirez at gmail dot com> 890075Sobrien * 9119256Skan * All rights reserved. 10119256Skan * 1190075Sobrien * Redistribution and use in source and binary forms, with or without 1290075Sobrien * modification, are permitted provided that the following conditions are 13119256Skan * met: 14119256Skan * 1590075Sobrien * * Redistributions of source code must retain the above copyright 1690075Sobrien * notice, this list of conditions and the following disclaimer. 17117395Skan * 1890075Sobrien * * Redistributions in binary form must reproduce the above copyright 1990075Sobrien * notice, this list of conditions and the following disclaimer in the 20119256Skan * documentation and/or other materials provided with the distribution. 2190075Sobrien * 2290075Sobrien * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 2390075Sobrien * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 2490075Sobrien * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25119256Skan * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 2690075Sobrien * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 2790075Sobrien * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 2890075Sobrien * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29119256Skan * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 3090075Sobrien * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 3190075Sobrien * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 3290075Sobrien * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 */ 34 35#define KILO_VERSION "0.0.1" 36 37#ifndef _BSD_SOURCE 38#define _BSD_SOURCE 39#endif 40#define _GNU_SOURCE 41 42#include <termios.h> 43#include <stdlib.h> 44#include <stdio.h> 45#include <errno.h> 46#include <string.h> 47#include <stdlib.h> 48#include <ctype.h> 49#include <sys/types.h> 50#include <sys/ioctl.h> 51#include <sys/time.h> 52#include <unistd.h> 53#include <stdarg.h> 54#include <fcntl.h> 55 56#ifdef __Fuchsia__ 57#include <zircon/device/pty.h> 58 59static time_t time(void* arg) { 60 return 0; 61} 62 63static int getConsoleSize(int *rows, int *cols) { 64 pty_window_size_t wsz; 65 ssize_t r = ioctl_pty_get_window_size(0, &wsz); 66 if (r != sizeof(wsz)) { 67 return -1; 68 } 69 *rows = wsz.height; 70 *cols = wsz.width; 71 return 0; 72} 73 74#endif 75 76/* Syntax highlight types */ 77#define HL_NORMAL 0 78#define HL_NONPRINT 1 79#define HL_COMMENT 2 /* Single line comment. */ 80#define HL_MLCOMMENT 3 /* Multi-line comment. */ 81#define HL_KEYWORD1 4 82#define HL_KEYWORD2 5 83#define HL_STRING 6 84#define HL_NUMBER 7 85#define HL_MATCH 8 /* Search match. */ 86 87#define HL_HIGHLIGHT_STRINGS (1<<0) 88#define HL_HIGHLIGHT_NUMBERS (1<<1) 89 90struct editorSyntax { 91 char **filematch; 92 char **keywords; 93 char singleline_comment_start[2]; 94 char multiline_comment_start[3]; 95 char multiline_comment_end[3]; 96 int flags; 97}; 98 99/* This structure represents a single line of the file we are editing. */ 100typedef struct erow { 101 int idx; /* Row index in the file, zero-based. */ 102 int size; /* Size of the row, excluding the null term. */ 103 int rsize; /* Size of the rendered row. */ 104 char *chars; /* Row content. */ 105 char *render; /* Row content "rendered" for screen (for TABs). */ 106 unsigned char *hl; /* Syntax highlight type for each character in render.*/ 107 int hl_oc; /* Row had open comment at end in last syntax highlight 108 check. */ 109} erow; 110 111typedef struct hlcolor { 112 int r,g,b; 113} hlcolor; 114 115struct editorConfig { 116 int cx,cy; /* Cursor x and y position in characters */ 117 int rowoff; /* Offset of row displayed. */ 118 int coloff; /* Offset of column displayed. */ 119 int screenrows; /* Number of rows that we can show */ 120 int screencols; /* Number of cols that we can show */ 121 int numrows; /* Number of rows */ 122 int rawmode; /* Is terminal raw mode enabled? */ 123 erow *row; /* Rows */ 124 int dirty; /* File modified but not saved. */ 125 char *filename; /* Currently open filename */ 126 char statusmsg[80]; 127 time_t statusmsg_time; 128 struct editorSyntax *syntax; /* Current syntax highlight, or NULL. */ 129}; 130 131static struct editorConfig E; 132 133enum KEY_ACTION{ 134 KEY_NULL = 0, /* NULL */ 135 CTRL_C = 3, /* Ctrl-c */ 136 CTRL_D = 4, /* Ctrl-d */ 137 CTRL_F = 6, /* Ctrl-f */ 138 CTRL_H = 8, /* Ctrl-h */ 139 TAB = 9, /* Tab */ 140 CTRL_L = 12, /* Ctrl+l */ 141 NEWLINE = 10, /* Newline */ 142 ENTER = 13, /* Enter */ 143 CTRL_Q = 17, /* Ctrl-q */ 144 CTRL_S = 19, /* Ctrl-s */ 145 CTRL_U = 21, /* Ctrl-u */ 146 ESC = 27, /* Escape */ 147 BACKSPACE = 127, /* Backspace */ 148 /* The following are just soft codes, not really reported by the 149 * terminal directly. */ 150 ARROW_LEFT = 1000, 151 ARROW_RIGHT, 152 ARROW_UP, 153 ARROW_DOWN, 154 DEL_KEY, 155 HOME_KEY, 156 END_KEY, 157 PAGE_UP, 158 PAGE_DOWN 159}; 160 161void editorSetStatusMessage(const char *fmt, ...); 162 163/* =========================== Syntax highlights DB ========================= 164 * 165 * In order to add a new syntax, define two arrays with a list of file name 166 * matches and keywords. The file name matches are used in order to match 167 * a given syntax with a given file name: if a match pattern starts with a 168 * dot, it is matched as the last past of the filename, for example ".c". 169 * Otherwise the pattern is just searched inside the filenme, like "Makefile"). 170 * 171 * The list of keywords to highlight is just a list of words, however if they 172 * a trailing '|' character is added at the end, they are highlighted in 173 * a different color, so that you can have two different sets of keywords. 174 * 175 * Finally add a stanza in the HLDB global variable with two two arrays 176 * of strings, and a set of flags in order to enable highlighting of 177 * comments and numbers. 178 * 179 * The characters for single and multi line comments must be exactly two 180 * and must be provided as well (see the C language example). 181 * 182 * There is no support to highlight patterns currently. */ 183 184/* C / C++ */ 185char *C_HL_extensions[] = {".c",".cpp",NULL}; 186char *C_HL_keywords[] = { 187 /* A few C / C++ keywords */ 188 "switch","if","while","for","break","continue","return","else", 189 "struct","union","typedef","static","enum","class", 190 /* C types */ 191 "int|","long|","double|","float|","char|","unsigned|","signed|", 192 "void|",NULL 193}; 194 195/* Here we define an array of syntax highlights by extensions, keywords, 196 * comments delimiters and flags. */ 197struct editorSyntax HLDB[] = { 198 { 199 /* C / C++ */ 200 C_HL_extensions, 201 C_HL_keywords, 202 "//","/*","*/", 203 HL_HIGHLIGHT_STRINGS | HL_HIGHLIGHT_NUMBERS 204 } 205}; 206 207#define HLDB_ENTRIES (sizeof(HLDB)/sizeof(HLDB[0])) 208 209/* ======================= Low level terminal handling ====================== */ 210 211static struct termios orig_termios; /* In order to restore at exit.*/ 212 213void disableRawMode(int fd) { 214 /* Don't even check the return value as it's too late. */ 215 if (E.rawmode) { 216 tcsetattr(fd,TCSAFLUSH,&orig_termios); 217 E.rawmode = 0; 218 } 219} 220 221/* Called at exit to avoid remaining in raw mode. */ 222void editorAtExit(void) { 223 disableRawMode(STDIN_FILENO); 224} 225 226/* Raw mode: 1960 magic shit. */ 227int enableRawMode(int fd) { 228#ifdef __Fuchsia__ 229 return 0; 230#else 231 struct termios raw; 232 233 if (E.rawmode) return 0; /* Already enabled. */ 234 if (!isatty(STDIN_FILENO)) goto fatal; 235 atexit(editorAtExit); 236 if (tcgetattr(fd,&orig_termios) == -1) goto fatal; 237 238 raw = orig_termios; /* modify the original mode */ 239 /* input modes: no break, no CR to NL, no parity check, no strip char, 240 * no start/stop output control. */ 241 raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON); 242 /* output modes - disable post processing */ 243 raw.c_oflag &= ~(OPOST); 244 /* control modes - set 8 bit chars */ 245 raw.c_cflag |= (CS8); 246 /* local modes - choing off, canonical off, no extended functions, 247 * no signal chars (^Z,^C) */ 248 raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG); 249 /* control chars - set return condition: min number of bytes and timer. */ 250 raw.c_cc[VMIN] = 0; /* Return each byte, or zero for timeout. */ 251 raw.c_cc[VTIME] = 1; /* 100 ms timeout (unit is tens of second). */ 252 253 /* put terminal in raw mode after flushing */ 254 if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal; 255 E.rawmode = 1; 256 return 0; 257 258fatal: 259 errno = ENOTTY; 260 return -1; 261#endif 262} 263 264/* Read a key from the terminal put in raw mode, trying to handle 265 * escape sequences. */ 266int editorReadKey(int fd) { 267 int nread; 268 char c, seq[3]; 269 while ((nread = read(fd,&c,1)) == 0); 270 if (nread == -1) exit(1); 271 272 while(1) { 273 switch(c) { 274 case NEWLINE: 275 return ENTER; 276 case ESC: /* escape sequence */ 277 /* If this is just an ESC, we'll timeout here. */ 278 if (read(fd,seq,1) == 0) return ESC; 279 if (read(fd,seq+1,1) == 0) return ESC; 280 281 /* ESC [ sequences. */ 282 if (seq[0] == '[') { 283 if (seq[1] >= '0' && seq[1] <= '9') { 284 /* Extended escape, read additional byte. */ 285 if (read(fd,seq+2,1) == 0) return ESC; 286 if (seq[2] == '~') { 287 switch(seq[1]) { 288 case '3': return DEL_KEY; 289 case '5': return PAGE_UP; 290 case '6': return PAGE_DOWN; 291 } 292 } 293 } else { 294 switch(seq[1]) { 295 case 'A': return ARROW_UP; 296 case 'B': return ARROW_DOWN; 297 case 'C': return ARROW_RIGHT; 298 case 'D': return ARROW_LEFT; 299 case 'H': return HOME_KEY; 300 case 'F': return END_KEY; 301 } 302 } 303 } 304 305 /* ESC O sequences. */ 306 else if (seq[0] == 'O') { 307 switch(seq[1]) { 308 case 'H': return HOME_KEY; 309 case 'F': return END_KEY; 310 } 311 } 312 break; 313 default: 314 return c; 315 } 316 } 317} 318 319/* Use the ESC [6n escape sequence to query the horizontal cursor position 320 * and return it. On error -1 is returned, on success the position of the 321 * cursor is stored at *rows and *cols and 0 is returned. */ 322int getCursorPosition(int ifd, int ofd, int *rows, int *cols) { 323 char buf[32]; 324 unsigned int i = 0; 325 326 /* Report cursor location */ 327 if (write(ofd, "\x1b[6n", 4) != 4) return -1; 328 329 /* Read the response: ESC [ rows ; cols R */ 330 while (i < sizeof(buf)-1) { 331 if (read(ifd,buf+i,1) != 1) break; 332 if (buf[i] == 'R') break; 333 i++; 334 } 335 buf[i] = '\0'; 336 337 /* Parse it. */ 338 if (buf[0] != ESC || buf[1] != '[') return -1; 339 if (sscanf(buf+2,"%d;%d",rows,cols) != 2) return -1; 340 return 0; 341} 342 343/* Try to get the number of columns in the current terminal. If the ioctl() 344 * call fails the function will try to query the terminal itself. 345 * Returns 0 on success, -1 on error. */ 346int getWindowSize(int ifd, int ofd, int *rows, int *cols) { 347#ifdef __Fuchsia__ 348 if (getConsoleSize(rows, cols) == 0) { 349 return 0; 350 } else { 351#else 352 struct winsize ws; 353 if (ioctl(1, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) { 354#endif 355 /* ioctl() failed. Try to query the terminal itself. */ 356 int orig_row, orig_col, retval; 357 358 /* Get the initial position so we can restore it later. */ 359 retval = getCursorPosition(ifd,ofd,&orig_row,&orig_col); 360 if (retval == -1) goto failed; 361 362 /* Go to right/bottom margin and get position. */ 363 if (write(ofd,"\x1b[999C\x1b[999B",12) != 12) goto failed; 364 retval = getCursorPosition(ifd,ofd,rows,cols); 365 if (retval == -1) goto failed; 366 367 /* Restore position. */ 368 char seq[32]; 369 snprintf(seq,32,"\x1b[%d;%dH",orig_row,orig_col); 370 if (write(ofd,seq,strlen(seq)) == -1) { 371 /* Can't recover... */ 372 } 373 return 0; 374#ifndef __Fuchsia__ 375 } else { 376 *cols = ws.ws_col; 377 *rows = ws.ws_row; 378 return 0; 379#endif 380 } 381 382failed: 383 return -1; 384} 385 386/* ====================== Syntax highlight color scheme ==================== */ 387 388int is_separator(int c) { 389 return c == '\0' || isspace(c) || strchr(",.()+-/*=~%[];",c) != NULL; 390} 391 392/* Return true if the specified row last char is part of a multi line comment 393 * that starts at this row or at one before, and does not end at the end 394 * of the row but spawns to the next row. */ 395int editorRowHasOpenComment(erow *row) { 396 if (row->hl && row->rsize && row->hl[row->rsize-1] == HL_MLCOMMENT && 397 (row->rsize < 2 || (row->render[row->rsize-2] != '*' || 398 row->render[row->rsize-1] != '/'))) return 1; 399 return 0; 400} 401 402/* Set every byte of row->hl (that corresponds to every character in the line) 403 * to the right syntax highlight type (HL_* defines). */ 404void editorUpdateSyntax(erow *row) { 405 row->hl = realloc(row->hl,row->rsize); 406 memset(row->hl,HL_NORMAL,row->rsize); 407 408 if (E.syntax == NULL) return; /* No syntax, everything is HL_NORMAL. */ 409 410 int i, prev_sep, in_string, in_comment; 411 char *p; 412 char **keywords = E.syntax->keywords; 413 char *scs = E.syntax->singleline_comment_start; 414 char *mcs = E.syntax->multiline_comment_start; 415 char *mce = E.syntax->multiline_comment_end; 416 417 /* Point to the first non-space char. */ 418 p = row->render; 419 i = 0; /* Current char offset */ 420 while(*p && isspace(*p)) { 421 p++; 422 i++; 423 } 424 prev_sep = 1; /* Tell the parser if 'i' points to start of word. */ 425 in_string = 0; /* Are we inside "" or '' ? */ 426 in_comment = 0; /* Are we inside multi-line comment? */ 427 428 /* If the previous line has an open comment, this line starts 429 * with an open comment state. */ 430 if (row->idx > 0 && editorRowHasOpenComment(&E.row[row->idx-1])) 431 in_comment = 1; 432 433 while(*p) { 434 /* Handle // comments. */ 435 if (prev_sep && *p == scs[0] && *(p+1) == scs[1]) { 436 /* From here to end is a comment */ 437 memset(row->hl+i,HL_COMMENT,row->size-i); 438 return; 439 } 440 441 /* Handle multi line comments. */ 442 if (in_comment) { 443 row->hl[i] = HL_MLCOMMENT; 444 if (*p == mce[0] && *(p+1) == mce[1]) { 445 row->hl[i+1] = HL_MLCOMMENT; 446 p += 2; i += 2; 447 in_comment = 0; 448 prev_sep = 1; 449 continue; 450 } else { 451 prev_sep = 0; 452 p++; i++; 453 continue; 454 } 455 } else if (*p == mcs[0] && *(p+1) == mcs[1]) { 456 row->hl[i] = HL_MLCOMMENT; 457 row->hl[i+1] = HL_MLCOMMENT; 458 p += 2; i += 2; 459 in_comment = 1; 460 prev_sep = 0; 461 continue; 462 } 463 464 /* Handle "" and '' */ 465 if (in_string) { 466 row->hl[i] = HL_STRING; 467 if (*p == '\\') { 468 row->hl[i+1] = HL_STRING; 469 p += 2; i += 2; 470 prev_sep = 0; 471 continue; 472 } 473 if (*p == in_string) in_string = 0; 474 p++; i++; 475 continue; 476 } else { 477 if (*p == '"' || *p == '\'') { 478 in_string = *p; 479 row->hl[i] = HL_STRING; 480 p++; i++; 481 prev_sep = 0; 482 continue; 483 } 484 } 485 486 /* Handle non printable chars. */ 487 if (!isprint(*p)) { 488 row->hl[i] = HL_NONPRINT; 489 p++; i++; 490 prev_sep = 0; 491 continue; 492 } 493 494 /* Handle numbers */ 495 if ((isdigit(*p) && (prev_sep || row->hl[i-1] == HL_NUMBER)) || 496 (*p == '.' && i >0 && row->hl[i-1] == HL_NUMBER)) { 497 row->hl[i] = HL_NUMBER; 498 p++; i++; 499 prev_sep = 0; 500 continue; 501 } 502 503 /* Handle keywords and lib calls */ 504 if (prev_sep) { 505 int j; 506 for (j = 0; keywords[j]; j++) { 507 int klen = strlen(keywords[j]); 508 int kw2 = keywords[j][klen-1] == '|'; 509 if (kw2) klen--; 510 511 if (!memcmp(p,keywords[j],klen) && 512 is_separator(*(p+klen))) 513 { 514 /* Keyword */ 515 memset(row->hl+i,kw2 ? HL_KEYWORD2 : HL_KEYWORD1,klen); 516 p += klen; 517 i += klen; 518 break; 519 } 520 } 521 if (keywords[j] != NULL) { 522 prev_sep = 0; 523 continue; /* We had a keyword match */ 524 } 525 } 526 527 /* Not special chars */ 528 prev_sep = is_separator(*p); 529 p++; i++; 530 } 531 532 /* Propagate syntax change to the next row if the open commen 533 * state changed. This may recursively affect all the following rows 534 * in the file. */ 535 int oc = editorRowHasOpenComment(row); 536 if (row->hl_oc != oc && row->idx+1 < E.numrows) 537 editorUpdateSyntax(&E.row[row->idx+1]); 538 row->hl_oc = oc; 539} 540 541/* Maps syntax highlight token types to terminal colors. */ 542int editorSyntaxToColor(int hl) { 543 switch(hl) { 544 case HL_COMMENT: 545 case HL_MLCOMMENT: return 36; /* cyan */ 546 case HL_KEYWORD1: return 33; /* yellow */ 547 case HL_KEYWORD2: return 32; /* green */ 548 case HL_STRING: return 35; /* zircon */ 549 case HL_NUMBER: return 31; /* red */ 550 case HL_MATCH: return 34; /* blu */ 551 default: return 37; /* white */ 552 } 553} 554 555/* Select the syntax highlight scheme depending on the filename, 556 * setting it in the global state E.syntax. */ 557void editorSelectSyntaxHighlight(char *filename) { 558 for (unsigned int j = 0; j < HLDB_ENTRIES; j++) { 559 struct editorSyntax *s = HLDB+j; 560 unsigned int i = 0; 561 while(s->filematch[i]) { 562 char *p; 563 int patlen = strlen(s->filematch[i]); 564 if ((p = strstr(filename,s->filematch[i])) != NULL) { 565 if (s->filematch[i][0] != '.' || p[patlen] == '\0') { 566 E.syntax = s; 567 return; 568 } 569 } 570 i++; 571 } 572 } 573} 574 575/* ======================= Editor rows implementation ======================= */ 576 577/* Update the rendered version and the syntax highlight of a row. */ 578void editorUpdateRow(erow *row) { 579 int tabs = 0, nonprint = 0, j, idx; 580 581 /* Create a version of the row we can directly print on the screen, 582 * respecting tabs, substituting non printable characters with '?'. */ 583 free(row->render); 584 for (j = 0; j < row->size; j++) 585 if (row->chars[j] == TAB) tabs++; 586 587 row->render = malloc(row->size + tabs*8 + nonprint*9 + 1); 588 idx = 0; 589 for (j = 0; j < row->size; j++) { 590 if (row->chars[j] == TAB) { 591 row->render[idx++] = ' '; 592 while((idx+1) % 8 != 0) row->render[idx++] = ' '; 593 } else { 594 row->render[idx++] = row->chars[j]; 595 } 596 } 597 row->rsize = idx; 598 row->render[idx] = '\0'; 599 600 /* Update the syntax highlighting attributes of the row. */ 601 editorUpdateSyntax(row); 602} 603 604/* Insert a row at the specified position, shifting the other rows on the bottom 605 * if required. */ 606void editorInsertRow(int at, char *s, size_t len) { 607 if (at > E.numrows) return; 608 E.row = realloc(E.row,sizeof(erow)*(E.numrows+1)); 609 if (at != E.numrows) { 610 memmove(E.row+at+1,E.row+at,sizeof(E.row[0])*(E.numrows-at)); 611 for (int j = at+1; j <= E.numrows; j++) E.row[j].idx++; 612 } 613 E.row[at].size = len; 614 E.row[at].chars = malloc(len+1); 615 memcpy(E.row[at].chars,s,len+1); 616 E.row[at].hl = NULL; 617 E.row[at].hl_oc = 0; 618 E.row[at].render = NULL; 619 E.row[at].rsize = 0; 620 E.row[at].idx = at; 621 editorUpdateRow(E.row+at); 622 E.numrows++; 623 E.dirty++; 624} 625 626/* Free row's heap allocated stuff. */ 627void editorFreeRow(erow *row) { 628 free(row->render); 629 free(row->chars); 630 free(row->hl); 631} 632 633/* Remove the row at the specified position, shifting the remainign on the 634 * top. */ 635void editorDelRow(int at) { 636 erow *row; 637 638 if (at >= E.numrows) return; 639 row = E.row+at; 640 editorFreeRow(row); 641 memmove(E.row+at,E.row+at+1,sizeof(E.row[0])*(E.numrows-at-1)); 642 for (int j = at; j < E.numrows-1; j++) E.row[j].idx++; 643 E.numrows--; 644 E.dirty++; 645} 646 647/* Turn the editor rows into a single heap-allocated string. 648 * Returns the pointer to the heap-allocated string and populate the 649 * integer pointed by 'buflen' with the size of the string, escluding 650 * the final nulterm. */ 651char *editorRowsToString(int *buflen) { 652 char *buf = NULL, *p; 653 int totlen = 0; 654 int j; 655 656 /* Compute count of bytes */ 657 for (j = 0; j < E.numrows; j++) 658 totlen += E.row[j].size+1; /* +1 is for "\n" at end of every row */ 659 *buflen = totlen; 660 totlen++; /* Also make space for nulterm */ 661 662 p = buf = malloc(totlen); 663 for (j = 0; j < E.numrows; j++) { 664 memcpy(p,E.row[j].chars,E.row[j].size); 665 p += E.row[j].size; 666 *p = '\n'; 667 p++; 668 } 669 *p = '\0'; 670 return buf; 671} 672 673/* Insert a character at the specified position in a row, moving the remaining 674 * chars on the right if needed. */ 675void editorRowInsertChar(erow *row, int at, int c) { 676 if (at > row->size) { 677 /* Pad the string with spaces if the insert location is outside the 678 * current length by more than a single character. */ 679 int padlen = at-row->size; 680 /* In the next line +2 means: new char and null term. */ 681 row->chars = realloc(row->chars,row->size+padlen+2); 682 memset(row->chars+row->size,' ',padlen); 683 row->chars[row->size+padlen+1] = '\0'; 684 row->size += padlen+1; 685 } else { 686 /* If we are in the middle of the string just make space for 1 new 687 * char plus the (already existing) null term. */ 688 row->chars = realloc(row->chars,row->size+2); 689 memmove(row->chars+at+1,row->chars+at,row->size-at+1); 690 row->size++; 691 } 692 row->chars[at] = c; 693 editorUpdateRow(row); 694 E.dirty++; 695} 696 697/* Append the string 's' at the end of a row */ 698void editorRowAppendString(erow *row, char *s, size_t len) { 699 row->chars = realloc(row->chars,row->size+len+1); 700 memcpy(row->chars+row->size,s,len); 701 row->size += len; 702 row->chars[row->size] = '\0'; 703 editorUpdateRow(row); 704 E.dirty++; 705} 706 707/* Delete the character at offset 'at' from the specified row. */ 708void editorRowDelChar(erow *row, int at) { 709 if (row->size <= at) return; 710 memmove(row->chars+at,row->chars+at+1,row->size-at); 711 editorUpdateRow(row); 712 row->size--; 713 E.dirty++; 714} 715 716/* Insert the specified char at the current prompt position. */ 717void editorInsertChar(int c) { 718 int filerow = E.rowoff+E.cy; 719 int filecol = E.coloff+E.cx; 720 erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 721 722 /* If the row where the cursor is currently located does not exist in our 723 * logical representaion of the file, add enough empty rows as needed. */ 724 if (!row) { 725 while(E.numrows <= filerow) 726 editorInsertRow(E.numrows,"",0); 727 } 728 row = &E.row[filerow]; 729 editorRowInsertChar(row,filecol,c); 730 if (E.cx == E.screencols-1) 731 E.coloff++; 732 else 733 E.cx++; 734 E.dirty++; 735} 736 737/* Inserting a newline is slightly complex as we have to handle inserting a 738 * newline in the middle of a line, splitting the line as needed. */ 739void editorInsertNewline(void) { 740 int filerow = E.rowoff+E.cy; 741 int filecol = E.coloff+E.cx; 742 erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 743 744 if (!row) { 745 if (filerow == E.numrows) { 746 editorInsertRow(filerow,"",0); 747 goto fixcursor; 748 } 749 return; 750 } 751 /* If the cursor is over the current line size, we want to conceptually 752 * think it's just over the last character. */ 753 if (filecol >= row->size) filecol = row->size; 754 if (filecol == 0) { 755 editorInsertRow(filerow,"",0); 756 } else { 757 /* We are in the middle of a line. Split it between two rows. */ 758 editorInsertRow(filerow+1,row->chars+filecol,row->size-filecol); 759 row = &E.row[filerow]; 760 row->chars[filecol] = '\0'; 761 row->size = filecol; 762 editorUpdateRow(row); 763 } 764fixcursor: 765 if (E.cy == E.screenrows-1) { 766 E.rowoff++; 767 } else { 768 E.cy++; 769 } 770 E.cx = 0; 771 E.coloff = 0; 772} 773 774/* Delete the char at the current prompt position. */ 775void editorDelChar(void) { 776 int filerow = E.rowoff+E.cy; 777 int filecol = E.coloff+E.cx; 778 erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 779 780 if (!row || (filecol == 0 && filerow == 0)) return; 781 if (filecol == 0) { 782 /* Handle the case of column 0, we need to move the current line 783 * on the right of the previous one. */ 784 filecol = E.row[filerow-1].size; 785 editorRowAppendString(&E.row[filerow-1],row->chars,row->size); 786 editorDelRow(filerow); 787 row = NULL; 788 if (E.cy == 0) 789 E.rowoff--; 790 else 791 E.cy--; 792 E.cx = filecol; 793 if (E.cx >= E.screencols) { 794 int shift = (E.screencols-E.cx)+1; 795 E.cx -= shift; 796 E.coloff += shift; 797 } 798 } else { 799 editorRowDelChar(row,filecol-1); 800 if (E.cx == 0 && E.coloff) 801 E.coloff--; 802 else 803 E.cx--; 804 } 805 if (row) editorUpdateRow(row); 806 E.dirty++; 807} 808 809/* Load the specified program in the editor memory and returns 0 on success 810 * or 1 on error. */ 811int editorOpen(char *filename) { 812 FILE *fp; 813 814 E.dirty = 0; 815 free(E.filename); 816 E.filename = strdup(filename); 817 818 fp = fopen(filename,"r"); 819 if (!fp) { 820 if (errno != ENOENT) { 821 perror("Opening file"); 822 exit(1); 823 } 824 return 1; 825 } 826 827 char *line = NULL; 828 size_t linecap = 0; 829 ssize_t linelen; 830 while((linelen = getline(&line,&linecap,fp)) != -1) { 831 if (linelen && (line[linelen-1] == '\n' || line[linelen-1] == '\r')) 832 line[--linelen] = '\0'; 833 editorInsertRow(E.numrows,line,linelen); 834 } 835 free(line); 836 fclose(fp); 837 E.dirty = 0; 838 return 0; 839} 840 841#define UNSAFE_SAVES 1 842 843/* Save the current file on disk. Return 0 on success, 1 on error. */ 844int editorSave(void) { 845 int len; 846 char *buf = editorRowsToString(&len); 847#if UNSAFE_SAVES 848 unlink(E.filename); 849#endif 850 int fd = open(E.filename,O_RDWR|O_CREAT,0644); 851 if (fd == -1) goto writeerr; 852 853#if !UNSAFE_SAVES 854 /* Use truncate + a single write(2) call in order to make saving 855 * a bit safer, under the limits of what we can do in a small editor. */ 856 if (ftruncate(fd,len) == -1) goto writeerr; 857#endif 858 if (write(fd,buf,len) != len) goto writeerr; 859 860 close(fd); 861 free(buf); 862 E.dirty = 0; 863 editorSetStatusMessage("%d bytes written on disk", len); 864 return 0; 865 866writeerr: 867 free(buf); 868 if (fd != -1) close(fd); 869 editorSetStatusMessage("Can't save! I/O error: %s",strerror(errno)); 870 return 1; 871} 872 873/* ============================= Terminal update ============================ */ 874 875/* We define a very simple "append buffer" structure, that is an heap 876 * allocated string where we can append to. This is useful in order to 877 * write all the escape sequences in a buffer and flush them to the standard 878 * output in a single call, to avoid flickering effects. */ 879struct abuf { 880 char *b; 881 int len; 882}; 883 884#define ABUF_INIT {NULL,0} 885 886void abAppend(struct abuf *ab, const char *s, int len) { 887 char *new = realloc(ab->b,ab->len+len); 888 889 if (new == NULL) return; 890 memcpy(new+ab->len,s,len); 891 ab->b = new; 892 ab->len += len; 893} 894 895void abFree(struct abuf *ab) { 896 free(ab->b); 897} 898 899/* This function writes the whole screen using VT100 escape characters 900 * starting from the logical state of the editor in the global state 'E'. */ 901void editorRefreshScreen(void) { 902 int y; 903 erow *r; 904 char buf[32]; 905 struct abuf ab = ABUF_INIT; 906 907 abAppend(&ab,"\x1b[?25l",6); /* Hide cursor. */ 908 abAppend(&ab,"\x1b[H",3); /* Go home. */ 909 for (y = 0; y < E.screenrows; y++) { 910 int filerow = E.rowoff+y; 911 912 if (filerow >= E.numrows) { 913 if (E.numrows == 0 && y == E.screenrows/3) { 914 char welcome[80]; 915 int welcomelen = snprintf(welcome,sizeof(welcome), 916 "Kilo editor -- verison %s\x1b[0K\r\n", KILO_VERSION); 917 int padding = (E.screencols-welcomelen)/2; 918 if (padding) { 919 abAppend(&ab,"~",1); 920 padding--; 921 } 922 while(padding--) abAppend(&ab," ",1); 923 abAppend(&ab,welcome,welcomelen); 924 } else { 925 abAppend(&ab,"~\x1b[0K\r\n",7); 926 } 927 continue; 928 } 929 930 r = &E.row[filerow]; 931 932 int len = r->rsize - E.coloff; 933 int current_color = -1; 934 if (len > 0) { 935 if (len > E.screencols) len = E.screencols; 936 char *c = r->render+E.coloff; 937 unsigned char *hl = r->hl+E.coloff; 938 int j; 939 for (j = 0; j < len; j++) { 940 if (hl[j] == HL_NONPRINT) { 941 char sym; 942 abAppend(&ab,"\x1b[7m",4); 943 if (c[j] <= 26) 944 sym = '@'+c[j]; 945 else 946 sym = '?'; 947 abAppend(&ab,&sym,1); 948 abAppend(&ab,"\x1b[0m",4); 949 } else if (hl[j] == HL_NORMAL) { 950 if (current_color != -1) { 951 abAppend(&ab,"\x1b[39m",5); 952 current_color = -1; 953 } 954 abAppend(&ab,c+j,1); 955 } else { 956 int color = editorSyntaxToColor(hl[j]); 957 if (color != current_color) { 958 char buf[16]; 959 int clen = snprintf(buf,sizeof(buf),"\x1b[%dm",color); 960 current_color = color; 961 abAppend(&ab,buf,clen); 962 } 963 abAppend(&ab,c+j,1); 964 } 965 } 966 } 967 abAppend(&ab,"\x1b[39m",5); 968 abAppend(&ab,"\x1b[0K",4); 969 abAppend(&ab,"\r\n",2); 970 } 971 972 /* Create a two rows status. First row: */ 973 abAppend(&ab,"\x1b[0K",4); 974 abAppend(&ab,"\x1b[7m",4); 975 char status[80], rstatus[80]; 976 int len = snprintf(status, sizeof(status), "%.20s - %d lines %s", 977 E.filename, E.numrows, E.dirty ? "(modified)" : ""); 978 int rlen = snprintf(rstatus, sizeof(rstatus), 979 "%d/%d",E.rowoff+E.cy+1,E.numrows); 980 if (len > E.screencols) len = E.screencols; 981 abAppend(&ab,status,len); 982 while(len < E.screencols) { 983 if (E.screencols - len == rlen) { 984 abAppend(&ab,rstatus,rlen); 985 break; 986 } else { 987 abAppend(&ab," ",1); 988 len++; 989 } 990 } 991 abAppend(&ab,"\x1b[0m\r\n",6); 992 993 /* Second row depends on E.statusmsg and the status message update time. */ 994 abAppend(&ab,"\x1b[0K",4); 995 int msglen = strlen(E.statusmsg); 996 if (msglen && time(NULL)-E.statusmsg_time < 5) 997 abAppend(&ab,E.statusmsg,msglen <= E.screencols ? msglen : E.screencols); 998 999 /* Put cursor at its current position. Note that the horizontal position 1000 * at which the cursor is displayed may be different compared to 'E.cx' 1001 * because of TABs. */ 1002 int j; 1003 int cx = 1; 1004 int filerow = E.rowoff+E.cy; 1005 erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1006 if (row) { 1007 for (j = E.coloff; j < (E.cx+E.coloff); j++) { 1008 if (j < row->size && row->chars[j] == TAB) cx += 7-((cx)%8); 1009 cx++; 1010 } 1011 } 1012 snprintf(buf,sizeof(buf),"\x1b[%d;%dH",E.cy+1,cx); 1013 abAppend(&ab,buf,strlen(buf)); 1014 abAppend(&ab,"\x1b[?25h",6); /* Show cursor. */ 1015 write(STDOUT_FILENO,ab.b,ab.len); 1016 abFree(&ab); 1017} 1018 1019/* Set an editor status message for the second line of the status, at the 1020 * end of the screen. */ 1021void editorSetStatusMessage(const char *fmt, ...) { 1022 va_list ap; 1023 va_start(ap,fmt); 1024 vsnprintf(E.statusmsg,sizeof(E.statusmsg),fmt,ap); 1025 va_end(ap); 1026 E.statusmsg_time = time(NULL); 1027} 1028 1029/* =============================== Find mode ================================ */ 1030 1031#define KILO_QUERY_LEN 256 1032 1033void editorFind(int fd) { 1034 char query[KILO_QUERY_LEN+1] = {0}; 1035 int qlen = 0; 1036 int last_match = -1; /* Last line where a match was found. -1 for none. */ 1037 int find_next = 0; /* if 1 search next, if -1 search prev. */ 1038 int saved_hl_line = -1; /* No saved HL */ 1039 char *saved_hl = NULL; 1040 1041#define FIND_RESTORE_HL do { \ 1042 if (saved_hl) { \ 1043 memcpy(E.row[saved_hl_line].hl,saved_hl, E.row[saved_hl_line].rsize); \ 1044 saved_hl = NULL; \ 1045 } \ 1046} while (0) 1047 1048 /* Save the cursor position in order to restore it later. */ 1049 int saved_cx = E.cx, saved_cy = E.cy; 1050 int saved_coloff = E.coloff, saved_rowoff = E.rowoff; 1051 1052 while(1) { 1053 editorSetStatusMessage( 1054 "Search: %s (Use ESC/Arrows/Enter)", query); 1055 editorRefreshScreen(); 1056 1057 int c = editorReadKey(fd); 1058 if (c == DEL_KEY || c == CTRL_H || c == BACKSPACE) { 1059 if (qlen != 0) query[--qlen] = '\0'; 1060 last_match = -1; 1061 } else if (c == ESC || c == ENTER) { 1062 if (c == ESC) { 1063 E.cx = saved_cx; E.cy = saved_cy; 1064 E.coloff = saved_coloff; E.rowoff = saved_rowoff; 1065 } 1066 FIND_RESTORE_HL; 1067 editorSetStatusMessage(""); 1068 return; 1069 } else if (c == ARROW_RIGHT || c == ARROW_DOWN) { 1070 find_next = 1; 1071 } else if (c == ARROW_LEFT || c == ARROW_UP) { 1072 find_next = -1; 1073 } else if (isprint(c)) { 1074 if (qlen < KILO_QUERY_LEN) { 1075 query[qlen++] = c; 1076 query[qlen] = '\0'; 1077 last_match = -1; 1078 } 1079 } 1080 1081 /* Search occurrence. */ 1082 if (last_match == -1) find_next = 1; 1083 if (find_next) { 1084 char *match = NULL; 1085 int match_offset = 0; 1086 int i, current = last_match; 1087 1088 for (i = 0; i < E.numrows; i++) { 1089 current += find_next; 1090 if (current == -1) current = E.numrows-1; 1091 else if (current == E.numrows) current = 0; 1092 match = strstr(E.row[current].render,query); 1093 if (match) { 1094 match_offset = match-E.row[current].render; 1095 break; 1096 } 1097 } 1098 find_next = 0; 1099 1100 /* Highlight */ 1101 FIND_RESTORE_HL; 1102 1103 if (match) { 1104 erow *row = &E.row[current]; 1105 last_match = current; 1106 if (row->hl) { 1107 saved_hl_line = current; 1108 saved_hl = malloc(row->rsize); 1109 memcpy(saved_hl,row->hl,row->rsize); 1110 memset(row->hl+match_offset,HL_MATCH,qlen); 1111 } 1112 E.cy = 0; 1113 E.cx = match_offset; 1114 E.rowoff = current; 1115 E.coloff = 0; 1116 /* Scroll horizontally as needed. */ 1117 if (E.cx > E.screencols) { 1118 int diff = E.cx - E.screencols; 1119 E.cx -= diff; 1120 E.coloff += diff; 1121 } 1122 } 1123 } 1124 } 1125} 1126 1127/* ========================= Editor events handling ======================== */ 1128 1129/* Handle cursor position change because arrow keys were pressed. */ 1130void editorMoveCursor(int key) { 1131 int filerow = E.rowoff+E.cy; 1132 int filecol = E.coloff+E.cx; 1133 int rowlen; 1134 erow *row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1135 1136 switch(key) { 1137 case ARROW_LEFT: 1138 if (E.cx == 0) { 1139 if (E.coloff) { 1140 E.coloff--; 1141 } else { 1142 if (filerow > 0) { 1143 E.cy--; 1144 E.cx = E.row[filerow-1].size; 1145 if (E.cx > E.screencols-1) { 1146 E.coloff = E.cx-E.screencols+1; 1147 E.cx = E.screencols-1; 1148 } 1149 } 1150 } 1151 } else { 1152 E.cx -= 1; 1153 } 1154 break; 1155 case ARROW_RIGHT: 1156 if (row && filecol < row->size) { 1157 if (E.cx == E.screencols-1) { 1158 E.coloff++; 1159 } else { 1160 E.cx += 1; 1161 } 1162 } else if (row && filecol == row->size) { 1163 E.cx = 0; 1164 E.coloff = 0; 1165 if (E.cy == E.screenrows-1) { 1166 E.rowoff++; 1167 } else { 1168 E.cy += 1; 1169 } 1170 } 1171 break; 1172 case ARROW_UP: 1173 if (E.cy == 0) { 1174 if (E.rowoff) E.rowoff--; 1175 } else { 1176 E.cy -= 1; 1177 } 1178 break; 1179 case ARROW_DOWN: 1180 if (filerow < E.numrows) { 1181 if (E.cy == E.screenrows-1) { 1182 E.rowoff++; 1183 } else { 1184 E.cy += 1; 1185 } 1186 } 1187 break; 1188 } 1189 /* Fix cx if the current line has not enough chars. */ 1190 filerow = E.rowoff+E.cy; 1191 filecol = E.coloff+E.cx; 1192 row = (filerow >= E.numrows) ? NULL : &E.row[filerow]; 1193 rowlen = row ? row->size : 0; 1194 if (filecol > rowlen) { 1195 E.cx -= filecol-rowlen; 1196 if (E.cx < 0) { 1197 E.coloff += E.cx; 1198 E.cx = 0; 1199 } 1200 } 1201} 1202 1203/* Process events arriving from the standard input, which is, the user 1204 * is typing stuff on the terminal. */ 1205#define KILO_QUIT_TIMES 3 1206void editorProcessKeypress(int fd) { 1207 /* When the file is modified, requires Ctrl-q to be pressed N times 1208 * before actually quitting. */ 1209 static int quit_times = KILO_QUIT_TIMES; 1210 1211 int c = editorReadKey(fd); 1212 switch(c) { 1213 case ENTER: /* Enter */ 1214 editorInsertNewline(); 1215 break; 1216 case CTRL_C: /* Ctrl-c */ 1217 /* We ignore ctrl-c, it can't be so simple to lose the changes 1218 * to the edited file. */ 1219 break; 1220 case CTRL_Q: /* Ctrl-q */ 1221 /* Quit if the file was already saved. */ 1222 if (E.dirty && quit_times) { 1223 editorSetStatusMessage("WARNING!!! File has unsaved changes. " 1224 "Press Ctrl-Q %d more times to quit.", quit_times); 1225 quit_times--; 1226 return; 1227 } 1228 exit(0); 1229 break; 1230 case CTRL_S: /* Ctrl-s */ 1231 editorSave(); 1232 break; 1233 case CTRL_F: 1234 editorFind(fd); 1235 break; 1236 case BACKSPACE: /* Backspace */ 1237 case CTRL_H: /* Ctrl-h */ 1238 case DEL_KEY: 1239 editorDelChar(); 1240 break; 1241 case PAGE_UP: 1242 case PAGE_DOWN: 1243 if (c == PAGE_UP && E.cy != 0) 1244 E.cy = 0; 1245 else if (c == PAGE_DOWN && E.cy != E.screenrows-1) 1246 E.cy = E.screenrows-1; 1247 { 1248 int times = E.screenrows; 1249 while(times--) 1250 editorMoveCursor(c == PAGE_UP ? ARROW_UP: 1251 ARROW_DOWN); 1252 } 1253 break; 1254 1255 case ARROW_UP: 1256 case ARROW_DOWN: 1257 case ARROW_LEFT: 1258 case ARROW_RIGHT: 1259 editorMoveCursor(c); 1260 break; 1261 case CTRL_L: /* ctrl+l, clear screen */ 1262 /* Just refresht the line as side effect. */ 1263 break; 1264 case ESC: 1265 /* Nothing to do for ESC in this mode. */ 1266 break; 1267 default: 1268 editorInsertChar(c); 1269 break; 1270 } 1271 1272 quit_times = KILO_QUIT_TIMES; /* Reset it to the original value. */ 1273} 1274 1275int editorFileWasModified(void) { 1276 return E.dirty; 1277} 1278 1279void initEditor(void) { 1280 E.cx = 0; 1281 E.cy = 0; 1282 E.rowoff = 0; 1283 E.coloff = 0; 1284 E.numrows = 0; 1285 E.row = NULL; 1286 E.dirty = 0; 1287 E.filename = NULL; 1288 E.syntax = NULL; 1289 if (getWindowSize(STDIN_FILENO,STDOUT_FILENO, 1290 &E.screenrows,&E.screencols) == -1) 1291 { 1292 perror("Unable to query the screen for size (columns / rows)"); 1293 exit(1); 1294 } 1295 E.screenrows -= 2; /* Get room for status bar. */ 1296} 1297 1298int main(int argc, char **argv) { 1299 if (argc != 2) { 1300 fprintf(stderr,"Usage: kilo <filename>\n"); 1301 exit(1); 1302 } 1303 1304 initEditor(); 1305 editorSelectSyntaxHighlight(argv[1]); 1306 editorOpen(argv[1]); 1307 enableRawMode(STDIN_FILENO); 1308 editorSetStatusMessage( 1309 "HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find"); 1310 while(1) { 1311 editorRefreshScreen(); 1312 editorProcessKeypress(STDIN_FILENO); 1313 } 1314 return 0; 1315} 1316