From 4d57539ffc2954e5c3b9c42da41511f69c4fcde9 Mon Sep 17 00:00:00 2001 From: pommicket Date: Wed, 18 Oct 2023 12:58:59 -0400 Subject: start editorconfig support --- config.c | 159 +++++++++++++++++++++++++++++++++++++++++++++++---------- main.c | 1 + ted-internal.h | 25 ++++++++- ted.c | 20 ++++++-- util.c | 13 +++++ util.h | 4 ++ 6 files changed, 190 insertions(+), 32 deletions(-) diff --git a/config.c b/config.c index 56a4ac7..387c797 100644 --- a/config.c +++ b/config.c @@ -286,6 +286,7 @@ static void config_free(Config *cfg) { settings_free(&cfg->settings); pcre2_code_free_8(cfg->path); free(cfg->path_regex); + rc_str_decref(&cfg->source); memset(cfg, 0, sizeof *cfg); } @@ -339,13 +340,12 @@ static void config_err_unexpected_eof(ConfigReader *reader) { config_err(reader, "Unexpected EOF (no newline at end of file?)"); } -static char config_getc(ConfigReader *reader) { +static char config_getc_no_err_on_eof(ConfigReader *reader) { int c = getc(reader->fp); if (c == 0) { config_err(reader, "Null byte in config file"); } if (c == EOF) { - config_err_unexpected_eof(reader); c = 0; } if (c == '\n') { @@ -354,6 +354,13 @@ static char config_getc(ConfigReader *reader) { return (char)c; } +static char config_getc(ConfigReader *reader) { + char c = config_getc_no_err_on_eof(reader); + if (!c) + config_err_unexpected_eof(reader); + return c; +} + static void config_ungetc(ConfigReader *reader, char c) { if (c == '\n') reader->line_number -= 1; @@ -915,7 +922,20 @@ void settings_finalize(Ted *ted, Settings *settings) { settings->text_size = clamp_u16((u16)roundf((float)settings->text_size_no_dpi * ted_get_ui_scaling(ted)), TEXT_SIZE_MIN, TEXT_SIZE_MAX); } -static void config_read_file(Ted *ted, const char *cfg_path, const char ***include_stack) { +static void config_compile_regex(Config *cfg, ConfigReader *reader) { + if (cfg->path_regex) { + // compile regex + int error_code = 0; + PCRE2_SIZE error_offset = 0; + cfg->path = pcre2_compile_8((const u8 *)cfg->path_regex, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED, &error_code, &error_offset, NULL); + if (!cfg->path) { + config_err(reader, "Bad regex (at offset %u): %s", (unsigned)error_offset, cfg->path_regex); + free(cfg->path_regex); cfg->path_regex = NULL; + } + } +} + +static bool config_read_ted_cfg(Ted *ted, const char *cfg_path, const char ***include_stack) { // check for, e.g. %include ted.cfg inside ted.cfg arr_foreach_ptr(*include_stack, const char *, p_include) { if (streq(cfg_path, *p_include)) { @@ -931,18 +951,17 @@ static void config_read_file(Ted *ted, const char *cfg_path, const char ***inclu strbuf_cat(text, ", which"); strbuf_catf(text, " includes %s", cfg_path); ted_error(ted, "%s", text); - return; + return false; } } arr_add(*include_stack, cfg_path); FILE *fp = fopen(cfg_path, "rb"); if (!fp) { - ted_error(ted, "Couldn't open config file %s: %s.", cfg_path, strerror(errno)); - return; + return false; } - + RcStr *cfg_path_rc = rc_str_new(cfg_path, -1); ConfigReader reader_data = { .ted = ted, .filename = cfg_path, @@ -955,12 +974,8 @@ static void config_read_file(Ted *ted, const char *cfg_path, const char ***inclu Config *cfg = NULL; while (true) { - int ic = getc(reader->fp); - if (ic == EOF) - break; - char c = (char)ic; - if (c == '\n') ++reader->line_number; - + char c = config_getc_no_err_on_eof(reader); + if (!c) break; if (c == '[') { // a new section! #define SECTION_HEADER_HELP "Section headers should look like this: [(path//)(language.)section-name]" @@ -1036,16 +1051,9 @@ static void config_read_file(Ted *ted, const char *cfg_path, const char ***inclu cfg = arr_addp(ted->all_configs); cfg->path_regex = *path ? str_dup(path) : NULL; cfg->language = language; - if (cfg->path_regex) { - // compile regex - int error_code = 0; - PCRE2_SIZE error_offset = 0; - cfg->path = pcre2_compile_8((const u8 *)cfg->path_regex, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED, &error_code, &error_offset, NULL); - if (!cfg->path) { - config_err(reader, "Bad regex (at offset %u): %s", (unsigned)error_offset, cfg->path_regex); - free(cfg->path_regex); cfg->path_regex = NULL; - } - } + cfg->format = CONFIG_TED_CFG; + cfg->source = rc_str_copy(cfg_path_rc); + config_compile_regex(cfg, reader); } } else if (c == '%') { char line[2048]; @@ -1056,7 +1064,7 @@ static void config_read_file(Ted *ted, const char *cfg_path, const char ***inclu strbuf_cpy(included, line + strlen("include ")); str_trim(included); get_config_path(ted, expanded, sizeof expanded, included); - config_read_file(ted, expanded, include_stack); + config_read_ted_cfg(ted, expanded, include_stack); } else { config_err(reader, "Unrecognized directive: %s", line); } @@ -1073,11 +1081,90 @@ static void config_read_file(Ted *ted, const char *cfg_path, const char ***inclu config_err(reader, "Config has text before first section header."); } } - + rc_str_decref(&cfg_path_rc); if (ferror(fp)) ted_error(ted, "Error reading %s.", cfg_path); fclose(fp); arr_remove_last(*include_stack); + return true; +} + +static bool config_read_editorconfig(Ted *ted, const char *path) { + FILE *fp = fopen(path, "r"); + if (!fp) return false; + ConfigReader reader_data = { + .ted = ted, + .filename = path, + .line_number = 1, + .fp = fp, + }; + ConfigReader *const reader = &reader_data; + Config *cfg = NULL; + char line[4096]; + bool is_root = false; + RcStr *path_rc = rc_str_new(path, -1); + while (true) { + char c = config_getc_no_err_on_eof(reader); + if (!c) break; + if (isspace(c)) continue; + switch (c) { + case '#': + case ';': + config_read_to_eol(reader, line, sizeof line); + break; + case '[': + // new section + config_read_to_eol(reader, line, sizeof line); + str_trim(line); + if (strlen(line) == 0 || line[strlen(line) - 1] != ']') { + config_err(reader, "Unmatched ["); + break; + } + cfg = arr_addp(ted->all_configs); + cfg->source = rc_str_copy(path_rc); + cfg->is_editorconfig_root = is_root; + cfg->format = CONFIG_EDITORCONFIG; + cfg->path_regex = str_dup("TODO"); // TODO + config_compile_regex(cfg, reader); + break; + default: { + char key[64] = {c}; + for (size_t i = 1; i < sizeof key - 1; i++) { + c = config_getc(reader); + if (!c || c == '\n') break; + if (c == '=') break; + key[i] = c; + } + if (c != '=') { + config_err(reader, "expected key = value but didn't find ="); + break; + } + str_trim(key); + str_ascii_to_lowercase(key); + char value[64] = {0}; + for (size_t i = 0; i < sizeof value - 1; i++) { + c = config_getc(reader); + if (!c || c == '\n') break; + value[i] = c; + } + str_trim(value); + if (streq(key, "root")) { + str_ascii_to_lowercase(value); + if (cfg) { + config_err(reader, "root cannot be set outside of preamble"); + } + if (streq(value, "true")) + is_root = true; + else + is_root = false; + } + } + break; + } + } + rc_str_decref(&path_rc); + fclose(fp); + return true; } void config_free_all(Ted *ted) { @@ -1153,11 +1240,27 @@ char *settings_get_root_dir(const Settings *settings, const char *path) { } } -void config_read(Ted *ted, const char *filename) { +bool config_read(Ted *ted, const char *path, ConfigFormat format) { const char **include_stack = NULL; config_init_settings(); - config_read_file(ted, filename, &include_stack); - ted_compute_settings(ted, "", LANG_NONE, &ted->default_settings); + // check if we've already read this + arr_foreach_ptr(ted->all_configs, Config, c) { + if (streq(rc_str(c->source, ""), path)) + return true; + } + switch (format) { + case CONFIG_TED_CFG: + if (config_read_ted_cfg(ted, path, &include_stack)) { + // recompute default settings + ted_compute_settings(ted, "", LANG_NONE, &ted->default_settings); + return true; + } + return false; + case CONFIG_EDITORCONFIG: + return config_read_editorconfig(ted, path); + } + assert(0); + return false; } u32 settings_color(const Settings *settings, ColorSetting color) { diff --git a/main.c b/main.c index 81bd118..2658ca0 100644 --- a/main.c +++ b/main.c @@ -2,6 +2,7 @@ TODO: - .editorconfig (see https://editorconfig.org/) FUTURE FEATURES: +- prepare rename support - autodetect indentation (tabs vs spaces) - custom file/build command associations - config variables diff --git a/ted-internal.h b/ted-internal.h index af5f2dd..f6649a2 100644 --- a/ted-internal.h +++ b/ted-internal.h @@ -153,11 +153,29 @@ struct Settings { KeyAction *key_actions; }; +typedef enum { + CONFIG_TED_CFG = 1, + CONFIG_EDITORCONFIG = 2, +} ConfigFormat; + typedef struct { + /// path to config file + RcStr *source; + /// format of config file + ConfigFormat format; + /// is this from a root .editorconfig file? + /// + /// (if so, we don't want to apply editorconfigs in higher-up directories) + bool is_editorconfig_root; + /// language this config applies to Language language; + /// path regex this config applies to struct pcre2_real_code_8 *path; + /// path regex string char *path_regex; + /// settings which this config specifies Settings settings; + /// which bytes of settings are actually set bool settings_set[sizeof (Settings)]; } Config; @@ -479,7 +497,12 @@ void command_init(void); void command_execute_ex(Ted *ted, Command c, const CommandArgument *argument, const CommandContext *context); // === config.c === -void config_read(Ted *ted, const char *filename); +/// read a config file. +/// +/// returns true on success. if the file at `path` does not exist, this returns false but doesn't show an error message. +/// +/// if the config with this path has already been read, this does nothing. +bool config_read(Ted *ted, const char *path, ConfigFormat format); void config_free_all(Ted *ted); void config_merge_into(Settings *dest, const Config *src_cfg); /// call this after all your calls to \ref config_merge_into diff --git a/ted.c b/ted.c index 85e1a0b..5693092 100644 --- a/ted.c +++ b/ted.c @@ -233,6 +233,20 @@ static int applicable_configs_cmp(void *context, const void *av, const void *bv) void ted_compute_settings(Ted *ted, const char *path, Language language, Settings *settings) { settings_free(settings); + + if (path && *path) { + // check for .editorconfig + char editorconfig[2048]; + for (size_t i = 0; path[i] && i < sizeof editorconfig - 16; i++) { + editorconfig[i] = path[i]; + editorconfig[i+1] = 0; + if (strchr(ALL_PATH_SEPARATORS, path[i])) { + strbuf_cat(editorconfig, ".editorconfig"); + config_read(ted, editorconfig, CONFIG_EDITORCONFIG); + } + } + } + u32 *applicable_configs = NULL; for (u32 i = 0; i < arr_len(ted->all_configs); i++) { Config *cfg = &ted->all_configs[i]; @@ -736,13 +750,13 @@ void ted_load_configs(Ted *ted) { } - config_read(ted, global_config_filename); - config_read(ted, local_config_filename); + config_read(ted, global_config_filename, CONFIG_TED_CFG); + config_read(ted, local_config_filename, CONFIG_TED_CFG); if (ted->search_start_cwd) { // read config in start_cwd char start_cwd_filename[TED_PATH_MAX]; strbuf_printf(start_cwd_filename, "%s%c" TED_CFG, ted->start_cwd, PATH_SEPARATOR); - config_read(ted, start_cwd_filename); + config_read(ted, start_cwd_filename, CONFIG_TED_CFG); } } diff --git a/util.c b/util.c index 1649f91..b86a4ca 100644 --- a/util.c +++ b/util.c @@ -38,6 +38,12 @@ void rc_str_incref(RcStr *str) { str->ref_count += 1; } + +RcStr *rc_str_copy(RcStr *str) { + rc_str_incref(str); + return str; +} + void rc_str_decref(RcStr **pstr) { RcStr *const str = *pstr; if (!str) return; @@ -300,6 +306,13 @@ void str_trim(char *str) { str_trim_start(str); } +void str_ascii_to_lowercase(char *str) { + for (char *p = str; *p; p++) { + if (*p > 0 && *p < 127) + *p = (char)tolower(*p); + } +} + size_t str_count_char(const char *s, char c) { const char *p = s; size_t count = 0; diff --git a/util.h b/util.h index 1d60cf9..e190cd6 100644 --- a/util.h +++ b/util.h @@ -53,6 +53,8 @@ RcStr *rc_str_new(const char *s, i64 len); /// /// does nothing if `str` is `NULL`. void rc_str_incref(RcStr *str); +/// increases reference count of `str` (if non-NULL) and returns it. +RcStr *rc_str_copy(RcStr *str); /// decrease reference count of `*str` and set `*str` to `NULL`. /// /// this frees `*str` if the reference count hits 0. @@ -122,6 +124,8 @@ void str_trim_start(char *str); void str_trim_end(char *str); /// trim whitespace from both sides of a string void str_trim(char *str); +/// convert ASCII to lowercase +void str_ascii_to_lowercase(char *str); /// count occurences of `c` in `s` size_t str_count_char(const char *s, char c); /// equivalent to GNU function asprintf (like sprintf, but allocates the string with malloc). -- cgit v1.2.3