diff --git a/meson.build b/meson.build index 9d0df4c..2915523 100644 --- a/meson.build +++ b/meson.build @@ -18,5 +18,9 @@ add_project_arguments( language: 'c', ) +tree_sitter_dep = dependency('tree-sitter') +cc = meson.get_compiler('c') +dl_dep = cc.find_library('dl', required: false) + subdir('src') subdir('tests') diff --git a/src/buffer.c b/src/buffer.c index 51bd831..fa95c52 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -41,7 +41,6 @@ static void buffer_append_line(buffer_t *buf, const char *data, size_t len) buffer_ensure_line_cap(buf, buf->num_lines + 1); line_t *line = &buf->lines[buf->num_lines]; line->chars = NULL; - line->cap = 0; line->len = 0; if (len > 0) { line_ensure_cap(line, len); @@ -58,6 +57,7 @@ void buffer_init(buffer_t *buf) buf->cap_lines = 0; buf->filename = NULL; buf->dirty = 0; + buf->version = 0; buf->encoding = ENCODING_UTF8; buf->use_crlf = 0; buf->trailing_newline = 1; @@ -117,6 +117,7 @@ int buffer_load(buffer_t *buf, const char *filename) buf->use_crlf = 0; buf->trailing_newline = 1; buf->dirty = 0; + buf->version = 0; size_t line_start = offset; int first_line = 1; @@ -139,15 +140,15 @@ int buffer_load(buffer_t *buf, const char *filename) line_start = i + 1; } - if (line_start < total) { - buffer_append_line(buf, (const char *)data + line_start, total - line_start); + if (line_start < total || buf->num_lines == 0) { + size_t line_end = total; + if (line_end > line_start && data[line_end - 1] == '\r') { + line_end--; + } + buffer_append_line(buf, (const char *)data + line_start, line_end - line_start); buf->trailing_newline = 0; } - if (buf->num_lines == 0) { - buffer_append_line(buf, "", 0); - } - free(data); return 0; } @@ -204,6 +205,7 @@ void buffer_insert_bytes(buffer_t *buf, size_t row, size_t col, const char *byte memcpy(line->chars + col, bytes, n); line->len += n; buf->dirty = 1; + buf->version++; } void buffer_delete_range(buffer_t *buf, size_t r1, size_t c1, size_t r2, size_t c2) @@ -214,6 +216,7 @@ void buffer_delete_range(buffer_t *buf, size_t r1, size_t c1, size_t r2, size_t memmove(line->chars + c1, line->chars + c1 + n, line->len - c1 - n); line->len -= n; buf->dirty = 1; + buf->version++; return; } @@ -235,6 +238,7 @@ void buffer_delete_range(buffer_t *buf, size_t r1, size_t c1, size_t r2, size_t (buf->num_lines - r2 - 1) * sizeof(line_t)); buf->num_lines -= remove_count; buf->dirty = 1; + buf->version++; } void buffer_split_line(buffer_t *buf, size_t row, size_t col) @@ -260,6 +264,7 @@ void buffer_split_line(buffer_t *buf, size_t row, size_t col) buf->num_lines += 1; buf->dirty = 1; + buf->version++; } void buffer_join_lines(buffer_t *buf, size_t row) @@ -276,61 +281,78 @@ void buffer_join_lines(buffer_t *buf, size_t row) (buf->num_lines - row - 2) * sizeof(line_t)); buf->num_lines -= 1; buf->dirty = 1; + buf->version++; } char *buffer_extract_text(const buffer_t *buf, size_t r1, size_t c1, size_t r2, size_t c2, - size_t *out_len) + size_t *out_len) { if (r1 == r2) { const line_t *line = &buf->lines[r1]; size_t len = c2 - c1; - char *text = xmalloc(len > 0 ? len : 1); + char *text = xmalloc(len + 1); memcpy(text, line->chars + c1, len); - *out_len = len; + text[len] = '\0'; + if (out_len != NULL) { + *out_len = len; + } return text; } - size_t total = (buf->lines[r1].len - c1) + 1; - for (size_t row = r1 + 1; row < r2; row++) { - total += buf->lines[row].len + 1; + size_t total = 0; + total += buf->lines[r1].len - c1 + 1; + for (size_t r = r1 + 1; r < r2; r++) { + total += buf->lines[r].len + 1; } total += c2; - char *text = xmalloc(total > 0 ? total : 1); - size_t pos = 0; + char *text = xmalloc(total + 1); + size_t offset = 0; - memcpy(text + pos, buf->lines[r1].chars + c1, buf->lines[r1].len - c1); - pos += buf->lines[r1].len - c1; - text[pos++] = '\n'; + size_t first_len = buf->lines[r1].len - c1; + memcpy(text + offset, buf->lines[r1].chars + c1, first_len); + offset += first_len; + text[offset++] = '\n'; - for (size_t row = r1 + 1; row < r2; row++) { - memcpy(text + pos, buf->lines[row].chars, buf->lines[row].len); - pos += buf->lines[row].len; - text[pos++] = '\n'; + for (size_t r = r1 + 1; r < r2; r++) { + size_t len = buf->lines[r].len; + memcpy(text + offset, buf->lines[r].chars, len); + offset += len; + text[offset++] = '\n'; } - memcpy(text + pos, buf->lines[r2].chars, c2); - pos += c2; + memcpy(text + offset, buf->lines[r2].chars, c2); + offset += c2; + text[offset] = '\0'; - *out_len = pos; + if (out_len != NULL) { + *out_len = offset; + } return text; } void buffer_insert_text(buffer_t *buf, size_t row, size_t col, const char *text, size_t len) { - size_t start = 0; - for (size_t i = 0; i < len; i++) { - if (text[i] != '\n') { - continue; + size_t cur_row = row; + size_t cur_col = col; + size_t seg_start = 0; + + for (size_t i = 0; i <= len; i++) { + if (i == len || text[i] == '\n') { + size_t seg_len = i - seg_start; + if (seg_len > 0 && text[i - 1] == '\r') { + seg_len--; + } + if (seg_len > 0) { + buffer_insert_bytes(buf, cur_row, cur_col, text + seg_start, seg_len); + cur_col += seg_len; + } + if (i < len && text[i] == '\n') { + buffer_split_line(buf, cur_row, cur_col); + cur_row++; + cur_col = 0; + } + seg_start = i + 1; } - buffer_insert_bytes(buf, row, col, text + start, i - start); - col += i - start; - buffer_split_line(buf, row, col); - row += 1; - col = 0; - start = i + 1; - } - if (start < len) { - buffer_insert_bytes(buf, row, col, text + start, len - start); } } diff --git a/src/buffer.h b/src/buffer.h index c97ec88..673351a 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -21,6 +21,7 @@ typedef struct { size_t cap_lines; char *filename; int dirty; + size_t version; encoding_t encoding; int use_crlf; int trailing_newline; @@ -41,7 +42,7 @@ void buffer_split_line(buffer_t *buf, size_t row, size_t col); void buffer_join_lines(buffer_t *buf, size_t row); char *buffer_extract_text(const buffer_t *buf, size_t r1, size_t c1, size_t r2, size_t c2, - size_t *out_len); + size_t *out_len); void buffer_insert_text(buffer_t *buf, size_t row, size_t col, const char *text, size_t len); diff --git a/src/editor.c b/src/editor.c index 831c989..d84dd52 100644 --- a/src/editor.c +++ b/src/editor.c @@ -224,9 +224,23 @@ void editor_free(editor_t *ed) free(ed->redo_stack); free(ed->clipboard); free(ed->search_matches); + syntax_destroy(ed->syntax); + ed->syntax = NULL; buffer_free(&ed->buf); } +void editor_setup_syntax(editor_t *ed) +{ + if (ed->syntax != NULL) { + syntax_destroy(ed->syntax); + ed->syntax = NULL; + } + ed->syntax = syntax_create(ed->filetype); + if (ed->syntax != NULL) { + syntax_update(ed->syntax, &ed->buf); + } +} + void editor_open(editor_t *ed, const char *filename) { if (buffer_load(&ed->buf, filename) == 0) { @@ -237,6 +251,7 @@ void editor_open(editor_t *ed, const char *filename) editor_set_status(ed, "New file \"%s\"", filename); } ed->filetype = filetype_detect(filename); + editor_setup_syntax(ed); ed->cy = 0; ed->cx = 0; ed->rowoff = 0; diff --git a/src/editor.h b/src/editor.h index 8b95119..07f81ac 100644 --- a/src/editor.h +++ b/src/editor.h @@ -10,6 +10,7 @@ #include "buffer.h" #include "search.h" +#include "syntax.h" #include "theme.h" typedef enum { @@ -63,6 +64,7 @@ typedef struct { char status_msg[256]; const char *filetype; const theme_t *theme; + syntax_highlighter_t *syntax; edit_record_t *undo_stack; size_t undo_count; @@ -81,6 +83,7 @@ typedef struct { void editor_init(editor_t *ed, int screen_rows, int screen_cols); void editor_free(editor_t *ed); void editor_open(editor_t *ed, const char *filename); +void editor_setup_syntax(editor_t *ed); void editor_update_screen_size(editor_t *ed, int screen_rows, int screen_cols); void editor_set_status(editor_t *ed, const char *fmt, ...); diff --git a/src/input.c b/src/input.c index 9af1dbf..5eda539 100644 --- a/src/input.c +++ b/src/input.c @@ -134,6 +134,7 @@ static void confirm_prompt(editor_t *ed) if (ed->prompt_len > 0) { if (buffer_save_as(&ed->buf, ed->prompt_input) == 0) { ed->filetype = filetype_detect(ed->buf.filename); + editor_setup_syntax(ed); editor_set_status(ed, "Saved \"%s\"", ed->buf.filename); } else { editor_set_status(ed, "Error: could not save \"%s\"", ed->prompt_input); diff --git a/src/meson.build b/src/meson.build index 9f46da4..6c78acb 100644 --- a/src/meson.build +++ b/src/meson.build @@ -10,6 +10,7 @@ daf_lib_sources = files( 'render.c', 'input.c', 'editor.c', + 'syntax.c', ) daf_inc = include_directories('.') @@ -18,6 +19,7 @@ daf_lib = static_library( 'daf_lib', daf_lib_sources, include_directories: daf_inc, + dependencies: [tree_sitter_dep, dl_dep], ) executable( @@ -25,5 +27,6 @@ executable( 'main.c', include_directories: daf_inc, link_with: daf_lib, + dependencies: [tree_sitter_dep, dl_dep], install: true, ) diff --git a/src/render.c b/src/render.c index f5c5cab..39e49c8 100644 --- a/src/render.c +++ b/src/render.c @@ -144,26 +144,54 @@ static bool byte_in_search_match(const editor_t *ed, size_t row, size_t byte_col } typedef enum { - ROW_STYLE_UNSET = -1, - ROW_STYLE_NORMAL, - ROW_STYLE_MATCH, - ROW_STYLE_SELECTION, -} row_style_t; - -static void set_row_style(abuf_t *ab, const theme_t *theme, row_style_t *current, row_style_t wanted) + STYLE_UNSET = -1, + STYLE_NORMAL, + STYLE_MATCH, + STYLE_SELECTION, + STYLE_KEYWORD, + STYLE_STRING, + STYLE_COMMENT, + STYLE_TYPE, + STYLE_FUNCTION, + STYLE_NUMBER, + STYLE_OPERATOR, +} active_style_t; + +static void set_row_style(abuf_t *ab, const theme_t *theme, active_style_t *current, active_style_t wanted) { if (wanted == *current) { return; } abuf_append_str(ab, theme->reset); switch (wanted) { - case ROW_STYLE_SELECTION: + case STYLE_SELECTION: abuf_append_str(ab, theme->selection); break; - case ROW_STYLE_MATCH: + case STYLE_MATCH: abuf_append_str(ab, theme->search_match); break; - case ROW_STYLE_NORMAL: + case STYLE_KEYWORD: + abuf_append_str(ab, theme->syntax_keyword); + break; + case STYLE_STRING: + abuf_append_str(ab, theme->syntax_string); + break; + case STYLE_COMMENT: + abuf_append_str(ab, theme->syntax_comment); + break; + case STYLE_TYPE: + abuf_append_str(ab, theme->syntax_type); + break; + case STYLE_FUNCTION: + abuf_append_str(ab, theme->syntax_function); + break; + case STYLE_NUMBER: + abuf_append_str(ab, theme->syntax_number); + break; + case STYLE_OPERATOR: + abuf_append_str(ab, theme->syntax_operator); + break; + case STYLE_NORMAL: default: abuf_append_str(ab, theme->normal); break; @@ -194,18 +222,39 @@ static void render_text_row(abuf_t *ab, const editor_t *ed, size_t file_row) ed->mode == MODE_PROMPT_REPLACE_WITH) && ed->search_match_count > 0; + uint8_t *syntax_roles = NULL; + if (line->len > 0) { + syntax_roles = xmalloc(line->len); + syntax_get_line_roles(ed->syntax, file_row, line, syntax_roles); + } + size_t i = 0; size_t rx = 0; int emitted = 0; - row_style_t style = ROW_STYLE_UNSET; - set_row_style(ab, theme, &style, ROW_STYLE_NORMAL); + active_style_t style = STYLE_UNSET; + set_row_style(ab, theme, &style, STYLE_NORMAL); while (i < line->len && emitted < text_cols) { unsigned char c = (unsigned char)line->chars[i]; bool should_hl = row_has_hl && i >= hl_start && i < hl_end; bool should_match = show_matches && byte_in_search_match(ed, file_row, i); - row_style_t wanted = - should_hl ? ROW_STYLE_SELECTION : (should_match ? ROW_STYLE_MATCH : ROW_STYLE_NORMAL); + active_style_t wanted = STYLE_NORMAL; + if (should_hl) { + wanted = STYLE_SELECTION; + } else if (should_match) { + wanted = STYLE_MATCH; + } else if (syntax_roles != NULL) { + switch (syntax_roles[i]) { + case SYNTAX_ROLE_KEYWORD: wanted = STYLE_KEYWORD; break; + case SYNTAX_ROLE_STRING: wanted = STYLE_STRING; break; + case SYNTAX_ROLE_COMMENT: wanted = STYLE_COMMENT; break; + case SYNTAX_ROLE_TYPE: wanted = STYLE_TYPE; break; + case SYNTAX_ROLE_FUNCTION: wanted = STYLE_FUNCTION; break; + case SYNTAX_ROLE_NUMBER: wanted = STYLE_NUMBER; break; + case SYNTAX_ROLE_OPERATOR: wanted = STYLE_OPERATOR; break; + default: wanted = STYLE_NORMAL; break; + } + } set_row_style(ab, theme, &style, wanted); if (c == '\t') { @@ -232,7 +281,8 @@ static void render_text_row(abuf_t *ab, const editor_t *ed, size_t file_row) } } - set_row_style(ab, theme, &style, ROW_STYLE_NORMAL); + set_row_style(ab, theme, &style, STYLE_NORMAL); + free(syntax_roles); } static void render_status_bar(abuf_t *ab, const editor_t *ed) @@ -302,6 +352,7 @@ static void render_message_bar(abuf_t *ab, const editor_t *ed) void render_screen(editor_t *ed) { + syntax_update(ed->syntax, &ed->buf); editor_scroll(ed); abuf_t ab = {0}; diff --git a/src/syntax.c b/src/syntax.c new file mode 100644 index 0000000..d01b75a --- /dev/null +++ b/src/syntax.c @@ -0,0 +1,352 @@ +/* SPDX-FileCopyrightText: AnmiTaliDev + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "syntax.h" + +#include +#include +#include +#include +#include + +#include "common.h" + +struct syntax_highlighter_s { + void *dl_handle; + TSParser *parser; + TSTree *tree; + TSQuery *query; + TSQueryCursor *cursor; + syntax_role_t *capture_roles; + uint32_t capture_count; + size_t parsed_version; +}; + +static const char *filetype_to_lang_name(const char *filetype) +{ + if (filetype == NULL) { + return NULL; + } + if (strcmp(filetype, "C") == 0) return "c"; + if (strcmp(filetype, "C++") == 0) return "cpp"; + if (strcmp(filetype, "Python") == 0) return "python"; + if (strcmp(filetype, "Shell") == 0) return "bash"; + if (strcmp(filetype, "Markdown") == 0) return "markdown"; + if (strcmp(filetype, "JSON") == 0) return "json"; + if (strcmp(filetype, "JavaScript") == 0) return "javascript"; + if (strcmp(filetype, "TypeScript") == 0) return "typescript"; + if (strcmp(filetype, "Rust") == 0) return "rust"; + if (strcmp(filetype, "Go") == 0) return "go"; + if (strcmp(filetype, "Java") == 0) return "java"; + if (strcmp(filetype, "Ruby") == 0) return "ruby"; + if (strcmp(filetype, "HTML") == 0) return "html"; + if (strcmp(filetype, "CSS") == 0) return "css"; + if (strcmp(filetype, "XML") == 0) return "xml"; + if (strcmp(filetype, "YAML") == 0) return "yaml"; + if (strcmp(filetype, "TOML") == 0) return "toml"; + if (strcmp(filetype, "Lua") == 0) return "lua"; + if (strcmp(filetype, "PHP") == 0) return "php"; + return NULL; +} + +static void *load_grammar_library(const char *lang, const TSLanguage **out_lang) +{ + char libname[128]; + void *handle = NULL; + + snprintf(libname, sizeof(libname), "libtree-sitter-%s.so", lang); + handle = dlopen(libname, RTLD_NOW | RTLD_LOCAL); + if (handle == NULL) { + snprintf(libname, sizeof(libname), "libtree-sitter-%s.so.0", lang); + handle = dlopen(libname, RTLD_NOW | RTLD_LOCAL); + } + if (handle == NULL) { + snprintf(libname, sizeof(libname), "tree-sitter-%s.so", lang); + handle = dlopen(libname, RTLD_NOW | RTLD_LOCAL); + } + if (handle == NULL) { + return NULL; + } + + char symname[128]; + snprintf(symname, sizeof(symname), "tree_sitter_%s", lang); + for (size_t i = 0; symname[i] != '\0'; i++) { + if (symname[i] == '-') { + symname[i] = '_'; + } + } + + typedef const TSLanguage *(*lang_fn_t)(void); + union { + void *obj; + lang_fn_t func; + } sym; + sym.obj = dlsym(handle, symname); + if (sym.func == NULL) { + dlclose(handle); + return NULL; + } + + *out_lang = sym.func(); + return handle; +} + +static char *read_file_content(const char *path, uint32_t *out_len) +{ + FILE *fp = fopen(path, "rb"); + if (fp == NULL) { + return NULL; + } + if (fseek(fp, 0, SEEK_END) != 0) { + fclose(fp); + return NULL; + } + long sz = ftell(fp); + if (sz <= 0) { + fclose(fp); + return NULL; + } + rewind(fp); + + char *buf = xmalloc((size_t)sz + 1); + size_t read_bytes = fread(buf, 1, (size_t)sz, fp); + fclose(fp); + buf[read_bytes] = '\0'; + if (out_len != NULL) { + *out_len = (uint32_t)read_bytes; + } + return buf; +} + +static char *find_and_read_query(const char *lang, uint32_t *out_len) +{ + char path[512]; + + const char *xdg = getenv("XDG_CONFIG_HOME"); + if (xdg != NULL && xdg[0] != '\0') { + snprintf(path, sizeof(path), "%s/daf/queries/%s/highlights.scm", xdg, lang); + char *content = read_file_content(path, out_len); + if (content != NULL) { + return content; + } + } + + const char *home = getenv("HOME"); + if (home != NULL && home[0] != '\0') { + snprintf(path, sizeof(path), "%s/.config/daf/queries/%s/highlights.scm", home, lang); + char *content = read_file_content(path, out_len); + if (content != NULL) { + return content; + } + } + + snprintf(path, sizeof(path), "/usr/share/tree-sitter/queries/%s/highlights.scm", lang); + char *content = read_file_content(path, out_len); + if (content != NULL) { + return content; + } + + snprintf(path, sizeof(path), "/usr/share/nvim/runtime/queries/%s/highlights.scm", lang); + return read_file_content(path, out_len); +} + +static syntax_role_t capture_name_to_role(const char *name) +{ + if (strncmp(name, "keyword", 7) == 0 || strcmp(name, "repeat") == 0 || + strcmp(name, "conditional") == 0 || strcmp(name, "include") == 0) { + return SYNTAX_ROLE_KEYWORD; + } + if (strncmp(name, "string", 6) == 0 || strcmp(name, "character") == 0) { + return SYNTAX_ROLE_STRING; + } + if (strncmp(name, "comment", 7) == 0) { + return SYNTAX_ROLE_COMMENT; + } + if (strncmp(name, "type", 4) == 0 || strcmp(name, "storageclass") == 0 || + strcmp(name, "structure") == 0) { + return SYNTAX_ROLE_TYPE; + } + if (strncmp(name, "function", 8) == 0 || strncmp(name, "method", 6) == 0 || + strcmp(name, "constructor") == 0) { + return SYNTAX_ROLE_FUNCTION; + } + if (strncmp(name, "number", 6) == 0 || strcmp(name, "float") == 0 || + strcmp(name, "boolean") == 0) { + return SYNTAX_ROLE_NUMBER; + } + if (strncmp(name, "operator", 8) == 0) { + return SYNTAX_ROLE_OPERATOR; + } + return SYNTAX_ROLE_NONE; +} + +syntax_highlighter_t *syntax_create(const char *filetype) +{ + const char *lang = filetype_to_lang_name(filetype); + if (lang == NULL) { + return NULL; + } + + const TSLanguage *ts_lang = NULL; + void *handle = load_grammar_library(lang, &ts_lang); + if (handle == NULL || ts_lang == NULL) { + return NULL; + } + + TSParser *parser = ts_parser_new(); + if (parser == NULL || !ts_parser_set_language(parser, ts_lang)) { + if (parser != NULL) { + ts_parser_delete(parser); + } + dlclose(handle); + return NULL; + } + + uint32_t query_len = 0; + char *query_src = find_and_read_query(lang, &query_len); + TSQuery *query = NULL; + TSQueryCursor *cursor = NULL; + syntax_role_t *roles = NULL; + uint32_t capture_count = 0; + + if (query_src != NULL) { + uint32_t err_offset = 0; + TSQueryError err_type = TSQueryErrorNone; + query = ts_query_new(ts_lang, query_src, query_len, &err_offset, &err_type); + free(query_src); + if (query != NULL) { + cursor = ts_query_cursor_new(); + capture_count = ts_query_capture_count(query); + roles = xmalloc(capture_count * sizeof(syntax_role_t)); + for (uint32_t i = 0; i < capture_count; i++) { + uint32_t nlen = 0; + const char *name = ts_query_capture_name_for_id(query, i, &nlen); + roles[i] = capture_name_to_role(name); + } + } + } + + syntax_highlighter_t *hl = xmalloc(sizeof(syntax_highlighter_t)); + hl->dl_handle = handle; + hl->parser = parser; + hl->tree = NULL; + hl->query = query; + hl->cursor = cursor; + hl->capture_roles = roles; + hl->capture_count = capture_count; + hl->parsed_version = (size_t)-1; + return hl; +} + +void syntax_destroy(syntax_highlighter_t *hl) +{ + if (hl == NULL) { + return; + } + if (hl->cursor != NULL) { + ts_query_cursor_delete(hl->cursor); + } + if (hl->query != NULL) { + ts_query_delete(hl->query); + } + if (hl->tree != NULL) { + ts_tree_delete(hl->tree); + } + if (hl->parser != NULL) { + ts_parser_delete(hl->parser); + } + free(hl->capture_roles); + if (hl->dl_handle != NULL) { + dlclose(hl->dl_handle); + } + free(hl); +} + +static char *buffer_to_string(const buffer_t *buf, size_t *out_len) +{ + size_t total = 0; + for (size_t i = 0; i < buf->num_lines; i++) { + total += buf->lines[i].len + 1; + } + char *text = xmalloc(total + 1); + size_t offset = 0; + for (size_t i = 0; i < buf->num_lines; i++) { + memcpy(text + offset, buf->lines[i].chars, buf->lines[i].len); + offset += buf->lines[i].len; + text[offset++] = '\n'; + } + text[offset] = '\0'; + if (out_len != NULL) { + *out_len = offset; + } + return text; +} + +void syntax_update(syntax_highlighter_t *hl, const buffer_t *buf) +{ + if (hl == NULL || hl->parser == NULL) { + return; + } + if (hl->parsed_version == buf->version && hl->tree != NULL) { + return; + } + + size_t text_len = 0; + char *text = buffer_to_string(buf, &text_len); + TSTree *new_tree = ts_parser_parse_string(hl->parser, NULL, text, (uint32_t)text_len); + free(text); + if (new_tree != NULL) { + if (hl->tree != NULL) { + ts_tree_delete(hl->tree); + } + hl->tree = new_tree; + hl->parsed_version = buf->version; + } +} + +void syntax_get_line_roles(syntax_highlighter_t *hl, size_t row, const line_t *line, uint8_t *roles) +{ + if (line->len == 0) { + return; + } + memset(roles, SYNTAX_ROLE_NONE, line->len); + if (hl == NULL || hl->tree == NULL || hl->query == NULL || hl->cursor == NULL) { + return; + } + + TSNode root = ts_tree_root_node(hl->tree); + TSPoint start_point = {(uint32_t)row, 0}; + TSPoint end_point = {(uint32_t)(row + 1), 0}; + ts_query_cursor_set_point_range(hl->cursor, start_point, end_point); + ts_query_cursor_exec(hl->cursor, hl->query, root); + + TSQueryMatch match; + uint32_t capture_index; + while (ts_query_cursor_next_capture(hl->cursor, &match, &capture_index)) { + const TSQueryCapture *capture = &match.captures[capture_index]; + if (capture->index >= hl->capture_count) { + continue; + } + syntax_role_t role = hl->capture_roles[capture->index]; + if (role == SYNTAX_ROLE_NONE) { + continue; + } + TSPoint p_start = ts_node_start_point(capture->node); + TSPoint p_end = ts_node_end_point(capture->node); + if (p_start.row > row || p_end.row < row) { + continue; + } + size_t c_start = (p_start.row == row) ? p_start.column : 0; + size_t c_end = (p_end.row == row) ? p_end.column : line->len; + if (c_start > line->len) { + c_start = line->len; + } + if (c_end > line->len) { + c_end = line->len; + } + for (size_t c = c_start; c < c_end; c++) { + roles[c] = (uint8_t)role; + } + } +} diff --git a/src/syntax.h b/src/syntax.h new file mode 100644 index 0000000..a5ded7c --- /dev/null +++ b/src/syntax.h @@ -0,0 +1,33 @@ +/* SPDX-FileCopyrightText: AnmiTaliDev + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifndef DAF_SYNTAX_H +#define DAF_SYNTAX_H + +#include +#include +#include + +#include "buffer.h" + +typedef enum { + SYNTAX_ROLE_NONE = 0, + SYNTAX_ROLE_KEYWORD, + SYNTAX_ROLE_STRING, + SYNTAX_ROLE_COMMENT, + SYNTAX_ROLE_TYPE, + SYNTAX_ROLE_FUNCTION, + SYNTAX_ROLE_NUMBER, + SYNTAX_ROLE_OPERATOR, +} syntax_role_t; + +typedef struct syntax_highlighter_s syntax_highlighter_t; + +syntax_highlighter_t *syntax_create(const char *filetype); +void syntax_destroy(syntax_highlighter_t *hl); + +void syntax_update(syntax_highlighter_t *hl, const buffer_t *buf); +void syntax_get_line_roles(syntax_highlighter_t *hl, size_t row, const line_t *line, uint8_t *roles); + +#endif diff --git a/src/theme.c b/src/theme.c index f29f2fc..16c0c57 100644 --- a/src/theme.c +++ b/src/theme.c @@ -28,6 +28,13 @@ static const theme_t k_theme_dark = { .status_bar = "\x1b[38;5;234;48;5;250m", .tilde = "\x1b[38;5;238;48;5;234m", .reset = "\x1b[0m", + .syntax_keyword = "\x1b[38;5;204;48;5;234m", + .syntax_string = "\x1b[38;5;114;48;5;234m", + .syntax_comment = "\x1b[38;5;244;48;5;234m", + .syntax_type = "\x1b[38;5;75;48;5;234m", + .syntax_function = "\x1b[38;5;186;48;5;234m", + .syntax_number = "\x1b[38;5;173;48;5;234m", + .syntax_operator = "\x1b[38;5;250;48;5;234m", }; static const theme_t k_theme_light = { @@ -40,6 +47,13 @@ static const theme_t k_theme_light = { .status_bar = "\x1b[38;5;255;48;5;60m", .tilde = "\x1b[38;5;250;48;5;255m", .reset = "\x1b[0m", + .syntax_keyword = "\x1b[38;5;161;48;5;255m", + .syntax_string = "\x1b[38;5;28;48;5;255m", + .syntax_comment = "\x1b[38;5;245;48;5;255m", + .syntax_type = "\x1b[38;5;25;48;5;255m", + .syntax_function = "\x1b[38;5;94;48;5;255m", + .syntax_number = "\x1b[38;5;166;48;5;255m", + .syntax_operator = "\x1b[38;5;240;48;5;255m", }; static const theme_t k_theme_mono = { @@ -52,6 +66,13 @@ static const theme_t k_theme_mono = { .status_bar = "\x1b[7m", .tilde = "", .reset = "\x1b[0m", + .syntax_keyword = "", + .syntax_string = "", + .syntax_comment = "", + .syntax_type = "", + .syntax_function = "", + .syntax_number = "", + .syntax_operator = "", }; const theme_t *theme_find(const char *name) @@ -142,6 +163,20 @@ static void apply_role(theme_t *custom, const char *key, char *sgr) custom->status_bar = sgr; } else if (strcmp(key, "tilde") == 0) { custom->tilde = sgr; + } else if (strcmp(key, "keyword") == 0) { + custom->syntax_keyword = sgr; + } else if (strcmp(key, "string") == 0) { + custom->syntax_string = sgr; + } else if (strcmp(key, "comment") == 0) { + custom->syntax_comment = sgr; + } else if (strcmp(key, "type") == 0) { + custom->syntax_type = sgr; + } else if (strcmp(key, "function") == 0) { + custom->syntax_function = sgr; + } else if (strcmp(key, "number") == 0) { + custom->syntax_number = sgr; + } else if (strcmp(key, "operator") == 0) { + custom->syntax_operator = sgr; } else { free(sgr); } diff --git a/src/theme.h b/src/theme.h index fce36a2..1600087 100644 --- a/src/theme.h +++ b/src/theme.h @@ -15,6 +15,13 @@ typedef struct { const char *status_bar; const char *tilde; const char *reset; + const char *syntax_keyword; + const char *syntax_string; + const char *syntax_comment; + const char *syntax_type; + const char *syntax_function; + const char *syntax_number; + const char *syntax_operator; } theme_t; const theme_t *theme_find(const char *name); diff --git a/tests/meson.build b/tests/meson.build index 9fbf216..532d692 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -1,6 +1,6 @@ test_names = [ 'buffer', 'search', 'encoding', 'filetype', 'editor', 'undo', 'clipboard', - 'replace', 'autoindent', 'theme', 'config', + 'replace', 'autoindent', 'theme', 'config', 'syntax', ] foreach name : test_names @@ -9,6 +9,7 @@ foreach name : test_names 'test_' + name + '.c', include_directories: daf_inc, link_with: daf_lib, + dependencies: [tree_sitter_dep, dl_dep], ) test(name, test_exe) endforeach diff --git a/tests/test_syntax.c b/tests/test_syntax.c new file mode 100644 index 0000000..e7e2254 --- /dev/null +++ b/tests/test_syntax.c @@ -0,0 +1,79 @@ +/* SPDX-FileCopyrightText: AnmiTaliDev + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include +#include + +#include "editor.h" +#include "syntax.h" + +static void test_syntax_unsupported_or_null(void) +{ + assert(syntax_create(NULL) == NULL); + assert(syntax_create("UnknownType") == NULL); +} + +static void test_syntax_c_highlight(void) +{ + syntax_highlighter_t *hl = syntax_create("C"); + if (hl == NULL) { + return; + } + + buffer_t buf; + buffer_init(&buf); + + const char *line_text = "int count = 42;"; + buffer_insert_bytes(&buf, 0, 0, line_text, strlen(line_text)); + + syntax_update(hl, &buf); + + uint8_t roles[64]; + memset(roles, 0, sizeof(roles)); + syntax_get_line_roles(hl, 0, &buf.lines[0], roles); + + assert(roles[0] == SYNTAX_ROLE_TYPE); + assert(roles[1] == SYNTAX_ROLE_TYPE); + assert(roles[2] == SYNTAX_ROLE_TYPE); + + assert(roles[12] == SYNTAX_ROLE_NUMBER); + assert(roles[13] == SYNTAX_ROLE_NUMBER); + + buffer_free(&buf); + syntax_destroy(hl); +} + +static void test_editor_syntax_lifecycle(void) +{ + editor_t ed; + editor_init(&ed, 24, 80); + + ed.filetype = "C"; + editor_setup_syntax(&ed); + + const char *code = "return 10;"; + buffer_insert_bytes(&ed.buf, 0, 0, code, strlen(code)); + + if (ed.syntax != NULL) { + syntax_update(ed.syntax, &ed.buf); + + uint8_t roles[32]; + memset(roles, 0, sizeof(roles)); + syntax_get_line_roles(ed.syntax, 0, &ed.buf.lines[0], roles); + + assert(roles[0] == SYNTAX_ROLE_KEYWORD); + assert(roles[5] == SYNTAX_ROLE_KEYWORD); + } + + editor_free(&ed); +} + +int main(void) +{ + test_syntax_unsupported_or_null(); + test_syntax_c_highlight(); + test_editor_syntax_lifecycle(); + return 0; +}