Added syntax highlighting.
This commit is contained in:
1 parent
4c42101051
commit
b440e82542
1 file changed
+347
-1
@@ -37,16 +37,45 @@
|
||||
#define TTE_TAB_STOP 8
|
||||
// Times to press Ctrl-Q before exiting
|
||||
#define TTE_QUIT_TIMES 3
|
||||
// Highlight flags
|
||||
#define HL_HIGHLIGHT_NUMBERS (1 << 0)
|
||||
#define HL_HIGHLIGHT_STRINGS (1 << 1)
|
||||
|
||||
/*** Data section ***/
|
||||
|
||||
typedef struct editor_row {
|
||||
int idx; // Row own index within the file.
|
||||
int size;
|
||||
int render_size;
|
||||
char* chars;
|
||||
char* render;
|
||||
unsigned char* highlight; // This will tell you if a character is part of a string, comment, number...
|
||||
int hl_open_comment; // True if the line is part of a ML comment.
|
||||
} editor_row;
|
||||
|
||||
struct editor_syntax {
|
||||
// filetype field is the name of the filetype that will be displayed
|
||||
// to the user in the status bar.
|
||||
char* file_type;
|
||||
// filematch is an array of strings, where each string contains a
|
||||
// pattern to match a filename against. If the filename matches,
|
||||
// then the file will be recognized as having that filetype.
|
||||
char** file_match;
|
||||
// This will be a NULL-terminated array of strings, each string containing
|
||||
// a keyword. To differentiate between the two types of keywords,
|
||||
// we’ll terminate the second type of keywords with a pipe (|)
|
||||
// character (also known as a vertical bar).
|
||||
char** keywords;
|
||||
// We let each language specify its own single-line comment pattern.
|
||||
char* singleline_comment_start;
|
||||
// flags is a bit field that will contain flags for whether to
|
||||
// highlight numbers and whether to highlight strings for that
|
||||
// filetype.
|
||||
char* multiline_comment_start;
|
||||
char* multiline_comment_end;
|
||||
int flags;
|
||||
};
|
||||
|
||||
struct editor_config {
|
||||
int cursor_x;
|
||||
int cursor_y;
|
||||
@@ -61,6 +90,7 @@ struct editor_config {
|
||||
char* file_name;
|
||||
char status_msg[80];
|
||||
time_t status_msg_time;
|
||||
struct editor_syntax* syntax;
|
||||
struct termios orig_termios;
|
||||
} ec;
|
||||
|
||||
@@ -85,6 +115,42 @@ enum editor_key {
|
||||
DEL_KEY
|
||||
};
|
||||
|
||||
enum editor_highlight {
|
||||
HL_NORMAL = 0,
|
||||
HL_SL_COMMENT,
|
||||
HL_ML_COMMENT,
|
||||
HL_KEYWORD_1,
|
||||
HL_KEYWORD_2,
|
||||
HL_STRING,
|
||||
HL_NUMBER,
|
||||
HL_MATCH
|
||||
};
|
||||
|
||||
/*** Filetypes ***/
|
||||
|
||||
char* C_HL_extensions[] = {".c", ".h", ".cpp", NULL}; // Array must be terminated with NULL.
|
||||
char *C_HL_keywords[] = {
|
||||
"switch", "if", "while", "for", "break", "continue", "return", "else",
|
||||
"struct", "union", "typedef", "static", "enum", "class", "case",
|
||||
"int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|",
|
||||
"void|", NULL
|
||||
};
|
||||
|
||||
struct editor_syntax HL_DB[] = {
|
||||
{
|
||||
"c",
|
||||
C_HL_extensions,
|
||||
C_HL_keywords,
|
||||
"//",
|
||||
"/*",
|
||||
"*/",
|
||||
HL_HIGHLIGHT_NUMBERS | HL_HIGHLIGHT_STRINGS
|
||||
},
|
||||
};
|
||||
|
||||
// Size of the "Hightlight Database" (HL_DB).
|
||||
#define HL_DB_ENTRIES (sizeof(HL_DB) / sizeof(HL_DB[0]))
|
||||
|
||||
/*** Declarations section ***/
|
||||
|
||||
void editorClearScreen();
|
||||
@@ -282,6 +348,214 @@ void consoleBufferClose() {
|
||||
editorClearScreen();
|
||||
}
|
||||
|
||||
/*** Syntax highlighting ***/
|
||||
|
||||
int isSeparator(int c) {
|
||||
// strchr() looks to see if any one of the characters in the first string
|
||||
// appear in the second string. If so, it returns a pointer to the
|
||||
// character in the second string that matched. Otherwise, it
|
||||
// returns NULL.
|
||||
return isspace(c) || c == '\0' || strchr(",.()+-/*=~%<>[];", c) != NULL;
|
||||
}
|
||||
|
||||
int isAlsoNumber(int c) {
|
||||
return c == '.' || c == 'x' || c == 'a' || c == 'b' || c == 'c' || c == 'd' || c == 'e' || c == 'f';
|
||||
}
|
||||
|
||||
void editorUpdateSyntax(editor_row* row) {
|
||||
row -> highlight = realloc(row -> highlight, row -> render_size);
|
||||
// void * memset ( void * ptr, int value, size_t num );
|
||||
// Sets the first num bytes of the block of memory pointed by ptr to
|
||||
// the specified value. With this we set all characters to HL_NORMAL.
|
||||
memset(row -> highlight, HL_NORMAL, row -> render_size);
|
||||
|
||||
if (ec.syntax == NULL)
|
||||
return;
|
||||
|
||||
char** keywords = ec.syntax -> keywords;
|
||||
|
||||
char* scs = ec.syntax -> singleline_comment_start;
|
||||
char* mcs = ec.syntax -> multiline_comment_start;
|
||||
char* mce = ec.syntax -> multiline_comment_end;
|
||||
|
||||
int scs_len = scs ? strlen(scs) : 0;
|
||||
int mcs_len = mcs ? strlen(mcs) : 0;
|
||||
int mce_len = mce ? strlen(mce) : 0;
|
||||
|
||||
int prev_sep = 1; // True (1) if the previous char is a separator, false otherwise.
|
||||
int in_string = 0; // If != 0, inside a string. We also keep track if it's ' or "
|
||||
int in_comment = (row -> idx > 0 && ec.row[row -> idx - 1].hl_open_comment); // This is ONLY used on ML comments.
|
||||
|
||||
int i = 0;
|
||||
while (i < row -> render_size) {
|
||||
char c = row -> render[i];
|
||||
// Highlight type of the previous character.
|
||||
unsigned char prev_highlight = (i > 0) ? row -> highlight[i - 1] : HL_NORMAL;
|
||||
|
||||
if (scs_len && !in_string && !in_comment) {
|
||||
// int strncmp ( const char * str1, const char * str2, size_t num );
|
||||
// Compares up to num characters of the C string str1 to those of the C string str2.
|
||||
// This function starts comparing the first character of each string. If they are
|
||||
// equal to each other, it continues with the following pairs until the characters
|
||||
// differ, until a terminating null-character is reached, or until num characters
|
||||
// match in both strings, whichever happens first.
|
||||
if (!strncmp(&row -> render[i], scs, scs_len)) {
|
||||
memset(&row -> highlight[i], HL_SL_COMMENT, row -> render_size - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mcs_len && mce_len && !in_string) {
|
||||
if (in_comment) {
|
||||
row -> highlight[i] = HL_ML_COMMENT;
|
||||
if (!strncmp(&row -> render[i], mce, mce_len)) {
|
||||
memset(&row -> highlight[i], HL_ML_COMMENT, mce_len);
|
||||
i += mce_len;
|
||||
in_comment = 0;
|
||||
prev_sep = 1;
|
||||
continue;
|
||||
} else {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
} else if (!strncmp(&row -> render[i], mcs, mcs_len)) {
|
||||
memset(&row -> highlight[i], HL_ML_COMMENT, mcs_len);
|
||||
i += mcs_len;
|
||||
in_comment = 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (ec.syntax -> flags & HL_HIGHLIGHT_STRINGS) {
|
||||
if (in_string) {
|
||||
row -> highlight[i] = HL_STRING;
|
||||
// If we’re in a string and the current character is a backslash (\),
|
||||
// and there’s at least one more character in that line that comes
|
||||
// after the backslash, then we highlight the character that comes
|
||||
// after the backslash with HL_STRING and consume it. We increment
|
||||
// i by 2 to consume both characters at once.
|
||||
if (c == '\\' && i + 1 < row -> render_size) {
|
||||
row -> highlight[i + 1] = HL_STRING;
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == in_string)
|
||||
in_string = 0;
|
||||
i++;
|
||||
prev_sep = 1;
|
||||
continue;
|
||||
} else {
|
||||
if (c == '"' || c == '\'') {
|
||||
in_string = c;
|
||||
row -> highlight[i] = HL_STRING;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ec.syntax -> flags & HL_HIGHLIGHT_NUMBERS) {
|
||||
if ((isdigit(c) && (prev_sep || prev_highlight == HL_NUMBER)) ||
|
||||
(isAlsoNumber(c) && prev_highlight == HL_NUMBER)) {
|
||||
row -> highlight[i] = HL_NUMBER;
|
||||
i++;
|
||||
prev_sep = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (prev_sep) {
|
||||
int j;
|
||||
for (j = 0; keywords[j]; j++) {
|
||||
int kw_len = strlen(keywords[j]);
|
||||
int kw_2 = keywords[j][kw_len - 1] == '|';
|
||||
if (kw_2)
|
||||
kw_len--;
|
||||
|
||||
// Keywords require a separator both before and after the keyword.
|
||||
if (!strncmp(&row -> render[i], keywords[j], kw_len) &&
|
||||
isSeparator(row -> render[i + kw_len])) {
|
||||
memset(&row -> highlight[i], kw_2 ? HL_KEYWORD_2 : HL_KEYWORD_1, kw_len);
|
||||
i += kw_len;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (keywords[j] != NULL) {
|
||||
prev_sep = 0;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
prev_sep = isSeparator(c);
|
||||
i++;
|
||||
}
|
||||
|
||||
int changed = (row -> hl_open_comment != in_comment);
|
||||
// This tells us whether the row ended as an unclosed multi-line
|
||||
// comment or not.
|
||||
row -> hl_open_comment = in_comment;
|
||||
// A user could comment out an entire file just by changing one line.
|
||||
// So it seems like we need to update the syntax of all the lines
|
||||
// following the current line. However, we know the highlighting
|
||||
// of the next line will not change if the value of this line’s
|
||||
// // // hl_open_comment did not change. So we check if it changed, and
|
||||
// // only call editorUpdateSyntax() on the next line if
|
||||
// hl_open_comment changed (and if there is a next line in the file).
|
||||
// Because editorUpdateSyntax() keeps calling itself with the next
|
||||
// line, the change will continue to propagate to more and more lines
|
||||
// until one of them is unchanged, at which point we know that all
|
||||
// the lines after that one must be unchanged as well.
|
||||
if (changed && row -> idx + 1 < ec.num_rows)
|
||||
editorUpdateSyntax(&ec.row[row -> idx + 1]);
|
||||
}
|
||||
|
||||
int editorSyntaxToColor(int highlight) {
|
||||
// We return ANSI codes for colors.
|
||||
switch (highlight) {
|
||||
case HL_SL_COMMENT:
|
||||
case HL_ML_COMMENT: return 36; // Cyan
|
||||
case HL_KEYWORD_1: return 33; // Yellow
|
||||
case HL_KEYWORD_2: return 32; // Green
|
||||
case HL_STRING: return 35; // Magenta.
|
||||
case HL_NUMBER: return 31; // Red.
|
||||
case HL_MATCH: return 34; // Blue.
|
||||
default: return 37; // White.
|
||||
}
|
||||
}
|
||||
|
||||
void editorSelectSyntaxHighlight() {
|
||||
ec.syntax = NULL;
|
||||
if (ec.file_name == NULL)
|
||||
return;
|
||||
|
||||
for (unsigned int j = 0; j < HL_DB_ENTRIES; j++) {
|
||||
struct editor_syntax* es = &HL_DB[j];
|
||||
unsigned int i = 0;
|
||||
|
||||
while (es -> file_match[i]) {
|
||||
char* p = strstr(ec.file_name, es -> file_match[i]);
|
||||
if (p != NULL) {
|
||||
// Returns a pointer to the first occurrence of str2 in str1,
|
||||
// or a null pointer if str2 is not part of str1.
|
||||
int pat_len = strlen(es -> file_match[i]);
|
||||
if (es -> file_match[i][0] != '.' || p[pat_len] == '\0') {
|
||||
ec.syntax = es;
|
||||
|
||||
int file_row;
|
||||
for (file_row = 0; file_row < ec.num_rows; file_row++) {
|
||||
editorUpdateSyntax(&ec.row[file_row]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*** Row operations ***/
|
||||
|
||||
int editorRowCursorXToRenderX(editor_row* row, int cursor_x) {
|
||||
@@ -350,6 +624,8 @@ void editorUpdateRow(editor_row* row) {
|
||||
}
|
||||
row -> render[idx] = '\0';
|
||||
row -> render_size = idx;
|
||||
|
||||
editorUpdateSyntax(row);
|
||||
}
|
||||
|
||||
void editorInsertRow(int at, char* s, size_t line_len) {
|
||||
@@ -359,6 +635,12 @@ void editorInsertRow(int at, char* s, size_t line_len) {
|
||||
ec.row = realloc(ec.row, sizeof(editor_row) * (ec.num_rows + 1));
|
||||
memmove(&ec.row[at + 1], &ec.row[at], sizeof(editor_row) * (ec.num_rows - at));
|
||||
|
||||
for (int j = at + 1; j <= ec.num_rows; j++) {
|
||||
ec.row[j].idx++;
|
||||
}
|
||||
|
||||
ec.row[at].idx = at;
|
||||
|
||||
ec.row[at].size = line_len;
|
||||
ec.row[at].chars = malloc(line_len + 1); // We want to add terminator char '\0' at the end
|
||||
memcpy(ec.row[at].chars, s, line_len);
|
||||
@@ -366,6 +648,8 @@ void editorInsertRow(int at, char* s, size_t line_len) {
|
||||
|
||||
ec.row[at].render_size = 0;
|
||||
ec.row[at].render = NULL;
|
||||
ec.row[at].highlight = NULL;
|
||||
ec.row[at].hl_open_comment = 0;
|
||||
editorUpdateRow(&ec.row[at]);
|
||||
|
||||
ec.num_rows++;
|
||||
@@ -375,6 +659,7 @@ void editorInsertRow(int at, char* s, size_t line_len) {
|
||||
void editorFreeRow(editor_row* row) {
|
||||
free(row -> render);
|
||||
free(row -> chars);
|
||||
free(row -> highlight);
|
||||
}
|
||||
|
||||
void editorDelRow(int at) {
|
||||
@@ -382,6 +667,11 @@ void editorDelRow(int at) {
|
||||
return;
|
||||
editorFreeRow(&ec.row[at]);
|
||||
memmove(&ec.row[at], &ec.row[at + 1], sizeof(editor_row) * (ec.num_rows - at - 1));
|
||||
|
||||
for (int j = at; j < ec.num_rows - 1; j++) {
|
||||
ec.row[j].idx--;
|
||||
}
|
||||
|
||||
ec.num_rows--;
|
||||
ec.dirty++;
|
||||
}
|
||||
@@ -504,6 +794,8 @@ void editorOpen(char* file_name) {
|
||||
free(ec.file_name);
|
||||
ec.file_name = strdup(file_name);
|
||||
|
||||
editorSelectSyntaxHighlight();
|
||||
|
||||
FILE* file = fopen(file_name, "r");
|
||||
if (!file)
|
||||
die("Failed to open the file");
|
||||
@@ -532,6 +824,7 @@ void editorSave() {
|
||||
editorSetStatusMessage("Save aborted");
|
||||
return;
|
||||
}
|
||||
editorSelectSyntaxHighlight();
|
||||
}
|
||||
|
||||
int len;
|
||||
@@ -569,6 +862,15 @@ void editorSearchCallback(char* query, int key) {
|
||||
// 1 for searching forward and -1 for searching backwards.
|
||||
static int direction = 1;
|
||||
|
||||
static int saved_highlight_line;
|
||||
static char* saved_hightlight = NULL;
|
||||
|
||||
if (saved_hightlight) {
|
||||
memcpy(ec.row[saved_highlight_line].highlight, saved_hightlight, ec.row[saved_highlight_line].render_size);
|
||||
free(saved_hightlight);
|
||||
saved_hightlight = NULL;
|
||||
}
|
||||
|
||||
// Checking if the user pressed Enter or Escape, in which case
|
||||
// they are leaving search mode so we return immediately.
|
||||
if (key == '\r' || key == '\x1b') {
|
||||
@@ -606,6 +908,11 @@ void editorSearchCallback(char* query, int key) {
|
||||
// that the next screen refresh will cause the matching line to
|
||||
// be at the very top of the screen.
|
||||
ec.row_offset = ec.num_rows;
|
||||
|
||||
saved_highlight_line = current;
|
||||
saved_hightlight = malloc(row -> render_size);
|
||||
memcpy(saved_hightlight, row -> highlight, row -> render_size);
|
||||
memset(&row -> highlight[match - row -> render], HL_MATCH, strlen(query));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -785,7 +1092,44 @@ void editorDrawRows(struct a_buf* ab) {
|
||||
len = 0;
|
||||
if (len > ec.screen_cols)
|
||||
len = ec.screen_cols;
|
||||
abufAppend(ab, &ec.row[file_row].render[ec.col_offset], len);
|
||||
|
||||
char* c = &ec.row[file_row].render[ec.col_offset];
|
||||
unsigned char* highlight = &ec.row[file_row].highlight[ec.col_offset];
|
||||
int current_color = -1;
|
||||
int j;
|
||||
for (j = 0; j < len; j++) {
|
||||
// Displaying nonprintable characters as (A-Z, @, and ?).
|
||||
if (iscntrl(c[j])) {
|
||||
char sym = (c[j] <= 26) ? '@' + c[j] : '?';
|
||||
abufAppend(ab, "\x1b[7m", 4);
|
||||
abufAppend(ab, &sym, 1);
|
||||
abufAppend(ab, "\x1b[m", 3);
|
||||
if (current_color != -1) {
|
||||
char buf[16];
|
||||
int c_len = snprintf(buf, sizeof(buf), "\x1b[%dm", current_color);
|
||||
abufAppend(ab, buf, c_len);
|
||||
}
|
||||
} else if (highlight[j] == HL_NORMAL) {
|
||||
if (current_color != -1) {
|
||||
abufAppend(ab, "\x1b[39m", 5);
|
||||
current_color = -1;
|
||||
}
|
||||
abufAppend(ab, &c[j], 1);
|
||||
} else {
|
||||
int color = editorSyntaxToColor(highlight[j]);
|
||||
// We only use escape sequence if the new color is different
|
||||
// from the last character's color.
|
||||
if (color != current_color) {
|
||||
current_color = color;
|
||||
char buf[16];
|
||||
int c_len = snprintf(buf, sizeof(buf), "\x1b[%dm", color);
|
||||
abufAppend(ab, buf, c_len);
|
||||
}
|
||||
|
||||
abufAppend(ab, &c[j], 1);
|
||||
}
|
||||
}
|
||||
abufAppend(ab, "\x1b[39m", 5);
|
||||
}
|
||||
|
||||
// Redrawing each line instead of the whole screen.
|
||||
@@ -1003,6 +1347,8 @@ void initEditor() {
|
||||
ec.file_name = NULL;
|
||||
ec.status_msg[0] = '\0';
|
||||
ec.status_msg_time = 0;
|
||||
ec.syntax = NULL;
|
||||
|
||||
editorUpdateWindowSize();
|
||||
// The SIGWINCH signal is sent to a process when its controlling
|
||||
// terminal changes its size (a window change).
|
||||
|
||||
Reference in new issue
Block a user