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); }