From 782cfc50ef9af042b152502020b0d6c762f5f278 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 19 Oct 2025 21:43:28 +0200 Subject: [PATCH 01/35] add String_Split() and String_Trim() with tests --- src/common/string.c | 46 +++++++++++++++++++++++++++++ src/common/string.h | 10 +++++++ tests/test_string.c | 70 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+) diff --git a/src/common/string.c b/src/common/string.c index 5871844..673f7e8 100644 --- a/src/common/string.c +++ b/src/common/string.c @@ -19,6 +19,7 @@ #include #include #include +#include #include "common/logging.h" #include "common/utf8_helper.h" @@ -579,3 +580,48 @@ void String_Shorten(String *str, size_t n) { // changed and lookups would do unnecessary iterations str->multibytes_invalid = true; } + +void String_Trim(String *string) { + if (string->char_count == 0) { + return; + } + StringIterator it = StringIterator_FromString(string); + size_t start = 0; + while (StringIterator_Next(&it)) { + if (!isspace(it.current[0])) { + break; + } + } + start = it.char_index; + String_Set(string, String_Substring(string, start, string->char_count - start)); + it = StringIterator_FromString(string); + size_t last_char_index = string->char_count - 1; + while (StringIterator_Next(&it)) { + if (!isspace(it.current[0])) { + last_char_index = it.char_index; + } + } + String_Shorten(string, last_char_index + 1); +} + +size_t String_Split(String *string, String *delimiter, String *out) { + if (!string || !delimiter || !out) { + return 0; + } + StringIterator it = StringIterator_FromString(string); + size_t count = 0; + size_t start_idx = 0; + while (StringIterator_Next(&it)) { + if (start_idx > it.char_index) { // skip delimiter + continue; + } + if (strncmp(it.current, delimiter->bytes, delimiter->bytes_size) == 0) { + out[count] = String_Substring(string, start_idx, it.char_index - start_idx); + start_idx = it.char_index + delimiter->char_count; // set next start_idx to skip delimiter + count++; + } + } + out[count] = String_Substring(string, start_idx, String_Length(string) - start_idx); + count++; + return count; +} diff --git a/src/common/string.h b/src/common/string.h index 5afa9f5..437ba45 100644 --- a/src/common/string.h +++ b/src/common/string.h @@ -282,5 +282,15 @@ String String_Spaces(size_t n); */ void String_Shorten(String *str, size_t n); +/** + * @brief Trim whitespaces from begin and end of a string. + */ +void String_Trim(String *string); + +/** + * @brief Split a string into an array of StringViews. + */ +size_t String_Split(String *string, String *delimiter, String *out); + #endif \ No newline at end of file diff --git a/tests/test_string.c b/tests/test_string.c index 3cd13e7..d0c5a83 100644 --- a/tests/test_string.c +++ b/tests/test_string.c @@ -172,6 +172,74 @@ void test_misc(void) { TEST_CHECK(strcmp(String_AsCStr(&str), "xyz") == 0); String_Deinit(&str); + + // String_Trim() + str = String_FromCStr(" ", 3); + String_Trim(&str); + TEST_CHECK(strcmp(String_AsCStr(&str), "") == 0); + String_Deinit(&str); + str = String_FromCStr(" Foo bar ", 13); + String_Trim(&str); + TEST_CHECK(strcmp(String_AsCStr(&str), "Foo bar") == 0); + String_Deinit(&str); +} + +void test_split(void) { + String delimiter = String_FromCStr(",", strlen(",")); + String str = String_FromCStr("Foo,bar,€uro,", strlen("Foo,bar,€uro,")); + String list[4]; + size_t count = String_Split(&str, &delimiter, list); + TEST_CHECK(count == 4); + TEST_CHECK(strcmp(String_AsCStr(&list[0]), "Foo") == 0); + TEST_CHECK(strcmp(String_AsCStr(&list[1]), "bar") == 0); + TEST_CHECK(strcmp(String_AsCStr(&list[2]), "€uro") == 0); + TEST_CHECK(strcmp(String_AsCStr(&list[3]), "") == 0); + + for (size_t i=0; i Date: Mon, 20 Oct 2025 20:07:58 +0200 Subject: [PATCH 02/35] add example syntax definition --- data/syntax/ini.ini | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 data/syntax/ini.ini diff --git a/data/syntax/ini.ini b/data/syntax/ini.ini new file mode 100644 index 0000000..dc4d5d3 --- /dev/null +++ b/data/syntax/ini.ini @@ -0,0 +1,43 @@ +# This is a simplified version of the INI file format supported by the projects IniParser module + +[meta] +name = INI +file_extensions = ini + +[block:root] +allowed_blocks = comment, section, keyvalue + +[block:comment] +# comments start with ";" or "#" +start = "^[;#]" +end = "$" +color = 242 # gray +allowed_blocks = + +[block:section] +# section: [section_name] +start = "^[ \t]*\\[.*\\]" +end = "$" +color = 33 # cyan +allowed_blocks = + +[block:keyvalue] +# key = Value +start = "^[ \t]*[^;#= \t][^=]*=" +end = "$" +color = 226 # yellow +allowed_blocks = string + +[block:string] +# string-values +start = "\"" +end = "\"" +color = 82 # green +allowed_blocks = escape_sequence + +[block:escape_sequence] +# escape inside strings +start = "\\\\" +end = "." +color = 208 # orange +allowed_blocks = From 08ce38fcf7099693409de50302da952e14438128 Mon Sep 17 00:00:00 2001 From: Bodo Date: Wed, 22 Oct 2025 06:28:04 +0200 Subject: [PATCH 03/35] Feat/Implement Syntax Definitions (#84) * add SyntaxBlockDef and SyntaxDefinition declarations * add TableIterator (#79) * add TableIterator * change include that provides ssize_t * add additional tests * make current slot pointer const * include stdbool.h for bool * Add table iteration API and improve parsing robustness (#83) * add TableIterator (#79) * add TableIterator * change include that provides ssize_t * add additional tests * make current slot pointer const * include stdbool.h for bool * WIP: implement definition loader * add Table_GetUsage() * allow ':' in INI file key names * fix location of tests * fix typo in Doxygen comment * add NULL guard to String_Take() * Add early-exit guards to table lookup functions (#80) * Fix bug in reading operations on an empty table * add check for empty table also to Table_Delete() * add functiond to create SyntaxDefinition_FromTable() with tests. More tests needed * additional tests (still not enough) * fix: free correct regex in regex_end branch in SyntaxBlockDef_FromTable() * fix: wrong numbe of arguments in String_Format() call in SymtaxBlockDef_FromTable() * add explanation * fix correct recognition of root block * fix memory lealk * add NULL guard for strdup in init_definition() * fix last fix * add additional test --- CMakeLists.txt | 2 + src/common/iniparser.c | 2 +- src/common/iniparser.h | 2 +- src/common/string.c | 3 + src/common/string.h | 2 +- src/common/table.c | 19 ++ src/common/table.h | 4 + src/common/tableiterator.c | 24 +++ src/common/tableiterator.h | 17 ++ src/syntax/definition.c | 316 +++++++++++++++++++++++++++++++++ src/syntax/definition.h | 76 ++++++++ tests/CMakeLists.txt | 7 +- tests/test_syntax_definition.c | 211 ++++++++++++++++++++++ tests/test_table.c | 8 +- tests/test_tableiterator.c | 88 +++++++++ 15 files changed, 771 insertions(+), 10 deletions(-) create mode 100644 src/common/tableiterator.c create mode 100644 src/common/tableiterator.h create mode 100644 src/syntax/definition.c create mode 100644 src/syntax/definition.h create mode 100644 tests/test_syntax_definition.c create mode 100644 tests/test_tableiterator.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bcba1a..57e2577 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -53,6 +53,8 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_OBJECTS}) add_compile_definitions(_XOPEN_SOURCE=700) +# CTest-Unterstützung aktivieren +enable_testing() # --- Add tools --- add_subdirectory(tools) diff --git a/src/common/iniparser.c b/src/common/iniparser.c index 29c40e2..ada7601 100644 --- a/src/common/iniparser.c +++ b/src/common/iniparser.c @@ -142,7 +142,7 @@ static bool is_digit(char ch) { } static bool is_key_char(char ch) { - return is_letter(ch) || is_digit(ch) || ch == '.' || ch == '-' || ch == '_'; + return is_letter(ch) || is_digit(ch) || ch == '.' || ch == '-' || ch == '_' || ch == ':'; } static bool is_at_end(const IniParser *parser) { diff --git a/src/common/iniparser.h b/src/common/iniparser.h index b5fd734..3049065 100644 --- a/src/common/iniparser.h +++ b/src/common/iniparser.h @@ -57,7 +57,7 @@ ::= ' ' | '\t' ::= [a-zA-Z] - ::= | | '.' | '-' | '_' + ::= | | '.' | '-' | '_' | ':' ::= * *****************************************/ diff --git a/src/common/string.c b/src/common/string.c index a3220ac..941ebe2 100644 --- a/src/common/string.c +++ b/src/common/string.c @@ -316,6 +316,9 @@ void String_Set(String *str, String src) { } void String_Take(String *dst, String *src) { + if (!dst || ! src) { + return; + } String_Deinit(dst); *dst = String_TakeCStr(src->bytes); diff --git a/src/common/string.h b/src/common/string.h index b9dfd65..88eede7 100644 --- a/src/common/string.h +++ b/src/common/string.h @@ -178,7 +178,7 @@ void String_Set(String *str, String src); * * Its for situation where dst is already initialized and your want to fill it * with the content of another string without moving memory around. - * dst will be deinitialized! + * src will be deinitialized! */ void String_Take(String *dst, String *src); diff --git a/src/common/table.c b/src/common/table.c index 3dd739e..9b78e30 100644 --- a/src/common/table.c +++ b/src/common/table.c @@ -220,6 +220,9 @@ void *Table_Get(const Table *table, const char *key) { if (!table) { logFatal("Invalid table in Table_Get()."); } + if (table->used == 0) { + return NULL; + } if (!key) { logWarn("Table_Get() was called with key == NULL."); return NULL; @@ -236,6 +239,9 @@ void Table_Delete(Table *table, const char *key) { if (!table) { logFatal("Invalid table in Table_Delete()."); } + if (table->used == 0) { + return; + } if (!key) { logWarn("Table_Delete() is called with key == NULL."); return; @@ -255,6 +261,9 @@ bool Table_Has(const Table *table, const char *key) { if (!table) { logFatal("Invalid table in Table_Has()."); } + if (table->used == 0) { + return false; + } if (!key) { logWarn("Table_Has() was called with key == NULL."); return false; @@ -267,6 +276,9 @@ bool Table_HasOwnership(const Table *table, const char *key) { if (!table) { logFatal("Invalid table in Table_HasOwnership()."); } + if (table->used == 0) { + return false; + } if (!key) { logWarn("Table_HasOwnership() was called with key == NULL."); return false; @@ -274,3 +286,10 @@ bool Table_HasOwnership(const Table *table, const char *key) { TableSlot *slot = find_slot(table, key); return (slot->state == TABLE_SLOT_USED && slot->destructor != NULL); } + +size_t Table_GetUsage(const Table *table) { + if (!table) { + return 0; + } + return table->used; +} \ No newline at end of file diff --git a/src/common/table.h b/src/common/table.h index a4c916a..1ec71ee 100644 --- a/src/common/table.h +++ b/src/common/table.h @@ -97,5 +97,9 @@ bool Table_Has(const Table *table, const char *key); */ bool Table_HasOwnership(const Table *table, const char *key); +/** + * @brief Return the current number of non-free table slots. + */ +size_t Table_GetUsage(const Table *table); #endif \ No newline at end of file diff --git a/src/common/tableiterator.c b/src/common/tableiterator.c new file mode 100644 index 0000000..538d6d3 --- /dev/null +++ b/src/common/tableiterator.c @@ -0,0 +1,24 @@ +#include "tableiterator.h" + +TableIterator TableIterator_Begin(const Table *table) { + return (TableIterator){ + .table = table, + .index = -1, + .current = NULL + }; +} + +bool TableIterator_Next(TableIterator *it) { + if (!it || !it->table) { + return false; + } + for (size_t i = (size_t)(it->index + 1); i < it->table->capacity; i++) { + TableSlot *slot = &it->table->slots[i]; + if (slot->state == TABLE_SLOT_USED) { + it->index = i; + it->current = slot; + return true; + } + } + return false; +} diff --git a/src/common/tableiterator.h b/src/common/tableiterator.h new file mode 100644 index 0000000..bf7f06c --- /dev/null +++ b/src/common/tableiterator.h @@ -0,0 +1,17 @@ +#ifndef TABLEITERATOR_H +#define TABLEITERATOR_H + +#include +#include +#include "table.h" + +typedef struct _TableIterator { + const Table *table; + ssize_t index; + const TableSlot *current; +} TableIterator; + +TableIterator TableIterator_Begin(const Table *table); +bool TableIterator_Next(TableIterator *it); + +#endif \ No newline at end of file diff --git a/src/syntax/definition.c b/src/syntax/definition.c new file mode 100644 index 0000000..04fa511 --- /dev/null +++ b/src/syntax/definition.c @@ -0,0 +1,316 @@ +#include "definition.h" +#include +#include +#include +#include "common/logging.h" +#include "common/typedtable.h" +#include "common/tableiterator.h" + + +#define NO_ERROR ((SyntaxDefinitionError){ .code = SYNTAXDEFINITION_NO_ERROR, .message = String_Empty() }) +#define ERROR(err_code, ...) ((SyntaxDefinitionError){ .code = err_code, .message = String_Format(__VA_ARGS__) }) + +static void set_error(SyntaxDefinitionError *error, SyntaxDefinitionErrorCode code, String message) { + error->code = code; + logError("Error in SyntaxDefinition: %s", String_AsCStr(&message)); + String_Take(&error->message, &message); +} + + +void SyntaxDefinitionError_Deinit(SyntaxDefinitionError *error) { + String_Deinit(&error->message); +} + + +SyntaxBlockDef *SyntaxBlockDef_Create() { + SyntaxBlockDef *block = malloc(sizeof(SyntaxBlockDef)); + if (!block) { + logFatal("Cannot allocate memory for SyntaxBlockDef."); + } + block->name = NULL; + block->only_start = false; + block->children = NULL; + block->children_count = 0; + return block; +} + + +void SyntaxBlockDef_Destroy(SyntaxBlockDef *block) { + if (block->children) { + free(block->children); + } + if (block->name) { + free(block->name); + } + regfree(&block->start); + if (!block->only_start) { + regfree(&block->end); + } + free(block); +} + + +SyntaxBlockDef *SyntaxBlockDef_FromTable(const char *name, const Table *table, SyntaxDefinitionError *error) { + *error = NO_ERROR; + const char *start_regex = TypedTable_GetString(table, "start"); + const char *end_regex = TypedTable_GetString(table, "end"); + + SyntaxBlockDef *block = SyntaxBlockDef_Create(); + block->name = strdup(name); + block->only_start = true; + block->color = (uint8_t)TypedTable_GetNumber(table, "color"); + + if (!start_regex) { + set_error(error, + SYNTAXDEFINITION_BLOCK_NO_START_REGEX, + String_Format("Block \"%s\" has no start regex defined.", name) + ); + free(block->name); + free(block); + return NULL; + } + int ret = regcomp(&block->start, start_regex, REG_EXTENDED); + if (ret != 0) { + char errbuf[256]; + regerror(ret, &block->start, errbuf, sizeof(errbuf)); + set_error(error, + SYNTAXDEFINITION_REGEX_ERROR_START, + String_Format("Error in start regex \"%s\" in block \"%s\": %s", start_regex, name, errbuf) + ); + free(block->name); + free(block); + return NULL; + } + if (end_regex) { + ret = regcomp(&block->end, end_regex, REG_EXTENDED); + if (ret != 0) { + char errbuf[256]; + regerror(ret, &block->end, errbuf, sizeof(errbuf)); + set_error(error, + SYNTAXDEFINITION_REGEX_ERROR_END, + String_Format("Error in end regex \"%s\" in block \"%s\": %s", start_regex, name, errbuf) + ); + block->only_start = true; // must be set that SyntaxBlockDef_Destroy() does not try to free end regex + SyntaxBlockDef_Destroy(block); // frees everything including start regex + return NULL; + } + block->only_start = false; + } + + return block; +} + + +// helper struct +typedef struct _table_block_mapping { + const Table *table; + SyntaxBlockDef *block; +} table_block_mapping; + + +static SyntaxDefinitionError init_definition(SyntaxDefinition *def, const Table *table) { + def->name = NULL; + def->blocks = NULL; + def->blocks_count = 0; + def->root = NULL; + + // Get meta table + Table *meta = TypedTable_GetTable(table, "meta"); + if (!meta) { + return ERROR(SYNTAXDEFINITION_NO_META, "No meta section found."); + } + const char *name = TypedTable_GetString(meta, "name"); + def->name = strdup(name ? name : ""); + + // initialize block list + def->blocks = malloc(sizeof(SyntaxBlockDef*) * (Table_GetUsage(table) - 1)); + if (!def->blocks) { + logFatal("Cannot allocate memory for SyntaxDefinition blocks"); + } + + return NO_ERROR; +} + + +static SyntaxDefinitionError build_blocks(SyntaxDefinition *def, const Table *table, Table *blocks) { + // get all block tables and build all block definitions (without children) + TableIterator it = TableIterator_Begin(table); + while (TableIterator_Next(&it)) { + const char *key = it.current->key; + + if (strncmp(key, "block:", 6) == 0) { + const char *block_name = key + 6; + const TypedValue *value = (TypedValue*)it.current->value; + if (value->type != VALUE_TYPE_TABLE) { + // block is no block, but a value or something + return ERROR(SYNTAXDEFINITION_BLOCK_NOT_A_SECTION, "Block \"block:%s\" is not a section.", block_name); + } + if (strlen(block_name) == 0) { + // block name is empty + return ERROR(SYNTAXDEFINITION_BLOCK_NAME_EMPTY, "Empty block name found."); + } + const Table *block_table = value->data.table_value; + + // Create the block (without linking children) + SyntaxDefinitionError error; + SyntaxBlockDef *block = SyntaxBlockDef_FromTable(block_name, block_table, &error); + + if (!block) { + // block definition was invalid + return error; + } + SyntaxDefinitionError_Deinit(&error); + + // set the block in the temporary table + table_block_mapping *mapping = malloc(sizeof(table_block_mapping)); + if (!mapping) { + logFatal("Cannot allocate memory for table_block_mapping."); + } + mapping->table = block_table; + mapping->block = block; + Table_Set(blocks, block_name, mapping, free); + + // add the block to the list of all blocks (important for freeing also unreferenced blocks later) + def->blocks[def->blocks_count++] = block; + + if (strcmp(block_name, "root") == 0) { + def->root = block; + } + } + } + + if (!def->root) { + return ERROR(SYNTAXDEFINITION_NO_ROOT_BLOCK, "No root block defined."); + } + + return NO_ERROR; +} + + +SyntaxDefinitionError link_children(SyntaxDefinition *def, Table *blocks) { + for (size_t i=0; iblocks_count; i++) { + SyntaxBlockDef *block = def->blocks[i]; + + // get the mapping + table_block_mapping *mapping = Table_Get(blocks, block->name); + if (!mapping) { + logFatal("Some serious design flaw detected."); + } + const Table *block_table = mapping->table; + + const char *children_str = TypedTable_GetString(block_table, "allowed_blocks"); + if (!children_str) { + continue; + } + + String s = String_FromCStr(children_str, strlen(children_str)); + ssize_t children_count = 0; + String delimiter = String_FromCStr(",", 1); + StringView *children = String_Split(&s, &delimiter, &children_count); + if (children_count < 0) { + logFatal("Potential rrror in String_Split()."); + } + String_Deinit(&delimiter); + if (children_count == 0) { + String_Deinit(&s); + free(children); + continue; + } + + // malloc memory for the children of SyntaxBlockDef + block->children = malloc(sizeof(SyntaxBlockDef*) * children_count); + if (!block->children) { + logFatal("Cannot allocate memory for children of SyntaxBlockDef."); + } + + for (size_t child_idx=0; child_idx<(size_t)children_count; child_idx++) { + String child_name = String_FromView(children[child_idx]); + String_Trim(&child_name); + table_block_mapping *child_mapping = Table_Get(blocks, child_name.bytes); + + if (!child_mapping) { + // no block with the given child name + // something like + // allowed_blocks = block1, non_existing_block + SyntaxDefinitionError error = ERROR( + SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST, + "Block \"%s\" defines non-existing block \"%s\" as it's child.", block->name, child_name.bytes + ); + String_Deinit(&child_name); + String_Deinit(&s); + free(children); + return error; + } + String_Deinit(&child_name); + block->children[child_idx] = child_mapping->block; + } + block->children_count = children_count; + String_Deinit(&s); + free(children); + } + return NO_ERROR; +} + + +SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitionError *error) { + if (!table) { + return NULL; + } + + SyntaxDefinition *def = malloc(sizeof(SyntaxDefinition)); + if (!def) { + logFatal("Cannot allocate memory for SyntaxDefinition."); + } + + // Initialize SyntaxDefinition instance + *error = init_definition(def, table); + if (error->code != SYNTAXDEFINITION_NO_ERROR) { + SyntaxDefinition_Destroy(def); + return NULL; + } + SyntaxDefinitionError_Deinit(error); + + // temporary table mapping all block names to it's SyntaxBlockDef + Table *blocks = Table_Create(); + *error = build_blocks(def, table, blocks); + if (error->code != SYNTAXDEFINITION_NO_ERROR) { + SyntaxDefinition_Destroy(def); + Table_Destroy(blocks); + return NULL; + } + SyntaxDefinitionError_Deinit(error); + + // construct the children lists + *error = link_children(def, blocks); + if (error->code != SYNTAXDEFINITION_NO_ERROR) { + SyntaxDefinition_Destroy(def); + Table_Destroy(blocks); + return NULL; + } + SyntaxDefinitionError_Deinit(error); + + // cleanup + Table_Destroy(blocks); + + return def; +} + +void SyntaxDefinition_Destroy(SyntaxDefinition *def) { + if (!def) { + return; + } + if (def->name) { + free(def->name); + } + if (def->blocks) { + for (size_t i=0; iblocks_count; i++) { + SyntaxBlockDef_Destroy(def->blocks[i]); + } + free(def->blocks); + } + free(def); +} + + +#undef NO_ERROR +#undef ERROR \ No newline at end of file diff --git a/src/syntax/definition.h b/src/syntax/definition.h new file mode 100644 index 0000000..aa1cacb --- /dev/null +++ b/src/syntax/definition.h @@ -0,0 +1,76 @@ +#ifndef SYNTAX_DEFINITION_H +#define SYNTAX_DEFINITION_H + +#include +#include +#include "common/table.h" +#include "common/string.h" + + +typedef enum { + SYNTAXDEFINITION_NO_ERROR, + SYNTAXDEFINITION_BLOCK_NO_START_REGEX, + SYNTAXDEFINITION_REGEX_ERROR_START, + SYNTAXDEFINITION_REGEX_ERROR_END, + SYNTAXDEFINITION_NO_META, + SYNTAXDEFINITION_BLOCK_NOT_A_SECTION, + SYNTAXDEFINITION_BLOCK_NAME_EMPTY, + SYNTAXDEFINITION_NO_ROOT_BLOCK, + SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST +} SyntaxDefinitionErrorCode; + +/** + * @brief Holds error information. + */ +typedef struct _SyntaxDefinitionError { + SyntaxDefinitionErrorCode code; + String message; +} SyntaxDefinitionError; + +void SyntaxDefinitionError_Deinit(SyntaxDefinitionError *error); + + +/** + * @brief Holds the definition of a syntax block. + */ +typedef struct _SyntaxBlockDef { + char *name; //< name of the block definition. for easier debugging + regex_t start; //< compiled regex to determine the begin of the block + regex_t end; //< compiled regex to determine the end of the block (optional) + + bool only_start; //< if true only start is tested and no children are allowed + + struct _SyntaxBlockDef **children; //< list of children definition which are allowed inside the block + size_t children_count; //< number of children + + uint8_t color; //< the color to render the block +} SyntaxBlockDef; + +SyntaxBlockDef *SyntaxBlockDef_Create(); +void SyntaxBlockDef_Destroy(SyntaxBlockDef *block); +SyntaxBlockDef *SyntaxBlockDef_FromTable(const char *name, const Table *table, SyntaxDefinitionError *error); + + +/** + * @brief Holds a complete syntax definition. + */ +typedef struct _SyntaxDefinition { + char *name; //< name of the filetype (e.g. INI) + SyntaxBlockDef *root; //< root block definition + SyntaxBlockDef **blocks; //< list of all blocks + size_t blocks_count; //< number of blocks +} SyntaxDefinition; + + +/** + * @brief Constructs a SyntaxDefinition from a typed Table. + * + * @param table A typed Table (see common/typedtable.h) holding the syntax definition. + * + * @returns + * A SyntaxDefinition or NULL if the definition in table is not valid. + */ +SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitionError *error); +void SyntaxDefinition_Destroy(SyntaxDefinition *def); + +#endif \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9a75117..214670b 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,8 +1,3 @@ - -# --- Test-Konfiguration --- -# CTest-Unterstützung aktivieren -enable_testing() - # Finde alle Testdateien file(GLOB TEST_SOURCES "test_*.c") @@ -26,6 +21,6 @@ foreach(TEST_SOURCE_FILE ${TEST_SOURCES}) target_link_libraries(${TEST_NAME} PRIVATE ${TEST_LINK_OBJECTS}) # Den Test zu CTest hinzufügen, damit er mit `ctest` ausgeführt werden kann - add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) + add_test(NAME ${TEST_NAME} COMMAND $) endforeach() \ No newline at end of file diff --git a/tests/test_syntax_definition.c b/tests/test_syntax_definition.c new file mode 100644 index 0000000..3be386b --- /dev/null +++ b/tests/test_syntax_definition.c @@ -0,0 +1,211 @@ +#include "acutest.h" +#include "syntax/definition.h" +#include "common/iniparser.h" + + + +void test_block_simple(void) { + Table *block_table = Table_Create(); + TypedTable_SetStringCopy(block_table, "start", ".*"); + TypedTable_SetNumber(block_table, "color", 10); + SyntaxDefinitionError error; + SyntaxBlockDef *block = SyntaxBlockDef_FromTable("test", block_table, &error); + TEST_CHECK(block != NULL); + TEST_CHECK(block->name != NULL); + TEST_CHECK(strcmp(block->name, "test") == 0); + TEST_CHECK(block->start.re_nsub == 0); + TEST_CHECK(block->color == 10); + SyntaxBlockDef_Destroy(block); + Table_Destroy(block_table); + SyntaxDefinitionError_Deinit(&error); +} + +void test_block_with_end(void) { + Table *block_table = Table_Create(); + TypedTable_SetStringCopy(block_table, "start", ".*"); + TypedTable_SetStringCopy(block_table, "end", ".*"); + TypedTable_SetNumber(block_table, "color", 10); + SyntaxDefinitionError error; + SyntaxBlockDef *block = SyntaxBlockDef_FromTable("test", block_table, &error); + TEST_CHECK(block != NULL); + TEST_CHECK(block->name != NULL); + TEST_CHECK(strcmp(block->name, "test") == 0); + TEST_CHECK(block->start.re_nsub == 0); + TEST_CHECK(block->color == 10); + SyntaxBlockDef_Destroy(block); + Table_Destroy(block_table); + SyntaxDefinitionError_Deinit(&error); +} + +void test_block_errors(void) { + Table *block_table = Table_Create(); + + // no start regex + SyntaxDefinitionError error; + SyntaxBlockDef *block = SyntaxBlockDef_FromTable("test", block_table, &error); + TEST_CHECK(block == NULL); + TEST_CHECK(error.code == SYNTAXDEFINITION_BLOCK_NO_START_REGEX); + SyntaxDefinitionError_Deinit(&error); + + // error in start regex + TypedTable_SetStringCopy(block_table, "start", "(\\("); + block = SyntaxBlockDef_FromTable("test", block_table, &error); + TEST_CHECK(block == NULL); + TEST_CHECK(error.code == SYNTAXDEFINITION_REGEX_ERROR_START); + SyntaxDefinitionError_Deinit(&error); + + // error in end regex + TypedTable_SetStringCopy(block_table, "start", ".*"); + TypedTable_SetStringCopy(block_table, "end", "(\\("); + block = SyntaxBlockDef_FromTable("test", block_table, &error); + TEST_CHECK(block == NULL); + TEST_CHECK(error.code == SYNTAXDEFINITION_REGEX_ERROR_END); + SyntaxDefinitionError_Deinit(&error); + + Table_Destroy(block_table); +} + +Table *table_from_ini(const char *ini_str) { + IniParser parser; + IniParser_Init(&parser); + IniParser_SetText(&parser, ini_str); + Table *table = IniParser_Parse(&parser); + TEST_ASSERT(table != NULL); + IniParser_Deinit(&parser); + return table; +} + +void test_minimal_definition(void) { + const char *ini = + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "start=\".*\"\n"; + Table *table = table_from_ini(ini); + SyntaxDefinitionError error; + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error); + TEST_CHECK(def != NULL); + TEST_CHECK(def->name != NULL); + TEST_CHECK(strcmp(def->name, "MINI") == 0); + SyntaxDefinitionError_Deinit(&error); + SyntaxDefinition_Destroy(def); + Table_Destroy(table); +} + +SyntaxDefinitionErrorCode test_error(const char *ini) { + Table *table = table_from_ini(ini); + SyntaxDefinitionError error; + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error); + TEST_ASSERT(def == NULL); + Table_Destroy(table); + SyntaxDefinitionErrorCode code = error.code; + SyntaxDefinitionError_Deinit(&error); + return code; +} + +typedef struct { + const char *ini; + SyntaxDefinitionErrorCode error_code; +} error_test_case; + +void test_errors(void) { + error_test_case cases[] = { + { + "[met]\n" + "name = MINI\n" + "[block:root]\n" + "start=\".*\"\n", + SYNTAXDEFINITION_NO_META + }, + { + "[meta]\n" + "name = MINI\n", + SYNTAXDEFINITION_NO_ROOT_BLOCK + }, + { + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "start=\".*\"\n" + "[block:]\n" + "start=\".*\"\n", + SYNTAXDEFINITION_BLOCK_NAME_EMPTY + }, + + { + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "start=\".*)(\"\n", + SYNTAXDEFINITION_REGEX_ERROR_START + }, + { + "block:root = foo\n" + "[meta]\n" + "name = MINI\n", + SYNTAXDEFINITION_BLOCK_NOT_A_SECTION + }, + { + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "start=.*\n" + "allowed_blocks=foo", + SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST + }, + { + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "start=.*\n" + "allowed_blocks=foo, bar\n" + "[block:foo]\n" + "start=.*\n", + SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST + }, + }; + + int cases_num = sizeof(cases) / sizeof(cases[0]); + + for (int i=0; iblocks_count == 3); + TEST_CHECK(def->root != NULL); + TEST_CHECK(def->root->children_count == 2); + + Table_Destroy(table); + SyntaxDefinition_Destroy(def); + SyntaxDefinitionError_Deinit(&error); +} + + +TEST_LIST = { + { "SyntaxDefinition: Block Simple", test_block_simple }, + { "SyntaxDefinitions: Block with End", test_block_with_end }, + { "SyntaxDefinition: Block Errors", test_block_errors }, + { "SyntaxDefinition: Minimal Example", test_minimal_definition }, + { "SyntaxDefinition: Errors", test_errors }, + { "SyntaxDefinition: Children", test_children }, + { NULL, NULL } +}; diff --git a/tests/test_table.c b/tests/test_table.c index b967aa0..4cc41c1 100644 --- a/tests/test_table.c +++ b/tests/test_table.c @@ -105,7 +105,13 @@ void test_edge_case(void) { } Table_Get(table, "key0"); - + Table_Destroy(table); + + // checks on empty table + table = Table_Create(); + TEST_CHECK(!Table_Get(table, "key")); + TEST_CHECK(!Table_Has(table, "key")); + TEST_CHECK(!Table_HasOwnership(table, "key")); Table_Destroy(table); } diff --git a/tests/test_tableiterator.c b/tests/test_tableiterator.c new file mode 100644 index 0000000..ae1aca0 --- /dev/null +++ b/tests/test_tableiterator.c @@ -0,0 +1,88 @@ +#include "acutest.h" +#include "common/tableiterator.h" +#include "common/table.h" + +void fill_table(const char **key, const char **values, size_t count) { + Table *table = Table_Create(); + + for (size_t i=0; istate == TABLE_SLOT_USED); + slots++; + } + TEST_CHECK(slots == count); + Table_Destroy(table); +} + +void test_standard(void) { + const char *keys[] = { + "key0", + "key1", + "key2", + "key3" + }; + + const char *values[] = { + "value0", + "value1", + "value2", + "value3" + }; + size_t count = sizeof(keys) / sizeof(keys[0]); + + fill_table(keys, values, count); + fill_table(keys, values, 0); + fill_table(keys, values, 1); +} + +void test_iterating_twice(void) { + Table *table = Table_Create(); + Table_Set(table, "key", "value", NULL); + Table_Set(table, "key2", "value2", NULL); + Table_Set(table, "key3", "value3", NULL); + + TableIterator it = TableIterator_Begin(table); + size_t count = 0; + while (TableIterator_Next(&it)) { + count++; + } + TEST_CHECK(count == 3); + + // run second time without reinitializing + count = 0; + while (TableIterator_Next(&it)) { + count++; + } + TEST_CHECK(count == 0); + + // run again with reinitializing + it = TableIterator_Begin(table); + count = 0; + while (TableIterator_Next(&it)) { + count++; + } + TEST_CHECK(count == 3); + + Table_Destroy(table); +} + +void test_edgecases(void) { + TableIterator it = TableIterator_Begin(NULL); + TEST_CHECK(!TableIterator_Next(&it)); + + TEST_CHECK(!TableIterator_Next(NULL)); +} + + +TEST_LIST = { + { "TableIterator: Standard", test_standard }, + { "TableIterator: Iterating twice", test_iterating_twice }, + { "TableIterator: Edgecases", test_edgecases }, + { NULL, NULL } +}; + From 8cbe14ec84a8dec0ade770e7ff3ed697447b8d23 Mon Sep 17 00:00:00 2001 From: defname Date: Wed, 22 Oct 2025 12:39:45 +0200 Subject: [PATCH 04/35] add documentation --- src/syntax/definition.c | 18 +++++++++- src/syntax/definition.h | 63 ++++++++++++++++++++++++++++++++-- tests/test_syntax_definition.c | 2 -- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/src/syntax/definition.c b/src/syntax/definition.c index 04fa511..cf83044 100644 --- a/src/syntax/definition.c +++ b/src/syntax/definition.c @@ -1,3 +1,19 @@ +/* Copyright (C) 2025 defname + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + #include "definition.h" #include #include @@ -7,7 +23,7 @@ #include "common/tableiterator.h" -#define NO_ERROR ((SyntaxDefinitionError){ .code = SYNTAXDEFINITION_NO_ERROR, .message = String_Empty() }) +#define NO_ERROR ((SyntaxDefinitionError){ .code = SYNTAXDEFINITION_NO_ERROR, .message = (String){0} }) #define ERROR(err_code, ...) ((SyntaxDefinitionError){ .code = err_code, .message = String_Format(__VA_ARGS__) }) static void set_error(SyntaxDefinitionError *error, SyntaxDefinitionErrorCode code, String message) { diff --git a/src/syntax/definition.h b/src/syntax/definition.h index aa1cacb..c81e0ce 100644 --- a/src/syntax/definition.h +++ b/src/syntax/definition.h @@ -1,3 +1,49 @@ +/* Copyright (C) 2025 defname + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + + /** + * @file definition.h + * @brief Module for creating and managing SyntaxDefinition instances. + * + * This module constructs `SyntaxDefinition` objects from the output of the + * IniParser module. + * + * The INI file must contain a `[meta]` section and one or more block sections + * named `block:`. At least one block named `root` must exist. + * + * ### Minimal example + * ``` + * [meta] + * name = TEST + * + * [block:root] + * start = . + * ``` + * + * Each block must define a `start` regex (an extended POSIX regular expression). + * Optionally, a block may also define an `end` regex. + * + * Blocks without an `end` expression represent single tokens or patterns that + * cannot contain nested elements (for example, keywords or inline constructs). + * + * Blocks that define an `end` expression may additionally define a list of + * `allowed_blocks`, which specifies which block types are permitted to appear + * inside the block. + */ + #ifndef SYNTAX_DEFINITION_H #define SYNTAX_DEFINITION_H @@ -27,6 +73,11 @@ typedef struct _SyntaxDefinitionError { String message; } SyntaxDefinitionError; +/** + * @brief Deinitialize an error. + * + * This only needs to be called if `SyntaxDefinition_FromTable()` returns `NULL`. + */ void SyntaxDefinitionError_Deinit(SyntaxDefinitionError *error); @@ -61,16 +112,24 @@ typedef struct _SyntaxDefinition { size_t blocks_count; //< number of blocks } SyntaxDefinition; - + /** * @brief Constructs a SyntaxDefinition from a typed Table. * - * @param table A typed Table (see common/typedtable.h) holding the syntax definition. + * @param table A typed Table (see common/typedtable.h) holding the syntax definition (should be the result of `IniParser`). + * @param error A pointer to an uninitialized `SyntaxDefinitionError` strcut. * * @returns * A SyntaxDefinition or NULL if the definition in table is not valid. + * + * If an error occures `error`contains an error code and a message. In the case of an error + * `error` need to be deinitialized with `SyntaxDefinitionError_Deinit()`. */ SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitionError *error); + +/** + * @brief Deinitialize and destroy def. + */ void SyntaxDefinition_Destroy(SyntaxDefinition *def); #endif \ No newline at end of file diff --git a/tests/test_syntax_definition.c b/tests/test_syntax_definition.c index 3be386b..fa97531 100644 --- a/tests/test_syntax_definition.c +++ b/tests/test_syntax_definition.c @@ -17,7 +17,6 @@ void test_block_simple(void) { TEST_CHECK(block->color == 10); SyntaxBlockDef_Destroy(block); Table_Destroy(block_table); - SyntaxDefinitionError_Deinit(&error); } void test_block_with_end(void) { @@ -34,7 +33,6 @@ void test_block_with_end(void) { TEST_CHECK(block->color == 10); SyntaxBlockDef_Destroy(block); Table_Destroy(block_table); - SyntaxDefinitionError_Deinit(&error); } void test_block_errors(void) { From 59c477a18e191dea193574f3ace26e1216b030d3 Mon Sep 17 00:00:00 2001 From: Bodo Date: Thu, 23 Oct 2025 09:03:35 +0200 Subject: [PATCH 05/35] Feat/Syntax Highlighting: Add module to generate Highlighting information (#85) * add Stack_Copy() and Stack_IsEmpty() * fix: return copy in Stack_Copy() * implement syntax highlighting * add Table_CreateCustom() and Table_CreatePtr() * fix minor typing bugs * add Stack_Create(), Stack_Destroy() * fix but in find_first_child to actually return the child with the first occurence * automatically add patterns to end only_start blocks and to start/end root * fix memory leak produced in last commit * fix tests * fix bug in SyntaxHighllighting_HighlightString(): check if child or block end comes first * add basic tests * use root block it open_blocks parameter is NULL in SyntaxHighlighting_HighlightString() * add random tests * handle src->capacity == 0 in Stack_Copy() * add NULL pointer guard before using the key_free_func() function pointer in the Table module * fix error message for end_regex compilation error in SyntaxBlockDef_FromTable() * correct inlcudes * fix typo * add missing includes * change examples in documentation * change "key == NULL" to "!key" for non pointer keys * do not shadow parameter match in find_first_block() * change first parameter name in SyntaxHighlighting_HighlightString() for consitency --- src/common/stack.c | 51 +++++ src/common/stack.h | 7 + src/common/table.c | 75 +++++-- src/common/table.h | 44 +++- src/syntax/definition.c | 22 +- src/syntax/definition.h | 20 +- src/syntax/highlighting.c | 207 ++++++++++++++++++ src/syntax/highlighting.h | 108 ++++++++++ tests/test_syntax_definition.c | 11 + tests/test_syntax_highlighting.c | 354 +++++++++++++++++++++++++++++++ tests/test_table.c | 13 ++ 11 files changed, 881 insertions(+), 31 deletions(-) create mode 100644 src/syntax/highlighting.c create mode 100644 src/syntax/highlighting.h create mode 100644 tests/test_syntax_highlighting.c diff --git a/src/common/stack.c b/src/common/stack.c index 63f7741..2e8eb0f 100644 --- a/src/common/stack.c +++ b/src/common/stack.c @@ -14,8 +14,10 @@ * along with this program. If not, see . */ #include "stack.h" +#include #include "logging.h" + static void increase_capacity(Stack *stack) { if (!stack) { return; @@ -49,6 +51,51 @@ void Stack_Deinit(Stack *stack) { stack->capacity = 0; } +Stack *Stack_Create() { + Stack *stack = malloc(sizeof(Stack)); + if (!stack) { + logFatal("Failed to allocate memory for stack."); + } + Stack_Init(stack); + return stack; +} + +void Stack_Destroy(Stack *stack) { + if (!stack) { + return; + } + Stack_Deinit(stack); + free(stack); +} + +Stack *Stack_Copy(const Stack *src) { + if (!src) { + return NULL; + } + Stack *copy = malloc(sizeof(Stack)); + if (!copy) { + logFatal("Failed to allocate memory for stack copy."); + } + + size_t cap = src->capacity ? src->capacity : STACK_INITIAL_CAPACITY; // handle capacity == 0 + + // allocate memory + copy->items = malloc(cap * sizeof(void *)); + if (!copy->items) { + logFatal("Failed to allocate memory for stack copy items."); + } + // only copy items + memcpy(copy->items, src->items, src->size * sizeof(void *)); + // set remainder to 0 + if (cap > src->size) { + memset(copy->items + src->size, 0, (cap - src->size) * sizeof(void *)); + } + copy->size = src->size; + copy->capacity = src->capacity; + + return copy; +} + void Stack_Push(Stack *stack, void *item) { if (!stack) { return; @@ -86,6 +133,10 @@ bool Stack_Has(const Stack *stack, const void *item) { return false; } +bool Stack_IsEmpty(const Stack *stack) { + return stack->size == 0; +} + void Stack_Clear(Stack *stack) { if (!stack) { return; diff --git a/src/common/stack.h b/src/common/stack.h index f982e4f..1e0b09c 100644 --- a/src/common/stack.h +++ b/src/common/stack.h @@ -32,12 +32,19 @@ typedef struct _Stack { void Stack_Init(Stack *stack); void Stack_Deinit(Stack *stack); +Stack *Stack_Create(); +void Stack_Destroy(Stack *stack); + +Stack *Stack_Copy(const Stack *stack); + void Stack_Push(Stack *stack, void *item); void *Stack_Pop(Stack *stack); void *Stack_Peek(const Stack *stack); bool Stack_Has(const Stack *stack, const void *item); +bool Stack_IsEmpty(const Stack *stack); + void Stack_Clear(Stack *stack); diff --git a/src/common/table.c b/src/common/table.c index 9b78e30..8608c54 100644 --- a/src/common/table.c +++ b/src/common/table.c @@ -27,12 +27,12 @@ void TableSlot_Init(TableSlot *slot) { slot->state = TABLE_SLOT_EMPTY; } -void TableSlot_Deinit(TableSlot *slot) { +void TableSlot_Deinit(TableSlot *slot, void (*free_key_func)(void *key)) { if (!slot) { return; } - if (slot->key) { - free(slot->key); + if (slot->key && free_key_func) { + free_key_func(slot->key); } if (slot->destructor) { slot->destructor(slot->value); @@ -51,7 +51,20 @@ static uint32_t hash_string(const char *str) { return hash; } -static TableSlot *find_slot(const Table *table, const char *key) { +static uint32_t hash_ptr(const void *p) { + uintptr_t x = (uintptr_t)p; + return (uint32_t)(x ^ (x >> 32)); +} + +static int cmp_ptr(const void *a, const void *b) { + return a != b; +} + +static void *cpy_ptr(const void *p) { + return (void*)p; +} + +static TableSlot *find_slot(const Table *table, const void *key) { if (!table || !key) { logFatal("Invalid table or key in find_slot()"); } @@ -59,7 +72,7 @@ static TableSlot *find_slot(const Table *table, const char *key) { if (table->used >= table->capacity * TABLE_MAX_LOAD_FACTOR + 1) { logFatal("Table too loaded."); // this can only happen there is an error in the code } - uint32_t hash = hash_string(key); + uint32_t hash = table->hash_func(key); size_t index = hash % table->capacity; TableSlot *tombstone = NULL; for (;;) { @@ -73,7 +86,7 @@ static TableSlot *find_slot(const Table *table, const char *key) { else if (slot->state == TABLE_SLOT_TOMBSTONE && tombstone == NULL) { tombstone = slot; } - else if (slot->state == TABLE_SLOT_USED && strcmp(slot->key, key) == 0) { + else if (slot->state == TABLE_SLOT_USED && table->key_cmp_func(slot->key, key) == 0) { return slot; } @@ -130,8 +143,8 @@ static void increase_capacity(Table *table) { // free old table keys for (size_t i=0; icapacity; i++) { TableSlot *slot = &table->slots[i]; - if (slot->state == TABLE_SLOT_USED) { - free(slot->key); + if (slot->state == TABLE_SLOT_USED && table->key_free_func) { + table->key_free_func(slot->key); } } // free old table slots @@ -164,7 +177,7 @@ void Table_Deinit(Table *table) { if (table->slots) { for (size_t i=0; icapacity; i++) { TableSlot *entry = &table->slots[i]; - TableSlot_Deinit(entry); + TableSlot_Deinit(entry, table->key_free_func); } free(table->slots); } @@ -174,8 +187,36 @@ void Table_Deinit(Table *table) { } Table *Table_Create() { + return Table_CreateCustom( + (uint32_t(*)(const void*))hash_string, + (int(*)(const void*, const void*))strcmp, + (void*(*)(const void*))strdup, + (void(*)(void*))free); +} + +Table *Table_CreatePtr() { + return Table_CreateCustom( + hash_ptr, + cmp_ptr, + cpy_ptr, + NULL); +} + +Table *Table_CreateCustom( + uint32_t (*hash_func)(const void *p), + int (*key_cmp_func)(const void*, const void *), + void *(*key_copy_func)(const void *p), + void (*key_free_func)(void *p) +) { Table *table = malloc(sizeof(Table)); + if (!table) { + logFatal("Failed to allocate memory for table."); + } Table_Init(table); + table->hash_func = hash_func; + table->key_cmp_func = key_cmp_func; + table->key_copy_func = key_copy_func; + table->key_free_func = key_free_func; return table; } @@ -188,7 +229,7 @@ void Table_Destroy(Table *table) { } -void Table_Set(Table *table, const char*key, void *value, void (*destructor)(void *value)) { +void Table_Set(Table *table, const void *key, void *value, void (*destructor)(void *value)) { if (!table || !key) { return; } @@ -202,8 +243,8 @@ void Table_Set(Table *table, const char*key, void *value, void (*destructor)(voi if (slot->state == TABLE_SLOT_EMPTY) { slot->state = TABLE_SLOT_USED; - slot->key = strdup(key); - if (slot->key == NULL) { + slot->key = table->key_copy_func(key); + if (!slot->key) { logFatal("No memory for string copy in Table_Set()."); } table->used++; @@ -216,7 +257,7 @@ void Table_Set(Table *table, const char*key, void *value, void (*destructor)(voi slot->value = value; // potential pointers in value are copied and ownership is taken } -void *Table_Get(const Table *table, const char *key) { +void *Table_Get(const Table *table, const void *key) { if (!table) { logFatal("Invalid table in Table_Get()."); } @@ -235,7 +276,7 @@ void *Table_Get(const Table *table, const char *key) { return slot->value; } -void Table_Delete(Table *table, const char *key) { +void Table_Delete(Table *table, const void *key) { if (!table) { logFatal("Invalid table in Table_Delete()."); } @@ -250,14 +291,14 @@ void Table_Delete(Table *table, const char *key) { if (slot->state != TABLE_SLOT_USED) { return; } - TableSlot_Deinit(slot); + TableSlot_Deinit(slot, table->key_free_func); slot->state = TABLE_SLOT_TOMBSTONE; // used is not decremented intentionally! // tombstones are counted to trigger rehash earlier and decrease probing } -bool Table_Has(const Table *table, const char *key) { +bool Table_Has(const Table *table, const void *key) { if (!table) { logFatal("Invalid table in Table_Has()."); } @@ -272,7 +313,7 @@ bool Table_Has(const Table *table, const char *key) { return (slot->state == TABLE_SLOT_USED); } -bool Table_HasOwnership(const Table *table, const char *key) { +bool Table_HasOwnership(const Table *table, const void *key) { if (!table) { logFatal("Invalid table in Table_HasOwnership()."); } diff --git a/src/common/table.h b/src/common/table.h index 1ec71ee..3b62330 100644 --- a/src/common/table.h +++ b/src/common/table.h @@ -20,6 +20,8 @@ * Simple implementation of a hashtable using the open addressing * with linear probing. For hashing the djb2 hash algorithm is used. * + * Custom keys are supported via the Table_CreateCustom() function. + * * The load factor is calculated from all slots that are not empty * (tombstones included). It's assumed that there not too many deletions * occure. Otherwise the worst that can happen is that the used memory @@ -28,6 +30,7 @@ #ifndef TABLE_H #define TABLE_H +#include #include #include @@ -42,25 +45,52 @@ typedef enum { } TableSlotState; typedef struct _TableSlot { - char *key; + void *key; void *value; void (*destructor)(void *value); TableSlotState state; } TableSlot; void TableSlot_Init(TableSlot *slot); -void TableSlot_Deinit(TableSlot *slot); +void TableSlot_Deinit(TableSlot *slot, void (*free_key_func)(void *key)); typedef struct _Table { TableSlot *slots; size_t capacity; size_t used; + + uint32_t (*hash_func)(const void *p); + int (*key_cmp_func)(const void*, const void *); + void *(*key_copy_func)(const void *p); + void (*key_free_func)(void *p); } Table; void Table_Init(Table *table); void Table_Deinit(Table *table); +/** + * @brief Create a `Table` with char* keys. + */ Table *Table_Create(); + +/** + * @brief Create a `Table` with pointer addresses as keys. + */ +Table *Table_CreatePtr(); + +/** + * @brief Create a `Table`with custom keys. + */ +Table *Table_CreateCustom( + uint32_t (*hash_func)(const void *p), + int (*key_cmp_func)(const void*, const void *), + void *(*key_copy_func)(const void *p), + void (*key_free_func)(void *p) +); + +/** + * @brief Destroy `table` + */ void Table_Destroy(Table *table); /** @@ -70,14 +100,14 @@ void Table_Destroy(Table *table); * for destroying it (in the case of overwriting or deleting it). If destructor is NULL the * ownership stays by the owner and he need to take care of freeing value. */ -void Table_Set(Table *table, const char *key, void *value, void (*destructor)(void *value)); +void Table_Set(Table *table, const void *key, void *value, void (*destructor)(void *value)); /** * @brief Return the value for key or NULL if key is not found. * * If NULL is a ligit value for key use Table_Has() to check if the key exists in the table. */ -void *Table_Get(const Table *table, const char *key); +void *Table_Get(const Table *table, const void *key); /** * @brief Delete the entry for key in table. @@ -85,17 +115,17 @@ void *Table_Get(const Table *table, const char *key); * Delete the entry of key if it exists. If table has the ownership for value (a destructor is present) * value will be destructed. */ -void Table_Delete(Table *table, const char *key); +void Table_Delete(Table *table, const void *key); /** * @brief Return true if the key exists in table. */ -bool Table_Has(const Table *table, const char *key); +bool Table_Has(const Table *table, const void *key); /** * @brief Return true if table has the ownership for the entry of key */ -bool Table_HasOwnership(const Table *table, const char *key); +bool Table_HasOwnership(const Table *table, const void *key); /** * @brief Return the current number of non-free table slots. diff --git a/src/syntax/definition.c b/src/syntax/definition.c index cf83044..f8e23a4 100644 --- a/src/syntax/definition.c +++ b/src/syntax/definition.c @@ -59,9 +59,8 @@ void SyntaxBlockDef_Destroy(SyntaxBlockDef *block) { free(block->name); } regfree(&block->start); - if (!block->only_start) { - regfree(&block->end); - } + regfree(&block->end); + free(block); } @@ -75,6 +74,13 @@ SyntaxBlockDef *SyntaxBlockDef_FromTable(const char *name, const Table *table, S block->name = strdup(name); block->only_start = true; block->color = (uint8_t)TypedTable_GetNumber(table, "color"); + + if (strcmp(name, "root") == 0) { + regcomp(&block->start, "^", REG_EXTENDED); // matches always without consuming + regcomp(&block->end, "a^", REG_EXTENDED); // matches never + block->only_start = false; + return block; + } if (!start_regex) { set_error(error, @@ -104,14 +110,18 @@ SyntaxBlockDef *SyntaxBlockDef_FromTable(const char *name, const Table *table, S regerror(ret, &block->end, errbuf, sizeof(errbuf)); set_error(error, SYNTAXDEFINITION_REGEX_ERROR_END, - String_Format("Error in end regex \"%s\" in block \"%s\": %s", start_regex, name, errbuf) + String_Format("Error in end regex \"%s\" in block \"%s\": %s", end_regex, name, errbuf) ); - block->only_start = true; // must be set that SyntaxBlockDef_Destroy() does not try to free end regex - SyntaxBlockDef_Destroy(block); // frees everything including start regex + regfree(&block->start); + free(block->name); + free(block); return NULL; } block->only_start = false; } + else { + regcomp(&block->end, "^", REG_EXTENDED); // matches everything without consuming + } return block; } diff --git a/src/syntax/definition.h b/src/syntax/definition.h index c81e0ce..a5c343b 100644 --- a/src/syntax/definition.h +++ b/src/syntax/definition.h @@ -30,7 +30,22 @@ * name = TEST * * [block:root] - * start = . + * ``` + * + * ### Another example + * ``` + * [meta] + * name = TEST + * + * [block:root] + * allowed_blocks = string, keyword + * + * [block:string] + * start = "'" + * end = "'" + * + * [block:keyword] + * start = if|then|else * ``` * * Each block must define a `start` regex (an extended POSIX regular expression). @@ -42,6 +57,9 @@ * Blocks that define an `end` expression may additionally define a list of * `allowed_blocks`, which specifies which block types are permitted to appear * inside the block. + * + * The root block does not need defined `start` or `end` patterns but should + * define an `allowed_blocks` list (otherwise nothing will be found). */ #ifndef SYNTAX_DEFINITION_H diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c new file mode 100644 index 0000000..6d75c63 --- /dev/null +++ b/src/syntax/highlighting.c @@ -0,0 +1,207 @@ +#include "highlighting.h" +#include "common/logging.h" + +/*****************************************************************************/ +/* SyntaxHighlightingTag */ + +void SyntaxHighlightingTag_Init(SyntaxHighlightingTag *tag) { + tag->text = NULL; + tag->byte_offset = 0; + tag->block = NULL; +} + +void SyntaxHighlightingTag_Deinit(SyntaxHighlightingTag *tag) { + SyntaxHighlightingTag_Init(tag); +} + +/*****************************************************************************/ +/* SyntaxHighlightingString */ + +SyntaxHighlightingString *SyntaxHighlightingString_Create(const String *text) { + SyntaxHighlightingString *shs = malloc(sizeof(SyntaxHighlightingString)); + if (!shs) { + logFatal("Cannot allocate memory for SyntaxHighlightingString."); + } + shs->text = text; + shs->tags = NULL; + shs->tags_count = 0; + shs->tags_capacity = 0; + Stack_Init(&shs->open_blocks_at_end); + + shs->tags = malloc(sizeof(SyntaxHighlightingTag) * SHS_TAGS_INITIAL_CAPACITY); + if (!shs->tags) { + logFatal("Cannot allocate memory for SyntaxHighlightingString tags."); + } + + return shs; +} + +void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs) { + if (shs->tags) { + free(shs->tags); + } + shs->tags = NULL; + shs->tags_count = 0; + shs->tags_capacity = 0; + shs->text = NULL; + Stack_Deinit(&shs->open_blocks_at_end); + free(shs); +} + +static void increase_tags_capacity(SyntaxHighlightingString *shs) { + if (!shs) { + return; + } + if (shs->tags_capacity == 0) { + shs->tags_capacity = SHS_TAGS_INITIAL_CAPACITY; + } + else { + shs->tags_capacity *= SHS_TAGS_GROW_FACTOR; + } + shs->tags = realloc(shs->tags, shs->tags_capacity * sizeof(SyntaxHighlightingTag)); + if (!shs->tags) { + logFatal("Failed to reallocate SyntaxHighlightingString tags."); + } +} + +void SyntaxHighlightingString_AddTag(SyntaxHighlightingString *shs, SyntaxHighlightingTag tag) { + if (!shs) { + return; + } + if (shs->tags_count + 1 >= shs->tags_capacity) { + increase_tags_capacity(shs); + } + shs->tags[shs->tags_count++] = tag; +} + +void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs) { + if (!shs) { + return; + } + shs->tags_count = 0; + Stack_Clear(&shs->open_blocks_at_end); +} + + +/*****************************************************************************/ +/* SyntaxHighlighting */ + +void SyntaxHighlighting_Init(SyntaxHighlighting *hl, const SyntaxDefinition *def) { + hl->def = def; + hl->strings = Table_CreatePtr(); +} + +void SyntaxHighlighting_Deinit(SyntaxHighlighting *hl) { + if (hl->strings) { + Table_Destroy(hl->strings); + } + hl->strings = NULL; + hl->def = NULL; +} + +static const SyntaxBlockDef *find_first_child(const char *str, const SyntaxBlockDef *current, regmatch_t *match) { + bool had_match = false; + regmatch_t first_match; + const SyntaxBlockDef *first_block; + for (size_t i=0; ichildren_count; i++) { + const SyntaxBlockDef *child = current->children[i]; + regmatch_t curr_match; + if (regexec(&child->start, str, 1, &curr_match, 0) == 0) { + if (!had_match || curr_match.rm_so < first_match.rm_so) { + first_match = curr_match; + first_block = child; + had_match = true; + } + } + } + if (had_match) { + *match = first_match; + return first_block; + } + return NULL; +} + +static bool find_end_of_block(const char *str, const SyntaxBlockDef *current, regmatch_t *match) { + regmatch_t end_match; + if (regexec(¤t->end, str, 1, &end_match, 0) == 0) { + *match = end_match; + return true; + } + return false; +} + +Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { + // check if there is already old infomation about text in the table + SyntaxHighlightingString *shs = Table_Get(sh->strings, text); + if (shs) { + // clear it if so + SyntaxHighlightingString_Clear(shs); + } + else { + // otherwise create and store + shs = SyntaxHighlightingString_Create(text); + Table_Set(sh->strings, text, shs, (void(*)(void*))SyntaxHighlightingString_Destroy); + } + + // create a working copy of the stack + Stack *open_blocks; + if (open_blocks_at_begin) { + open_blocks = Stack_Copy(open_blocks_at_begin); + } + else { + open_blocks = Stack_Create(); + Stack_Push(open_blocks, sh->def->root); + } + + // iterate over the string + size_t offset = 0; + for (;;) { + // take the current block from the stack (but keep it there) + const SyntaxBlockDef *current_block = (SyntaxBlockDef*)Stack_Peek(open_blocks); + + // find the first child block + regmatch_t child_match; + const SyntaxBlockDef *child = find_first_child(text->bytes + offset, current_block, &child_match); + regmatch_t end_match; + bool end_found = find_end_of_block(text->bytes + offset, current_block, &end_match); + + if (child && (!end_found || child_match.rm_so < end_match.rm_so)) { + // if there is a child block found create and add a tag to SyntaxHighlightingString tag list + SyntaxHighlightingTag tag; + tag.text = text; + tag.byte_offset = offset + child_match.rm_so; + tag.block = child; + SyntaxHighlightingString_AddTag(shs, tag); + + // increase the offset to the end of the match + offset += child_match.rm_eo; + + // push the child to the stack to continue with it in the next iteration + Stack_Push(open_blocks, (void*)child); + continue; + } + // no child block found + // so check if the last block ends + if (end_found) { + // end of block found so remove it from stack + Stack_Pop(open_blocks); // removes current (which was just peeked before) + + // and add a tag for the (new) begin of the surrounding block + SyntaxHighlightingTag tag; + tag.text = text; + tag.byte_offset = offset + end_match.rm_eo; // the current block goes behind the match of its end + tag.block = Stack_Peek(open_blocks); // new current block + SyntaxHighlightingString_AddTag(shs, tag); + + // increase offset + offset += end_match.rm_eo; + continue; + } + // neither the beginning of a new block nor the end of the current block found + return open_blocks; + } + + // if this happens there is an error in the SyntaxDefinition. + // the end pattern of the root block must *never* match + return NULL; +} diff --git a/src/syntax/highlighting.h b/src/syntax/highlighting.h new file mode 100644 index 0000000..5c1739e --- /dev/null +++ b/src/syntax/highlighting.h @@ -0,0 +1,108 @@ +/* Copyright (C) 2025 defname + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * @file highlighting.h + * @brief System to store the information about the highlighting, like text positions and colors. + * + * ### Limitations + * Regular expressions that mark the begin or the end of an block are not tested across different + * Strings. If for example a keyword starts in one string and continues in another one, even if + * they are parsed in the correct order, the keyword will not be recognized. + * So make sure the `text` parts are sperarated in a compatible way (like by newlines or similar). + */ +#ifndef SYNTAX_HIGHLIGHTING_H +#define SYNTAX_HIGHLIGHTING_H + +#include +#include "definition.h" +#include "common/stack.h" +#include "common/table.h" +#include "common/string.h" + +/** + * @brief Holds information about the beginning of a block. + * + * Instead of keeping track of the end of a block it is just treated + * as a new beginning of the surrounding block. + * + * Note that this struct does not have the ownership of any of the members. + */ +typedef struct _SyntaxHighlightingTag { + const String *text; //< pointer to the string this tag is for + size_t byte_offset; //< byte offset where the tag occurs + const SyntaxBlockDef *block; //< pointer to the block this tag is for +} SyntaxHighlightingTag; + +void SyntaxHighlightingTag_Init(SyntaxHighlightingTag *tag); +void SyntaxHighlightingTag_Deinit(SyntaxHighlightingTag *tag); + + +/** + * @brief Holds the complete highlighting information for the String `text`. + */ +typedef struct _SyntaxHighlightingString { + const String *text; //< pointer to the text information are stored for + SyntaxHighlightingTag *tags; //< list of tags for `text` + size_t tags_count; //< number of tags in `tags` + size_t tags_capacity; //< capacity of `tags` + + Stack open_blocks_at_end; //< Stack of (SyntaxBlockDef*) elements that are open at the end of `text` +} SyntaxHighlightingString; + +#define SHS_TAGS_INITIAL_CAPACITY 16 +#define SHS_TAGS_GROW_FACTOR 2 + +SyntaxHighlightingString *SyntaxHighlightingString_Create(const String *text); +void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs); + +void SyntaxHighlightingString_AddTag(SyntaxHighlightingString *shs, SyntaxHighlightingTag tag); +void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs); + +/**match + * @brief Holds the highlighting information for text of multiple `Strings`. + */ +typedef struct _SyntaxHighlighting { + const SyntaxDefinition *def; //< SyntaxDefinition to use for highlighting + Table *strings; //< Table of (String -> SyntaxHighlightingString*) elements (holds the ownership of the SyntaxHighlightingString's) +} SyntaxHighlighting; + + +/** + * @brief Initialize syntax highlighting using def for definitions. + */ +void SyntaxHighlighting_Init(SyntaxHighlighting *sh, const SyntaxDefinition *def); + +/** + * @brief Deinitialize sh. + */ +void SyntaxHighlighting_Deinit(SyntaxHighlighting *sh); + +/** + * @brief Highlight a string according to given context. + * + * Add a `SyntaxHighlightingString`to `hl->strings` or update an existing one. + * + * @param hl The `SyntaxHighlighting` instance to use. + * @param text The text to hightlight. + * @param open_blocks An pointer to a `Stack` instance that holds the open blocks at the beginning of string. If NULL it's assumed that the root block is current. + * + * @returns + * A new created `Stack` containing all open blocks at the end of `text`. + */ +Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks); + +#endif \ No newline at end of file diff --git a/tests/test_syntax_definition.c b/tests/test_syntax_definition.c index fa97531..58d8250 100644 --- a/tests/test_syntax_definition.c +++ b/tests/test_syntax_definition.c @@ -134,9 +134,19 @@ void test_errors(void) { "[meta]\n" "name = MINI\n" "[block:root]\n" + "[block:error]\n" "start=\".*)(\"\n", SYNTAXDEFINITION_REGEX_ERROR_START }, + { + "[meta]\n" + "name = MINI\n" + "[block:root]\n" + "[block:error]\n" + "start=.*\n" + "end=\".*)(\"\n", + SYNTAXDEFINITION_REGEX_ERROR_END + }, { "block:root = foo\n" "[meta]\n" @@ -166,6 +176,7 @@ void test_errors(void) { int cases_num = sizeof(cases) / sizeof(cases[0]); for (int i=0; i +#include +#include +#include +#include "syntax/highlighting.h" +#include "syntax/definition.h" +#include "common/iniparser.h" + +static SyntaxDefinition *create_definition(const char *ini) { + IniParser parser; + IniParser_Init(&parser); + IniParser_SetText(&parser, ini); + Table *table = IniParser_Parse(&parser); + TEST_ASSERT(table != NULL); + SyntaxDefinitionError error; + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error); + TEST_ASSERT(def != NULL); + IniParser_Deinit(&parser); + Table_Destroy(table); + return def; +} + +Table *build_blocks_table(SyntaxDefinition *def) { + Table *table = Table_Create(); + for (size_t i=0; iblocks_count; i++) { + SyntaxBlockDef *block = def->blocks[i]; + Table_Set(table, block->name, block, NULL); + } + return table; +} + +typedef struct { + const char *ini; + const char *str; + size_t tags_count; + size_t tag_offsets[32]; + const char *tag_blocks[32]; + size_t open_blocks_count; + const char *open_blocks[32]; +} TagTestCase; + +static void assert_highlight_tags( + TagTestCase testcase +) { + const char *ini = testcase.ini; + String str = String_Format(testcase.str); + size_t tags_count = testcase.tags_count; + size_t *tag_offsets = testcase.tag_offsets; + const char **tag_blocks = testcase.tag_blocks; + size_t open_blocks_count_expected = testcase.open_blocks_count; + const char **open_blocks_expected = testcase.open_blocks; + + + SyntaxDefinition *def = create_definition(ini); + SyntaxHighlighting hl; + SyntaxHighlighting_Init(&hl, def); + + Stack *open_blocks_at_begin = Stack_Create(); + Stack_Push(open_blocks_at_begin, def->root); + Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &str, open_blocks_at_begin); + + TEST_CHECK(!Stack_IsEmpty(open_blocks)); + TEST_MSG("Expected open_blocks to not be not empty."); + + for (size_t i=0; iname) == 0); + TEST_MSG("Expected open block #%zu to be '%s' but got '%s'.", i, open_blocks_expected[i], block->name); + } + + SyntaxHighlightingString *shs = Table_Get(hl.strings, &str); + + TEST_CHECK(shs->tags_count == tags_count); + TEST_MSG("Expected %zu tags but got %zu.", tags_count, shs->tags_count); + + for (size_t i=0; itags[i].byte_offset == tag_offsets[i]); + TEST_MSG("Expected offset #%zu to be %zu but got %zu.", i, tag_offsets[i], shs->tags[i].byte_offset); + + TEST_CHECK(strcmp(shs->tags[i].block->name, tag_blocks[i]) == 0); + TEST_MSG("Expected block #%zu to be '%s' but got '%s'.", i, tag_blocks[i], shs->tags[i].block->name); + } + + Stack_Destroy(open_blocks); + Stack_Destroy(open_blocks_at_begin); + String_Deinit(&str); + SyntaxHighlighting_Deinit(&hl); + SyntaxDefinition_Destroy(def); +} + +const char *test_ini0 = +"[meta]\n" +"name = TEST\n" +"[block:root]\n" +"start=^\n" +"end=a^\n" +"allowed_blocks=string\n" +"\n" +"[block:string]\n" +"start=\"'\"\n" +"end=\"'\"\n"; + + +void test_highlight_string_simple(void) { + SyntaxDefinition *def = create_definition(test_ini0); + SyntaxHighlighting hl; + SyntaxHighlighting_Init(&hl, def); + + String test1 = String_Format("root 'string' root"); + Stack *open_blocks_at_begin = Stack_Create(); + Stack_Push(open_blocks_at_begin, def->root); + Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test1, open_blocks_at_begin); + + TEST_CHECK(open_blocks != NULL); + TEST_CHECK(Stack_Peek(open_blocks) == def->root); + SyntaxHighlightingString *shs = Table_Get(hl.strings, &test1); + TEST_CHECK(shs->tags_count == 2); + TEST_CHECK(shs->tags[0].byte_offset == 5); // start of 'string' + TEST_CHECK(strcmp(shs->tags[0].block->name, "string") == 0); + TEST_CHECK(shs->tags[1].byte_offset == 13); // end of 'string' + TEST_CHECK(shs->tags[1].block == def->root); + + Stack_Destroy(open_blocks); + Stack_Destroy(open_blocks_at_begin); + String_Deinit(&test1); + SyntaxHighlighting_Deinit(&hl); + SyntaxDefinition_Destroy(def); +} + +const char *test_ini1 = R"( +[meta] +name = TEST + +[block:root] +allowed_blocks=string, comment, keyword, brackets + +[block:brackets] +start=\( +end=\) +allowed_blocks=string, keyword, brackets + +[block:keyword] +start=keyword + +[block:string] +start=' +end=' + +[block:comment] +start = // +end = $ +)"; + +void test_basics(void) { + TagTestCase cases[] = { + { + test_ini1, + "foobar // blabla ' bla", // comment + 2, + {7, 22}, + {"comment", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "foo 'bar' foo", // string + 2, + {4, 9}, + {"string", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "foo keyword foo", // keyword + 2, + {4, 11}, + {"keyword", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "foo '//not a comment' foo", // string with comment inside (should be ignored) + 2, + {4, 21}, + {"string", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "foo 'not a keyword' foo", // keyword inside string + 2, + {4, 19}, + {"string", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "foo (no brackets) foo", + 2, + {4, 17}, + {"brackets", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "()", + 2, + {0, 2}, + {"brackets", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "(keyword)keyword", + 6, + {0, 1, 8, 9, 9, 16}, + {"brackets", "keyword", "brackets", "root", "keyword", "root"}, + 1, + {"root"} + }, + { + test_ini1, + "", + 0, + {}, + {}, + 1, + {"root"} + }, + { + test_ini1, + "(('not a keyword'))", + 6, + {0, 1, 2, 17, 18, 19}, + {"brackets", "brackets", "string", "brackets", "brackets", "root"}, + 1, + {"root"} + }, + + }; + size_t count = sizeof(cases) / sizeof(cases[0]); + + for (size_t i=0; i= max_len) { + break; + } + memcpy(out + l, token, token_len); + l += token_len; + } + else { + out[l++] = random_char(); + } + } + out[l] = '\0'; +} + + +void test_stress(void) { + // function should just not crash on random input + const char *tokens[] = { + "(", ")", "keyword", "'", "//" + }; + + srand(time(NULL)); + + for (int i=0; i<1000; i++) { + SyntaxDefinition *def = create_definition(test_ini1); + SyntaxHighlighting hl; + SyntaxHighlighting_Init(&hl, def); + + char str[4096]; + generate_random_string(str, 1024, tokens, sizeof(tokens) / sizeof(tokens[0])); + TEST_CASE(str); + + String test = String_FromCStr(str, strlen(str)); + Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test, NULL); + + TEST_CHECK(open_blocks != NULL); + TEST_CHECK(!Stack_IsEmpty(open_blocks)); + + Stack_Destroy(open_blocks); + String_Deinit(&test); + SyntaxHighlighting_Deinit(&hl); + SyntaxDefinition_Destroy(def); + } + +} + +TEST_LIST = { + { "SyntaxHighlighting: Simple", test_highlight_string_simple }, + { "SyntaxHighlighting: Basics", test_basics }, + { "SyntaxHighlighting: Open blocks", test_open_blocks }, + { "SyntaxHighlighting: Random Tests", test_stress }, + { NULL, NULL } +}; \ No newline at end of file diff --git a/tests/test_table.c b/tests/test_table.c index 4cc41c1..fd8f75c 100644 --- a/tests/test_table.c +++ b/tests/test_table.c @@ -115,11 +115,24 @@ void test_edge_case(void) { Table_Destroy(table); } +void test_ptr_table(void) { + Table *table = Table_CreatePtr(); + + Table_Set(table, (void*)1, strdup("Foobar"), free); + Table_Set(table, (void*)2, strdup("Blub"), free); + + TEST_CHECK(strcmp(Table_Get(table, (void*)1), "Foobar") == 0); + TEST_CHECK(strcmp(Table_Get(table, (void*)2), "Blub") == 0); + + Table_Destroy(table); +} + TEST_LIST = { { "Table: Creation", test_creation }, { "Table: Set and Get", test_set_get }, { "Table: Existence and Owndership", test_has_hasownership }, { "Table: Rehashing", test_rehashing }, { "Table: Edge Case", test_edge_case }, + { "Table: Pointer Keys", test_ptr_table }, { NULL, NULL } }; \ No newline at end of file From e349fc7ca22a10ed939569573b48f297dc8145d7 Mon Sep 17 00:00:00 2001 From: Bodo Date: Thu, 23 Oct 2025 20:34:55 +0200 Subject: [PATCH 06/35] Cache regex matches and add ends_on option for Syntax definitions (#86) * add const to parameter if function does not modify it. add Buffer_Has_Space(), Buffer_Clear() * cache regex results for better performance * add "ends_on" (INI) property to SyntaxBlockDef * implement ends_on in the SyntaxHighlight modul * remove the check if the ends_on block is allowed inside the surrounding block. this leads to more flexibility and better performance * add additional (but just a single) test for the new feature --- src/common/buffer.c | 17 +++- src/common/buffer.h | 16 +++- src/syntax/definition.c | 142 +++++++++++++++++++++---------- src/syntax/definition.h | 18 +++- src/syntax/highlighting.c | 113 +++++++++++++++++++----- tests/test_syntax_definition.c | 35 +++++++- tests/test_syntax_highlighting.c | 50 ++++++++++- 7 files changed, 309 insertions(+), 82 deletions(-) diff --git a/src/common/buffer.c b/src/common/buffer.c index 4140631..5cfc1ee 100644 --- a/src/common/buffer.c +++ b/src/common/buffer.c @@ -39,6 +39,11 @@ void Buffer_Deinit(Buffer *buffer) { buffer->buffer = NULL; } +void Buffer_Clear(Buffer *buffer) { + buffer->count = 0; + buffer->cursor = 0; +} + void Buffer_Enqueue(Buffer *buffer, const void *element) { if (buffer->count >= buffer->capacity) { logError("Buffer overflow. Element dropped."); @@ -65,19 +70,23 @@ bool Buffer_Dequeue(Buffer *buffer, void *out_element) { return true; } -bool Buffer_IsEmpty(Buffer *buffer) { +bool Buffer_IsEmpty(const Buffer *buffer) { return buffer->count == 0; } -size_t Buffer_Size(Buffer *buffer) { +size_t Buffer_Size(const Buffer *buffer) { return buffer->count; } -size_t Buffer_Capacity(Buffer *buffer) { +size_t Buffer_Capacity(const Buffer *buffer) { return buffer->capacity; } -bool Buffer_Peek(Buffer *buffer, size_t lookahead, void *out_element) { +bool Buffer_HasSpace(const Buffer *buffer) { + return buffer->count < buffer->capacity - 1; +} + +bool Buffer_Peek(const Buffer *buffer, size_t lookahead, void *out_element) { if (lookahead >= buffer->count) { logError("Buffer underflow on Peek (lookahead: %zu, count: %zu).", lookahead, buffer->count); return false; diff --git a/src/common/buffer.h b/src/common/buffer.h index 6f8421a..a8f3973 100644 --- a/src/common/buffer.h +++ b/src/common/buffer.h @@ -48,6 +48,11 @@ void Buffer_Init(Buffer *buffer, size_t capacity, size_t element_size); */ void Buffer_Deinit(Buffer *buffer); +/** + * @brief Set buffer count to 0 and cursor to the first element. + */ +void Buffer_Clear(Buffer *buffer); + /** * @brief Adds an element to the end (tail) of the buffer. * If the buffer is full, the element is dropped. @@ -70,13 +75,16 @@ bool Buffer_Dequeue(Buffer *buffer, void *out_element); * @param buffer A pointer to the Buffer struct. * @return true if the buffer contains no elements, false otherwise. */ -bool Buffer_IsEmpty(Buffer *buffer); +bool Buffer_IsEmpty(const Buffer *buffer); /** @brief Returns the current number of elements in the buffer. */ -size_t Buffer_Size(Buffer *buffer); +size_t Buffer_Size(const Buffer *buffer); /** @brief Returns the maximum capacity of the buffer. */ -size_t Buffer_Capacity(Buffer *buffer); +size_t Buffer_Capacity(const Buffer *buffer); + +/** @brief Return true if there are empty slots. */ +bool Buffer_HasSpace(const Buffer *buffer); /** * @brief Peeks at an element in the buffer without removing it. @@ -85,6 +93,6 @@ size_t Buffer_Capacity(Buffer *buffer); * @param out_element A pointer to a variable where the peeked element will be copied. * @return true on success, false if the lookahead index is out of bounds. */ -bool Buffer_Peek(Buffer *buffer, size_t lookahead, void *out_element); +bool Buffer_Peek(const Buffer *buffer, size_t lookahead, void *out_element); #endif \ No newline at end of file diff --git a/src/syntax/definition.c b/src/syntax/definition.c index f8e23a4..82a3c6d 100644 --- a/src/syntax/definition.c +++ b/src/syntax/definition.c @@ -47,6 +47,8 @@ SyntaxBlockDef *SyntaxBlockDef_Create() { block->only_start = false; block->children = NULL; block->children_count = 0; + block->ends_on = NULL; + block->ends_on_count = 0; return block; } @@ -55,6 +57,9 @@ void SyntaxBlockDef_Destroy(SyntaxBlockDef *block) { if (block->children) { free(block->children); } + if (block->ends_on) { + free(block->ends_on); + } if (block->name) { free(block->name); } @@ -212,6 +217,68 @@ static SyntaxDefinitionError build_blocks(SyntaxDefinition *def, const Table *ta return NO_ERROR; } +static SyntaxDefinitionError map_block_names_to_blocks(SyntaxBlockDef *current_block, StringView *block_names, size_t count, SyntaxBlockDef **out, Table *blocks) { + for (size_t i=0; iname, name.bytes + ); + String_Deinit(&name); + return error; + } + String_Deinit(&name); + out[i] = mapping->block; + } + return NO_ERROR; +} + +static SyntaxDefinitionError block_name_list_str_to_blocks(SyntaxBlockDef *current_block, const char *list_str, SyntaxBlockDef ***out, size_t *out_count, Table *blocks) { + if (!list_str) { + *out_count = 0; + return NO_ERROR; + } + + String s = String_FromCStr(list_str, strlen(list_str)); + ssize_t count = 0; + String delimiter = String_FromCStr(",", 1); + StringView *children = String_Split(&s, &delimiter, &count); + if (count < 0) { + logFatal("Potential error in String_Split()."); + } + String_Deinit(&delimiter); + if (count == 0) { + String_Deinit(&s); + free(children); + return NO_ERROR; + } + + // malloc memory for the children of SyntaxBlockDef + *out = malloc(sizeof(SyntaxBlockDef*) * count); + if (!(*out)) { + logFatal("Cannot allocate memory for children of SyntaxBlockDef."); + } + + SyntaxDefinitionError error = map_block_names_to_blocks(current_block, children, count, *out, blocks); + if (error.code != SYNTAXDEFINITION_NO_ERROR) { + String_Deinit(&s); + free(children); + return error; + } + + *out_count = count; + String_Deinit(&s); + free(children); + + return NO_ERROR; +} SyntaxDefinitionError link_children(SyntaxDefinition *def, Table *blocks) { for (size_t i=0; iblocks_count; i++) { @@ -224,55 +291,31 @@ SyntaxDefinitionError link_children(SyntaxDefinition *def, Table *blocks) { } const Table *block_table = mapping->table; - const char *children_str = TypedTable_GetString(block_table, "allowed_blocks"); - if (!children_str) { - continue; - } - - String s = String_FromCStr(children_str, strlen(children_str)); - ssize_t children_count = 0; - String delimiter = String_FromCStr(",", 1); - StringView *children = String_Split(&s, &delimiter, &children_count); - if (children_count < 0) { - logFatal("Potential rrror in String_Split()."); - } - String_Deinit(&delimiter); - if (children_count == 0) { - String_Deinit(&s); - free(children); - continue; + const char *children_str = TypedTable_GetString(block_table, "child_blocks"); + SyntaxDefinitionError error = block_name_list_str_to_blocks(block, children_str, &block->children, &block->children_count, blocks); + if (error.code != SYNTAXDEFINITION_NO_ERROR) { + return error; } + } + return NO_ERROR; +} - // malloc memory for the children of SyntaxBlockDef - block->children = malloc(sizeof(SyntaxBlockDef*) * children_count); - if (!block->children) { - logFatal("Cannot allocate memory for children of SyntaxBlockDef."); +SyntaxDefinitionError link_ends_with(SyntaxDefinition *def, Table *blocks) { + for (size_t i=0; iblocks_count; i++) { + SyntaxBlockDef *block = def->blocks[i]; + + // get the mapping + table_block_mapping *mapping = Table_Get(blocks, block->name); + if (!mapping) { + logFatal("Some serious design flaw detected."); } + const Table *block_table = mapping->table; - for (size_t child_idx=0; child_idx<(size_t)children_count; child_idx++) { - String child_name = String_FromView(children[child_idx]); - String_Trim(&child_name); - table_block_mapping *child_mapping = Table_Get(blocks, child_name.bytes); - - if (!child_mapping) { - // no block with the given child name - // something like - // allowed_blocks = block1, non_existing_block - SyntaxDefinitionError error = ERROR( - SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST, - "Block \"%s\" defines non-existing block \"%s\" as it's child.", block->name, child_name.bytes - ); - String_Deinit(&child_name); - String_Deinit(&s); - free(children); - return error; - } - String_Deinit(&child_name); - block->children[child_idx] = child_mapping->block; + const char *ends_with_str = TypedTable_GetString(block_table, "ends_on"); + SyntaxDefinitionError error = block_name_list_str_to_blocks(block, ends_with_str, &block->ends_on, &block->ends_on_count, blocks); + if (error.code != SYNTAXDEFINITION_NO_ERROR) { + return error; } - block->children_count = children_count; - String_Deinit(&s); - free(children); } return NO_ERROR; } @@ -294,7 +337,6 @@ SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitio SyntaxDefinition_Destroy(def); return NULL; } - SyntaxDefinitionError_Deinit(error); // temporary table mapping all block names to it's SyntaxBlockDef Table *blocks = Table_Create(); @@ -304,7 +346,6 @@ SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitio Table_Destroy(blocks); return NULL; } - SyntaxDefinitionError_Deinit(error); // construct the children lists *error = link_children(def, blocks); @@ -313,7 +354,14 @@ SyntaxDefinition *SyntaxDefinition_FromTable(const Table *table, SyntaxDefinitio Table_Destroy(blocks); return NULL; } - SyntaxDefinitionError_Deinit(error); + + // construct the ends_by list + *error = link_ends_with(def, blocks); + if (error->code != SYNTAXDEFINITION_NO_ERROR) { + SyntaxDefinition_Destroy(def); + Table_Destroy(blocks); + return NULL; + } // cleanup Table_Destroy(blocks); diff --git a/src/syntax/definition.h b/src/syntax/definition.h index a5c343b..f174914 100644 --- a/src/syntax/definition.h +++ b/src/syntax/definition.h @@ -38,7 +38,7 @@ * name = TEST * * [block:root] - * allowed_blocks = string, keyword + * child_blocks = string, keyword * * [block:string] * start = "'" @@ -99,6 +99,16 @@ typedef struct _SyntaxDefinitionError { void SyntaxDefinitionError_Deinit(SyntaxDefinitionError *error); +/** + * @brief Helper struct to cache regex match results. This information are used by the Highlight module. + */ +typedef struct _MatchCache { + ssize_t offset; // total offset from where match was calculated + regmatch_t match; // last match + bool done; // if true the last match was already found +} MatchCache; + + /** * @brief Holds the definition of a syntax block. */ @@ -111,8 +121,14 @@ typedef struct _SyntaxBlockDef { struct _SyntaxBlockDef **children; //< list of children definition which are allowed inside the block size_t children_count; //< number of children + + struct _SyntaxBlockDef **ends_on; //< list of blocks which may end this block implicitly + size_t ends_on_count; //< number of blocks which may end this block implicitly uint8_t color; //< the color to render the block + + MatchCache start_cache; //< used by the Highlight module + MatchCache end_cache; //< used by the Highlight module } SyntaxBlockDef; SyntaxBlockDef *SyntaxBlockDef_Create(); diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 6d75c63..837523a 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -99,17 +99,36 @@ void SyntaxHighlighting_Deinit(SyntaxHighlighting *hl) { hl->def = NULL; } -static const SyntaxBlockDef *find_first_child(const char *str, const SyntaxBlockDef *current, regmatch_t *match) { +static bool regexec_with_cache(const regex_t *regex, const char *str, size_t offset, MatchCache *cache, regmatch_t *match) { + if (cache->done) { + return false; + } + if (cache->match.rm_so + cache->offset >= (regoff_t)offset) { + *match = cache->match; + match->rm_so = cache->match.rm_so + cache->offset - offset; + match->rm_eo = cache->match.rm_eo + cache->offset - offset; + return true; + } + if (regexec(regex, str + offset, 1, match, 0) == 0) { + cache->match = *match; + cache->offset = offset; + return true; + } + cache->done = true; + return false; +} + +static SyntaxBlockDef *find_first_block(SyntaxBlockDef **block_list, size_t block_list_count, const char *str, size_t offset, regmatch_t *match) { bool had_match = false; - regmatch_t first_match; - const SyntaxBlockDef *first_block; - for (size_t i=0; ichildren_count; i++) { - const SyntaxBlockDef *child = current->children[i]; + regmatch_t first_match = {0}; + SyntaxBlockDef *first_block = NULL; + for (size_t i=0; istart, str, 1, &curr_match, 0) == 0) { + if (regexec_with_cache(&curr->start, str, offset, &curr->start_cache, &curr_match)) { if (!had_match || curr_match.rm_so < first_match.rm_so) { first_match = curr_match; - first_block = child; + first_block = curr; had_match = true; } } @@ -121,13 +140,32 @@ static const SyntaxBlockDef *find_first_child(const char *str, const SyntaxBlock return NULL; } -static bool find_end_of_block(const char *str, const SyntaxBlockDef *current, regmatch_t *match) { - regmatch_t end_match; - if (regexec(¤t->end, str, 1, &end_match, 0) == 0) { - *match = end_match; - return true; +static SyntaxBlockDef *find_first_child(const char *str, size_t offset, SyntaxBlockDef *current, regmatch_t *match) { + return find_first_block(current->children, current->children_count, str, offset, match); +} + +static SyntaxBlockDef *find_first_ends_on_block(const char *str, size_t offset, SyntaxBlockDef *current, regmatch_t *match) { + return find_first_block(current->ends_on, current->ends_on_count, str, offset, match); +} + +static bool find_end_of_block(const char *str, size_t offset, SyntaxBlockDef *current, regmatch_t *match) { + return regexec_with_cache(¤t->end, str, offset, ¤t->end_cache, match); +} + + +static void init_match_cache(SyntaxHighlighting *sh) { + for (size_t i=0; idef->blocks_count; i++) { + SyntaxBlockDef *block = sh->def->blocks[i]; + block->start_cache.match.rm_so = -1; + block->start_cache.match.rm_eo = -1; + block->start_cache.offset = 0; + block->start_cache.done = false; + block->end_cache.match.rm_so = -1; + block->end_cache.match.rm_eo = -1; + block->end_cache.offset = 0; + block->end_cache.done = false; + } - return false; } Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { @@ -153,19 +191,34 @@ Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String * Stack_Push(open_blocks, sh->def->root); } + // initialize the cache fields of the SyntaxBlockDefs + init_match_cache(sh); + // iterate over the string size_t offset = 0; for (;;) { // take the current block from the stack (but keep it there) - const SyntaxBlockDef *current_block = (SyntaxBlockDef*)Stack_Peek(open_blocks); + SyntaxBlockDef *current_block = (SyntaxBlockDef*)Stack_Peek(open_blocks); // find the first child block regmatch_t child_match; - const SyntaxBlockDef *child = find_first_child(text->bytes + offset, current_block, &child_match); - regmatch_t end_match; - bool end_found = find_end_of_block(text->bytes + offset, current_block, &end_match); + SyntaxBlockDef *child = find_first_child(text->bytes, offset, current_block, &child_match); + // find the first ends_on block + regmatch_t ends_on_match; + SyntaxBlockDef *ends_on = find_first_ends_on_block(text->bytes, offset, current_block, &ends_on_match); + + // find the block_end + regmatch_t end_match = {0, 0}; // current position with no consumption + bool end_found = true; // true for the case current_block is an only start block + if (!current_block->only_start) { + end_found = find_end_of_block(text->bytes, offset, current_block, &end_match); + } - if (child && (!end_found || child_match.rm_so < end_match.rm_so)) { + // check if child is the first match + if (child + && (!ends_on || child_match.rm_so < ends_on_match.rm_so) + && (!end_found || child_match.rm_so < end_match.rm_so)) + { // if there is a child block found create and add a tag to SyntaxHighlightingString tag list SyntaxHighlightingTag tag; tag.text = text; @@ -180,8 +233,28 @@ Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String * Stack_Push(open_blocks, (void*)child); continue; } - // no child block found - // so check if the last block ends + // check if ends_on is the first match + if (ends_on + && (!end_found || ends_on_match.rm_so < end_match.rm_so)) + { + // the current block ends by the occurence of the ends_on block + Stack_Pop(open_blocks); // removes current (which was just peeked before) + + // add a tag for the beginning of the ends_on block + SyntaxHighlightingTag tag; + tag.text = text; + tag.byte_offset = offset + ends_on_match.rm_so; + tag.block = ends_on; + SyntaxHighlightingString_AddTag(shs, tag); + + // add the ends_on block to the stack + Stack_Push(open_blocks, (void*)ends_on); + + // increase the offset to the end of the match + offset += ends_on_match.rm_eo; + continue; + } + // last case.... if end_found it's the first match automatically if (end_found) { // end of block found so remove it from stack Stack_Pop(open_blocks); // removes current (which was just peeked before) diff --git a/tests/test_syntax_definition.c b/tests/test_syntax_definition.c index 58d8250..a7ea232 100644 --- a/tests/test_syntax_definition.c +++ b/tests/test_syntax_definition.c @@ -158,7 +158,7 @@ void test_errors(void) { "name = MINI\n" "[block:root]\n" "start=.*\n" - "allowed_blocks=foo", + "child_blocks=foo", SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST }, { @@ -166,7 +166,7 @@ void test_errors(void) { "name = MINI\n" "[block:root]\n" "start=.*\n" - "allowed_blocks=foo, bar\n" + "child_blocks=foo, bar\n" "[block:foo]\n" "start=.*\n", SYNTAXDEFINITION_BLOCK_DOES_NOT_EXIST @@ -190,7 +190,7 @@ void test_children(void) { "name = Children\n" "[block:root]\n" "start=\".\"\n" - "allowed_blocks=block1,block2\n" + "child_blocks=block1,block2\n" "[block:block1]\n" "start=\".\"\n" "[block:block2]\n" @@ -208,6 +208,34 @@ void test_children(void) { SyntaxDefinitionError_Deinit(&error); } +void test_ends_on(void) { + const char *ini = + "[meta]\n" + "name = EndsOn\n" + "[block:root]\n" + "child_blocks=block1\n" + "[block:block1]\n" + "start=\".\"\n" + "ends_on=block3,block2\n" + "[block:block2]\n" + "start=\".\"\n" + "[block:block3]\n" + "start=\".\"\n"; + Table *table = table_from_ini(ini); + SyntaxDefinitionError error; + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error); + TEST_ASSERT(def != NULL); + TEST_CHECK(def->blocks_count == 4); + TEST_CHECK(def->root != NULL); + SyntaxBlockDef *block1 = def->root->children[0]; + TEST_CHECK(block1 != NULL); + TEST_CHECK(block1->ends_on_count == 2); + TEST_MSG("%zu", block1->ends_on_count); + + Table_Destroy(table); + SyntaxDefinition_Destroy(def); + SyntaxDefinitionError_Deinit(&error); +} TEST_LIST = { { "SyntaxDefinition: Block Simple", test_block_simple }, @@ -216,5 +244,6 @@ TEST_LIST = { { "SyntaxDefinition: Minimal Example", test_minimal_definition }, { "SyntaxDefinition: Errors", test_errors }, { "SyntaxDefinition: Children", test_children }, + { "SyntaxDefinition: Ends On", test_ends_on}, { NULL, NULL } }; diff --git a/tests/test_syntax_highlighting.c b/tests/test_syntax_highlighting.c index c73406e..54ecfe6 100644 --- a/tests/test_syntax_highlighting.c +++ b/tests/test_syntax_highlighting.c @@ -96,7 +96,7 @@ const char *test_ini0 = "[block:root]\n" "start=^\n" "end=a^\n" -"allowed_blocks=string\n" +"child_blocks=string\n" "\n" "[block:string]\n" "start=\"'\"\n" @@ -134,12 +134,12 @@ const char *test_ini1 = R"( name = TEST [block:root] -allowed_blocks=string, comment, keyword, brackets +child_blocks=string, comment, keyword, brackets [block:brackets] start=\( end=\) -allowed_blocks=string, keyword, brackets +child_blocks=string, keyword, brackets [block:keyword] start=keyword @@ -153,6 +153,7 @@ start = // end = $ )"; + void test_basics(void) { TagTestCase cases[] = { { @@ -245,7 +246,49 @@ void test_basics(void) { 1, {"root"} }, + }; + size_t count = sizeof(cases) / sizeof(cases[0]); + + for (size_t i=0; i Date: Fri, 24 Oct 2025 14:24:40 +0200 Subject: [PATCH 07/35] add Stack_CopyTo() --- src/common/stack.c | 62 +++++++++++++++++++++++++--------------------- src/common/stack.h | 3 ++- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/src/common/stack.c b/src/common/stack.c index 2e8eb0f..4fee243 100644 --- a/src/common/stack.c +++ b/src/common/stack.c @@ -17,21 +17,23 @@ #include #include "logging.h" - -static void increase_capacity(Stack *stack) { - if (!stack) { +static void resize_capacity(Stack *stack, size_t new_capacity) { + if (!stack || stack->capacity >= new_capacity) { return; } - if (stack->capacity == 0) { - stack->capacity = STACK_INITIAL_CAPACITY; - } - else { - stack->capacity *= STACK_GROW_FACTOR; - } - stack->items = realloc(stack->items, stack->capacity * sizeof(void *)); + stack->items = realloc(stack->items, new_capacity * sizeof(void *)); if (!stack->items) { logFatal("Failed to reallocate stack items."); } + stack->capacity = new_capacity; +} + +static void increase_capacity(Stack *stack) { + if (!stack) { + return; + } + size_t new_cap = stack->capacity == 0 ? STACK_INITIAL_CAPACITY : stack->capacity * STACK_GROW_FACTOR; + resize_capacity(stack, new_cap); } void Stack_Init(Stack *stack) { @@ -72,28 +74,32 @@ Stack *Stack_Copy(const Stack *src) { if (!src) { return NULL; } - Stack *copy = malloc(sizeof(Stack)); - if (!copy) { - logFatal("Failed to allocate memory for stack copy."); - } - - size_t cap = src->capacity ? src->capacity : STACK_INITIAL_CAPACITY; // handle capacity == 0 + Stack *copy = Stack_Create(); + Stack_CopyTo(copy, src); + return copy; +} - // allocate memory - copy->items = malloc(cap * sizeof(void *)); - if (!copy->items) { - logFatal("Failed to allocate memory for stack copy items."); +void Stack_CopyTo(Stack *dst, const Stack *src) { + if (!dst || !src) { + return; + } + if (src->size == 0) { + // ealry exit if src is empty + Stack_Clear(dst); + return; } - // only copy items - memcpy(copy->items, src->items, src->size * sizeof(void *)); + if (src->size > dst->capacity) { + // resize dst to a multiple of STACK_INITIAL_SIZE + size_t new_cap = ((src->size % STACK_INITIAL_CAPACITY) + 1) * STACK_INITIAL_CAPACITY; + resize_capacity(dst, new_cap); + } + // copy items + memcpy(dst->items, src->items, src->size * sizeof(void *)); // set remainder to 0 - if (cap > src->size) { - memset(copy->items + src->size, 0, (cap - src->size) * sizeof(void *)); + if (dst->capacity > src->size) { + memset(dst->items + src->size, 0, (dst->capacity - src->size) * sizeof(void *)); } - copy->size = src->size; - copy->capacity = src->capacity; - - return copy; + dst->size = src->size; } void Stack_Push(Stack *stack, void *item) { diff --git a/src/common/stack.h b/src/common/stack.h index 1e0b09c..33bf584 100644 --- a/src/common/stack.h +++ b/src/common/stack.h @@ -35,7 +35,8 @@ void Stack_Deinit(Stack *stack); Stack *Stack_Create(); void Stack_Destroy(Stack *stack); -Stack *Stack_Copy(const Stack *stack); +Stack *Stack_Copy(const Stack *stack); //< create a new copy of stack +void Stack_CopyTo(Stack *dst, const Stack *src); //< copy src to the (already initialized) dst void Stack_Push(Stack *stack, void *item); void *Stack_Pop(Stack *stack); From 517e9416eb094d90040c03d86139278e1bb4eecf Mon Sep 17 00:00:00 2001 From: defname Date: Fri, 24 Oct 2025 14:25:22 +0200 Subject: [PATCH 08/35] save open_blocks_at_begin and open_blocks_at_end to SyntaxHighlightingString instances ins SyntaxHighlighting_HighlightString() --- src/syntax/highlighting.c | 16 +++++++++++----- src/syntax/highlighting.h | 5 +++-- tests/test_syntax_highlighting.c | 13 ++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 837523a..b0385a5 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -26,6 +26,7 @@ SyntaxHighlightingString *SyntaxHighlightingString_Create(const String *text) { shs->tags = NULL; shs->tags_count = 0; shs->tags_capacity = 0; + Stack_Init(&shs->open_blocks_at_begin); Stack_Init(&shs->open_blocks_at_end); shs->tags = malloc(sizeof(SyntaxHighlightingTag) * SHS_TAGS_INITIAL_CAPACITY); @@ -44,7 +45,9 @@ void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs) { shs->tags_count = 0; shs->tags_capacity = 0; shs->text = NULL; + Stack_Deinit(&shs->open_blocks_at_begin); Stack_Deinit(&shs->open_blocks_at_end); + free(shs); } @@ -168,7 +171,7 @@ static void init_match_cache(SyntaxHighlighting *sh) { } } -Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { +const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { // check if there is already old infomation about text in the table SyntaxHighlightingString *shs = Table_Get(sh->strings, text); if (shs) { @@ -182,14 +185,17 @@ Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String * } // create a working copy of the stack - Stack *open_blocks; if (open_blocks_at_begin) { - open_blocks = Stack_Copy(open_blocks_at_begin); + Stack_CopyTo(&shs->open_blocks_at_begin, open_blocks_at_begin); + Stack_CopyTo(&shs->open_blocks_at_end, open_blocks_at_begin); } else { - open_blocks = Stack_Create(); - Stack_Push(open_blocks, sh->def->root); + Stack_Clear(&shs->open_blocks_at_begin); + Stack_Push(&shs->open_blocks_at_begin, sh->def->root); + Stack_CopyTo(&shs->open_blocks_at_end, &shs->open_blocks_at_begin); } + Stack *open_blocks = &shs->open_blocks_at_end; + // initialize the cache fields of the SyntaxBlockDefs init_match_cache(sh); diff --git a/src/syntax/highlighting.h b/src/syntax/highlighting.h index 5c1739e..02f367e 100644 --- a/src/syntax/highlighting.h +++ b/src/syntax/highlighting.h @@ -60,7 +60,8 @@ typedef struct _SyntaxHighlightingString { size_t tags_count; //< number of tags in `tags` size_t tags_capacity; //< capacity of `tags` - Stack open_blocks_at_end; //< Stack of (SyntaxBlockDef*) elements that are open at the end of `text` + Stack open_blocks_at_begin; //< Stack of (SyntaxBlockDef*) elements that are open + Stack open_blocks_at_end; //< Stack of (SyntaxBlockDef*) elements that are open at the end of `text` } SyntaxHighlightingString; #define SHS_TAGS_INITIAL_CAPACITY 16 @@ -103,6 +104,6 @@ void SyntaxHighlighting_Deinit(SyntaxHighlighting *sh); * @returns * A new created `Stack` containing all open blocks at the end of `text`. */ -Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks); +const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks); #endif \ No newline at end of file diff --git a/tests/test_syntax_highlighting.c b/tests/test_syntax_highlighting.c index 54ecfe6..215bc9d 100644 --- a/tests/test_syntax_highlighting.c +++ b/tests/test_syntax_highlighting.c @@ -56,9 +56,10 @@ static void assert_highlight_tags( SyntaxHighlighting hl; SyntaxHighlighting_Init(&hl, def); - Stack *open_blocks_at_begin = Stack_Create(); - Stack_Push(open_blocks_at_begin, def->root); - Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &str, open_blocks_at_begin); + const Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &str, NULL); + + SyntaxHighlightingString *shs = Table_Get(hl.strings, &str); + TEST_ASSERT(open_blocks == &shs->open_blocks_at_end); TEST_CHECK(!Stack_IsEmpty(open_blocks)); TEST_MSG("Expected open_blocks to not be not empty."); @@ -69,8 +70,6 @@ static void assert_highlight_tags( TEST_CHECK(strcmp(open_blocks_expected[i], block->name) == 0); TEST_MSG("Expected open block #%zu to be '%s' but got '%s'.", i, open_blocks_expected[i], block->name); } - - SyntaxHighlightingString *shs = Table_Get(hl.strings, &str); TEST_CHECK(shs->tags_count == tags_count); TEST_MSG("Expected %zu tags but got %zu.", tags_count, shs->tags_count); @@ -83,8 +82,6 @@ static void assert_highlight_tags( TEST_MSG("Expected block #%zu to be '%s' but got '%s'.", i, tag_blocks[i], shs->tags[i].block->name); } - Stack_Destroy(open_blocks); - Stack_Destroy(open_blocks_at_begin); String_Deinit(&str); SyntaxHighlighting_Deinit(&hl); SyntaxDefinition_Destroy(def); @@ -122,7 +119,6 @@ void test_highlight_string_simple(void) { TEST_CHECK(shs->tags[1].byte_offset == 13); // end of 'string' TEST_CHECK(shs->tags[1].block == def->root); - Stack_Destroy(open_blocks); Stack_Destroy(open_blocks_at_begin); String_Deinit(&test1); SyntaxHighlighting_Deinit(&hl); @@ -380,7 +376,6 @@ void test_stress(void) { TEST_CHECK(open_blocks != NULL); TEST_CHECK(!Stack_IsEmpty(open_blocks)); - Stack_Destroy(open_blocks); String_Deinit(&test); SyntaxHighlighting_Deinit(&hl); SyntaxDefinition_Destroy(def); From a9ed0c18e58a79cae12bf61e1a492c39ee11c438 Mon Sep 17 00:00:00 2001 From: Bodo Date: Sat, 25 Oct 2025 09:24:54 +0200 Subject: [PATCH 09/35] Add Syntax Highlighting binding for TextBuffer (#87) * add TableIterator (#79) * add TableIterator * change include that provides ssize_t * add additional tests * make current slot pointer const * include stdbool.h for bool * Add early-exit guards to table lookup functions (#80) * Fix bug in reading operations on an empty table * add check for empty table also to Table_Delete() * Add CodeRabbit configuration template Added a comprehensive configuration template for CodeRabbit, including global settings, review settings, chat configurations, knowledge base settings, and code generation options. * add SyntaxHighlightingBinding to connect SyntaxHighlighting with the TextBuffer/TextLayout * fix some obvious bugs in SyntaxHighlighingBinding * fix memory leak * add basic test * add Stack_Size() * rename test function * add Stack_CopyTo() * save open_blocks_at_begin and open_blocks_at_end to SyntaxHighlightingString instances ins SyntaxHighlighting_HighlightString() * adept code to the changed SyntaxHighlighingString scheme * improve tests * Compare the open_blocks_at_begin with the open_blocks parameter for an early exit int SyntaxHighlighting_HighlightString() * add testcases and improve test framework's flexibility * fix coderabbit config * fix resizing in Stack_CopyTo() * add NULL guard to SyntaxHighlighting_HighlightString() * change multiline string to valid C string * change multiline strings to valid C strings * set shs->tags_capacity correctly in SyntaxHighlioghtingString_Create() --- .coderabbit.yaml | 805 +++++++++++++++++++++++++ src/common/stack.c | 66 +- src/common/stack.h | 4 +- src/syntax/highlighting.c | 38 +- src/syntax/highlighting.h | 10 +- src/syntax/textlayoutbindings.c | 51 ++ src/syntax/textlayoutbindings.h | 17 + tests/test_syntax_highlighting.c | 197 ++++-- tests/test_syntax_textlayoutbindings.c | 166 +++++ 9 files changed, 1252 insertions(+), 102 deletions(-) create mode 100644 .coderabbit.yaml create mode 100644 src/syntax/textlayoutbindings.c create mode 100644 src/syntax/textlayoutbindings.h create mode 100644 tests/test_syntax_textlayoutbindings.c diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..56adac6 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,805 @@ +# CodeRabbit Configuration Template +# Complete reference: https://docs.coderabbit.ai/reference/configuration +# Copy this file to .coderabbit.yaml in your repository root + +# ============================================================================= +# GLOBAL SETTINGS +# These settings apply to your entire CodeRabbit configuration +# ============================================================================= + +# Set the language for reviews by using the corresponding ISO language code. +# Options: de, de-DE, de-AT, de-CH, en, en-US, en-AU, en-GB, en-CA, en-NZ, en-ZA, es, es-AR, fr, fr-CA, fr-CH, fr-BE, nl, nl-BE, pt-AO, pt, pt-BR, pt-MZ, pt-PT, ar, ast-ES, ast, be-BY, be, br-FR, br, ca-ES, ca, ca-ES-valencia, ca-ES-balear, da-DK, da, de-DE-x-simple-language, el-GR, el, eo, fa, ga-IE, ga, gl-ES, gl, it, ja-JP, ja, km-KH, km, ko-KR, ko, pl-PL, pl, ro-RO, ro, ru-RU, ru, sk-SK, sk, sl-SI, sl, sv, ta-IN, ta, tl-PH, tl, tr, uk-UA, uk, zh-CN, zh, crh-UA, crh, cs-CZ, cs, nb, no, nl-NL, de-DE-x-simple-language-DE, es-ES, it-IT, fa-IR, sv-SE, de-LU, fr-FR, bg-BG, bg, he-IL, he, hi-IN, hi, vi-VN, vi, th-TH, th, bn-BD, bn +# Default: "en-US" +language: "en-US" + +# Set the tone of reviews and chat. Example: 'You must use talk like Mr. T. I pity the fool who doesn't!' +# Default: "" +tone_instructions: "" + +# Enable early-access features. +# Default: false +early_access: false + +# Enable free tier features for users not on a paid plan. +# Default: true +enable_free_tier: true + +# ============================================================================= +# REVIEWS +# Settings related to reviews. +# ============================================================================= + +# Settings related to reviews. +# Default: {} +reviews: + + # Set the profile for reviews. Assertive profile yields more feedback, that may be considered nitpicky. + # Options: chill, assertive + # Default: "chill" + profile: "chill" + + # Approve the review once CodeRabbit’s comments are resolved and no pre-merge checks are in an error state. Note: In GitLab, all discussions must be resolved. + # Default: false + request_changes_workflow: false + + # Generate a high level summary of the changes in the PR/MR description. + # Default: true + high_level_summary: true + + # Placeholder in the PR/MR description that gets replaced with the high level summary. + # Default: "@coderabbitai summary" + high_level_summary_placeholder: "@coderabbitai summary" + + # Include the high level summary in the walkthrough comment. + # Default: false + high_level_summary_in_walkthrough: false + + # Add this keyword in the PR/MR title to auto-generate the title. + # Default: "@coderabbitai" + auto_title_placeholder: "@coderabbitai" + + # Auto Title Instructions - Custom instructions for auto-generating the PR/MR title. + # Default: "" + auto_title_instructions: "" + + # Post review details on each review. Additionally, post a review status when a review is skipped in certain cases. + # Default: true + review_status: true + + # Set the commit status to 'pending' when the review is in progress and 'success' when it is complete. + # Default: true + commit_status: true + + # Set the commit status to 'failure' when the PR cannot be reviewed by CodeRabbit for any reason. + # Default: false + fail_commit_status: false + + # Generate walkthrough in a markdown collapsible section. + # Default: false + collapse_walkthrough: false + + # Generate a summary of the changed files in the walkthrough. + # Default: true + changed_files_summary: true + + # Generate sequence diagrams in the walkthrough. + # Default: true + sequence_diagrams: true + + # Estimate the code review effort in the walkthrough. + # Default: true + estimate_code_review_effort: true + + # Generate an assessment of how well the changes address the linked issues in the walkthrough. + # Default: true + assess_linked_issues: true + + # Include possibly related issues in the walkthrough. + # Default: true + related_issues: true + + # Related PRs - Include possibly related pull requests in the walkthrough. + # Default: true + related_prs: true + + # Suggest labels based on the changes in the pull request in the walkthrough. + # Default: true + suggested_labels: true + + # Labeling Instructions - Provide guidelines for suggesting labels for the PR/MR. When specific labels or instructions are provided, only those labels are considered, though previous examples are still used to inform the suggestions. If no such labels are provided, suggestions are based solely on previous PR/MRs. + # Default: [] + labeling_instructions: [] + + # Automatically apply the suggested labels to the PR/MR. + # Default: false + auto_apply_labels: false + + # Suggest reviewers based on the changes in the pull request in the walkthrough. + # Default: true + suggested_reviewers: true + + # Automatically assign suggested reviewers to the pull request + # Default: false + auto_assign_reviewers: false + + # Post an in-progress fortune message while the review is in progress. + # Default: true + in_progress_fortune: true + + # Generate a poem in the walkthrough comment. + # Default: true + poem: true + + # Specify file patterns to include or exclude in a review using glob patterns (e.g., !dist/**, src/**). These patterns also apply to 'git sparse-checkout', including specified patterns and ignoring excluded ones (starting with '!') when cloning the repository. + # Default: [] + path_filters: [] + + # Path Instructions - Provide specific additional guidelines for code review based on file paths. + # Default: [] + path_instructions: [] + + # Abort the in-progress review if the pull request is closed or merged. + # Default: true + abort_on_close: true + + # Disable caching of code and dependencies. This will force CodeRabbit to download the code and dependencies fresh from the repository each time. + # Default: false + disable_cache: false + + # Configuration for auto review + # Default: {} + auto_review: + + # Automatic Review - Automatic code review + # Default: true + enabled: true + + # Automatic Incremental Review - Automatic incremental code review on each push + # Default: true + auto_incremental_review: true + + # Ignore reviewing if the title of the pull request contains any of these keywords (case-insensitive). + # Default: [] + ignore_title_keywords: [] + + # List of labels to control which PRs/MRs to review. Labels starting with '!' are negative matches. Examples: ['bug', 'feature'] - reviews PRs with 'bug' OR 'feature' label. ['!wip'] - reviews all PRs except those with 'wip' label. ['bug', '!wip'] - reviews PRs with 'bug' label but not if they have 'wip' label. + # Default: [] + labels: [] + + # Review draft PRs/MRs. + # Default: false + drafts: true + + # Base branches (other than the default branch) to review. Accepts regex patterns. Use '.*' to match all branches. + # Default: [] + base_branches: + - ".*" + + # Ignore reviewing pull requests by these usernames. These should match the Git platform usernames exactly, not the email addresses. + # Default: [] + ignore_usernames: [] + + # Configuration for finishing touches + # Default: {} + finishing_touches: + + # Docstrings - Options for generating Docstrings for your PRs/MRs. + # Default: {} + docstrings: + + # Docstrings - Allow CodeRabbit to generate docstrings for PRs/MRs. + # Default: true + enabled: true + + # Unit Tests - Options for generating unit tests for your PRs/MRs. + # Default: {} + unit_tests: + + # Unit Tests - Allow CodeRabbit to generate unit tests for PRs/MRs. + # Default: true + enabled: true + + # Configuration for pre merge checks + # Default: {} + pre_merge_checks: + + # Docstring Coverage - Checks if the code has sufficient docstrings. + # Default: {} + docstrings: + + # Mode - Determines how strictly the docstring coverage check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved. + # Options: off, warning, error + # Default: "warning" + mode: "warning" + + # Percentage threshold for docstring coverage check. + # Default: 80 + threshold: 80 + + # Title Check - Checks if the pull request title is appropriate and follows best practices. + # Default: {} + title: + + # Mode - Determines how strictly the title check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved. + # Options: off, warning, error + # Default: "warning" + mode: "warning" + + # Requirements - Requirements for the pull request title. Example: 'Title should be concise and descriptive, ideally under 50 characters.' + # Default: "" + requirements: "" + + # Description Check - Checks if the pull request description is appropriate and follows best practices. + # Default: {} + description: + + # Mode - Determines how strictly the description check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved. + # Options: off, warning, error + # Default: "warning" + mode: "warning" + + # Linked Issue Assessment - Checks if the pull request addresses the linked issues. Generate an assessment of how well the changes address the linked issues. + # Default: {} + issue_assessment: + + # Mode - Determines how strictly the issue assessment check is enforced. Warning will only generate a warning and does not require the user to resolve the check. Error requires the user to resolve issues before merging the pull request. If set to error and the request changes workflow is enabled, the pull request will be blocked until the issues are resolved. + # Options: off, warning, error + # Default: "warning" + mode: "warning" + + # Custom Pre-merge Checks - Add unique checks to enforce your team's standards before merging a pull request. Each check must have a unique name (up to 50 characters) and clear instructions (up to 10000 characters). Use these to automatically verify coding, security, documentation, or business rules and maintain code quality. + # Default: [] + custom_checks: [] + + # Tools that provide additional context to code reviews. + # Default: {} + tools: + + # Enable ast-grep - ast-grep is a code analysis tool that helps you to find patterns in your codebase using abstract syntax trees patterns. - v0.39.6 + # Default: {} + ast-grep: + + # List of rules directories. + # Default: [] + rule_dirs: [] + + # List of utils directories. + # Default: [] + util_dirs: [] + + # Use ast-grep essentials package. + # Default: true + essential_rules: true + + # Predefined packages to be used. + # Default: [] + packages: [] + + # ShellCheck is a static analysis tool that finds bugs in your shell scripts. + # Default: {} + shellcheck: + + # Enable ShellCheck - ShellCheck is a static analysis tool that finds bugs in your shell. - Enable ShellCheck integration. - v0.11.0 + # Default: true + enabled: true + + # Ruff is a Python linter and code formatter. + # Default: {} + ruff: + + # Enable Ruff - Ruff is a Python linter and code formatter. - Enable Ruff integration. - v0.14.1 + # Default: true + enabled: true + + # markdownlint-cli2 is a static analysis tool to enforce standards and consistency for Markdown files. + # Default: {} + markdownlint: + + # Enable markdownlint - markdownlint-cli2 is a static analysis tool to enforce standards and consistency for Markdown files. - Enable markdownlint integration. - v0.18.1 + # Default: true + enabled: true + + # GitHub Checks integration configuration. + # Default: {} + github-checks: + + # Enable GitHub Checks - Enable integration, defaults to true - Enable GitHub Checks integration. + # Default: true + enabled: true + + # Time in milliseconds to wait for all GitHub Checks to conclude. Default 90 seconds, max 15 minutes (900000ms). + # Default: 90000 + timeout_ms: 90000 + + # LanguageTool is a style and grammar checker for 30+ languages. + # Default: {} + languagetool: + + # Enable LanguageTool - Enable LanguageTool integration. + # Default: true + enabled: true + + # IDs of rules to be enabled. The rule won't run unless 'level' is set to a level that activates the rule. + # Default: [] + enabled_rules: [] + + # IDs of rules to be disabled. Note: EN_UNPAIRED_BRACKETS, and EN_UNPAIRED_QUOTES are always disabled. + # Default: [] + disabled_rules: [] + + # IDs of categories to be enabled. + # Default: [] + enabled_categories: [] + + # IDs of categories to be disabled. Note: TYPOS, TYPOGRAPHY, and CASING are always disabled. + # Default: [] + disabled_categories: [] + + # Only the rules and categories whose IDs are specified with 'enabledRules' or 'enabledCategories' are enabled. + # Default: false + enabled_only: false + + # If set to 'picky', additional rules will be activated, i.e. rules that you might only find useful when checking formal text. + # Options: default, picky + # Default: "default" + level: "default" + + # Biome is a fast formatter, linter, and analyzer for web projects. + # Default: {} + biome: + + # Enable Biome - Biome is a fast formatter, linter, and analyzer for web projects. - Enable Biome integration. - v2.1.2 + # Default: true + enabled: true + + # Hadolint is a Dockerfile linter. + # Default: {} + hadolint: + + # Enable Hadolint - Hadolint is a Dockerfile linter. - Enable Hadolint integration. - v2.14.0 + # Default: true + enabled: true + + # SwiftLint integration configuration object. + # Default: {} + swiftlint: + + # Enable SwiftLint - SwiftLint is a Swift linter. - Enable SwiftLint integration. - v0.57.0 + # Default: true + enabled: true + + # Optional path to the SwiftLint configuration file relative to the repository. This is useful when the configuration file is named differently than the default '.swiftlint.yml' or '.swiftlint.yaml'. + config_file: "example-value" + + # PHPStan is a tool to analyze PHP code. + # Default: {} + phpstan: + + # Enable PHPStan - PHPStan requires [config file](https://phpstan.org/config-reference#config-file) in your repository root. Please ensure that this file contains the `paths:` parameter. - v2.1.31 + # Default: true + enabled: true + + # Level - Specify the [rule level](https://phpstan.org/user-guide/rule-levels) to run. This setting is ignored if your configuration file already has a `level:` parameter. + # Options: default, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, max + # Default: "default" + level: "default" + + # PHPMD is a tool to find potential problems in PHP code. + # Default: {} + phpmd: + + # Enable PHPMD - PHPMD is a tool to find potential problems in PHP code. - v2.15.0 + # Default: true + enabled: true + + # PHP CodeSniffer is a PHP linter and coding standard checker. + # Default: {} + phpcs: + + # Enable PHP CodeSniffer - PHP CodeSniffer is a PHP linter and coding standard checker. - v3.7.2 + # Default: true + enabled: true + + # golangci-lint is a fast linters runner for Go. + # Default: {} + golangci-lint: + + # Enable golangci-lint - golangci-lint is a fast linters runner for Go. - Enable golangci-lint integration. - v2.5.0 + # Default: true + enabled: true + + # Optional path to the golangci-lint configuration file relative to the repository. Useful when the configuration file is named differently than the default '.golangci.yml', '.golangci.yaml', '.golangci.toml', '.golangci.json'. + config_file: "example-value" + + # YAMLlint is a linter for YAML files. + # Default: {} + yamllint: + + # Enable YAMLlint - YAMLlint is a linter for YAML files. - Enable YAMLlint integration. - v1.37.1 + # Default: true + enabled: true + + # Gitleaks is a secret scanner. + # Default: {} + gitleaks: + + # Enable Gitleaks - Gitleaks is a secret scanner. - Enable Gitleaks integration. - v8.28.0 + # Default: true + enabled: true + + # Checkov is a static code analysis tool for infrastructure-as-code files. + # Default: {} + checkov: + + # Enable Checkov - Checkov is a static code analysis tool for infrastructure-as-code files. - v3.2.334 + # Default: true + enabled: true + + # Detekt is a static code analysis tool for Kotlin files. + # Default: {} + detekt: + + # Enable detekt - detekt is a static code analysis tool for Kotlin files. - v1.23.8 + # Default: true + enabled: true + + # Optional path to the detekt configuration file relative to the repository. + config_file: "example-value" + + # ESLint is a static code analysis tool for JavaScript files. + # Default: {} + eslint: + + # Enable ESLint - ESLint is a static code analysis tool for JavaScript files. + # Default: true + enabled: true + + # Flake8 is a Python linter that wraps PyFlakes, pycodestyle and Ned Batchelder's McCabe script. + # Default: {} + flake8: + + # Enable Flake8 - Flake8 is a Python linter that wraps PyFlakes, pycodestyle and Ned Batchelder's McCabe script. - v7.3.0 + # Default: true + enabled: true + + # RuboCop is a Ruby static code analyzer (a.k.a. linter ) and code formatter. + # Default: {} + rubocop: + + # Enable RuboCop - RuboCop is a Ruby static code analyzer (a.k.a. linter ) and code formatter. - v1.81.1 + # Default: true + enabled: true + + # Buf offers linting for Protobuf files. + # Default: {} + buf: + + # Enable Buf - Buf offers linting for Protobuf files. - v1.58.0 + # Default: true + enabled: true + + # Regal is a linter and language server for Rego. + # Default: {} + regal: + + # Enable Regal - Regal is a linter and language server for Rego. - v0.36.1 + # Default: true + enabled: true + + # actionlint is a static checker for GitHub Actions workflow files. + # Default: {} + actionlint: + + # Enable actionlint - is a static checker for GitHub Actions workflow files. - v1.7.8 + # Default: true + enabled: true + + # PMD is an extensible multilanguage static code analyzer. It’s mainly concerned with Java. + # Default: {} + pmd: + + # Enable PMD - PMD is an extensible multilanguage static code analyzer. It’s mainly concerned with Java. - v7.17.0 + # Default: true + enabled: true + + # Optional path to the PMD configuration file relative to the repository. + config_file: "example-value" + + # Configuration for Clang to perform static analysis on C and C++ code + # Default: {} + clang: + + # Enable Clang for C/C++ static analysis and code quality checks - v14.0.6 + # Default: true + enabled: true + + # Cppcheck is a static code analysis tool for the C and C++ programming languages. + # Default: {} + cppcheck: + + # Enable Cppcheck - Cppcheck is a static code analysis tool for the C and C++ programming languages. - v2.18.0 + # Default: true + enabled: true + + # Semgrep is a static analysis tool designed to scan code for security vulnerabilities and code quality issues. + # Default: {} + semgrep: + + # Enable Semgrep - Semgrep is a static analysis tool designed to scan code for security vulnerabilities and code quality issues. - Enable Semgrep integration. - v1.140.0 + # Default: true + enabled: true + + # Optional path to the Semgrep configuration file relative to the repository. + config_file: "example-value" + + # CircleCI tool is a static checker for CircleCI config files. + # Default: {} + circleci: + + # Enable CircleCI - CircleCI tool is a static checker for CircleCI config files. - v0.1.33494 + # Default: true + enabled: true + + # Clippy is a collection of lints to catch common mistakes and improve your Rust code. + # Default: {} + clippy: + + # Enable Clippy - Clippy is a collection of lints to catch common mistakes and improve your Rust code. - Enable Clippy integration. + # Default: true + enabled: true + + # SQLFluff is an open source, dialect-flexible and configurable SQL linter. + # Default: {} + sqlfluff: + + # Enable SQLFluff - SQLFluff is an open source, dialect-flexible and configurable SQL linter. - v3.5.0 + # Default: true + enabled: true + + # Configuration for Prisma Schema linting to ensure schema file quality + # Default: {} + prismaLint: + + # Enable Prisma Schema linting - Prisma Schema linting helps maintain consistent and error-free schema files - v0.10.3 + # Default: true + enabled: true + + # Pylint is a Python static code analysis tool. + # Default: {} + pylint: + + # Enable Pylint - Pylint is a Python static code analysis tool. - v4.0.1 + # Default: true + enabled: true + + # Oxlint is a JavaScript/TypeScript linter for OXC written in Rust. + # Default: {} + oxc: + + # Enable Oxlint - Oxlint is a JavaScript/TypeScript linter for OXC written in Rust. - v1.23.0 + # Default: true + enabled: true + + # Configuration for Shopify Theme Check to ensure theme quality and best practices + # Default: {} + shopifyThemeCheck: + + # Enable Shopify Theme Check - A linter for Shopify themes that helps you follow Shopify theme & Liquid best practices - cli 3.84.2 - theme 3.58.2 + # Default: true + enabled: true + + # Configuration for Lua code linting to ensure code quality + # Default: {} + luacheck: + + # Enable Lua code linting - Luacheck helps maintain consistent and error-free Lua code - v1.2.0 + # Default: true + enabled: true + + # Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications. - v7.1.0 + # Default: {} + brakeman: + + # Enable Brakeman - Brakeman is a static analysis security vulnerability scanner for Ruby on Rails applications. - v7.1.0 + # Default: true + enabled: true + + # dotenv-linter is a tool for checking and fixing .env files for problems and best practices + # Default: {} + dotenvLint: + + # Enable dotenv-linter - dotenv-linter is a tool for checking and fixing .env files for problems and best practices - v4.0.0 + # Default: true + enabled: true + + # HTMLHint is a static code analysis tool for HTML files. + # Default: {} + htmlhint: + + # Enable HTMLHint - HTMLHint is a static code analysis tool for HTML files. - Enable HTMLHint integration. - v1.7.1 + # Default: true + enabled: true + + # checkmake is a linter for Makefiles. + # Default: {} + checkmake: + + # Enable checkmake - checkmake is a linter for Makefiles. - v0.2.2 + # Default: true + enabled: true + + # OSV Scanner is a tool for vulnerability package scanning. + # Default: {} + osvScanner: + + # Enable OSV Scanner - OSV Scanner is a tool for vulnerability package scanning - v2.2.3 + # Default: true + enabled: true + +# ============================================================================= +# CHAT +# Configuration for chat +# ============================================================================= + +# Configuration for chat +# Default: {} +chat: + + # Generate art in response to chat messages. CodeRabbit expresses emotions as either ASCII or Emoji art. + # Default: true + art: true + + # Enable the bot to reply automatically without requiring the user to tag it. + # Default: true + auto_reply: true + + # Configuration for integrations + # Default: {} + integrations: + + # Configuration for jira + # Default: {} + jira: + + # Jira - Enable the Jira integration for opening issues, etc. 'auto' disables the integration for public repositories. + # Options: auto, enabled, disabled + # Default: "auto" + usage: "auto" + + # Configuration for linear + # Default: {} + linear: + + # Linear - Enable the Linear integration for opening issues, etc. 'auto' disables the integration for public repositories. + # Options: auto, enabled, disabled + # Default: "auto" + usage: "auto" + +# ============================================================================= +# KNOWLEDGE BASE +# Configuration for knowledge base +# ============================================================================= + +# Configuration for knowledge base +# Default: {} +knowledge_base: + + # Opt Out - Disable all knowledge base features that require data retention. If you opt out after opting in, all of your existing knowledge base data will be removed from the system. + # Default: false + opt_out: false + + # Configuration for web search + # Default: {} + web_search: + + # Web Search - Enable the web search integration. + # Default: true + enabled: true + + # CodeRabbit will analyse and learn from your organization's code guidelines, which you can mention in the file patterns section. These guidelines will then be used to conduct thorough code reviews. + # Default: {} + code_guidelines: + + # Enabled - Enable CodeRabbit to enforce your organization's coding standards during reviews. + # Default: true + enabled: true + + # File Patterns - Specify files for your coding guideline documents in this section. CodeRabbit will scan these files to understand your team's standards and apply them during code reviews. Multiple files supported. File names are case-sensitive. Common files like: (**/.cursorrules, .github/copilot-instructions.md, .github/instructions/*.instructions.md, **/CLAUDE.md, **/GEMINI.md, **/.cursor/rules/*, **/.windsurfrules, **/.clinerules/*, **/.rules/*, **/AGENT.md, **/AGENTS.md) are included by default. + # Default: [] + filePatterns: [] + + # Configuration for learnings + # Default: {} + learnings: + + # Learnings - Specify the scope of learnings to use for the knowledge base. 'local' uses the repository's learnings, 'global' uses the organization's learnings, and 'auto' uses repository's learnings for public repositories and organization's learnings for private repositories. + # Options: local, global, auto + # Default: "auto" + scope: "auto" + + # Configuration for issues + # Default: {} + issues: + + # Issues - Specify the scope of git platform (GitHub/GitLab) issues to use for the knowledge base. 'local' uses the repository's issues, 'global' uses the organization's issues, and 'auto' uses repository's issues for public repositories and organization's issues for private repositories. + # Options: local, global, auto + # Default: "auto" + scope: "auto" + + # Configuration for jira + # Default: {} + jira: + + # Jira - Enable the Jira knowledge base integration. 'auto' disables the integration for public repositories. + # Options: auto, enabled, disabled + # Default: "auto" + usage: "auto" + + # Jira Project Keys - Specify the Jira project keys to use for the knowledge base. + # Default: [] + project_keys: [] + + # Configuration for linear + # Default: {} + linear: + + # Linear - Enable the Linear knowledge base integration. 'auto' disables the integration for public repositories. + # Options: auto, enabled, disabled + # Default: "auto" + usage: "auto" + + # Linear Team Keys - Specify the Linear team keys (identifiers) to use for the knowledge base. E.g. 'ENG' + # Default: [] + team_keys: [] + + # Configuration for pull requests + # Default: {} + pull_requests: + + # Pull Requests - Specify the scope of pull requests to use for the knowledge base. 'local' uses the repository's pull requests, 'global' uses the organization's pull requests, and 'auto' uses repository's pull requests for public repositories and organization's pull requests for private repositories. + # Options: local, global, auto + # Default: "auto" + scope: "auto" + + # Configuration for mcp + # Default: {} + mcp: + + # MCP - Enable the MCP knowledge base integration. 'auto' disables the integration for public repositories. + # Options: auto, enabled, disabled + # Default: "auto" + usage: "auto" + + # MCP Disabled Servers - Specify MCP server labels to disable (case-insensitive). These servers will be excluded from reviews and knowledge base queries. + # Default: [] + disabled_servers: [] + +# ============================================================================= +# CODE GENERATION +# Configuration for code generation +# ============================================================================= + +# Configuration for code generation +# Default: {} +code_generation: + + # Settings related to the generation of docstrings. + # Default: {"path_instructions":[]} + docstrings: + + # Set the language for docstrings by using the corresponding ISO language code. + # Options: de, de-DE, de-AT, de-CH, en, en-US, en-AU, en-GB, en-CA, en-NZ, en-ZA, es, es-AR, fr, fr-CA, fr-CH, fr-BE, nl, nl-BE, pt-AO, pt, pt-BR, pt-MZ, pt-PT, ar, ast-ES, ast, be-BY, be, br-FR, br, ca-ES, ca, ca-ES-valencia, ca-ES-balear, da-DK, da, de-DE-x-simple-language, el-GR, el, eo, fa, ga-IE, ga, gl-ES, gl, it, ja-JP, ja, km-KH, km, ko-KR, ko, pl-PL, pl, ro-RO, ro, ru-RU, ru, sk-SK, sk, sl-SI, sl, sv, ta-IN, ta, tl-PH, tl, tr, uk-UA, uk, zh-CN, zh, crh-UA, crh, cs-CZ, cs, nb, no, nl-NL, de-DE-x-simple-language-DE, es-ES, it-IT, fa-IR, sv-SE, de-LU, fr-FR, bg-BG, bg, he-IL, he, hi-IN, hi, vi-VN, vi, th-TH, th, bn-BD, bn + # Default: "en-US" + language: "en-US" + + # Path Instructions - Provide additional guidelines for docstring generation based on file paths. + # Default: [] + path_instructions: [] + + # Settings related to the generation of unit tests. + # Default: {"path_instructions":[]} + unit_tests: + + # Unit Test Generation - Provide additional guidelines for unit test generation based on file paths. + # Default: [] + path_instructions: [] diff --git a/src/common/stack.c b/src/common/stack.c index 2e8eb0f..f065b52 100644 --- a/src/common/stack.c +++ b/src/common/stack.c @@ -17,21 +17,23 @@ #include #include "logging.h" - -static void increase_capacity(Stack *stack) { - if (!stack) { +static void resize_capacity(Stack *stack, size_t new_capacity) { + if (!stack || stack->capacity >= new_capacity) { return; } - if (stack->capacity == 0) { - stack->capacity = STACK_INITIAL_CAPACITY; - } - else { - stack->capacity *= STACK_GROW_FACTOR; - } - stack->items = realloc(stack->items, stack->capacity * sizeof(void *)); + stack->items = realloc(stack->items, new_capacity * sizeof(void *)); if (!stack->items) { logFatal("Failed to reallocate stack items."); } + stack->capacity = new_capacity; +} + +static void increase_capacity(Stack *stack) { + if (!stack) { + return; + } + size_t new_cap = stack->capacity == 0 ? STACK_INITIAL_CAPACITY : stack->capacity * STACK_GROW_FACTOR; + resize_capacity(stack, new_cap); } void Stack_Init(Stack *stack) { @@ -72,28 +74,32 @@ Stack *Stack_Copy(const Stack *src) { if (!src) { return NULL; } - Stack *copy = malloc(sizeof(Stack)); - if (!copy) { - logFatal("Failed to allocate memory for stack copy."); - } - - size_t cap = src->capacity ? src->capacity : STACK_INITIAL_CAPACITY; // handle capacity == 0 + Stack *copy = Stack_Create(); + Stack_CopyTo(copy, src); + return copy; +} - // allocate memory - copy->items = malloc(cap * sizeof(void *)); - if (!copy->items) { - logFatal("Failed to allocate memory for stack copy items."); +void Stack_CopyTo(Stack *dst, const Stack *src) { + if (!dst || !src) { + return; } - // only copy items - memcpy(copy->items, src->items, src->size * sizeof(void *)); + if (src->size == 0) { + // early exit if src is empty + Stack_Clear(dst); + return; + } + if (src->size > dst->capacity) { + // resize dst to a multiple of STACK_INITIAL_SIZE + size_t new_cap = ((src->size + STACK_INITIAL_CAPACITY - 1) / STACK_INITIAL_CAPACITY) * STACK_INITIAL_CAPACITY; + resize_capacity(dst, new_cap); + } + // copy items + memcpy(dst->items, src->items, src->size * sizeof(void *)); // set remainder to 0 - if (cap > src->size) { - memset(copy->items + src->size, 0, (cap - src->size) * sizeof(void *)); + if (dst->capacity > src->size) { + memset(dst->items + src->size, 0, (dst->capacity - src->size) * sizeof(void *)); } - copy->size = src->size; - copy->capacity = src->capacity; - - return copy; + dst->size = src->size; } void Stack_Push(Stack *stack, void *item) { @@ -137,6 +143,10 @@ bool Stack_IsEmpty(const Stack *stack) { return stack->size == 0; } +size_t Stack_Size(const Stack *stack) { + return stack->size; +} + void Stack_Clear(Stack *stack) { if (!stack) { return; diff --git a/src/common/stack.h b/src/common/stack.h index 1e0b09c..c72fc60 100644 --- a/src/common/stack.h +++ b/src/common/stack.h @@ -35,7 +35,8 @@ void Stack_Deinit(Stack *stack); Stack *Stack_Create(); void Stack_Destroy(Stack *stack); -Stack *Stack_Copy(const Stack *stack); +Stack *Stack_Copy(const Stack *stack); //< create a new copy of stack +void Stack_CopyTo(Stack *dst, const Stack *src); //< copy src to the (already initialized) dst void Stack_Push(Stack *stack, void *item); void *Stack_Pop(Stack *stack); @@ -44,6 +45,7 @@ void *Stack_Peek(const Stack *stack); bool Stack_Has(const Stack *stack, const void *item); bool Stack_IsEmpty(const Stack *stack); +size_t Stack_Size(const Stack *stack); void Stack_Clear(Stack *stack); diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 837523a..f9f0e06 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -26,12 +26,14 @@ SyntaxHighlightingString *SyntaxHighlightingString_Create(const String *text) { shs->tags = NULL; shs->tags_count = 0; shs->tags_capacity = 0; + Stack_Init(&shs->open_blocks_at_begin); Stack_Init(&shs->open_blocks_at_end); shs->tags = malloc(sizeof(SyntaxHighlightingTag) * SHS_TAGS_INITIAL_CAPACITY); if (!shs->tags) { logFatal("Cannot allocate memory for SyntaxHighlightingString tags."); } + shs->tags_capacity = SHS_TAGS_INITIAL_CAPACITY; return shs; } @@ -44,7 +46,9 @@ void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs) { shs->tags_count = 0; shs->tags_capacity = 0; shs->text = NULL; + Stack_Deinit(&shs->open_blocks_at_begin); Stack_Deinit(&shs->open_blocks_at_end); + free(shs); } @@ -168,28 +172,48 @@ static void init_match_cache(SyntaxHighlighting *sh) { } } -Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { +static bool stack_equal(const Stack *a, const Stack *b) { + if (a->size != b->size) { + return false; + } + for (size_t i=0; isize; i++) { + if (a->items[i] != b->items[i]) { + return false; + } + } + return true; +} + +const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { // check if there is already old infomation about text in the table SyntaxHighlightingString *shs = Table_Get(sh->strings, text); if (shs) { - // clear it if so + // early exit if nothing changed since the last calculation + if (open_blocks_at_begin && stack_equal(&shs->open_blocks_at_begin, open_blocks_at_begin)) { + return &shs->open_blocks_at_end; + } + // if open_blocks_at_begin changed clear the old information SyntaxHighlightingString_Clear(shs); } else { - // otherwise create and store + // if there is no object present in sh->strings table create one and store it shs = SyntaxHighlightingString_Create(text); Table_Set(sh->strings, text, shs, (void(*)(void*))SyntaxHighlightingString_Destroy); } // create a working copy of the stack - Stack *open_blocks; if (open_blocks_at_begin) { - open_blocks = Stack_Copy(open_blocks_at_begin); + Stack_CopyTo(&shs->open_blocks_at_begin, open_blocks_at_begin); + Stack_CopyTo(&shs->open_blocks_at_end, open_blocks_at_begin); } else { - open_blocks = Stack_Create(); - Stack_Push(open_blocks, sh->def->root); + // or create a new one if open_blocks_at_begin is NULL + Stack_Clear(&shs->open_blocks_at_begin); + Stack_Push(&shs->open_blocks_at_begin, sh->def->root); + Stack_CopyTo(&shs->open_blocks_at_end, &shs->open_blocks_at_begin); } + Stack *open_blocks = &shs->open_blocks_at_end; + // initialize the cache fields of the SyntaxBlockDefs init_match_cache(sh); diff --git a/src/syntax/highlighting.h b/src/syntax/highlighting.h index 5c1739e..17e0bb9 100644 --- a/src/syntax/highlighting.h +++ b/src/syntax/highlighting.h @@ -60,7 +60,8 @@ typedef struct _SyntaxHighlightingString { size_t tags_count; //< number of tags in `tags` size_t tags_capacity; //< capacity of `tags` - Stack open_blocks_at_end; //< Stack of (SyntaxBlockDef*) elements that are open at the end of `text` + Stack open_blocks_at_begin; //< Stack of (SyntaxBlockDef*) elements that are open + Stack open_blocks_at_end; //< Stack of (SyntaxBlockDef*) elements that are open at the end of `text` } SyntaxHighlightingString; #define SHS_TAGS_INITIAL_CAPACITY 16 @@ -95,14 +96,17 @@ void SyntaxHighlighting_Deinit(SyntaxHighlighting *sh); * @brief Highlight a string according to given context. * * Add a `SyntaxHighlightingString`to `hl->strings` or update an existing one. + * The table uses the pointer address of text as key, so make sure it does not change. + * open_blocks should only be NULL for the first line. In any other case if should be set to + * SyntaxHighlighlightingString->open_blocks_at_end of the previous line/text. * * @param hl The `SyntaxHighlighting` instance to use. * @param text The text to hightlight. * @param open_blocks An pointer to a `Stack` instance that holds the open blocks at the beginning of string. If NULL it's assumed that the root block is current. * * @returns - * A new created `Stack` containing all open blocks at the end of `text`. + * A reference to the `Stack` containing all open blocks at the end of `text`. */ -Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks); +const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks); #endif \ No newline at end of file diff --git a/src/syntax/textlayoutbindings.c b/src/syntax/textlayoutbindings.c new file mode 100644 index 0000000..5681678 --- /dev/null +++ b/src/syntax/textlayoutbindings.c @@ -0,0 +1,51 @@ +#include "textlayoutbindings.h" + +void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, const TextLayout *tl, SyntaxHighlighting *sh) { + binding->tl = tl; + binding->sh = sh; +} + +void SyntaxHighlightingBinding_Deinit(SyntaxHighlightingBinding *binding) { + binding->tl = NULL; + binding->sh = NULL; +} + +static void update_lines(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line, const Stack *open_blocks) { + const Stack *open_blocks_begin = open_blocks; + while (line) { + // open_blocks == NULL is also handled by the function + const Stack *open_blocks_end = SyntaxHighlighting_HighlightString(binding->sh, &line->text, open_blocks_begin); + open_blocks_begin = open_blocks_end; + if (line == last_line) { + break; + } + line = line->next; + } +} + +void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line) { + Line *prev_line = line->prev; + Stack *open_blocks = NULL; + if (prev_line) { + // check if highlighting for the previous line is already calculated + SyntaxHighlightingString *shs = Table_Get(binding->sh->strings, &prev_line->text); + if (shs) { + // use the open_blocks from the end of previous line + open_blocks = &shs->open_blocks_at_end; + } + else { + // highlighting for the line is not calculated so far + // so run this function for thr previous line + SyntaxHighlightingBinding_Update(binding, prev_line, last_line); + return; + } + } + if (line == binding->tl->tb->current_line) { + // this is super dirty cause the gap funcitonality is totally disabled this way!!! + // NEED A BETTER SOLUTION + TextBuffer_MergeGap((TextBuffer*)binding->tl->tb); + } + + // update all lines until last_line (including) + update_lines(binding, line, last_line, open_blocks); +} diff --git a/src/syntax/textlayoutbindings.h b/src/syntax/textlayoutbindings.h new file mode 100644 index 0000000..14cb1bb --- /dev/null +++ b/src/syntax/textlayoutbindings.h @@ -0,0 +1,17 @@ +#ifndef SYNTAX_TEXTLAYOUTBINDINGS_H +#define SYNTAX_TEXTLAYOUTBINDINGS_H + +#include "highlighting.h" +#include "document/textlayout.h" + +typedef struct _SyntaxHighlightingBinding { + const TextLayout *tl; + SyntaxHighlighting *sh; +} SyntaxHighlightingBinding; + +void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, const TextLayout *tl, SyntaxHighlighting *sh); +void SyntaxHighlightingBinding_Deinit(SyntaxHighlightingBinding *binding); + +void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line); + +#endif \ No newline at end of file diff --git a/tests/test_syntax_highlighting.c b/tests/test_syntax_highlighting.c index 54ecfe6..68b49ba 100644 --- a/tests/test_syntax_highlighting.c +++ b/tests/test_syntax_highlighting.c @@ -31,15 +31,26 @@ Table *build_blocks_table(SyntaxDefinition *def) { } typedef struct { - const char *ini; - const char *str; - size_t tags_count; - size_t tag_offsets[32]; - const char *tag_blocks[32]; - size_t open_blocks_count; - const char *open_blocks[32]; + const char *ini; //< text of an INI file for the syntax definition + const char *str; //< str to parse (single line) + size_t open_blocks_at_begin_count; //< number of open blocks at the begin + const char *open_blocks_at_begin[32]; //< list of open block names at the begin + size_t tags_count; //< expected count of tags + size_t tag_offsets[32]; //< list of expected offsets of the tags + const char *tag_blocks[32]; //< list of expected block names of the tags + size_t open_blocks_count; //< number of expected open blocks at the end + const char *open_blocks[32]; //< names of the expected open blocks at the end } TagTestCase; +static void get_blocks(const char **block_names, size_t count, SyntaxBlockDef **out, Table *blocks_table) { + for (size_t i=0; i blocks mapping table + Table *blocks_table = build_blocks_table(def); - Stack *open_blocks_at_begin = Stack_Create(); - Stack_Push(open_blocks_at_begin, def->root); - Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &str, open_blocks_at_begin); + // map the open_blocks_at_begin (str) list to a list of SyntaxBlockDef instances + SyntaxBlockDef *open_blocks_at_begin[32]; + get_blocks(testcase.open_blocks_at_begin, testcase.open_blocks_at_begin_count, open_blocks_at_begin, blocks_table); + + // create the open_blocks_at_begin_stack + Stack *open_blocks_at_begin_stack = Stack_Create(); + for (size_t i=0; iopen_blocks_at_end); TEST_CHECK(!Stack_IsEmpty(open_blocks)); TEST_MSG("Expected open_blocks to not be not empty."); + TEST_CHECK(open_blocks_count_expected == Stack_Size(open_blocks)); + TEST_MSG("Expected %zu open blocks but got %zu.", open_blocks_count_expected, Stack_Size(open_blocks)); + for (size_t i=0; iitems[open_blocks_count_expected-i-1]; TEST_CHECK(block != NULL); TEST_CHECK(strcmp(open_blocks_expected[i], block->name) == 0); TEST_MSG("Expected open block #%zu to be '%s' but got '%s'.", i, open_blocks_expected[i], block->name); } - - SyntaxHighlightingString *shs = Table_Get(hl.strings, &str); TEST_CHECK(shs->tags_count == tags_count); TEST_MSG("Expected %zu tags but got %zu.", tags_count, shs->tags_count); @@ -83,8 +111,9 @@ static void assert_highlight_tags( TEST_MSG("Expected block #%zu to be '%s' but got '%s'.", i, tag_blocks[i], shs->tags[i].block->name); } - Stack_Destroy(open_blocks); - Stack_Destroy(open_blocks_at_begin); + Stack_Destroy(open_blocks_at_begin_stack); + Table_Destroy(blocks_table); + String_Deinit(&str); SyntaxHighlighting_Deinit(&hl); SyntaxDefinition_Destroy(def); @@ -111,7 +140,7 @@ void test_highlight_string_simple(void) { String test1 = String_Format("root 'string' root"); Stack *open_blocks_at_begin = Stack_Create(); Stack_Push(open_blocks_at_begin, def->root); - Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test1, open_blocks_at_begin); + const Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test1, open_blocks_at_begin); TEST_CHECK(open_blocks != NULL); TEST_CHECK(Stack_Peek(open_blocks) == def->root); @@ -122,36 +151,34 @@ void test_highlight_string_simple(void) { TEST_CHECK(shs->tags[1].byte_offset == 13); // end of 'string' TEST_CHECK(shs->tags[1].block == def->root); - Stack_Destroy(open_blocks); Stack_Destroy(open_blocks_at_begin); String_Deinit(&test1); SyntaxHighlighting_Deinit(&hl); SyntaxDefinition_Destroy(def); } -const char *test_ini1 = R"( -[meta] -name = TEST - -[block:root] -child_blocks=string, comment, keyword, brackets - -[block:brackets] -start=\( -end=\) -child_blocks=string, keyword, brackets - -[block:keyword] -start=keyword - -[block:string] -start=' -end=' - -[block:comment] -start = // -end = $ -)"; +const char *test_ini1 = +"[meta]\n" +"name = TEST\n" +"\n" +"[block:root]\n" +"child_blocks=string, comment, keyword, brackets\n" +"\n" +"[block:brackets]\n" +"start=\\(\n" +"end=\\)\n" +"child_blocks=string, keyword, brackets\n" +"\n" +"[block:keyword]\n" +"start=keyword\n" +"\n" +"[block:string]\n" +"start='\n" +"end='\n" +"\n" +"[block:comment]\n" +"start = //\n" +"end = $\n"; void test_basics(void) { @@ -159,6 +186,8 @@ void test_basics(void) { { test_ini1, "foobar // blabla ' bla", // comment + 1, + { "root" }, 2, {7, 22}, {"comment", "root"}, @@ -168,6 +197,8 @@ void test_basics(void) { { test_ini1, "foo 'bar' foo", // string + 1, + { "root" }, 2, {4, 9}, {"string", "root"}, @@ -177,6 +208,8 @@ void test_basics(void) { { test_ini1, "foo keyword foo", // keyword + 1, + { "root" }, 2, {4, 11}, {"keyword", "root"}, @@ -186,6 +219,8 @@ void test_basics(void) { { test_ini1, "foo '//not a comment' foo", // string with comment inside (should be ignored) + 1, + { "root" }, 2, {4, 21}, {"string", "root"}, @@ -195,6 +230,8 @@ void test_basics(void) { { test_ini1, "foo 'not a keyword' foo", // keyword inside string + 1, + { "root" }, 2, {4, 19}, {"string", "root"}, @@ -204,6 +241,8 @@ void test_basics(void) { { test_ini1, "foo (no brackets) foo", + 1, + { "root" }, 2, {4, 17}, {"brackets", "root"}, @@ -213,6 +252,8 @@ void test_basics(void) { { test_ini1, "()", + 1, + { "root" }, 2, {0, 2}, {"brackets", "root"}, @@ -222,6 +263,8 @@ void test_basics(void) { { test_ini1, "(keyword)keyword", + 1, + { "root" }, 6, {0, 1, 8, 9, 9, 16}, {"brackets", "keyword", "brackets", "root", "keyword", "root"}, @@ -231,6 +274,8 @@ void test_basics(void) { { test_ini1, "", + 1, + { "root" }, 0, {}, {}, @@ -240,6 +285,8 @@ void test_basics(void) { { test_ini1, "(('not a keyword'))", + 1, + { "root" }, 6, {0, 1, 2, 17, 18, 19}, {"brackets", "brackets", "string", "brackets", "brackets", "root"}, @@ -256,33 +303,34 @@ void test_basics(void) { } -const char *test_ini2 = R"( -[meta] -name = TEST2 - -[block:root] -child_blocks=assignment, comment - -[block:assignment] -start=^[a-zA-Z_:]+= -end=$ -child_blocks=value - -[block:value] -start=. -end=$ -ends_on=comment - -[block:comment] -start = // -end = $ -)"; +const char *test_ini2 = +"[meta]\n" +"name = TEST2\n" +"\n" +"[block:root]\n" +"child_blocks=assignment, comment\n" +"\n" +"[block:assignment]\n" +"start=^[a-zA-Z_:]+=\n" +"end=$\n" +"child_blocks=value\n" +"\n" +"[block:value]\n" +"start=.\n" +"end=$\n" +"ends_on=comment\n" +"\n" +"[block:comment]\n" +"start = //\n" +"end = $\n"; void test_moderate(void) { TagTestCase cases[] = { { test_ini2, "foo=blub // should end assignment", // assignment with comment + 1, + { "root" }, 5, {0, 4, 9, 33, 33}, {"assignment", "value", "comment", "assignment", "root"}, @@ -304,12 +352,36 @@ void test_open_blocks(void) { { test_ini1, "(keyword", + 1, + { "root" }, 3, {0, 1, 8}, {"brackets", "keyword", "brackets"}, 2, {"brackets", "root"} }, + { + test_ini1, + "", + 2, + { "root", "brackets" }, + 0, + {}, + {}, + 2, + {"brackets", "root"} + }, + { + test_ini1, + ")", + 2, + { "root", "brackets" }, + 1, + {1}, + {"root"}, + 1, + { "root"} + }, }; size_t count = sizeof(cases) / sizeof(cases[0]); @@ -375,12 +447,11 @@ void test_stress(void) { TEST_CASE(str); String test = String_FromCStr(str, strlen(str)); - Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test, NULL); + const Stack *open_blocks = SyntaxHighlighting_HighlightString(&hl, &test, NULL); TEST_CHECK(open_blocks != NULL); TEST_CHECK(!Stack_IsEmpty(open_blocks)); - Stack_Destroy(open_blocks); String_Deinit(&test); SyntaxHighlighting_Deinit(&hl); SyntaxDefinition_Destroy(def); diff --git a/tests/test_syntax_textlayoutbindings.c b/tests/test_syntax_textlayoutbindings.c new file mode 100644 index 0000000..0b11099 --- /dev/null +++ b/tests/test_syntax_textlayoutbindings.c @@ -0,0 +1,166 @@ +#include "acutest.h" +#include "syntax/textlayoutbindings.h" +#include "syntax/definition.h" +#include "common/iniparser.h" +#include "common/string.h" +#include "document/textlayout.h" + +typedef struct { + SyntaxDefinition *def; + const Line **lines; + size_t line_count; + SyntaxHighlighting sh; + TextBuffer tb; + TextLayout tl; + SyntaxHighlightingBinding binding; +} TestFixture; + +static void init_textbuffer(TextBuffer *tb, const char *lines[], size_t line_count) { + TextBuffer_Init(tb); + TEST_ASSERT(line_count > 0); + String s = String_FromCStr(lines[0], strlen(lines[0])); + String_Take(&tb->current_line->text, &s); + for (size_t i=1; itext, &s); + TextBuffer_InsertLineAtBottom(tb, new_line); + } + TEST_CHECK(tb->line_count == line_count); + TEST_MSG("%zu", tb->line_count); +} + + +static SyntaxDefinition *create_syntax_definition(const char *ini) { + IniParser parser; + IniParser_Init(&parser); + IniParser_SetText(&parser, ini); + Table *table = IniParser_Parse(&parser); + TEST_ASSERT(table != NULL); + SyntaxDefinitionError error; + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error); + TEST_ASSERT(def != NULL); + IniParser_Deinit(&parser); + Table_Destroy(table); + return def; +} + +static void setup_lines(TestFixture *fixture) { + fixture->line_count = fixture->tb.line_count; + fixture->lines = malloc(sizeof(Line*) * fixture->line_count); + TEST_ASSERT(fixture->lines); + Line *line = TextBuffer_GetFirstLine(&fixture->tb); + size_t i = 0; + while (line) { + fixture->lines[i++] = line; + line = line->next; + } +} + +static void setup_fixture(TestFixture *fixture, const char *ini, const char *lines[], size_t line_count) { + init_textbuffer(&fixture->tb, lines, line_count); + TextLayout_Init(&fixture->tl, &fixture->tb, 10, 4); + fixture->def = create_syntax_definition(ini); + SyntaxHighlighting_Init(&fixture->sh, fixture->def); + SyntaxHighlightingBinding_Init(&fixture->binding, &fixture->tl, &fixture->sh); + setup_lines(fixture); +} + +static void cleanup_fixture(TestFixture *fixture) { + SyntaxHighlightingBinding_Deinit(&fixture->binding); + SyntaxHighlighting_Deinit(&fixture->sh); + TextLayout_Deinit(&fixture->tl); + TextBuffer_Deinit(&fixture->tb); + SyntaxDefinition_Destroy(fixture->def); + free(fixture->lines); +} + + + + +const char *test_ini = +"[meta]\n" +"name = TEST\n" +"\n" +"[block:root]\n" +"child_blocks=keyword, comment, string\n" +"\n" +"[block:keyword]\n" +"start = keyword\n" +"\n" +"[block:comment]\n" +"start = \"#\"\n" +"end = $\n" +"\n" +"[block:string]\n" +"start = \"'\"\n" +"end = \"'\"\n" +; + +void test_binding_basic(void) { + // data + const char *lines[] = { + "First line", + "Second line", + }; + size_t lines_count = sizeof(lines) / sizeof(char*); + + // 1. Setup + TestFixture fixture; + setup_fixture(&fixture, test_ini, lines, lines_count); + TextBuffer *tb = &fixture.tb; + SyntaxHighlighting *sh = &fixture.sh; + SyntaxHighlightingBinding *binding = &fixture.binding; + + + // 2. Calculate + SyntaxHighlightingBinding_Update(binding, tb->current_line->next, tb->current_line->next); + + // 3. Check + TEST_CHECK(Table_Get(sh->strings, &fixture.lines[0]->text) != NULL); + TEST_CHECK(Table_Get(sh->strings, &fixture.lines[1]->text) != NULL); + + // 4. Cleanup + cleanup_fixture(&fixture); +} + +void test_binding_basic2(void) { + // data + const char *lines[] = { + "First 'line", + "Second' // line", + }; + size_t lines_count = sizeof(lines) / sizeof(char*); + + // 1. Setup + TestFixture fixture; + setup_fixture(&fixture, test_ini, lines, lines_count); + TextBuffer *tb = &fixture.tb; + SyntaxHighlighting *sh = &fixture.sh; + SyntaxHighlightingBinding *binding = &fixture.binding; + + + // 2. Calculate + SyntaxHighlightingBinding_Update(binding, fixture.lines[1], fixture.lines[1]); + + // 3. Check + SyntaxHighlightingString *shs0 = Table_Get(sh->strings, &fixture.lines[0]->text); + SyntaxHighlightingString *shs1 = Table_Get(sh->strings, &fixture.lines[1]->text); + TEST_CHECK(shs0 != NULL); + TEST_CHECK(shs0->text == &fixture.lines[0]->text); + TEST_CHECK(shs0->tags_count == 1); + TEST_CHECK(shs0->tags[0].byte_offset == 6); + TEST_CHECK(shs0->open_blocks_at_end.size == 2); + TEST_MSG("%zu", shs0->open_blocks_at_end.size); + TEST_CHECK(shs0->open_blocks_at_end.size == shs1->open_blocks_at_begin.size); + TEST_CHECK(Table_Get(sh->strings, &fixture.lines[1]->text) != NULL); + + // 4. Cleanup + cleanup_fixture(&fixture); +} + +TEST_LIST = { + { "TextLayoutBindings: No Styling", test_binding_basic }, + { "TextLayoutBindings: String over two lines", test_binding_basic2 }, + { NULL, NULL } +}; From 2b0c6d3dbe12247079631aa23349421504b443be Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 10:31:48 +0200 Subject: [PATCH 10/35] make argv0 available through Config --- src/common/config.c | 16 ++++++++++++---- src/common/config.h | 2 +- src/main.c | 2 +- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/common/config.c b/src/common/config.c index 93c558c..71c57eb 100644 --- a/src/common/config.c +++ b/src/common/config.c @@ -17,6 +17,7 @@ #include #include +#include // PATH_MAX #include "common/typedtable.h" #include "common/iniparser.h" @@ -27,14 +28,17 @@ typedef struct _Config { bool dirty; + const char *exe_path; + int indent_size; bool use_spaces_for_indent; - char filename[FILENAME_MAX_LENGTH]; + char filename[PATH_MAX]; } Config; static Config config; -void Config_Init() { +void Config_Init(const char *argv0) { + config.exe_path = argv0; config.indent_size = 4; config.use_spaces_for_indent = true; config.filename[0] = '\0'; @@ -82,13 +86,17 @@ void Config_Loaded() { config.dirty = false; } +const char *Config_GetExePath() { + return config.exe_path; +} + void Config_SetFilename(const char *filename) { if (filename == NULL) { config.filename[0] = '\0'; return; } - strncpy(config.filename, filename, FILENAME_MAX_LENGTH); - config.filename[FILENAME_MAX_LENGTH - 1] = '\0'; + strncpy(config.filename, filename, PATH_MAX - 1); + config.filename[PATH_MAX - 1] = '\0'; } const char *Config_GetFilename() { diff --git a/src/common/config.h b/src/common/config.h index 2eea6a4..92b5f5d 100644 --- a/src/common/config.h +++ b/src/common/config.h @@ -29,7 +29,7 @@ void Config_LoadIni(const char *content); bool Config_IsDirty(); void Config_Loaded(); -#define FILENAME_MAX_LENGTH 256 +const char *Config_GetExePath(); void Config_SetFilename(const char *filename); const char* Config_GetFilename(); diff --git a/src/main.c b/src/main.c index 8ba7cf2..5c1f3ad 100644 --- a/src/main.c +++ b/src/main.c @@ -115,7 +115,7 @@ int main(int argc, char *argv[]) { ************************************/ srand(time(NULL)); - Config_Init(); + Config_Init(argv[0]); parse_arguments(argc, argv); load_environment(); From b4f044a48328919086e1549032b0aa96f9005282 Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 10:50:08 +0200 Subject: [PATCH 11/35] Add File_Exits(), File_OpenConfig() and File_OpenProjectFile() --- src/io/file.c | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/io/file.h | 19 ++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/src/io/file.c b/src/io/file.c index 2f8c353..d2a2676 100644 --- a/src/io/file.c +++ b/src/io/file.c @@ -18,6 +18,11 @@ #include #include #include +#include // access() +#include // dirname() +#include // PATH_MAX +#include +#include "common/config.h" #include "common/logging.h" static File *create_file_object() { @@ -31,6 +36,13 @@ static File *create_file_object() { return file; } +bool File_Exists(const char *path) { + if (!path) { + return false; + } + return (access(path, R_OK) == 0); +} + File *File_Open(const char *filename, FileAccessType access) { if (!filename || strlen(filename) == 0) { logFatal("Invalid filename."); @@ -53,6 +65,62 @@ File *File_Open(const char *filename, FileAccessType access) { return file; } +// find project file +char *find_project_file(const char *rel_path) { + static char path[PATH_MAX]; + char exe_dir[PATH_MAX]; + char *home = getenv("HOME"); + + // 1️. directory of the binary + if (realpath(Config_GetExePath(), exe_dir)) { + strcpy(exe_dir, dirname(exe_dir)); + snprintf(path, sizeof(path), "%s/%s", exe_dir, rel_path); + if (file_exists(path)) { + return path; + } + } + + // 2️. user config (~/.config/clieditor/config) + if (home) { + snprintf(path, sizeof(path), "%s/.config/clieditor/%s", home, rel_path); + if (file_exists(path)) { + return path; + } + } + + // 3️ system config (/usr/share/clieditor/config) + const char *system_paths[] = { + "/etc/clieditor/", + "/usr/share/clieditor/", + NULL + }; + for (int i = 0; system_paths[i]; ++i) { + snprintf(path, sizeof(path), "%s/%s", system_paths[i], rel_path); + if (file_exists(path)) { + return path; + } + } + + // nothing found + return NULL; +} + +File *File_OpenConfig(FileAccessType access) { + char *path = find_project_file("config.ini"); + if (!path) { + return NULL; + } + return File_Open(path, access); +} + +File *File_OpenProjectFile(const char *rel_path, FileAccessType access) { + char *path = find_project_file(rel_path); + if (!path) { + return NULL; + } + return File_Open(path, access); +} + void File_Close(File *file) { if (!file) { return; diff --git a/src/io/file.h b/src/io/file.h index 12bd553..4ee63d5 100644 --- a/src/io/file.h +++ b/src/io/file.h @@ -33,6 +33,11 @@ typedef struct _File { FileAccessType access; //< read/write access } File; +/** + * @brief Check if a file exists. + */ +bool File_Exists(const char *path); + /** * @brief Open a file for read/write access. * @@ -42,6 +47,20 @@ typedef struct _File { */ File *File_Open(const char *path, FileAccessType access); +/** + * @brief Open the config file. + */ +File *File_OpenConfig(FileAccessType access); + +/** + * @brief Open a file that is located relative to a project's folder. + * + * Look in the binaries directory, ~/.config/APP_NAME/, /etc/APP_NAME/, /usr/share/APP_NAME/. + * @param rel_path Filepath relative to one of those locations + * @param access Type of access needed + */ +File *File_ProjectFile(const char *rel_path, FileAccessType access); + /** * @brief Close a file * From 050a4e9b22f7f5cdc45da06813d71d0ad69695dc Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:21:16 +0200 Subject: [PATCH 12/35] change defautl width --- src/widgets/components/bottombar.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widgets/components/bottombar.c b/src/widgets/components/bottombar.c index 6b71237..74b4afb 100644 --- a/src/widgets/components/bottombar.c +++ b/src/widgets/components/bottombar.c @@ -67,7 +67,7 @@ void BottomBar_Init(BottomBar *self, Widget *parent) { Widget_Init(AS_WIDGET(self), parent, &bottombar_ops); self->base.x = 0; self->base.y = 0; - self->base.width = FILENAME_MAX_LENGTH; + self->base.width = parent->width; self->base.height = 1; self->base.style.bg = Color_GetCodeById(COLOR_PRIMARY_BG); From 68014cf7bc20e3f265c990bd3e87fa3d25a212eb Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:21:40 +0200 Subject: [PATCH 13/35] fix wrong function calls --- src/io/file.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/io/file.c b/src/io/file.c index d2a2676..e51eeee 100644 --- a/src/io/file.c +++ b/src/io/file.c @@ -75,7 +75,7 @@ char *find_project_file(const char *rel_path) { if (realpath(Config_GetExePath(), exe_dir)) { strcpy(exe_dir, dirname(exe_dir)); snprintf(path, sizeof(path), "%s/%s", exe_dir, rel_path); - if (file_exists(path)) { + if (File_Exists(path)) { return path; } } @@ -83,7 +83,7 @@ char *find_project_file(const char *rel_path) { // 2️. user config (~/.config/clieditor/config) if (home) { snprintf(path, sizeof(path), "%s/.config/clieditor/%s", home, rel_path); - if (file_exists(path)) { + if (File_Exists(path)) { return path; } } @@ -96,7 +96,7 @@ char *find_project_file(const char *rel_path) { }; for (int i = 0; system_paths[i]; ++i) { snprintf(path, sizeof(path), "%s/%s", system_paths[i], rel_path); - if (file_exists(path)) { + if (File_Exists(path)) { return path; } } From 236b8f5669488e008452edf827eac615be9ae05b Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:21:53 +0200 Subject: [PATCH 14/35] use new File_LoadConfig() function --- src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 5c1f3ad..5ee749f 100644 --- a/src/main.c +++ b/src/main.c @@ -148,7 +148,7 @@ int main(int argc, char *argv[]) { } // Load config - File *config_file = File_Open("config.ini", FILE_ACCESS_READ); + File *config_file = File_OpenConfig(FILE_ACCESS_READ); if (config_file) { char *content = File_Read(config_file); if (content) { From 913de9f6d3a69501d75dec8575f89d7e580230fe Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:25:03 +0200 Subject: [PATCH 15/35] remove unused variable --- tests/test_syntax_textlayoutbindings.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_syntax_textlayoutbindings.c b/tests/test_syntax_textlayoutbindings.c index 0b11099..d7520e7 100644 --- a/tests/test_syntax_textlayoutbindings.c +++ b/tests/test_syntax_textlayoutbindings.c @@ -135,7 +135,7 @@ void test_binding_basic2(void) { // 1. Setup TestFixture fixture; setup_fixture(&fixture, test_ini, lines, lines_count); - TextBuffer *tb = &fixture.tb; + //TextBuffer *tb = &fixture.tb; SyntaxHighlighting *sh = &fixture.sh; SyntaxHighlightingBinding *binding = &fixture.binding; From dfe8e9744ec79317ad207efbe88da009ad96001a Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:31:37 +0200 Subject: [PATCH 16/35] check if snprintf() truncated the path --- src/io/file.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/io/file.c b/src/io/file.c index e51eeee..86f8a18 100644 --- a/src/io/file.c +++ b/src/io/file.c @@ -74,7 +74,9 @@ char *find_project_file(const char *rel_path) { // 1️. directory of the binary if (realpath(Config_GetExePath(), exe_dir)) { strcpy(exe_dir, dirname(exe_dir)); - snprintf(path, sizeof(path), "%s/%s", exe_dir, rel_path); + int n = snprintf(path, sizeof(path), "%s/%s", exe_dir, rel_path); + n = n >= (int)sizeof(path) ? (int)sizeof(path) - 1 : n; + path[n] = '\0'; if (File_Exists(path)) { return path; } @@ -82,7 +84,9 @@ char *find_project_file(const char *rel_path) { // 2️. user config (~/.config/clieditor/config) if (home) { - snprintf(path, sizeof(path), "%s/.config/clieditor/%s", home, rel_path); + int n = snprintf(path, sizeof(path), "%s/.config/clieditor/%s", home, rel_path); + n = n >= (int)sizeof(path) ? (int)sizeof(path) - 1 : n; + path[n] = '\0'; if (File_Exists(path)) { return path; } @@ -95,7 +99,9 @@ char *find_project_file(const char *rel_path) { NULL }; for (int i = 0; system_paths[i]; ++i) { - snprintf(path, sizeof(path), "%s/%s", system_paths[i], rel_path); + int n = snprintf(path, sizeof(path), "%s/%s", system_paths[i], rel_path); + n = n >= (int)sizeof(path) ? (int)sizeof(path) - 1 : n; + path[n] = '\0'; if (File_Exists(path)) { return path; } From b17766b21228fb67111b94a07e4cab765a6d2ed8 Mon Sep 17 00:00:00 2001 From: defname Date: Sat, 25 Oct 2025 11:35:17 +0200 Subject: [PATCH 17/35] fix function declaration of File_OpenProjectFile() --- src/io/file.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/file.h b/src/io/file.h index 4ee63d5..733d676 100644 --- a/src/io/file.h +++ b/src/io/file.h @@ -59,7 +59,7 @@ File *File_OpenConfig(FileAccessType access); * @param rel_path Filepath relative to one of those locations * @param access Type of access needed */ -File *File_ProjectFile(const char *rel_path, FileAccessType access); +File *File_OpenProjectFile(const char *rel_path, FileAccessType access); /** * @brief Close a file From 9b1ddaf1390fd20b4a1159e64f164a2da3a67dd6 Mon Sep 17 00:00:00 2001 From: Bodo Date: Sat, 25 Oct 2025 15:06:35 +0200 Subject: [PATCH 18/35] Implement a loader function to load a complete SyntaxHighlighting engine from an INI file Enable compile_commands.json and improve syntax handling (#88) * propagate PATH_MAX by file.h * create compile_commands.json (for correct using include paths in vscode) * add create and destroy functions to SyntaxHighlighting * SyntaxHighlighting now takes the ownership of the used SyntaxDefinition * must not destroy SyntaxDefinition anymore since this is handled by SyntaxHighlighting * fix bug in find_project_file() * implement the SyntaxHighlighting_LoadFromFile() function and tests * add NULL guard * Initialize error properly in SyntaxHighlighting_LoadFromFile() * guard againsst line == NULL and handle last_line == NULL in SyntaxHighlightingBinding_Update() * fix bug in concat_paths() * add test_definition_error to TEST_LIST --- CMakeLists.txt | 2 + src/io/file.c | 8 +- src/io/file.h | 1 + src/syntax/highlighting.c | 19 ++- src/syntax/highlighting.h | 23 ++- src/syntax/loader.c | 76 +++++++++ src/syntax/loader.h | 43 ++++++ src/syntax/textlayoutbindings.c | 8 + tests/test_syntax_highlighting.c | 3 - tests/test_syntax_loader.c | 206 +++++++++++++++++++++++++ tests/test_syntax_textlayoutbindings.c | 1 - 11 files changed, 375 insertions(+), 15 deletions(-) create mode 100644 src/syntax/loader.c create mode 100644 src/syntax/loader.h create mode 100644 tests/test_syntax_loader.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 57e2577..59b729e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,6 +5,8 @@ get_filename_component(PROJECT_NAME ${CMAKE_CURRENT_SOURCE_DIR} NAME) project(${PROJECT_NAME} C) set(CMAKE_C_STANDARD 99) +set(CMAKE_EXPORT_COMPILE_COMMANDS on) + # Set a default build type if none was specified if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) message(STATUS "Setting build type to 'Debug' as none was specified.") diff --git a/src/io/file.c b/src/io/file.c index 86f8a18..50f6c27 100644 --- a/src/io/file.c +++ b/src/io/file.c @@ -20,7 +20,6 @@ #include #include // access() #include // dirname() -#include // PATH_MAX #include #include "common/config.h" #include "common/logging.h" @@ -68,12 +67,13 @@ File *File_Open(const char *filename, FileAccessType access) { // find project file char *find_project_file(const char *rel_path) { static char path[PATH_MAX]; - char exe_dir[PATH_MAX]; + char exe_abs_path[PATH_MAX]; + char *exe_dir = NULL; char *home = getenv("HOME"); // 1️. directory of the binary - if (realpath(Config_GetExePath(), exe_dir)) { - strcpy(exe_dir, dirname(exe_dir)); + if (realpath(Config_GetExePath(), exe_abs_path)) { + exe_dir = dirname(exe_abs_path); int n = snprintf(path, sizeof(path), "%s/%s", exe_dir, rel_path); n = n >= (int)sizeof(path) ? (int)sizeof(path) - 1 : n; path[n] = '\0'; diff --git a/src/io/file.h b/src/io/file.h index 733d676..fa0eb6d 100644 --- a/src/io/file.h +++ b/src/io/file.h @@ -16,6 +16,7 @@ #ifndef FILE_H #define FILE_H +#include // PATH_MAX #include "stdio.h" #include "common/string.h" diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index f9f0e06..561c31f 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -90,7 +90,7 @@ void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs) { /*****************************************************************************/ /* SyntaxHighlighting */ -void SyntaxHighlighting_Init(SyntaxHighlighting *hl, const SyntaxDefinition *def) { +void SyntaxHighlighting_Init(SyntaxHighlighting *hl, SyntaxDefinition *def) { hl->def = def; hl->strings = Table_CreatePtr(); } @@ -99,10 +99,27 @@ void SyntaxHighlighting_Deinit(SyntaxHighlighting *hl) { if (hl->strings) { Table_Destroy(hl->strings); } + if (hl->def) { + SyntaxDefinition_Destroy(hl->def); + } hl->strings = NULL; hl->def = NULL; } +SyntaxHighlighting *SyntaxHighlighting_Create(SyntaxDefinition *def) { + SyntaxHighlighting *hl = malloc(sizeof(SyntaxHighlighting)); + if (!hl) { + logFatal("Cannot allocate memory for SyntaxHighlighting."); + } + SyntaxHighlighting_Init(hl, def); + return hl; +} + +void SyntaxHighlighting_Destroy(SyntaxHighlighting *hl) { + SyntaxHighlighting_Deinit(hl); + free(hl); +} + static bool regexec_with_cache(const regex_t *regex, const char *str, size_t offset, MatchCache *cache, regmatch_t *match) { if (cache->done) { return false; diff --git a/src/syntax/highlighting.h b/src/syntax/highlighting.h index 17e0bb9..c6dcc2e 100644 --- a/src/syntax/highlighting.h +++ b/src/syntax/highlighting.h @@ -73,25 +73,36 @@ void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs); void SyntaxHighlightingString_AddTag(SyntaxHighlightingString *shs, SyntaxHighlightingTag tag); void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs); -/**match +/** * @brief Holds the highlighting information for text of multiple `Strings`. */ typedef struct _SyntaxHighlighting { - const SyntaxDefinition *def; //< SyntaxDefinition to use for highlighting + SyntaxDefinition *def; //< SyntaxDefinition to use for highlighting Table *strings; //< Table of (String -> SyntaxHighlightingString*) elements (holds the ownership of the SyntaxHighlightingString's) } SyntaxHighlighting; /** * @brief Initialize syntax highlighting using def for definitions. + * + * Note that the ownership of `def` is taken!. */ -void SyntaxHighlighting_Init(SyntaxHighlighting *sh, const SyntaxDefinition *def); +void SyntaxHighlighting_Init(SyntaxHighlighting *sh, SyntaxDefinition *def); -/** - * @brief Deinitialize sh. - */ +/** @brief Deinitialize `sh`. */ void SyntaxHighlighting_Deinit(SyntaxHighlighting *sh); +/** + * @brief Instantiate `SyntaxHighlighting`. + * + * @param def The ownership of `def` is taken! + * @returns The newly created `SyntaxHighlighting` instance. +*/ +SyntaxHighlighting *SyntaxHighlighting_Create(SyntaxDefinition *def); + +/** @brief Destroy `sh` */ +void SyntaxHighlighting_Destroy(SyntaxHighlighting *sh); + /** * @brief Highlight a string according to given context. * diff --git a/src/syntax/loader.c b/src/syntax/loader.c new file mode 100644 index 0000000..ed0e49f --- /dev/null +++ b/src/syntax/loader.c @@ -0,0 +1,76 @@ +#include "loader.h" +#include +#include // snprintf +#include "io/file.h" + +void SyntaxHighlightingLoaderError_Init(SyntaxHighlightingLoaderError *error) { + memset(error, 0, sizeof(SyntaxHighlightingLoaderError)); + error->code = SYNTAX_LOADER_NO_ERROR; +} + + +void SyntaxHighlightingLoaderError_Deinit(SyntaxHighlightingLoaderError *error) { + SyntaxDefinitionError_Deinit(&error->def_error); + if (error->parsing_error.message) { + free(error->parsing_error.message); + } +} + + +SyntaxHighlighting *SyntaxHighlighting_LoadFromFile(const char *format, SyntaxHighlightingLoaderError *error) { + if (!error) { + return NULL; + } + SyntaxHighlightingLoaderError_Init(error); + + if (!format) { + error->code = SYNTAX_LOADER_NO_FILENAME; + return NULL; + } + + // 1. load the ini file + char filename[PATH_MAX]; + snprintf(filename, PATH_MAX, "data/syntax/%s.ini", format); + File *file = File_OpenProjectFile(filename, FILE_ACCESS_READ); + if (!file) { + error->code = SYNTAX_LOADER_FILE_NOT_FOUND; + return NULL; + } + + // 2. read the ini file + char *ini = File_Read(file); + if (!ini) { + error->code = SYNTAX_LOADER_FILE_READ_ERROR; + File_Close(file); + return NULL; + } + File_Close(file); + + // 3. parse the ini file + IniParser parser; + IniParser_Init(&parser); + IniParser_SetText(&parser, ini); + Table *table = IniParser_Parse(&parser); + if (!table) { + error->code = SYNTAX_LOADER_PARSE_ERROR; + error->parsing_error = *IniParser_GetError(&parser); + error->parsing_error.message = strdup(IniParser_GetError(&parser)->message); + IniParser_Deinit(&parser); + free(ini); + return NULL; + } + IniParser_Deinit(&parser); + free(ini); + + // 4. create the syntax definition + SyntaxDefinition *def = SyntaxDefinition_FromTable(table, &error->def_error); + if (!def) { + Table_Destroy(table); + error->code = SYNTAX_LOADER_DEFINITION_ERROR; + return NULL; + } + Table_Destroy(table); + + // 5. create the highlighting engine + return SyntaxHighlighting_Create(def); +} \ No newline at end of file diff --git a/src/syntax/loader.h b/src/syntax/loader.h new file mode 100644 index 0000000..eb646a8 --- /dev/null +++ b/src/syntax/loader.h @@ -0,0 +1,43 @@ +#ifndef SYNTAX_LOADER_H +#define SYNTAX_LOADER_H + +#include "highlighting.h" +#include "common/iniparser.h" + +typedef enum { + SYNTAX_LOADER_NO_ERROR, + SYNTAX_LOADER_NO_FILENAME, + SYNTAX_LOADER_FILE_NOT_FOUND, + SYNTAX_LOADER_FILE_READ_ERROR, + SYNTAX_LOADER_PARSE_ERROR, + SYNTAX_LOADER_DEFINITION_ERROR +} SyntaxHighlightingLoaderErrorCode; + +typedef struct { + SyntaxHighlightingLoaderErrorCode code; + SyntaxDefinitionError def_error; + ParsingError parsing_error; +} SyntaxHighlightingLoaderError; + +void SyntaxHighlightingLoaderError_Deinit(SyntaxHighlightingLoaderError *error); + + +/** + * @brief Load a syntax definition and create the highlight engine. + * + * Load the syntax definition for `format` from data/sytnax/.ini + * (from inside a project's folder) and create the `SyntaxHighlight` instance. + * + * @param format Format to load (something like "ini", "md"). + * @param error Will be set in the case of an error and need to be deinitialized by the caller. + * @returns + * The newly created SyntaxHighlighting Instance (ownership transfers to the caller). + * NULL there is definition file or if an error occured. + * In the case of an error `error` will be set and need to be deinitialized by the caller. + * + * Note theat `format` needs to be exactly the same as the filename. + * The convention is to use lower case. + */ +SyntaxHighlighting *SyntaxHighlighting_LoadFromFile(const char *format, SyntaxHighlightingLoaderError *error); + +#endif \ No newline at end of file diff --git a/src/syntax/textlayoutbindings.c b/src/syntax/textlayoutbindings.c index 5681678..1c4b256 100644 --- a/src/syntax/textlayoutbindings.c +++ b/src/syntax/textlayoutbindings.c @@ -24,6 +24,14 @@ static void update_lines(SyntaxHighlightingBinding *binding, const Line *line, c } void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line) { + if (!binding || !binding->sh || !binding->tl || !binding->tl->tb || !line) { + return; + } + + if (!last_line) { + last_line = line; + } + Line *prev_line = line->prev; Stack *open_blocks = NULL; if (prev_line) { diff --git a/tests/test_syntax_highlighting.c b/tests/test_syntax_highlighting.c index 68b49ba..e7742f0 100644 --- a/tests/test_syntax_highlighting.c +++ b/tests/test_syntax_highlighting.c @@ -116,7 +116,6 @@ static void assert_highlight_tags( String_Deinit(&str); SyntaxHighlighting_Deinit(&hl); - SyntaxDefinition_Destroy(def); } const char *test_ini0 = @@ -154,7 +153,6 @@ void test_highlight_string_simple(void) { Stack_Destroy(open_blocks_at_begin); String_Deinit(&test1); SyntaxHighlighting_Deinit(&hl); - SyntaxDefinition_Destroy(def); } const char *test_ini1 = @@ -454,7 +452,6 @@ void test_stress(void) { String_Deinit(&test); SyntaxHighlighting_Deinit(&hl); - SyntaxDefinition_Destroy(def); } } diff --git a/tests/test_syntax_loader.c b/tests/test_syntax_loader.c new file mode 100644 index 0000000..011aa1e --- /dev/null +++ b/tests/test_syntax_loader.c @@ -0,0 +1,206 @@ +void test_init(); +void test_deinit(); + +#define TEST_INIT { test_init(); } +#define TEST_FINI { test_deinit(); } + +#include "acutest.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include "io/file.h" +#include "syntax/loader.h" +#include "common/config.h" // to set a custom project folder + +/*****************************************************************/ +/* Functions to create a test environment */ +bool concat_paths(char *dst, const char *path1, const char *path2) { + if (!dst || !path1 || !path2) return false; + size_t cap = PATH_MAX; + size_t l1 = strlen(path1); + bool need_sep = (l1 > 0 && path1[l1 - 1] != '/'); + const char *p2 = (path2[0] == '/') ? path2 + 1 : path2; + int n = snprintf(dst, cap, "%s%s%s", path1, need_sep ? "/" : "", p2); + return n >= 0 && (size_t)n < cap; +} + +bool create_syntax_folder() { + char exe_abs_path[PATH_MAX]; + char data_dir[PATH_MAX]; + char syntax_dir[PATH_MAX]; + + TEST_CHECK(realpath(Config_GetExePath(), exe_abs_path)); + char *exe_dir = dirname(exe_abs_path); + + if (!concat_paths(data_dir, exe_dir, "data")) { + return false; + } + if (!concat_paths(syntax_dir, data_dir, "syntax")) { + return false; + } + + mkdir(data_dir, 0755); + mkdir(syntax_dir, 0755); + return true; +} + +bool remove_syntax_folder() { + char exe_abs_path[PATH_MAX]; + + TEST_CHECK(realpath(Config_GetExePath(), exe_abs_path)); + char *exe_dir = dirname(exe_abs_path); + char data_dir[PATH_MAX]; + char syntax_dir[PATH_MAX]; + + if (!concat_paths(data_dir, exe_dir, "data")) { + return false; + } + if (!concat_paths(syntax_dir, data_dir, "syntax")) { + return false; + } + + rmdir(syntax_dir); + rmdir(data_dir); + return true; +} + +void test_init() { + static char template[] = "/tmp/clieditor_XXXXXX"; + int fd = mkstemp(template); + TEST_ASSERT(fd != -1); + Config_Init(template); + close(fd); + + TEST_ASSERT(create_syntax_folder()); +} + +void test_deinit() { + remove_syntax_folder(); + unlink(Config_GetExePath()); + Config_Deinit(); +} + +void create_testfile(const char *filename, const char *content) { + char exe_abs_path[PATH_MAX]; + + TEST_CHECK(realpath(Config_GetExePath(), exe_abs_path)); + char *exe_dir = dirname(exe_abs_path); + + char filepath[PATH_MAX]; + snprintf(filepath, PATH_MAX, "%s/data/syntax/%s", exe_dir, filename); + FILE *fp = fopen(filepath, "w"); + TEST_ASSERT(fp); + fwrite(content, sizeof(char), strlen(content), fp); + fclose(fp); +} + + +void delete_testfile(const char *filename) { + char exe_abs_path[PATH_MAX]; + + TEST_CHECK(realpath(Config_GetExePath(), exe_abs_path)); + char *exe_dir = dirname(exe_abs_path); + + char filepath[PATH_MAX]; + snprintf(filepath, PATH_MAX, "%s/data/syntax/%s", exe_dir, filename); + unlink(filepath); +} + +/****************************************************************/ +/* Actual testing */ + +void test_basic(void) { + const char *ini = + "[meta]\n" + "name = TEST\n" + "\n" + "[block:root]\n" + "child_blocks = string\n" + "\n" + "[block:string]\n" + "start = '\n" + "end = '\n"; + + create_testfile("test.ini", ini); + SyntaxHighlightingLoaderError error; + SyntaxHighlighting *hl = SyntaxHighlighting_LoadFromFile("test", &error); + if (!hl) { + TEST_CHECK(hl); + TEST_MSG("SyntaxHighlightingLoaderErrorCode: %d", error.code); + TEST_MSG("message: %s", error.def_error.message.bytes); + SyntaxHighlightingLoaderError_Deinit(&error); + return; + } + SyntaxHighlighting_Destroy(hl); + delete_testfile("test.ini"); +} + +void test_no_filename(void) { + SyntaxHighlightingLoaderError error; + SyntaxHighlighting *hl = SyntaxHighlighting_LoadFromFile(NULL, &error); + TEST_CHECK(!hl); + TEST_CHECK(error.code == SYNTAX_LOADER_NO_FILENAME); + SyntaxHighlightingLoaderError_Deinit(&error); +} + +void test_file_not_found(void) { + SyntaxHighlightingLoaderError error; + SyntaxHighlighting *hl = SyntaxHighlighting_LoadFromFile("test", &error); + TEST_CHECK(!hl); + TEST_CHECK(error.code == SYNTAX_LOADER_FILE_NOT_FOUND); + SyntaxHighlightingLoaderError_Deinit(&error); +} + +void test_parse_error(void) { + const char *ini = + "[meta\n" // <---- INI format error + "name = TEST\n" + "\n" + "[block:root]\n" + "child_blocks = string\n" + "\n" + "[block:string]\n" + "start = '\n" + "end = '\n"; + create_testfile("test.ini", ini); + SyntaxHighlightingLoaderError error; + SyntaxHighlighting *hl = SyntaxHighlighting_LoadFromFile("test", &error); + TEST_CHECK(!hl); + TEST_CHECK(error.code == SYNTAX_LOADER_PARSE_ERROR); + SyntaxHighlightingLoaderError_Deinit(&error); + delete_testfile("test.ini"); +} + +void test_definition_error(void) { + const char *ini = + "[meta]\n" + "name = TEST\n" + "\n" + "[block:root]\n" + "child_blocks = string, blub\n" // <----- non-existing child + "\n" + "[block:string]\n" + "start = '\n" + "end = '\n"; + create_testfile("test.ini", ini); + SyntaxHighlightingLoaderError error; + SyntaxHighlighting *hl = SyntaxHighlighting_LoadFromFile("test", &error); + TEST_CHECK(!hl); + TEST_CHECK(error.code == SYNTAX_LOADER_DEFINITION_ERROR); + SyntaxHighlightingLoaderError_Deinit(&error); + delete_testfile("test.ini"); +} + +TEST_LIST = { + { "SyntaxLoader: Basic", test_basic }, + { "SyntaxLoader: No Filename", test_no_filename }, + { "SyntaxLoader: File Not Found", test_file_not_found }, + { "SyntaxLoader: Parse Error", test_parse_error }, + { "SyntaxLoader: Definition Error", test_definition_error }, + { NULL, NULL } +}; diff --git a/tests/test_syntax_textlayoutbindings.c b/tests/test_syntax_textlayoutbindings.c index d7520e7..0209ef1 100644 --- a/tests/test_syntax_textlayoutbindings.c +++ b/tests/test_syntax_textlayoutbindings.c @@ -71,7 +71,6 @@ static void cleanup_fixture(TestFixture *fixture) { SyntaxHighlighting_Deinit(&fixture->sh); TextLayout_Deinit(&fixture->tl); TextBuffer_Deinit(&fixture->tb); - SyntaxDefinition_Destroy(fixture->def); free(fixture->lines); } From df25499b48d8b602ec122e8843569fc0abed3b15 Mon Sep 17 00:00:00 2001 From: Bodo Date: Sun, 26 Oct 2025 13:00:04 +0100 Subject: [PATCH 19/35] Integrate SyntaxHighlighting to the editor (#89) * wip * link to the data directory in the build directory * example syntax definitions * massive changes to get syntax highlighting working... need to optimization and better organisation, lot of work arounds * comment out TESTFILE definition * destroy SyntaxHighlighting instance properly * fix Table_Get() call in draw_visual_line() to use the correct key * improve highlighting * add Config_SetSyntax(), Config_GetSyntax() * use getopt() to parse cmd arguments, take syntax type from cmd arguments * Trigger a full SyntaxHighlighting update once in the beginning * add hint that byte offset is used * update screenshot * improve markdown highlighting * remove TESTFILE debugging stuff --- CMakeLists.txt | 6 +++ assets/screenshot.png | Bin 72438 -> 101667 bytes data/syntax/ini.ini | 51 ++++++++++++--------- data/syntax/md.ini | 40 ++++++++++++++++ src/common/config.c | 15 ++++++ src/common/config.h | 4 ++ src/common/string.c | 2 +- src/common/table.c | 7 ++- src/main.c | 57 ++++++++++++++++++++++- src/syntax/definition.c | 13 ++++-- src/syntax/definition.h | 2 +- src/syntax/highlighting.c | 54 +++++++++++----------- src/syntax/highlighting.h | 11 +++++ src/syntax/textlayoutbindings.c | 61 +++++++++++++++++++++++-- src/syntax/textlayoutbindings.h | 10 ++-- src/widgets/components/editor.c | 60 ++++++++++++++++++++---- src/widgets/components/editor.h | 3 ++ tests/test_syntax_highlighting.c | 29 ++++++++++++ tests/test_syntax_textlayoutbindings.c | 4 +- 19 files changed, 352 insertions(+), 77 deletions(-) create mode 100644 data/syntax/md.ini diff --git a/CMakeLists.txt b/CMakeLists.txt index 59b729e..3158765 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -54,6 +54,12 @@ target_link_libraries(${PROJECT_NAME} PRIVATE ${PROJECT_OBJECTS}) add_compile_definitions(_XOPEN_SOURCE=700) +# --- symlink zum data Ordner erstellen +execute_process( + COMMAND ${CMAKE_COMMAND} -E create_symlink + ${CMAKE_SOURCE_DIR}/data + ${CMAKE_BINARY_DIR}/data +) # CTest-Unterstützung aktivieren enable_testing() diff --git a/assets/screenshot.png b/assets/screenshot.png index 53ec641465d00e72c49c0bf20445f8b82ce2584c..bcceffc3959ff877b915c040d3b9ec19e6437b66 100644 GIT binary patch literal 101667 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 P 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 str->bytes_capacity) { + if (new_byte_size + 1 > str->bytes_capacity) { resize_bytes_capacity(str, new_byte_size + 1); } // use memove to handle overlapping memory areas diff --git a/src/common/table.c b/src/common/table.c index 8608c54..072ce6e 100644 --- a/src/common/table.c +++ b/src/common/table.c @@ -123,7 +123,12 @@ static void increase_capacity(Table *table) { return; } // allocate memory for the increased table - Table *new_table = Table_Create(); + Table *new_table = Table_CreateCustom( + table->hash_func, + table->key_cmp_func, + table->key_copy_func, + table->key_free_func + ); new_table->capacity = table->capacity == 0 ? TABLE_INITIAL_CAPACITY : table->capacity * TABLE_GROWTH_FACTOR; new_table->slots = malloc(new_table->capacity * sizeof(TableSlot)); if (!new_table->slots) { diff --git a/src/main.c b/src/main.c index 5ee749f..e19e235 100644 --- a/src/main.c +++ b/src/main.c @@ -34,13 +34,15 @@ #include "common/logging.h" #include "common/iniparser.h" +#include "syntax/loader.h" + #include "widgets/primitives/frame.h" #include "widgets/primitives/menu.h" -//const char *TESTFILE = "/home/cypher/projekte/clieditor/README.md"; - TextBuffer tb; +SyntaxHighlighting *highlighting; + static void print_help(const char *program_name) { fprintf(stderr, "Usage:\n %s \n", program_name); @@ -56,6 +58,29 @@ static void parse_arguments(int argc, char *argv[]) { // No file provided, could set a default or leave it empty. Config_SetFilename(NULL); } + + int opt; + while ((opt = getopt(argc, argv, "hs:")) != -1) { + switch (opt) { + case 'h': + print_help(argv[0]); + return; + case 's': + Config_SetSyntax(optarg); + break; + case '?': + fprintf(stderr, "Unknown option: -%c\n", optopt); + exit(1); + } + } + + // Verbleibende Argumente: + for (int i = optind; i < argc; i++) { + Config_SetFilename(argv[i]); + return; + } + + print_help(argv[0]); } static void load_environment() { @@ -100,6 +125,7 @@ static void finish() { // called automatically (set with atexit()) Timer_Deinit(); App_Deinit(); Config_Deinit(); + SyntaxHighlighting_Destroy(highlighting); TextBuffer_Deinit(&tb); Input_Deinit(); Screen_Deinit(); @@ -132,6 +158,32 @@ int main(int argc, char *argv[]) { TextBuffer_Init(&tb); + SyntaxHighlightingLoaderError error; + highlighting = SyntaxHighlighting_LoadFromFile(Config_GetSyntax(), &error); + if (!highlighting) { + switch (error.code) { + case SYNTAX_LOADER_FILE_NOT_FOUND: + logFatal("Syntax file not found."); + break; + case SYNTAX_LOADER_FILE_READ_ERROR: + logFatal("Could not read syntax file."); + break; + case SYNTAX_LOADER_PARSE_ERROR: + logFatal("Could not parse syntax file.\n%s", error.parsing_error.message); + break; + case SYNTAX_LOADER_DEFINITION_ERROR: + logFatal("Syntax definition error.\n%s", error.def_error.message); + break; + default: + logFatal("Could not load Syntaxdefinition (Code: %d)", error.code); + break; + } + SyntaxHighlightingLoaderError_Deinit(&error); + highlighting = NULL; + exit(1); + } + + const char * fn = Config_GetFilename(); bool failure_on_file_load = false; // the failure message can only be shown after initializing the widget system if (strcmp(fn, "") != 0) { @@ -162,6 +214,7 @@ int main(int argc, char *argv[]) { EditorView *editor = EditorView_Create(AS_WIDGET(&app), &tb); Widget_Focus(AS_WIDGET(editor)); + editor->editor->sh_binding.sh = highlighting; (void)editor; BottomBar *bottombar = BottomBar_Create(AS_WIDGET(&app)); (void)bottombar; diff --git a/src/syntax/definition.c b/src/syntax/definition.c index 82a3c6d..f202dd5 100644 --- a/src/syntax/definition.c +++ b/src/syntax/definition.c @@ -217,10 +217,15 @@ static SyntaxDefinitionError build_blocks(SyntaxDefinition *def, const Table *ta return NO_ERROR; } -static SyntaxDefinitionError map_block_names_to_blocks(SyntaxBlockDef *current_block, StringView *block_names, size_t count, SyntaxBlockDef **out, Table *blocks) { +static SyntaxDefinitionError map_block_names_to_blocks(SyntaxBlockDef *current_block, StringView *block_names, size_t count, SyntaxBlockDef **out, size_t *out_count, Table *blocks) { + *out_count = 0; for (size_t i=0; iblock; + out[(*out_count)++] = mapping->block; } return NO_ERROR; } @@ -266,14 +271,14 @@ static SyntaxDefinitionError block_name_list_str_to_blocks(SyntaxBlockDef *curre logFatal("Cannot allocate memory for children of SyntaxBlockDef."); } - SyntaxDefinitionError error = map_block_names_to_blocks(current_block, children, count, *out, blocks); + SyntaxDefinitionError error = map_block_names_to_blocks(current_block, children, count, *out, out_count, blocks); if (error.code != SYNTAXDEFINITION_NO_ERROR) { + *out_count = 0; String_Deinit(&s); free(children); return error; } - *out_count = count; String_Deinit(&s); free(children); diff --git a/src/syntax/definition.h b/src/syntax/definition.h index f174914..cc9fa9e 100644 --- a/src/syntax/definition.h +++ b/src/syntax/definition.h @@ -103,7 +103,7 @@ void SyntaxDefinitionError_Deinit(SyntaxDefinitionError *error); * @brief Helper struct to cache regex match results. This information are used by the Highlight module. */ typedef struct _MatchCache { - ssize_t offset; // total offset from where match was calculated + ssize_t offset; // total **byte offset** from where match was calculated regmatch_t match; // last match bool done; // if true the last match was already found } MatchCache; diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 561c31f..b38fdbc 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -86,6 +86,18 @@ void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs) { Stack_Clear(&shs->open_blocks_at_end); } +const SyntaxHighlightingTag *SyntaxHighlightingString_GetTag(const SyntaxHighlightingString *shs, size_t offset) { + // TODO: Need massive optimization + if (!shs || shs->tags_count == 0) { + return NULL; + } + for (int i=(int)shs->tags_count-1; i>=0; i--) { + if (shs->tags[i].byte_offset == offset) { + return &shs->tags[i]; + } + } + return NULL; +} /*****************************************************************************/ /* SyntaxHighlighting */ @@ -96,6 +108,9 @@ void SyntaxHighlighting_Init(SyntaxHighlighting *hl, SyntaxDefinition *def) { } void SyntaxHighlighting_Deinit(SyntaxHighlighting *hl) { + if (!hl) { + return; + } if (hl->strings) { Table_Destroy(hl->strings); } @@ -116,6 +131,9 @@ SyntaxHighlighting *SyntaxHighlighting_Create(SyntaxDefinition *def) { } void SyntaxHighlighting_Destroy(SyntaxHighlighting *hl) { + if (!hl) { + return; + } SyntaxHighlighting_Deinit(hl); free(hl); } @@ -189,32 +207,18 @@ static void init_match_cache(SyntaxHighlighting *sh) { } } -static bool stack_equal(const Stack *a, const Stack *b) { - if (a->size != b->size) { - return false; - } - for (size_t i=0; isize; i++) { - if (a->items[i] != b->items[i]) { - return false; - } - } - return true; -} - const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const String *text, const Stack *open_blocks_at_begin) { // check if there is already old infomation about text in the table SyntaxHighlightingString *shs = Table_Get(sh->strings, text); if (shs) { - // early exit if nothing changed since the last calculation - if (open_blocks_at_begin && stack_equal(&shs->open_blocks_at_begin, open_blocks_at_begin)) { - return &shs->open_blocks_at_end; - } - // if open_blocks_at_begin changed clear the old information + // if open_blocks_at_begin changed, clear the old information and reuse the existing shs SyntaxHighlightingString_Clear(shs); } else { // if there is no object present in sh->strings table create one and store it shs = SyntaxHighlightingString_Create(text); + // Only set it in the table if it's new. If it existed, we just cleared it + // and will now re-populate it. Table_Set(sh->strings, text, shs, (void(*)(void*))SyntaxHighlightingString_Destroy); } @@ -229,8 +233,6 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St Stack_Push(&shs->open_blocks_at_begin, sh->def->root); Stack_CopyTo(&shs->open_blocks_at_end, &shs->open_blocks_at_begin); } - Stack *open_blocks = &shs->open_blocks_at_end; - // initialize the cache fields of the SyntaxBlockDefs init_match_cache(sh); @@ -239,7 +241,7 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St size_t offset = 0; for (;;) { // take the current block from the stack (but keep it there) - SyntaxBlockDef *current_block = (SyntaxBlockDef*)Stack_Peek(open_blocks); + SyntaxBlockDef *current_block = (SyntaxBlockDef*)Stack_Peek(&shs->open_blocks_at_end); // find the first child block regmatch_t child_match; @@ -271,7 +273,7 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St offset += child_match.rm_eo; // push the child to the stack to continue with it in the next iteration - Stack_Push(open_blocks, (void*)child); + Stack_Push(&shs->open_blocks_at_end, (void*)child); continue; } // check if ends_on is the first match @@ -279,7 +281,7 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St && (!end_found || ends_on_match.rm_so < end_match.rm_so)) { // the current block ends by the occurence of the ends_on block - Stack_Pop(open_blocks); // removes current (which was just peeked before) + Stack_Pop(&shs->open_blocks_at_end); // removes current (which was just peeked before) // add a tag for the beginning of the ends_on block SyntaxHighlightingTag tag; @@ -289,7 +291,7 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St SyntaxHighlightingString_AddTag(shs, tag); // add the ends_on block to the stack - Stack_Push(open_blocks, (void*)ends_on); + Stack_Push(&shs->open_blocks_at_end, (void*)ends_on); // increase the offset to the end of the match offset += ends_on_match.rm_eo; @@ -298,13 +300,13 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St // last case.... if end_found it's the first match automatically if (end_found) { // end of block found so remove it from stack - Stack_Pop(open_blocks); // removes current (which was just peeked before) + Stack_Pop(&shs->open_blocks_at_end); // removes current (which was just peeked before) // and add a tag for the (new) begin of the surrounding block SyntaxHighlightingTag tag; tag.text = text; tag.byte_offset = offset + end_match.rm_eo; // the current block goes behind the match of its end - tag.block = Stack_Peek(open_blocks); // new current block + tag.block = Stack_Peek(&shs->open_blocks_at_end); // new current block SyntaxHighlightingString_AddTag(shs, tag); // increase offset @@ -312,7 +314,7 @@ const Stack *SyntaxHighlighting_HighlightString(SyntaxHighlighting *sh, const St continue; } // neither the beginning of a new block nor the end of the current block found - return open_blocks; + return &shs->open_blocks_at_end; } // if this happens there is an error in the SyntaxDefinition. diff --git a/src/syntax/highlighting.h b/src/syntax/highlighting.h index c6dcc2e..c763ac4 100644 --- a/src/syntax/highlighting.h +++ b/src/syntax/highlighting.h @@ -73,6 +73,17 @@ void SyntaxHighlightingString_Destroy(SyntaxHighlightingString *shs); void SyntaxHighlightingString_AddTag(SyntaxHighlightingString *shs, SyntaxHighlightingTag tag); void SyntaxHighlightingString_Clear(SyntaxHighlightingString *shs); +/** + * @brief Return the tag for the given offset or NULL if there is no tag for the offset. + * + * @param shs The `SyntaxHighlightingString` from the `SyntaxHighlighting->strings` table. + * @param offset The offset in bytes. + * @returns + * A pointer to the internal SyntaxHighlightingTag. This pointer is only valid until the + * next recalculation of `shs`! + */ +const SyntaxHighlightingTag *SyntaxHighlightingString_GetTag(const SyntaxHighlightingString *shs, size_t offset); + /** * @brief Holds the highlighting information for text of multiple `Strings`. */ diff --git a/src/syntax/textlayoutbindings.c b/src/syntax/textlayoutbindings.c index 1c4b256..53f44d5 100644 --- a/src/syntax/textlayoutbindings.c +++ b/src/syntax/textlayoutbindings.c @@ -1,8 +1,9 @@ #include "textlayoutbindings.h" -void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, const TextLayout *tl, SyntaxHighlighting *sh) { +void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, TextLayout *tl, SyntaxHighlighting *sh) { binding->tl = tl; binding->sh = sh; + binding->need_full_update = true; } void SyntaxHighlightingBinding_Deinit(SyntaxHighlightingBinding *binding) { @@ -10,12 +11,32 @@ void SyntaxHighlightingBinding_Deinit(SyntaxHighlightingBinding *binding) { binding->sh = NULL; } -static void update_lines(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line, const Stack *open_blocks) { +static bool stack_equal(const Stack *a, const Stack *b) { + if (a->size != b->size) { + return false; + } + for (size_t i=0; isize; i++) { + if (a->items[i] != b->items[i]) { + return false; + } + } + return true; +} + +static void update_following_lines(SyntaxHighlightingBinding *binding, const Line *first_line, const Line *last_line, Stack *open_blocks) { const Stack *open_blocks_begin = open_blocks; + const Line *line = first_line; while (line) { + if (line != first_line) { + SyntaxHighlightingString *shs = Table_Get(binding->sh->strings, &line->text); + if (shs && stack_equal(&shs->open_blocks_at_begin, open_blocks_begin)) { + break; + } + } // open_blocks == NULL is also handled by the function const Stack *open_blocks_end = SyntaxHighlighting_HighlightString(binding->sh, &line->text, open_blocks_begin); open_blocks_begin = open_blocks_end; + //Stack_Destroy(open_blocks_end); if (line == last_line) { break; } @@ -23,7 +44,7 @@ static void update_lines(SyntaxHighlightingBinding *binding, const Line *line, c } } -void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line) { +void SyntaxHighlightingBinding_UpdateLine(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line) { if (!binding || !binding->sh || !binding->tl || !binding->tl->tb || !line) { return; } @@ -44,7 +65,7 @@ void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const else { // highlighting for the line is not calculated so far // so run this function for thr previous line - SyntaxHighlightingBinding_Update(binding, prev_line, last_line); + SyntaxHighlightingBinding_UpdateLine(binding, prev_line, last_line); return; } } @@ -55,5 +76,35 @@ void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const } // update all lines until last_line (including) - update_lines(binding, line, last_line, open_blocks); + update_following_lines(binding, line, last_line, open_blocks); + if (open_blocks) { + //Stack_Destroy(open_blocks); + } } + +void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding) { + if (!binding || !binding->sh || !binding->tl || !binding->tl->tb) { + return; + } + const TextBuffer *tb = binding->tl->tb; + Line *current = tb->current_line; + VisualLine *last_vl = TextLayout_GetVisualLine(binding->tl, binding->tl->height - 1); + Line *last = last_vl ? last_vl->src : TextBuffer_GetLastLine(tb); + if (!current || !last) { + return; + } + SyntaxHighlightingBinding_UpdateLine(binding, current, last); +} + +void SyntaxHighlightingBinding_UpdateAll(SyntaxHighlightingBinding *binding, bool force) { + if (!force && !binding->need_full_update) { + return; + } + binding->need_full_update = false; + SyntaxHighlightingBinding_UpdateLine( + binding, + TextBuffer_GetFirstLine(binding->tl->tb), + TextBuffer_GetLastLine(binding->tl->tb) + ); + +} \ No newline at end of file diff --git a/src/syntax/textlayoutbindings.h b/src/syntax/textlayoutbindings.h index 14cb1bb..a93aa0d 100644 --- a/src/syntax/textlayoutbindings.h +++ b/src/syntax/textlayoutbindings.h @@ -1,17 +1,21 @@ #ifndef SYNTAX_TEXTLAYOUTBINDINGS_H #define SYNTAX_TEXTLAYOUTBINDINGS_H +#include #include "highlighting.h" #include "document/textlayout.h" typedef struct _SyntaxHighlightingBinding { - const TextLayout *tl; + TextLayout *tl; SyntaxHighlighting *sh; + bool need_full_update; } SyntaxHighlightingBinding; -void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, const TextLayout *tl, SyntaxHighlighting *sh); +void SyntaxHighlightingBinding_Init(SyntaxHighlightingBinding *binding, TextLayout *tl, SyntaxHighlighting *sh); void SyntaxHighlightingBinding_Deinit(SyntaxHighlightingBinding *binding); -void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line); +void SyntaxHighlightingBinding_UpdateLine(SyntaxHighlightingBinding *binding, const Line *line, const Line *last_line); +void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding); +void SyntaxHighlightingBinding_UpdateAll(SyntaxHighlightingBinding *binding, bool force); #endif \ No newline at end of file diff --git a/src/widgets/components/editor.c b/src/widgets/components/editor.c index 72d2aed..b117383 100644 --- a/src/widgets/components/editor.c +++ b/src/widgets/components/editor.c @@ -36,6 +36,8 @@ static void alternate_cursor_visibility(uint8_t timer_id, void *user_data) { static void editor_destroy(Widget *self) { Editor *editor = AS_EDITOR(self); Timer_Stop(editor->cursor_timer); + SyntaxHighlightingBinding_Deinit(&editor->sh_binding); + TextSelection_Deinit(&editor->ts); TextLayout_Deinit(&editor->tl); TextEdit_Deinit(&editor->te); TextSelection_Deinit(&editor->ts); @@ -64,12 +66,23 @@ void draw_char(TextLayout *tl, Canvas *canvas, uint32_t cp, int x_pos, bool has_ // draw line and return new y_offset (changes to one if the cursor wraps to the next line) static int draw_visual_line(Editor *editor, Canvas *canvas, int y, int y_offset) { + // 1 --- Setup stuff + + // get visual line VisualLine *line = TextLayout_GetVisualLine(&editor->tl, y); TextSelection *ts = &editor->ts; - if (!line){ + if (!line){ // no visual line at this position return y_offset; } + // check if it's a line with a gap + bool is_gap_line = false; + if (line->src == editor->tb->current_line) { + is_gap_line = true; + } + // setup syntax highlighting stuff + SyntaxHighlighting *sh = editor->sh_binding.sh; + SyntaxHighlightingString *shs = Table_Get(sh->strings, &line->src->text); // Move cursor to the start position Canvas_MoveCursor(canvas, 0, y + y_offset); @@ -78,19 +91,38 @@ static int draw_visual_line(Editor *editor, Canvas *canvas, int y, int y_offset) CursorLayoutInfo cursor; TextLayout_GetCursorLayoutInfo(&editor->tl, &cursor); + // change the style if it's the current line - Style orig_style = canvas->current_style; - Style line_style; - if (line->src == editor->tb->current_line) { - line_style = editor->config.active; + if (shs && Stack_Peek(&shs->open_blocks_at_begin)) { + SyntaxBlockDef *first_block = Stack_Peek(&shs->open_blocks_at_begin); + canvas->current_style.fg = first_block->color; } - else { - line_style = orig_style; + Style orig_style = canvas->current_style; + Style line_style = orig_style; + if (is_gap_line) { + line_style.bg = editor->config.active.bg; } + - // draw the characters + // 2 --- Draw characters loop for (int i=0; ilength; i++) { - uint32_t cp = utf8_to_codepoint(VisualLine_GetChar(line, i)); + // get the character to draw (might not be in line->src->text.bytes if it's in the gap in general) + // in the moment the gap is always merged when updateing the syntax highlighting, so it should + // work at the moment + // Pay attention in future changes! + const char *ch = VisualLine_GetChar(line, i); + + // This will not always work if is_gap_line and the gap is not merged!! + size_t byte_offset = byte_offset = ch - line->src->text.bytes; + + const SyntaxHighlightingTag *tag = SyntaxHighlightingString_GetTag(shs, byte_offset); + if (tag) { + canvas->current_style.fg = tag->block->color; + line_style.fg = tag->block->color; + } + + + uint32_t cp = utf8_to_codepoint(ch); bool has_cursor = (line == cursor.line && i == cursor.idx && editor->mode == EDITOR_MODE_INPUT); if (TextSelection_IsSelected(ts, line->src, line->offset + i)) { canvas->current_style = editor->config.selected; @@ -319,6 +351,7 @@ static bool editor_handle_input(Widget *self, InputEvent input) { input_handled = input_handled || handle_input_scrolling(editor, input); input_handled = input_handled || handle_input_text_editing(editor, input, cursor); if (input_handled) { + SyntaxHighlightingBinding_Update(&editor->sh_binding); return true; } } @@ -346,14 +379,19 @@ static void editor_on_blur(Widget *self) { // this is basically if the cursor gets lost during terminal window resize static void editor_update(Widget *self) { Editor *editor = AS_EDITOR(self); + + // fix cursor CursorLayoutInfo cursor; int cursor_pos = TextLayout_GetCursorLayoutInfo(&editor->tl, &cursor); if (cursor_pos != 0) { editor->tl.first_line = editor->tb->current_line; editor->tl.first_visual_line_idx = 0; editor->tl.dirty = true; - return; } + + // initialize highlighting + // will only be called once due to internal flags + SyntaxHighlightingBinding_UpdateAll(&editor->sh_binding, false); } static void editor_on_config_changed(Widget *self) { @@ -417,6 +455,8 @@ void Editor_Init(Editor *self, Widget *parent, TextBuffer *tb) { self->config.active.bg = Color_GetCodeById(COLOR_HIGHLIGHT_BG); self->config.cursor = self->base.style; self->config.cursor.bg = Color_GetCodeById(COLOR_HIGHLIGHT_BG); + + SyntaxHighlightingBinding_Init(&self->sh_binding, &self->tl, NULL); } Editor *Editor_Create(Widget *parent, TextBuffer *tb) { diff --git a/src/widgets/components/editor.h b/src/widgets/components/editor.h index 099a1d8..5a7f461 100644 --- a/src/widgets/components/editor.h +++ b/src/widgets/components/editor.h @@ -22,6 +22,7 @@ #include "document/textedit.h" #include "document/textselection.h" #include "io/timer.h" +#include "syntax/textlayoutbindings.h" typedef enum { EDITOR_MODE_INPUT, @@ -50,6 +51,8 @@ typedef struct { uint8_t cursor_timer; bool cursor_visible; + SyntaxHighlightingBinding sh_binding; + EditorConfig config; } Editor; diff --git a/tests/test_syntax_highlighting.c b/tests/test_syntax_highlighting.c index e7742f0..7751d76 100644 --- a/tests/test_syntax_highlighting.c +++ b/tests/test_syntax_highlighting.c @@ -65,6 +65,7 @@ static void assert_highlight_tags( // the syntax definition SyntaxDefinition *def = create_definition(ini); + TEST_ASSERT(def); // the highlighting engine SyntaxHighlighting hl; SyntaxHighlighting_Init(&hl, def); @@ -322,6 +323,23 @@ const char *test_ini2 = "start = //\n" "end = $\n"; + +const char *test_ini3 = +"[meta]\n" +"name = Markdown\n" +"file_extensions = md\n" +"\n" +"[block:root]\n" +"child_blocks = title\n" +"\n" +"[block:title]\n" +"# comments start with \";\" or \"#\"\n" +"start = \"^# \"\n" +"end = \"$\"\n" +"color = 33 # gray\n" +"child_blocks =\n"; + + void test_moderate(void) { TagTestCase cases[] = { { @@ -335,6 +353,17 @@ void test_moderate(void) { 1, {"root"} }, + { + test_ini3, + "# Title", + 1, + { "root" }, + 2, + {0, 7}, + {"title", "root"}, + 1, + {"root"} + }, }; size_t count = sizeof(cases) / sizeof(cases[0]); diff --git a/tests/test_syntax_textlayoutbindings.c b/tests/test_syntax_textlayoutbindings.c index 0209ef1..be1d770 100644 --- a/tests/test_syntax_textlayoutbindings.c +++ b/tests/test_syntax_textlayoutbindings.c @@ -113,7 +113,7 @@ void test_binding_basic(void) { // 2. Calculate - SyntaxHighlightingBinding_Update(binding, tb->current_line->next, tb->current_line->next); + SyntaxHighlightingBinding_UpdateLine(binding, tb->current_line->next, tb->current_line->next); // 3. Check TEST_CHECK(Table_Get(sh->strings, &fixture.lines[0]->text) != NULL); @@ -140,7 +140,7 @@ void test_binding_basic2(void) { // 2. Calculate - SyntaxHighlightingBinding_Update(binding, fixture.lines[1], fixture.lines[1]); + SyntaxHighlightingBinding_UpdateLine(binding, fixture.lines[1], fixture.lines[1]); // 3. Check SyntaxHighlightingString *shs0 = Table_Get(sh->strings, &fixture.lines[0]->text); From 13dec61952e6b3320cc3171bfcaa57e73ba39f99 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 14:10:25 +0100 Subject: [PATCH 20/35] handle the case that no highlighting is selected --- src/main.c | 11 +++++------ src/widgets/components/editor.c | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/main.c b/src/main.c index e19e235..d938a9e 100644 --- a/src/main.c +++ b/src/main.c @@ -163,24 +163,23 @@ int main(int argc, char *argv[]) { if (!highlighting) { switch (error.code) { case SYNTAX_LOADER_FILE_NOT_FOUND: - logFatal("Syntax file not found."); + logError("Syntax file not found."); break; case SYNTAX_LOADER_FILE_READ_ERROR: - logFatal("Could not read syntax file."); + logError("Could not read syntax file."); break; case SYNTAX_LOADER_PARSE_ERROR: - logFatal("Could not parse syntax file.\n%s", error.parsing_error.message); + logError("Could not parse syntax file.\n%s", error.parsing_error.message); break; case SYNTAX_LOADER_DEFINITION_ERROR: - logFatal("Syntax definition error.\n%s", error.def_error.message); + logError("Syntax definition error.\n%s", error.def_error.message); break; default: - logFatal("Could not load Syntaxdefinition (Code: %d)", error.code); + logError("Could not load Syntaxdefinition (Code: %d)", error.code); break; } SyntaxHighlightingLoaderError_Deinit(&error); highlighting = NULL; - exit(1); } diff --git a/src/widgets/components/editor.c b/src/widgets/components/editor.c index b117383..0822200 100644 --- a/src/widgets/components/editor.c +++ b/src/widgets/components/editor.c @@ -82,7 +82,7 @@ static int draw_visual_line(Editor *editor, Canvas *canvas, int y, int y_offset) } // setup syntax highlighting stuff SyntaxHighlighting *sh = editor->sh_binding.sh; - SyntaxHighlightingString *shs = Table_Get(sh->strings, &line->src->text); + SyntaxHighlightingString *shs = !sh ? NULL : Table_Get(sh->strings, &line->src->text); // Move cursor to the start position Canvas_MoveCursor(canvas, 0, y + y_offset); From 5e44e6372d41196ff5bfd6baf5d45d06c1eee6e6 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 14:37:29 +0100 Subject: [PATCH 21/35] deinitialize IniParser at ealry exit if c == NULL in Config_LoadIni() --- src/common/config.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/config.c b/src/common/config.c index 878bd37..317a111 100644 --- a/src/common/config.c +++ b/src/common/config.c @@ -71,6 +71,7 @@ void Config_LoadIni(const char *content) { IniParser_SetText(&ini, content); Table *c = IniParser_Parse(&ini); if (!c) { + IniParser_Deinit(&ini); return; } if (config.table) { From 18ce3ff115dc20ea0f39fd40692cf3e246a9fe8a Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 14:38:55 +0100 Subject: [PATCH 22/35] remove double deinitialization of TextSelection in editor_destroy() --- src/widgets/components/editor.c | 1 - 1 file changed, 1 deletion(-) diff --git a/src/widgets/components/editor.c b/src/widgets/components/editor.c index 0822200..cb05636 100644 --- a/src/widgets/components/editor.c +++ b/src/widgets/components/editor.c @@ -40,7 +40,6 @@ static void editor_destroy(Widget *self) { TextSelection_Deinit(&editor->ts); TextLayout_Deinit(&editor->tl); TextEdit_Deinit(&editor->te); - TextSelection_Deinit(&editor->ts); } From f00bc048aff2674ab53115bcd5804af72464eae6 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 14:48:04 +0100 Subject: [PATCH 23/35] fix parse_arguments() and update print_help() --- src/main.c | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/main.c b/src/main.c index d938a9e..1b572e0 100644 --- a/src/main.c +++ b/src/main.c @@ -45,20 +45,15 @@ SyntaxHighlighting *highlighting; static void print_help(const char *program_name) { - fprintf(stderr, "Usage:\n %s \n", program_name); + fprintf(stderr, "Usage: %s [options] \n\n", program_name); + puts(" -h Show this help"); + puts(" -s Specify a format for syntax highlighting."); + puts(" The syntax definition file .ini need to"); + puts(" present in data/syntax."); exit(0); } static void parse_arguments(int argc, char *argv[]) { - // For now, we only handle a single filename argument. - if (argc >= 2) { - Config_SetFilename(argv[1]); - } else { - print_help(argv[0]); - // No file provided, could set a default or leave it empty. - Config_SetFilename(NULL); - } - int opt; while ((opt = getopt(argc, argv, "hs:")) != -1) { switch (opt) { From 1221353c9ee19658a56851d548cdb31dcc6f7e9b Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 14:58:33 +0100 Subject: [PATCH 24/35] fix critical bug in Table_Set/Get(): change state of tombstones when reusing them --- src/common/table.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/table.c b/src/common/table.c index 072ce6e..3344d39 100644 --- a/src/common/table.c +++ b/src/common/table.c @@ -246,7 +246,7 @@ void Table_Set(Table *table, const void *key, void *value, void (*destructor)(vo logFatal("No slot found in Table_Set()."); } - if (slot->state == TABLE_SLOT_EMPTY) { + if (slot->state != TABLE_SLOT_USED) { slot->state = TABLE_SLOT_USED; slot->key = table->key_copy_func(key); if (!slot->key) { @@ -275,7 +275,7 @@ void *Table_Get(const Table *table, const void *key) { } TableSlot *slot = find_slot(table, key); - if (slot->state == TABLE_SLOT_EMPTY) { + if (slot->state != TABLE_SLOT_USED) { return NULL; } return slot->value; From 242f5ee7ce7989da9334c302f7c27b7d38837790 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:00:33 +0100 Subject: [PATCH 25/35] anchor section block start to newline --- data/syntax/ini.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/syntax/ini.ini b/data/syntax/ini.ini index 99674f0..7cd89ff 100644 --- a/data/syntax/ini.ini +++ b/data/syntax/ini.ini @@ -17,7 +17,7 @@ child_blocks = [block:section] # section: [section_name] -start = "\\[.*\\]" +start = "^\\[.*\\]" end = "$" color = 33 # cyan child_blocks = comment From b3414acf325c306b401d9175326aed2c659007cb Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:01:28 +0100 Subject: [PATCH 26/35] fix comment --- data/syntax/md.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/syntax/md.ini b/data/syntax/md.ini index 6952dac..92c1fc8 100644 --- a/data/syntax/md.ini +++ b/data/syntax/md.ini @@ -21,7 +21,7 @@ start = "^### (.*)$" color = 33 [block:code] -start = `[^`]*` # make pattern greedy by explicitly exclude ` from permitted characters +start = `[^`]*` # make pattern non-greedy by explicitly exclude ` from permitted characters color = 64 [block:emph] From 1c9ebee92d82cecf47ca27e64b7352d9a873d551 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:02:25 +0100 Subject: [PATCH 27/35] fix off-by-one bug in Buffer_HasSpace() --- src/common/buffer.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/buffer.c b/src/common/buffer.c index 5cfc1ee..c76fa3d 100644 --- a/src/common/buffer.c +++ b/src/common/buffer.c @@ -83,7 +83,7 @@ size_t Buffer_Capacity(const Buffer *buffer) { } bool Buffer_HasSpace(const Buffer *buffer) { - return buffer->count < buffer->capacity - 1; + return buffer->count < buffer->capacity; } bool Buffer_Peek(const Buffer *buffer, size_t lookahead, void *out_element) { From c55881e30f7b11afa90842631e2144c0e6780733 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:10:32 +0100 Subject: [PATCH 28/35] guard against NULL in SyntaxHighlightingBinding_UpdateAll() --- src/syntax/textlayoutbindings.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/syntax/textlayoutbindings.c b/src/syntax/textlayoutbindings.c index 53f44d5..a66bb05 100644 --- a/src/syntax/textlayoutbindings.c +++ b/src/syntax/textlayoutbindings.c @@ -97,6 +97,9 @@ void SyntaxHighlightingBinding_Update(SyntaxHighlightingBinding *binding) { } void SyntaxHighlightingBinding_UpdateAll(SyntaxHighlightingBinding *binding, bool force) { + if (!binding || !binding->tl || !binding->tl->tb) { + return; + } if (!force && !binding->need_full_update) { return; } From d89ec89a4ec5809ac07710eadabdffd2340bd7f3 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:26:15 +0100 Subject: [PATCH 29/35] update syntax definitions --- data/syntax/ini.ini | 2 +- data/syntax/md.ini | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/data/syntax/ini.ini b/data/syntax/ini.ini index 7cd89ff..d6ee26b 100644 --- a/data/syntax/ini.ini +++ b/data/syntax/ini.ini @@ -5,7 +5,7 @@ name = INI file_extensions = ini [block:root] -child_blocks = comment, section, comment, assignment +child_blocks = comment, section, assignment color = 15 [block:comment] diff --git a/data/syntax/md.ini b/data/syntax/md.ini index 92c1fc8..425ddac 100644 --- a/data/syntax/md.ini +++ b/data/syntax/md.ini @@ -7,7 +7,6 @@ child_blocks = title1, title2, title3, codeblock, code, emph, img color = 15 [block:title1] -# comments start with ";" or "#" start = "^# (.*)$" color = 33 child_blocks = From 2098964243acd9df1fdf4acb84a9c52c1f5c9d70 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:27:02 +0100 Subject: [PATCH 30/35] fix off-by-one bug in SyntaxHighlightingString_AddTag() --- src/syntax/highlighting.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index b38fdbc..01c6855 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -72,7 +72,7 @@ void SyntaxHighlightingString_AddTag(SyntaxHighlightingString *shs, SyntaxHighli if (!shs) { return; } - if (shs->tags_count + 1 >= shs->tags_capacity) { + if (shs->tags_count >= shs->tags_capacity) { increase_tags_capacity(shs); } shs->tags[shs->tags_count++] = tag; From 9cde5424c077c5a0b4281aad66756a794fa3434a Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:28:49 +0100 Subject: [PATCH 31/35] fix potential int overflow --- src/syntax/highlighting.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 01c6855..7b84a8d 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -91,9 +91,9 @@ const SyntaxHighlightingTag *SyntaxHighlightingString_GetTag(const SyntaxHighlig if (!shs || shs->tags_count == 0) { return NULL; } - for (int i=(int)shs->tags_count-1; i>=0; i--) { - if (shs->tags[i].byte_offset == offset) { - return &shs->tags[i]; + for (size_t i=shs->tags_count; i>0; i--) { + if (shs->tags[i-1].byte_offset == offset) { + return &shs->tags[i-1]; } } return NULL; From 4c9e5f24255786a826a355c719eff835df0d1009 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:30:08 +0100 Subject: [PATCH 32/35] add comment --- src/syntax/highlighting.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/syntax/highlighting.c b/src/syntax/highlighting.c index 7b84a8d..9564296 100644 --- a/src/syntax/highlighting.c +++ b/src/syntax/highlighting.c @@ -11,6 +11,7 @@ void SyntaxHighlightingTag_Init(SyntaxHighlightingTag *tag) { } void SyntaxHighlightingTag_Deinit(SyntaxHighlightingTag *tag) { + // This function exists mainly for potential future use SyntaxHighlightingTag_Init(tag); } From b7275a6500a8617abf9572011019d993a6b1f211 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 15:42:39 +0100 Subject: [PATCH 33/35] minor change to const correctness in cpy_ptr --- src/common/table.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/table.c b/src/common/table.c index 3344d39..f4fc0f7 100644 --- a/src/common/table.c +++ b/src/common/table.c @@ -61,7 +61,7 @@ static int cmp_ptr(const void *a, const void *b) { } static void *cpy_ptr(const void *p) { - return (void*)p; + return (void*)(uintptr_t)p; } static TableSlot *find_slot(const Table *table, const void *key) { From 4d89c418ce4667b1ede22aefaeb240a2b7576035 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 16:18:02 +0100 Subject: [PATCH 34/35] update readme --- README.md | 154 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 100 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index a0f7b16..3450c22 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,128 @@ # clieditor -A simple, terminal-based text editor written in C from scratch. -This project is an exercise in building a terminal user interface (TUI) application, managing low-level terminal I/O, and structuring a C application in a modular way. -The goal was to use **no third-party libraries** like `ncurses`. +A terminal-based text editor written in C from scratch. This project serves primarily as a personal learning exercise in building a TUI application, exploring low-level terminal I/O, and structuring a C application in a modular way, all without dependencies like `ncurses`. -**Still absolutely unfinished and work in progress** +While it includes modern features, it is a hobby project with known limitations and areas for improvement. It is not intended to be a production-ready editor. ![Screenshot](assets/screenshot.png) +## Features + +- **Handwritten UI Toolkit**: A custom-built widget system from scratch, featuring a status bar, main menu, notifications, and an editor widget. +- **Syntax Highlighting**: A flexible, regex-based highlighting engine. Syntaxes are defined in simple `.ini` files, making it easy to add new languages. +- **Efficient UTF-8 Support**: A revised string library that handles UTF-8 characters correctly and efficiently, with optimized indexing for large strings. +- **Modern Text Editing**: A gap buffer implementation for the current line allows for efficient character insertion and deletion. +- **Advanced Text Layout**: Supports line wrapping that correctly handles wide characters and tab expansion. +- **Asynchronous Events**: A timer system for timed events like cursor blinking and auto-hiding notifications. + ## Building -Make sure to have `CMake` (you can also build it without though), a C compiler with the C standard library and POSIX C library available and run -``` + +Make sure to have `CMake`, a C compiler (like GCC or Clang) with the C standard library, and the POSIX C library available. + +```bash cmake -B build cmake --build build ``` -## Features -- A handwritten widget system with a status bar, main menu, notifications, and an editor widget forming the core of the UI. -- UTF-8 support -- Line wrapping with correct handling of wide characters and tabs -- A timer system for timed events such as cursor blinking and notifications +## Usage -## Design Overview -The code is organized into modules, each typically consisting of a data structure and a set of functions operating on it. +To open a file, simply pass its name as an argument: -### User Interface -The UI is built as a tree of widgets, all inheriting from the base `Widget` module. -The root of the widget tree is the `App` widget. -If a widget has focus, all its ancestors also have focus. -User input is passed from the root down to the deepest focused widget until one handles it. -Widgets can define handlers for common events such as input, drawing, or parent resize events. +```bash +./build/clieditor src/main.c +``` -### Drawing -Each widget draws itself onto a `Canvas`, which consists of `Cell`s. -Canvases are layered and clipped together, and finally compared to the special `Screen` canvas. -Only changed `Cell`s are written to the `Terminal`, the module that directly interacts with the terminal device. +### Command-Line Options -### Editor -The editor itself is composed of several tightly integrated modules: -- **`TextBuffer`** — holds the complete text as a doubly-linked list of `Line`s, with a gap buffer for efficient editing. -- **`TextLayout`** — calculates which lines are visible on screen, where wrapping occurs, how tabs expand and where the cursor visually is on screen. It's also responsible also for scrolling. -- **`TextEdit`** — handles text modifications like inserting or deleting characters in the buffer and moving the cursor. -The `Editor` widget brings these parts together and manages user interaction and rendering. +- `clieditor `: Open a specific file. +- `clieditor -s `: Open a file and force a specific syntax highlighting profile (e.g., `-s c ...`). The name corresponds to a `.ini` file in the `data/syntax` directory. +- `clieditor -h`: Show the help message. -## Directory Structure +## Architecture Overview -### `common/` -Fundamental, reusable modules with no dependencies on the rest of the editor, e.g., UTF-8 utilities and logging. +The editor is designed with a clean separation of concerns, organized into distinct modules. This makes the codebase easier to navigate, maintain, and extend. -### `io/` -Handles all direct interaction with the system — terminal I/O, file reading, etc. -This is the only layer with platform-specific code. +- `main.c`: The application's entry point. It handles initialization, argument parsing, and runs the main event loop. +- `common/`: A collection of fundamental, reusable data structures and utilities. This includes the core `String` library, a hash `Table`, a dynamic `Stack`, the `iniparser`, and the `Config` manager. +- `io/`: The layer for all direct interaction with the system, such as terminal I/O (`Terminal`, `Screen`), user `Input`, file operations (`File`), and the event `Timer`. +- `display/`: An abstract rendering layer. It defines *what* to draw through primitives like `Widget`, `Canvas`, and `Cell`, but not *how* it's rendered on screen. +- `document/`: The core data model of the editor, managing the text content independently of the UI. This includes the `TextBuffer`, `TextLayout` for visual calculation, and `TextEdit` for modification logic. +- `syntax/`: The syntax highlighting engine. It includes the `SyntaxDefinition` loader, the `SyntaxHighlighting` processor, and bindings to connect it to the editor's text layout. +- `widgets/`: A library of all concrete UI components, built upon the `display` primitives. It is divided into `primitives` (like `Label`, `Menu`) and `components` (like `EditorView`, `BottomBar`), with `App` serving as the root of the widget hierarchy. -### `display/` -Abstract rendering layer — defines *what* to draw (via `Canvas`, `Cell`, and `Widget`), but not *how* it’s shown on screen. +## Core Concepts -### `document/` -The core data model of the editor. Manages text independently of the UI. +The editor is built on several key concepts that are crucial to understanding its design and its current limitations. -### `widgets/` -All concrete UI components, like the editor view, status bar, or labels. -The `App` widget forms the root of the widget hierarchy. +### The String System -### `main.c` -Entry point of the application. Initializes all systems, loads the file, and runs the main loop. +The editor uses a custom, UTF-8 aware string library designed for a balance of performance and memory efficiency. -## Questionable Design Choices +- **Representation**: A `String` is a standard `char*` byte buffer, keeping memory usage minimal. +- **Efficient Indexing**: To overcome the O(N) cost of character indexing in variable-length UTF-8 strings, the `String` struct maintains an auxiliary array (`multibytes`) that caches the byte offsets of multi-byte characters. This allows for O(log N) character-based indexing via binary search. -### UTF8 Support -I had no idea how UTF-8 actually worked in C, so I quickly threw together an `UTF8Char` struct and `UTF8String` containers built from it. -To be fair, they turned out to be quite robust and easy to use — which is the good part. -The bad part is that I now use at least 5 bytes per character, which blows up memory usage pretty fast. -For example, the release build needs around *600 MB* to load a *100 MB* file, which seems… a bit much. +### The Widget System -## License +The UI is a tree of widgets, with `App` at its root. This system was built entirely from scratch as a learning exercise. -This project is licensed under the GNU General Public License v3.0 (GPL-3.0). +- **Inheritance**: Polymorphism is achieved using a `WidgetOps` struct within the base `Widget`, which acts as a "vtable" pointing to a widget's specific implementations for `draw`, `on_input`, `on_resize`, etc. +- **Drawing**: The system uses a double-buffering strategy. Each widget draws to a `Canvas`, and the final canvas is compared against the previous frame to write only changed `Cell`s to the terminal. +- **Limitations**: As a from-scratch implementation, the widget, drawing, and focus systems are quite basic. They demonstrate core TUI concepts but could be significantly improved for more complex UI scenarios and are a known area of weakness. + +### The Editor Engine + +The core of the text editing functionality is split into three main parts: + +1. **`TextBuffer`**: This is the data model. It stores the document's text as a doubly-linked list of `Line` structs. For efficient editing, it employs a **gap buffer** within the `String` of the *current line*. Insertions and deletions happen at the gap, which is almost always an O(1) operation. + - **Note**: This is a good example of the project's learning nature. The performance benefit of the gap buffer is currently limited, as other features (namely the syntax highlighter) frequently force the gap to be merged back into the text. Optimizing this interaction is a known area for future work. +2. **`TextLayout`**: This is the "view model". It is responsible for calculating how the text in the `TextBuffer` should be displayed, handling line wrapping, tab expansion, and scrolling. +3. **`TextEdit`**: This is the "controller". It provides an API for all text manipulations (e.g., `TextEdit_InsertChar`, `TextEdit_MoveUp`), modifying the `TextBuffer` and marking the `TextLayout` as dirty. + +### Syntax Highlighting + +The highlighting engine is designed to be extensible. + +- **Declarative Definitions**: Syntax rules are defined in simple `.ini` files, consisting of a `[meta]` section and multiple `[block:...]` sections. +- **Regex-Based Blocks**: Each block is defined by a `start` regex and an optional `end` regex. Blocks can be nested by specifying `child_blocks`. +- **Stateful Parsing**: The engine (`SyntaxHighlighting`) processes text line by line, maintaining a stack of open blocks. It uses the context from the end of the previous line to correctly highlight constructs that span multiple lines. -You may obtain a copy of the license at: +## How to Read the Code + +If you're new to the codebase, here's a recommended path: + +1. **Start at `src/main.c`**: Understand the initialization sequence and the main event loop. +2. **Explore `common/`**: Look at `string.h` and `table.h`, as these are used everywhere. +3. **Understand the UI**: `display/widget.h` is the base for all UI elements, and `widgets/components/editor.h` is the main editor widget. +4. **Dive into the Editor Core**: `document/textbuffer.h` (storage), `document/textlayout.h` (display logic), and `syntax/highlighting.h` (highlighting logic). + +## Coding Style + +The project follows a consistent C coding style. + +- **Naming Conventions**: + - Types and structs are `PascalCase` (e.g., `TextBuffer`). + - Public functions are `Module_Function` (e.g., `TextBuffer_Init`). + - Enums and macros are `ALL_CAPS`. +- **Object-Oriented C**: The code emulates object-oriented patterns. Modules are centered around a primary struct, and functions that operate on it take a pointer to that struct as their first argument (e.g., `void Widget_Draw(Widget *self, ...)`). +- **Memory Management**: All memory is managed manually. `_Create` functions `malloc` resources, and `_Destroy` functions `free` them. `_Init` and `_Deinit` pairs manage the contents of a struct without allocating/freeing the struct itself. +- **Formatting**: 4-space indentation. The opening brace `{` is always placed on the same line as the corresponding function declaration or control statement (`if`, `for`, `while`, etc.). + +## Future Goals + +This project is actively used for learning and experimentation. Here are some of the planned improvements and refactoring goals: + +- **Adopt a Rope Data Structure**: Replace the current `TextBuffer` implementation (a linked list of lines) with a Rope. This more advanced data structure should offer better performance for large files and complex editing operations. This will also require a corresponding redesign of the syntax highlighting engine to work efficiently with the new structure. + +- **Implement Common Editor Features**: While not the primary focus, implementing standard text editor functionalities such as searching, and find and replace, remains a long-term goal. + +- **Simplify the Widget System**: Refactor the core widget system, particularly the drawing and focus management logic. The goal is to create a simpler, more robust implementation that is easier to maintain, since the application's UI needs are relatively modest and centered around the main editor component. + +- **Refactor Application Setup**: Move the responsibility for creating and connecting widgets from the `main` function into the `App` module. This will centralize UI setup and make `App` the true root of the application, improving modularity. + +- **Improve `const`-Correctness in the String Library**: Modify the `String` implementation to rebuild its internal UTF-8 indexing cache immediately after any modification. This change will allow read-only functions (like `String_GetChar`) to accept a `const String*`, enforcing better `const`-correctness throughout the API. + +## License + +This project is licensed under the GNU General Public License v3.0 (GPL-3.0). -https://www.gnu.org/licenses/gpl-3.0.html +You may obtain a copy of the license at: https://www.gnu.org/licenses/gpl-3.0.html From e6a4289217e3f42a05a93bac2aaed12a18ef46e2 Mon Sep 17 00:00:00 2001 From: defname Date: Sun, 26 Oct 2025 16:23:01 +0100 Subject: [PATCH 35/35] fix intendation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3450c22..7124b16 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,9 @@ If you're new to the codebase, here's a recommended path: The project follows a consistent C coding style. - **Naming Conventions**: - - Types and structs are `PascalCase` (e.g., `TextBuffer`). - - Public functions are `Module_Function` (e.g., `TextBuffer_Init`). - - Enums and macros are `ALL_CAPS`. + - Types and structs are `PascalCase` (e.g., `TextBuffer`). + - Public functions are `Module_Function` (e.g., `TextBuffer_Init`). + - Enums and macros are `ALL_CAPS`. - **Object-Oriented C**: The code emulates object-oriented patterns. Modules are centered around a primary struct, and functions that operate on it take a pointer to that struct as their first argument (e.g., `void Widget_Draw(Widget *self, ...)`). - **Memory Management**: All memory is managed manually. `_Create` functions `malloc` resources, and `_Destroy` functions `free` them. `_Init` and `_Deinit` pairs manage the contents of a struct without allocating/freeing the struct itself. - **Formatting**: 4-space indentation. The opening brace `{` is always placed on the same line as the corresponding function declaration or control statement (`if`, `for`, `while`, etc.).