From 26d34216da04a2b91e65a0eeee9200ad808d48ce Mon Sep 17 00:00:00 2001 From: pommicket Date: Wed, 2 Nov 2022 13:02:38 -0400 Subject: (insufficiently tested) per-path settings also fixed memory bug in path_full, yet again --- buffer.c | 30 ++- config.c | 830 +++++++++++++++++++++++++++++++++++++-------------------------- main.c | 34 ++- ted.c | 21 +- ted.cfg | 2 +- ted.h | 33 ++- util.c | 3 +- 7 files changed, 584 insertions(+), 369 deletions(-) diff --git a/buffer.c b/buffer.c index be0c07e..a91257e 100644 --- a/buffer.c +++ b/buffer.c @@ -272,9 +272,37 @@ Language buffer_language(TextBuffer *buffer) { return LANG_NONE; } +// score is higher if buffer more closely matches context. +static long buffer_context_score(TextBuffer *buffer, const SettingsContext *context) { + long score = 0; + + if (buffer_language(buffer) == context->language) { + score += 100000; + } + + if (context->path) { + int i; + for (i = 0; i < TED_PATH_MAX && buffer->filename[i] == context->path[i]; ++i); + score += i; + } + + return score; +} + // Get the settings used for this buffer. Settings *buffer_settings(TextBuffer *buffer) { - return &buffer->ted->settings_by_language[buffer_language(buffer)]; + Ted *ted = buffer->ted; + long best_score = 0; + Settings *settings = ted->settings; + + arr_foreach_ptr(ted->all_settings, Settings, s) { + long score = buffer_context_score(buffer, &s->context); + if (score > best_score) { + best_score = score; + settings = s; + } + } + return settings; } diff --git a/config.c b/config.c index 3ed27d8..7a47d23 100644 --- a/config.c +++ b/config.c @@ -7,16 +7,10 @@ // [section2] // asdf = 123 -typedef enum { - SECTION_NONE, - SECTION_CORE, - SECTION_KEYBOARD, - SECTION_COLORS, - SECTION_EXTENSIONS -} Section; - // all worth it for the -Wformat warnings -#define config_err(cfg, ...) do { snprintf((cfg)->ted->error, sizeof (cfg)->ted->error - 1, "%s:%u: ", (cfg)->filename, (cfg)->line_number), \ +#define config_err(cfg, ...) do {\ + if ((cfg)->error) break;\ + snprintf((cfg)->ted->error, sizeof (cfg)->ted->error - 1, "%s:%u: ", (cfg)->filename, (cfg)->line_number), \ snprintf((cfg)->ted->error + strlen((cfg)->ted->error), sizeof (cfg)->ted->error - 1 - strlen((cfg)->ted->error), __VA_ARGS__), \ (cfg)->error = true; } while (0) @@ -27,6 +21,51 @@ typedef struct { bool error; } ConfigReader; +static void context_copy(SettingsContext *dest, const SettingsContext *src) { + *dest = *src; + if (src->path) + dest->path = str_dup(src->path); +} + +static bool context_is_parent(const SettingsContext *parent, const SettingsContext *child) { + if (child->language == 0 && parent->language != 0) + return false; + if (parent->language != 0 && child->language != 0 && parent->language != child->language) + return false; + if (parent->path) { + if (!child->path) + return false; + if (!str_is_prefix(parent->path, child->path)) + return false; + } + return true; +} + +static void settings_copy(Settings *dest, const Settings *src) { + context_copy(&dest->context, &src->context); + for (u32 i = 0; i < LANG_COUNT; ++i) + dest->language_extensions[i] = str_dup(src->language_extensions[i]); +} + +static void context_free(SettingsContext *ctx) { + free(ctx->path); + memset(ctx, 0, sizeof *ctx); +} + +static void settings_free(Settings *settings) { + context_free(&settings->context); + for (u32 i = 0; i < LANG_COUNT; ++i) + free(settings->language_extensions[i]); + memset(settings, 0, sizeof *settings); +} + +static void config_part_free(ConfigPart *part) { + context_free(&part->context); + arr_clear(part->text); + free(part->file); + memset(part, 0, sizeof *part); +} + // Returns the key combination described by str. static u32 config_parse_key_combo(ConfigReader *cfg, char const *str) { u32 modifier = 0; @@ -172,7 +211,7 @@ typedef struct { } OptionU16; typedef struct { char const *name; - char *control; + const char *control; size_t buf_size; bool per_language; } OptionString; @@ -197,127 +236,173 @@ typedef struct { } u; } OptionAny; +// core options +static Settings const options_zero = {0}; +static OptionBool const options_bool[] = { + {"auto-indent", &options_zero.auto_indent, true}, + {"auto-add-newline", &options_zero.auto_add_newline, true}, + {"auto-reload", &options_zero.auto_reload, true}, + {"auto-reload-config", &options_zero.auto_reload_config, false}, + {"syntax-highlighting", &options_zero.syntax_highlighting, true}, + {"line-numbers", &options_zero.line_numbers, true}, + {"restore-session", &options_zero.restore_session, false}, + {"regenerate-tags-if-not-found", &options_zero.regenerate_tags_if_not_found, true}, + {"indent-with-spaces", &options_zero.indent_with_spaces, true}, +}; +static OptionU8 const options_u8[] = { + {"tab-width", &options_zero.tab_width, 1, 100, true}, + {"cursor-width", &options_zero.cursor_width, 1, 100, true}, + {"undo-save-time", &options_zero.undo_save_time, 1, 200, true}, + {"border-thickness", &options_zero.border_thickness, 1, 30, false}, + {"padding", &options_zero.padding, 0, 100, false}, + {"scrolloff", &options_zero.scrolloff, 1, 100, true}, + {"tags-max-depth", &options_zero.tags_max_depth, 1, 100, false}, +}; +static OptionU16 const options_u16[] = { + {"text-size", &options_zero.text_size, TEXT_SIZE_MIN, TEXT_SIZE_MAX, true}, + {"max-menu-width", &options_zero.max_menu_width, 10, U16_MAX, false}, + {"error-display-time", &options_zero.error_display_time, 0, U16_MAX, false}, +}; +static OptionFloat const options_float[] = { + {"cursor-blink-time-on", &options_zero.cursor_blink_time_on, 0, 1000, true}, + {"cursor-blink-time-off", &options_zero.cursor_blink_time_off, 0, 1000, true}, +}; +static OptionString const options_string[] = { + {"build-default-command", options_zero.build_default_command, sizeof options_zero.build_default_command, true}, +}; + static void option_bool_set(Settings *settings, const OptionBool *opt, bool value) { - *(bool *)((char *)settings + (size_t)opt->control) = value; + *(bool *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_u8_set(Settings *settings, const OptionU8 *opt, u8 value) { if (value >= opt->min && value <= opt->max) - *(u8 *)((char *)settings + (size_t)opt->control) = value; + *(u8 *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_u16_set(Settings *settings, const OptionU16 *opt, u16 value) { if (value >= opt->min && value <= opt->max) - *(u16 *)((char *)settings + (size_t)opt->control) = value; + *(u16 *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_float_set(Settings *settings, const OptionFloat *opt, float value) { if (value >= opt->min && value <= opt->max) - *(float *)((char *)settings + (size_t)opt->control) = value; + *(float *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_string_set(Settings *settings, const OptionString *opt, const char *value) { - char *control = (char *)settings + (size_t)opt->control; + char *control = (char *)settings + (opt->control - (char*)&options_zero); str_cpy(control, opt->buf_size, value); } -// two passes are done -// pass 0 reads global settings -// pass 1 reads language-specific settings -void config_read(Ted *ted, char const *filename, int pass) { - ConfigReader cfg_reader = { - .ted = ted, - .filename = filename, - .line_number = 1, - .error = false - }; - ConfigReader *cfg = &cfg_reader; - Settings *settings = ted->settings; - - // core options - // (these go at the start so they don't need to be re-computed each time) - Settings *nullset = NULL; - OptionBool const options_bool[] = { - {"auto-indent", &nullset->auto_indent, true}, - {"auto-add-newline", &nullset->auto_add_newline, true}, - {"auto-reload", &nullset->auto_reload, true}, - {"auto-reload-config", &nullset->auto_reload_config, false}, - {"syntax-highlighting", &nullset->syntax_highlighting, true}, - {"line-numbers", &nullset->line_numbers, true}, - {"restore-session", &nullset->restore_session, false}, - {"regenerate-tags-if-not-found", &nullset->regenerate_tags_if_not_found, true}, - {"indent-with-spaces", &nullset->indent_with_spaces, true}, - }; - OptionU8 const options_u8[] = { - {"tab-width", &nullset->tab_width, 1, 100, true}, - {"cursor-width", &nullset->cursor_width, 1, 100, true}, - {"undo-save-time", &nullset->undo_save_time, 1, 200, true}, - {"border-thickness", &nullset->border_thickness, 1, 30, false}, - {"padding", &nullset->padding, 0, 100, false}, - {"scrolloff", &nullset->scrolloff, 1, 100, true}, - {"tags-max-depth", &nullset->tags_max_depth, 1, 100, false}, - }; - OptionU16 const options_u16[] = { - {"text-size", &nullset->text_size, TEXT_SIZE_MIN, TEXT_SIZE_MAX, true}, - {"max-menu-width", &nullset->max_menu_width, 10, U16_MAX, false}, - {"error-display-time", &nullset->error_display_time, 0, U16_MAX, false}, - }; - OptionFloat const options_float[] = { - {"cursor-blink-time-on", &nullset->cursor_blink_time_on, 0, 1000, true}, - {"cursor-blink-time-off", &nullset->cursor_blink_time_off, 0, 1000, true}, - }; - OptionString const options_string[] = { - {"build-default-command", nullset->build_default_command, sizeof nullset->build_default_command, true}, - }; + +static void parse_section_header(ConfigReader *cfg, char *line, ConfigPart *part) { + #define SECTION_HEADER_HELP "Section headers should look like this: [(path//)(language.)section-name]" + char *closing = strchr(line, ']'); + if (!closing) { + config_err(cfg, "Unmatched [. " SECTION_HEADER_HELP); + return; + } else if (closing[1] != '\0') { + config_err(cfg, "Text after section. " SECTION_HEADER_HELP); + return; + } else { + *closing = '\0'; + char *section = line + 1; + char *path_end = strstr(section, "//"); + if (path_end) { + size_t path_len = (size_t)(path_end - section); + // @TODO: expand ~ + part->context.path = strn_dup(section, path_len); + section = path_end + 2; + } + + char *dot = strchr(section, '.'); + + if (dot) { + *dot = '\0'; + Language language = part->context.language = language_from_str(section); + if (!language) { + config_err(cfg, "Unrecognized language: %s.", section); + return; + } + section = dot + 1; + } + + if (streq(section, "keyboard")) { + part->section = SECTION_KEYBOARD; + } else if (streq(section, "colors")) { + part->section = SECTION_COLORS; + } else if (streq(section, "core")) { + part->section = SECTION_CORE; + } else if (streq(section, "extensions")) { + part->section = SECTION_EXTENSIONS; + } else { + config_err(cfg, "Unrecognized section: [%s].", section); + return; + } + } +} + +static bool initialized_options = false; +static OptionAny all_options[1000] = {0}; + +static void config_init_options(void) { + if (initialized_options) return; - OptionAny all_options[1000] = {0}; - OptionAny *all_options_end = all_options; + OptionAny *opt = all_options; for (size_t i = 0; i < arr_count(options_bool); ++i) { - OptionAny *opt = all_options_end++; opt->type = OPTION_BOOL; opt->name = options_bool[i].name; opt->per_language = options_bool[i].per_language; opt->u._bool = options_bool[i]; + ++opt; } for (size_t i = 0; i < arr_count(options_u8); ++i) { - OptionAny *opt = all_options_end++; opt->type = OPTION_U8; opt->name = options_u8[i].name; opt->per_language = options_u8[i].per_language; opt->u._u8 = options_u8[i]; + ++opt; } for (size_t i = 0; i < arr_count(options_u16); ++i) { - OptionAny *opt = all_options_end++; opt->type = OPTION_U16; opt->name = options_u16[i].name; opt->per_language = options_u16[i].per_language; opt->u._u16 = options_u16[i]; + ++opt; } for (size_t i = 0; i < arr_count(options_float); ++i) { - OptionAny *opt = all_options_end++; opt->type = OPTION_FLOAT; opt->name = options_float[i].name; opt->per_language = options_float[i].per_language; opt->u._float = options_float[i]; + ++opt; } for (size_t i = 0; i < arr_count(options_string); ++i) { - OptionAny *opt = all_options_end++; opt->type = OPTION_STRING; opt->name = options_string[i].name; opt->per_language = options_string[i].per_language; opt->u._string = options_string[i]; + ++opt; } + initialized_options = true; +} +void config_read(Ted *ted, ConfigPart **parts, char const *filename) { FILE *fp = fopen(filename, "rb"); if (!fp) { ted_seterr(ted, "Couldn't open config file %s.", filename); return; } - char line[4096] = {0}; - int line_cap = sizeof line; + ConfigReader cfg_reader = { + .ted = ted, + .filename = filename, + .line_number = 1, + .error = false + }; + ConfigReader *cfg = &cfg_reader; - Section section = SECTION_NONE; - Language language = LANG_NONE; - bool skip_section = false; + ConfigPart *part = NULL; - while (fgets(line, line_cap, fp)) { + char line[4096] = {0}; + while (fgets(line, sizeof line, fp)) { char *newline = strchr(line, '\n'); if (!newline && !feof(fp)) { config_err(cfg, "Line is too long."); @@ -327,292 +412,359 @@ void config_read(Ted *ted, char const *filename, int pass) { if (newline) *newline = '\0'; char *carriage_return = strchr(line, '\r'); if (carriage_return) *carriage_return = '\0'; + + if (line[0] == '[') { + // a new part! + part = arr_addp(*parts); + part->file = str_dup(filename); + part->line = cfg_reader.line_number + 1; + parse_section_header(&cfg_reader, line, part); + } else if (part) { + for (int i = 0; line[i]; ++i) { + arr_add(part->text, line[i]); + } + arr_add(part->text, '\n'); + } else { + const char *p = line; + while (isspace(*p)) ++p; + if (*p == '\0' || *p == '#') { + // blank line + } else { + config_err(cfg, "Config has text before first section header."); + } + } + } + + if (ferror(fp)) + ted_seterr(ted, "Error reading %s.", filename); + fclose(fp); +} - // ok, we've now read a line. - switch (line[0]) { - case '#': // comment - case '\0': // blank line - break; - case '[': { // section header - #define SECTION_HEADER_HELP "Section headers should look like this: [section-name]" - char *closing = strchr(line, ']'); - if (!closing) { - config_err(cfg, "Unmatched [. " SECTION_HEADER_HELP); - } else if (closing[1] != '\0') { - config_err(cfg, "Text after section. " SECTION_HEADER_HELP); + +// REQUIREMENTS FOR THIS FUNCTION: +// - two configs compare equal iff their contexts are identical +// - less specific contexts compare as less +// (i.e. if context_is_parent(a.context, b.context), then we return -1, and vice versa.) +static int config_part_qsort_cmp(const void *av, const void *bv) { + const ConfigPart *ap = av, *bp = bv; + const SettingsContext *a = &ap->context, *b = &bp->context; + if (a->language == 0 && b->language != 0) + return -1; + if (a->language != 0 && b->language == 0) + return +1; + const char *a_path = a->path ? a->path : ""; + const char *b_path = b->path ? b->path : ""; + size_t a_path_len = strlen(a_path), b_path_len = strlen(b_path); + if (a_path_len < b_path_len) + return -1; + if (a_path_len > b_path_len) + return 1; + + // done with specificity, now on to identicalness + if (a->language < b->language) + return -1; + if (a->language > b->language) + return +1; + return strcmp(a_path, b_path); +} + +static void config_parse_line(ConfigReader *cfg, Settings *settings, const ConfigPart *part, const char *line) { + Ted *ted = cfg->ted; + + if (part->section == 0) { + // there was an error reading this section. don't bother with anything else. + return; + } + + switch (line[0]) { + case '#': // comment + case '\0': // blank line + return; + } + + char *equals = strchr(line, '='); + if (!equals) { + config_err(cfg, "Invalid line syntax. " + "Lines should either look like [section-name] or key = value"); + return; + } + + char const *key = line; + *equals = '\0'; + char const *value = equals + 1; + while (isspace(*key)) ++key; + while (isspace(*value)) ++value; + if (equals != line) { + for (char *p = equals - 1; p > line; --p) { + // remove trailing spaces after key + if (isspace(*p)) *p = '\0'; + else break; + } + } + if (key[0] == '\0') { + config_err(cfg, "Empty property name. This line should look like: key = value"); + return; + } + + switch (part->section) { + case SECTION_NONE: + config_err(cfg, "Line outside of any section." + "Try putting a section header, e.g. [keyboard] before this line?"); + break; + case SECTION_COLORS: { + ColorSetting setting = color_setting_from_str(key); + if (setting != COLOR_UNKNOWN) { + u32 color = 0; + if (color_from_str(value, &color)) { + settings->colors[setting] = color; } else { - *closing = '\0'; - char *section_name = line + 1; - char *dot = strchr(section_name, '.'); - - if (dot) { - *dot = '\0'; - language = language_from_str(section_name); - if (!language) { - config_err(cfg, "Unrecognized language: %s.", section_name); - break; // skip section name check - } - section_name = dot + 1; - } else { - language = 0; + config_err(cfg, "'%s' is not a valid color. Colors should look like #rgb, #rgba, #rrggbb, or #rrggbbaa.", value); + } + } else { + config_err(cfg, "No such color option: %s", key); + } + } break; + case SECTION_KEYBOARD: { + // lines like Ctrl+Down = 10 :down + u32 key_combo = config_parse_key_combo(cfg, key); + KeyAction *action = &settings->key_actions[key_combo]; + llong argument = 1; + if (isdigit(*value)) { + // read the argument + char *endp; + argument = strtoll(value, &endp, 10); + value = endp; + } else if (*value == '"') { + // string argument + int backslashes = 0; + char const *p; + for (p = value + 1; *p; ++p) { + bool done = false; + switch (*p) { + case '\\': + ++backslashes; + break; + case '"': + if (backslashes % 2 == 0) + done = true; + break; } + if (done) break; + } + if (!*p) { + config_err(cfg, "String doesn't end."); + break; + } + if (ted->nstrings < TED_MAX_STRINGS) { + char *str = strn_dup(value + 1, (size_t)(p - (value + 1))); + argument = ted->nstrings | ARG_STRING; + ted->strings[ted->nstrings++] = str; + } + value = p + 1; + } + while (isspace(*value)) ++value; // skip past space following argument + if (*value == ':') { + // read the command + Command command = command_from_str(value + 1); + if (command != CMD_UNKNOWN) { + action->command = command; + action->argument = argument; + action->line_number = cfg->line_number; + } else { + config_err(cfg, "Unrecognized command %s", value); + } + } else { + config_err(cfg, "Expected ':' for key action. This line should look something like: %s = :command.", key); + } + } break; + case SECTION_EXTENSIONS: { + Language lang = language_from_str(key); + if (lang == LANG_NONE) { + config_err(cfg, "Invalid programming language: %s.", key); + } else { + char *new_str = malloc(strlen(value) + 1); + if (!new_str) { + config_err(cfg, "Out of memory."); + } else { + char *dst = new_str; + // get rid of whitespace in extension list + for (char const *src = value; *src; ++src) + if (!isspace(*src)) + *dst++ = *src; + *dst = 0; + if (settings->language_extensions[lang]) + free(settings->language_extensions[lang]); + settings->language_extensions[lang] = new_str; + } + } + } break; + case SECTION_CORE: { + char const *endptr; + long long const integer = strtoll(value, (char **)&endptr, 10); + bool const is_integer = *endptr == '\0'; + double const floating = strtod(value, (char **)&endptr); + bool const is_floating = *endptr == '\0'; + bool is_bool = false; + bool boolean = false; + #define BOOL_HELP "(should be yes/no/on/off)" + if (streq(value, "yes") || streq(value, "on")) { + is_bool = true; + boolean = true; + } else if (streq(value, "no") || streq(value, "off")) { + is_bool = true; + boolean = false; + } + + // go through all options + bool recognized = false; + for (size_t i = 0; i < arr_count(all_options) && !recognized; ++i) { + OptionAny const *any = &all_options[i]; + if (any->type == 0) break; + if (streq(key, any->name)) { + recognized = true; - if (streq(section_name, "keyboard")) { - section = SECTION_KEYBOARD; - } else if (streq(section_name, "colors")) { - section = SECTION_COLORS; - } else if (streq(section_name, "core")) { - section = SECTION_CORE; - } else if (streq(section_name, "extensions")) { - section = SECTION_EXTENSIONS; - } else { - config_err(cfg, "Unrecognized section: [%s].", section_name); + if (part->context.language != 0 && !any->per_language) { + config_err(cfg, "Option %s cannot be controlled for individual languages.", key); break; } - skip_section = false; - if (language) { - switch (section) { - case SECTION_CORE: - case SECTION_COLORS: - case SECTION_KEYBOARD: - break; - default: - config_err(cfg, "%s settings cannot be configured for individual languages.", - section_name); - break; - } - if (pass == 0) { - skip_section = true; - } - } else { - if (pass == 1) { - skip_section = true; + switch (any->type) { + case OPTION_BOOL: { + OptionBool const *option = &any->u._bool; + if (is_bool) + option_bool_set(settings, option, boolean); + else + config_err(cfg, "Invalid %s: %s. This should be yes, no, on, or off.", option->name, value); + } break; + case OPTION_U8: { + OptionU8 const *option = &any->u._u8; + if (is_integer && integer >= option->min && integer <= option->max) + option_u8_set(settings, option, (u8)integer); + else + config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); + } break; + case OPTION_U16: { + OptionU16 const *option = &any->u._u16; + if (is_integer && integer >= option->min && integer <= option->max) + option_u16_set(settings, option, (u16)integer); + else + config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); + } break; + case OPTION_FLOAT: { + OptionFloat const *option = &any->u._float; + if (is_floating && floating >= option->min && floating <= option->max) + option_float_set(settings, option, (float)floating); + else + config_err(cfg, "Invalid %s: %s. This should be a number from %g to %g.", option->name, value, option->min, option->max); + } break; + case OPTION_STRING: { + OptionString const *option = &any->u._string; + if (strlen(value) >= option->buf_size) { + config_err(cfg, "%s is too long (length: %zu, maximum length: %zu).", key, strlen(value), option->buf_size - 1); + } else { + option_string_set(settings, option, value); } - } - if (pass == 1) { - settings = &ted->settings_by_language[language]; + } break; } } - } break; - default: { - if (skip_section) break; + } + + // this is probably a bad idea: + //if (!recognized) + // config_err(cfg, "Unrecognized option: %s", key); + // because if we ever remove an option in the future + // everyone will get errors + } break; + } +} + +void config_parse(Ted *ted, ConfigPart **pparts) { + config_init_options(); + + ConfigReader cfg_reader = { + .ted = ted, + .filename = NULL, + .line_number = 1, + .error = false + }; + ConfigReader *cfg = &cfg_reader; + + + ConfigPart *const parts = *pparts; + qsort(parts, arr_len(parts), sizeof *parts, config_part_qsort_cmp); + + Settings *settings = NULL; + + arr_foreach_ptr(parts, ConfigPart, part) { + cfg->filename = part->file; + cfg->line_number = part->line; + + if (part == parts || config_part_qsort_cmp(part, part - 1) != 0) { + // new settings + settings = arr_addp(ted->all_settings); + context_copy(&settings->context, &part->context); - char *equals = strchr(line, '='); - if (equals) { - char const *key = line; - *equals = '\0'; - char const *value = equals + 1; - while (isspace(*key)) ++key; - while (isspace(*value)) ++value; - if (equals != line) { - for (char *p = equals - 1; p > line; --p) { - // remove trailing spaces after key - if (isspace(*p)) *p = '\0'; - else break; - } + // go backwards to find most specific parent + ConfigPart *parent = part; + while (1) { + if (parent <= parts) { + parent = NULL; + break; } - if (key[0] == '\0') { - config_err(cfg, "Empty property name. This line should look like: key = value"); - } else { - switch (section) { - case SECTION_NONE: - config_err(cfg, "Line outside of any section." - "Try putting a section header, e.g. [keyboard] before this line?"); - break; - case SECTION_COLORS: { - ColorSetting setting = color_setting_from_str(key); - if (setting != COLOR_UNKNOWN) { - u32 color = 0; - if (color_from_str(value, &color)) { - settings->colors[setting] = color; - } else { - config_err(cfg, "'%s' is not a valid color. Colors should look like #rgb, #rgba, #rrggbb, or #rrggbbaa.", value); - } - } else { - config_err(cfg, "No such color option: %s", key); - } - } break; - case SECTION_KEYBOARD: { - // lines like Ctrl+Down = 10 :down - u32 key_combo = config_parse_key_combo(cfg, key); - KeyAction *action = &settings->key_actions[key_combo]; - llong argument = 1; - if (isdigit(*value)) { - // read the argument - char *endp; - argument = strtoll(value, &endp, 10); - value = endp; - } else if (*value == '"') { - // string argument - int backslashes = 0; - char const *p; - for (p = value + 1; *p; ++p) { - bool done = false; - switch (*p) { - case '\\': - ++backslashes; - break; - case '"': - if (backslashes % 2 == 0) - done = true; - break; - } - if (done) break; - } - if (!*p) { - config_err(cfg, "String doesn't end."); - break; - } - if (ted->nstrings < TED_MAX_STRINGS) { - char *str = strn_dup(value + 1, (size_t)(p - (value + 1))); - argument = ted->nstrings | ARG_STRING; - ted->strings[ted->nstrings++] = str; - } - value = p + 1; - } - while (isspace(*value)) ++value; // skip past space following argument - if (*value == ':') { - // read the command - Command command = command_from_str(value + 1); - if (command != CMD_UNKNOWN) { - action->command = command; - action->argument = argument; - action->line_number = cfg->line_number; - } else { - config_err(cfg, "Unrecognized command %s", value); - } - } else { - config_err(cfg, "Expected ':' for key action. This line should look something like: %s = :command.", key); - } - } break; - case SECTION_EXTENSIONS: { - Language lang = language_from_str(key); - if (lang == LANG_NONE) { - config_err(cfg, "Invalid programming language: %s.", key); - } else { - char *new_str = malloc(strlen(value) + 1); - if (!new_str) { - config_err(cfg, "Out of memory."); - } else { - char *dst = new_str; - // get rid of whitespace in extension list - for (char const *src = value; *src; ++src) - if (!isspace(*src)) - *dst++ = *src; - *dst = 0; - if (settings->language_extensions[lang]) - free(settings->language_extensions[lang]); - settings->language_extensions[lang] = new_str; - } - } - } break; - case SECTION_CORE: { - char const *endptr; - long long const integer = strtoll(value, (char **)&endptr, 10); - bool const is_integer = *endptr == '\0'; - double const floating = strtod(value, (char **)&endptr); - bool const is_floating = *endptr == '\0'; - bool is_bool = false; - bool boolean = false; - #define BOOL_HELP "(should be yes/no/on/off)" - if (streq(value, "yes") || streq(value, "on")) { - is_bool = true; - boolean = true; - } else if (streq(value, "no") || streq(value, "off")) { - is_bool = true; - boolean = false; - } - - // go through all options - bool recognized = false; - for (size_t i = 0; i < arr_count(all_options) && !recognized; ++i) { - OptionAny const *any = &all_options[i]; - if (any->type == 0) break; - if (streq(key, any->name)) { - recognized = true; - - if (language != 0 && !any->per_language) { - config_err(cfg, "Option %s cannot be controlled for individual languages.", key); - break; - } - - switch (any->type) { - case OPTION_BOOL: { - OptionBool const *option = &any->u._bool; - if (is_bool) - option_bool_set(settings, option, boolean); - else - config_err(cfg, "Invalid %s: %s. This should be yes, no, on, or off.", option->name, value); - } break; - case OPTION_U8: { - OptionU8 const *option = &any->u._u8; - if (is_integer && integer >= option->min && integer <= option->max) - option_u8_set(settings, option, (u8)integer); - else - config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); - } break; - case OPTION_U16: { - OptionU16 const *option = &any->u._u16; - if (is_integer && integer >= option->min && integer <= option->max) - option_u16_set(settings, option, (u16)integer); - else - config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); - } break; - case OPTION_FLOAT: { - OptionFloat const *option = &any->u._float; - if (is_floating && floating >= option->min && floating <= option->max) - option_float_set(settings, option, (float)floating); - else - config_err(cfg, "Invalid %s: %s. This should be a number from %g to %g.", option->name, value, option->min, option->max); - } break; - case OPTION_STRING: { - OptionString const *option = &any->u._string; - if (strlen(value) >= option->buf_size) { - config_err(cfg, "%s is too long (length: %zu, maximum length: %zu).", key, strlen(value), option->buf_size - 1); - } else { - option_string_set(settings, option, value); - } - } break; - } - } - } - - // this is probably a bad idea: - //if (!recognized) - // config_err(cfg, "Unrecognized option: %s", key); - // because if we ever remove an option in the future - // everyone will get errors - } break; - } + --parent; + if (context_is_parent(&parent->context, &part->context)) { + // copy parent's settings + settings_copy(settings, parent->settings); + break; } - } else { - config_err(cfg, "Invalid line syntax. " - "Lines should either look like [section-name] or key = value"); } - } break; } - if (cfg->error) break; - - ++cfg->line_number; + part->settings = settings; + + arr_add(part->text, '\0'); // null termination + char *line = part->text; + while (*line) { + char *newline = strchr(line, '\n'); + if (!newline) { + config_err(cfg, "No newline at end of file?"); + break; + } + + if (newline) *newline = '\0'; + char *carriage_return = strchr(line, '\r'); + if (carriage_return) *carriage_return = '\0'; + + config_parse_line(cfg, settings, part, line); + + if (cfg->error) break; + + ++cfg->line_number; + line = newline + 1; + } } + arr_foreach_ptr(ted->all_settings, Settings, s) { + SettingsContext *ctx = &s->context; + if (ctx->language == 0 && (!ctx->path || !*ctx->path)) { + ted->settings = s; + break; + } + } - if (ferror(fp)) - ted_seterr(ted, "Error reading %s.", filename); - fclose(fp); + arr_foreach_ptr(parts, ConfigPart, part) { + config_part_free(part); + } + + arr_clear(*pparts); } void config_free(Ted *ted) { - for (u16 i = 0; i < LANG_COUNT; ++i) { - free(ted->settings_by_language[0].language_extensions[i]); - for (u16 l = 0; l < LANG_COUNT; ++l) { - // these are just aliases to settings_by_language[0].language_extensions[i] - // (you cant change language extensions on a per language basis. that would be weird.) - ted->settings_by_language[l].language_extensions[i] = NULL; - } + arr_foreach_ptr(ted->all_settings, Settings, s) { + settings_free(s); } for (u32 i = 0; i < ted->nstrings; ++i) { free(ted->strings[i]); } + arr_clear(ted->all_settings); + ted->settings = NULL; } diff --git a/main.c b/main.c index 66f3d93..78e9e72 100644 --- a/main.c +++ b/main.c @@ -1,3 +1,15 @@ +/* +@TODO: +- make sure [/path//extensions] works + +FUTURE FEATURES: +- path-based settings +- custom shaders + - texture, time, time since last save +- config variables +- config multi-line strings +*/ + #include "base.h" no_warn_start #if _WIN32 @@ -95,7 +107,7 @@ bool tag_goto(Ted *ted, char const *tag); static Rect error_box_rect(Ted *ted) { Font *font = ted->font; - Settings const *settings = ted->settings; + Settings const *settings = ted_active_settings(ted); float padding = settings->padding; float window_width = ted->window_width, window_height = ted->window_height; float char_height = text_font_char_height(font); @@ -318,8 +330,6 @@ int main(int argc, char **argv) { die("Not enough memory available to run ted."); } - ted->settings = &ted->settings_by_language[0]; - // make sure signal handler has access to ted. error_signal_handler_ted = ted; @@ -510,7 +520,6 @@ int main(int argc, char **argv) { } - u32 *colors = ted->settings->colors; (void)colors; ted->cursor_ibeam = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_IBEAM); ted->cursor_arrow = SDL_CreateSystemCursor(SDL_SYSTEM_CURSOR_ARROW); @@ -765,7 +774,7 @@ int main(int argc, char **argv) { glViewport(0, 0, (GLsizei)window_width, (GLsizei)window_height); { // clear (background) float bg_color[4]; - rgba_u32_to_floats(colors[COLOR_BG], bg_color); + rgba_u32_to_floats(ted_color(ted, COLOR_BG), bg_color); glClearColor(bg_color[0], bg_color[1], bg_color[2], bg_color[3]); } glClear(GL_COLOR_BUFFER_BIT); @@ -777,7 +786,7 @@ int main(int argc, char **argv) { { - float const padding = ted->settings->padding; + float const padding = ted_active_settings(ted)->padding; float x1 = padding, y = window_height-padding, x2 = window_width-padding; Node *node = &ted->nodes[0]; if (ted->find) { @@ -827,7 +836,7 @@ int main(int argc, char **argv) { } else { ted->autocomplete = false; text_utf8_anchored(font, "Press Ctrl+O to open a file or Ctrl+N to create a new one.", - window_width * 0.5f, window_height * 0.5f, colors[COLOR_TEXT_SECONDARY], ANCHOR_MIDDLE); + window_width * 0.5f, window_height * 0.5f, ted_color(ted, COLOR_TEXT_SECONDARY), ANCHOR_MIDDLE); text_render(font); } } @@ -873,15 +882,16 @@ int main(int argc, char **argv) { if (*ted->error_shown) { double t = time_get_seconds(); double time_passed = t - ted->error_time; - if (time_passed > ted->settings->error_display_time) { + Settings *settings = ted_active_settings(ted); + if (time_passed > settings->error_display_time) { // stop showing error *ted->error_shown = '\0'; } else { Rect r = error_box_rect(ted); - float padding = ted->settings->padding; + float padding = settings->padding; - gl_geometry_rect(r, colors[COLOR_ERROR_BG]); - gl_geometry_rect_border(r, ted->settings->border_thickness, colors[COLOR_ERROR_BORDER]); + gl_geometry_rect(r, ted_color(ted, COLOR_ERROR_BG)); + gl_geometry_rect_border(r, settings->border_thickness, ted_color(ted, COLOR_ERROR_BORDER)); float text_x1 = rect_x1(r) + padding, text_x2 = rect_x2(r) - padding; float text_y1 = rect_y1(r) + padding; @@ -893,7 +903,7 @@ int main(int argc, char **argv) { text_state.x = text_x1; text_state.y = text_y1; text_state.wrap = true; - rgba_u32_to_floats(colors[COLOR_ERROR_TEXT], text_state.color); + rgba_u32_to_floats(ted_color(ted, COLOR_ERROR_TEXT), text_state.color); text_utf8_with_state(font, &text_state, ted->error_shown); gl_geometry_draw(); text_render(font); diff --git a/ted.c b/ted.c index eba5aad..a3acfdd 100644 --- a/ted.c +++ b/ted.c @@ -52,6 +52,10 @@ Settings *ted_active_settings(Ted *ted) { return ted->active_buffer ? buffer_settings(ted->active_buffer) : ted->settings; } +u32 ted_color(Ted *ted, ColorSetting color) { + return ted_active_settings(ted)->colors[color]; +} + static void ted_path_full(Ted *ted, char const *relpath, char *abspath, size_t abspath_size) { path_full(ted->cwd, relpath, abspath, abspath_size); } @@ -372,20 +376,15 @@ void ted_load_configs(Ted *ted, bool reloading) { } } - // read global settings - config_read(ted, global_config_filename, 0); - config_read(ted, local_config_filename, 0); + + ConfigPart *parts = NULL; + config_read(ted, &parts, global_config_filename); + config_read(ted, &parts, local_config_filename); if (ted->search_cwd) { // read config in cwd - config_read(ted, TED_CFG, 0); + config_read(ted, &parts, TED_CFG); } - // copy global settings to language-specific settings - for (int l = 1; l < LANG_COUNT; ++l) - ted->settings_by_language[l] = ted->settings_by_language[0]; - // read language-specific settings - config_read(ted, global_config_filename, 1); - config_read(ted, local_config_filename, 1); - if (ted->search_cwd) config_read(ted, TED_CFG, 1); + config_parse(ted, &parts); if (reloading) { // reset text size diff --git a/ted.cfg b/ted.cfg index 1a006c5..ecafe7c 100644 --- a/ted.cfg +++ b/ted.cfg @@ -233,7 +233,7 @@ Rust = .rs Python = .py Tex = .tex Markdown = .md -HTML = .html, .php, .xml, .xhtml +HTML = .html, .php, .xml, .xhtml, .iml Config = .cfg Javascript = .js Java = .java diff --git a/ted.h b/ted.h index 0eb0786..61e19c7 100644 --- a/ted.h +++ b/ted.h @@ -133,6 +133,12 @@ typedef struct KeyAction { } KeyAction; typedef struct { + Language language; // these settings apply to this language. + char *path; // these settings apply to all paths which start with this string, or all paths if path=NULL +} SettingsContext; + +typedef struct { + SettingsContext context; float cursor_blink_time_on, cursor_blink_time_off; u32 colors[COLOR_COUNT]; u16 text_size; @@ -172,6 +178,26 @@ typedef struct { char32_t *str; } Line; + +typedef enum { + SECTION_NONE, + SECTION_CORE, + SECTION_KEYBOARD, + SECTION_COLORS, + SECTION_EXTENSIONS +} ConfigSection; + +// this structure is used temporarily when loading settings +// it's needed because we want more specific contexts to be dealt with last. +typedef struct { + SettingsContext context; + ConfigSection section; + char *file; + u32 line; + char *text; + Settings *settings; // only used in config_parse +} ConfigPart; + // this refers to replacing prev_len characters (found in prev_text) at pos with new_len characters typedef struct { bool chain; // should this + the next edit be treated as one? @@ -302,8 +328,8 @@ typedef struct Ted { // the old active buffer needs to be restored. that's what this stores. TextBuffer *prev_active_buffer; Node *active_node; - Settings settings_by_language[LANG_COUNT]; - Settings *settings; // "default" settings (equal to &settings_per_language[0]) + Settings *all_settings; // dynamic array of Settings. use Settings.context to figure out which one to use. + Settings *settings; // "default" settings float window_width, window_height; u32 key_modifier; // which of shift, alt, ctrl are down right now. v2 mouse_pos; @@ -408,5 +434,6 @@ void ted_switch_to_buffer(Ted *ted, TextBuffer *buffer); Settings *ted_active_settings(Ted *ted); void ted_load_configs(Ted *ted, bool reloading); static TextBuffer *find_search_buffer(Ted *ted); -void config_read(Ted *ted, const char *filename, int pass); +void config_read(Ted *ted, ConfigPart **parts, const char *filename); +void config_parse(Ted *ted, ConfigPart **parts); void config_free(Ted *ted); diff --git a/util.c b/util.c index 71fce88..b36bcd4 100644 --- a/util.c +++ b/util.c @@ -237,7 +237,6 @@ static int qsort_with_context_cmp(const void *a, const void *b) { } static void qsort_with_context(void *base, size_t nmemb, size_t size, int (*compar)(void *, const void *, const void *), void *arg) { - // @TODO(eventually): write this yourself // just use global variables. hopefully we don't try to run this in something multithreaded! qsort_ctx_arg = arg; qsort_ctx_cmp = compar; @@ -299,7 +298,7 @@ static void path_full(char const *dir, char const *relpath, char *abspath, size_ else lastsep[0] = '\0'; } else { - if (abspath[len - 1] != PATH_SEPARATOR) + if (len == 0 || abspath[len - 1] != PATH_SEPARATOR) str_cat(abspath, abspath_size, PATH_SEPARATOR_STR); strn_cat(abspath, abspath_size, relpath, component_len); } -- cgit v1.2.3