// Read a ted configuration file. // Config files are formatted as several sections, each containing key = value pairs. // e.g.: // [section1] // thing1 = 33 // thing2 = 454 // [section2] // asdf = 123 #include "ted-internal.h" #include "pcre-inc.h" /// Sections of `ted.cfg` typedef enum { SECTION_NONE, SECTION_CORE, SECTION_KEYBOARD, SECTION_COLORS, SECTION_EXTENSIONS } ConfigSection; // all the "control" pointers here are relative to `settings_zero`. typedef struct { const char *name; const bool *control; bool per_language; // allow per-language control } SettingBool; typedef struct { const char *name; const u8 *control; u8 min, max; bool per_language; } SettingU8; typedef struct { const char *name; const float *control; float min, max; bool per_language; } SettingFloat; typedef struct { const char *name; const u16 *control; u16 min, max; bool per_language; } SettingU16; typedef struct { const char *name; const u32 *control; u32 min, max; bool per_language; } SettingU32; typedef struct { const char *name; RcStr *const *control; bool per_language; } SettingString; typedef struct { const char *name; const KeyCombo *control; bool per_language; } SettingKeyCombo; typedef enum { SETTING_BOOL = 1, SETTING_U8, SETTING_U16, SETTING_U32, SETTING_FLOAT, SETTING_STRING, SETTING_KEY_COMBO } SettingType; typedef struct { SettingType type; const char *name; bool per_language; union { SettingU8 _u8; SettingBool _bool; SettingU16 _u16; SettingU32 _u32; SettingFloat _float; SettingString _string; SettingKeyCombo _key; } u; } SettingAny; // core settings static const Settings settings_zero = {0}; static const SettingBool settings_bool[] = { {"auto-indent", &settings_zero.auto_indent, true}, #define SETTING_AUTO_ADD_NEWLINE {"auto-add-newline", &settings_zero.auto_add_newline, true} SETTING_AUTO_ADD_NEWLINE, #define SETTING_REMOVE_TRAILING_WHITESPACE {"remove-trailing-whitespace", &settings_zero.remove_trailing_whitespace, true} SETTING_REMOVE_TRAILING_WHITESPACE, {"auto-reload", &settings_zero.auto_reload, true}, {"auto-reload-config", &settings_zero.auto_reload_config, false}, {"syntax-highlighting", &settings_zero.syntax_highlighting, true}, {"autodetect-indentation", &settings_zero.autodetect_indentation, true}, {"line-numbers", &settings_zero.line_numbers, true}, {"restore-session", &settings_zero.restore_session, false}, {"regenerate-tags-if-not-found", &settings_zero.regenerate_tags_if_not_found, true}, #define SETTING_INDENT_WITH_SPACES {"indent-with-spaces", &settings_zero.indent_with_spaces, true} SETTING_INDENT_WITH_SPACES, {"trigger-characters", &settings_zero.trigger_characters, true}, {"identifier-trigger-characters", &settings_zero.identifier_trigger_characters, true}, {"phantom-completions", &settings_zero.phantom_completions, true}, {"signature-help-enabled", &settings_zero.signature_help_enabled, true}, {"document-links", &settings_zero.document_links, true}, {"lsp-enabled", &settings_zero.lsp_enabled, true}, {"lsp-log", &settings_zero.lsp_log, true}, {"hover-enabled", &settings_zero.hover_enabled, true}, {"vsync", &settings_zero.vsync, false}, {"highlight-enabled", &settings_zero.highlight_enabled, true}, {"highlight-auto", &settings_zero.highlight_auto, true}, {"save-backup", &settings_zero.save_backup, true}, #define SETTING_CRLF {"crlf", &settings_zero.crlf, true} SETTING_CRLF, #define SETTING_CRLF_WINDOWS {"crlf-windows", &settings_zero.crlf_windows, true} SETTING_CRLF_WINDOWS, {"jump-to-build-error", &settings_zero.jump_to_build_error, true}, {"force-monospace", &settings_zero.force_monospace, true}, {"show-diagnostics", &settings_zero.show_diagnostics, true}, }; static const SettingBool setting_auto_add_newline = SETTING_AUTO_ADD_NEWLINE; static const SettingBool setting_indent_with_spaces = SETTING_INDENT_WITH_SPACES; static const SettingBool setting_remove_trailing_whitespace = SETTING_REMOVE_TRAILING_WHITESPACE; static const SettingBool setting_crlf = SETTING_CRLF; static const SettingBool setting_crlf_windows = SETTING_CRLF_WINDOWS; static const SettingU8 settings_u8[] = { #define SETTING_TAB_WIDTH {"tab-width", &settings_zero.tab_width, 1, 100, true} SETTING_TAB_WIDTH, {"cursor-width", &settings_zero.cursor_width, 1, 100, true}, {"undo-save-time", &settings_zero.undo_save_time, 1, 200, true}, {"border-thickness", &settings_zero.border_thickness, 1, 30, false}, {"padding", &settings_zero.padding, 0, 100, false}, {"scrolloff", &settings_zero.scrolloff, 1, 100, true}, {"tags-max-depth", &settings_zero.tags_max_depth, 1, 100, false}, }; static const SettingU8 setting_tab_width = SETTING_TAB_WIDTH; static const SettingU16 settings_u16[] = { {"text-size", &settings_zero.text_size_no_dpi, TEXT_SIZE_MIN, TEXT_SIZE_MAX, false}, {"max-menu-width", &settings_zero.max_menu_width, 10, U16_MAX, false}, {"error-display-time", &settings_zero.error_display_time, 0, U16_MAX, false}, {"framerate-cap", &settings_zero.framerate_cap, 3, 1000, false}, {"lsp-port", &settings_zero.lsp_port, 0, 65535, true}, }; const SettingU16 setting_text_size_dpi_aware = {NULL, &settings_zero.text_size, 0, U16_MAX, false}; static const SettingU32 settings_u32[] = { {"max-file-size", &settings_zero.max_file_size, 100, 2000000000, false}, {"max-file-size-view-only", &settings_zero.max_file_size_view_only, 100, 2000000000, false}, }; static const SettingFloat settings_float[] = { {"cursor-blink-time-on", &settings_zero.cursor_blink_time_on, 0, 1000, true}, {"cursor-blink-time-off", &settings_zero.cursor_blink_time_off, 0, 1000, true}, {"hover-time", &settings_zero.hover_time, 0, INFINITY, true}, {"ctrl-scroll-adjust-text-size", &settings_zero.ctrl_scroll_adjust_text_size, -10, 10, true}, {"lsp-delay", &settings_zero.lsp_delay, 0, 100, true}, }; static const SettingString settings_string[] = { {"build-default-command", &settings_zero.build_default_command, true}, {"build-command", &settings_zero.build_command, true}, {"root-identifiers", &settings_zero.root_identifiers, true}, {"lsp", &settings_zero.lsp, true}, {"lsp-configuration", &settings_zero.lsp_configuration, true}, {"comment-start", &settings_zero.comment_start, true}, {"comment-end", &settings_zero.comment_end, true}, {"font", &settings_zero.font, false}, {"font-bold", &settings_zero.font_bold, false}, {"sync", &settings_zero.sync, false}, }; static const SettingKeyCombo settings_key_combo[] = { {"hover-key", &settings_zero.hover_key, true}, {"highlight-key", &settings_zero.highlight_key, true}, }; bool config_applies_to(Config *cfg, const char *path, Language language) { if (cfg->language && language != cfg->language) return false; if (cfg->path) { if (!path) return false; pcre2_match_data_8 *match_data = pcre2_match_data_create_from_pattern_8(cfg->path, NULL); bool match = pcre2_match_8(cfg->path, (const u8 *)path, PCRE2_ZERO_TERMINATED, 0, 0, match_data, NULL) > 0; pcre2_match_data_free_8(match_data); if (!match) return false; } return true; } // if this returns true, `a` and `b` have the same context (`a` only applies if `b` does) static bool config_definitely_has_same_context(const Config *a, const Config *b) { if (a->language != b->language) return false; if (a->path_regex && !b->path_regex) return false; if (!a->path_regex && b->path_regex) return false; if (a->path_regex && !streq(a->path_regex, b->path_regex)) return false; return true; } static void config_set_setting(Config *cfg, ptrdiff_t offset, const void *value, size_t size) { memmove((char *)&cfg->settings + offset, value, size); memset(&cfg->settings_set[offset], 1, size); } static void config_set_bool(Config *cfg, const SettingBool *set, bool value) { config_set_setting(cfg, (char*)set->control - (char*)&settings_zero, &value, sizeof value); } static void config_set_u8(Config *cfg, const SettingU8 *set, u8 value) { if (value >= set->min && value <= set->max) config_set_setting(cfg, (char*)set->control - (char*)&settings_zero, &value, sizeof value); } static void config_set_u16(Config *cfg, const SettingU16 *set, u16 value) { if (value >= set->min && value <= set->max) config_set_setting(cfg, (char*)set->control - (char*)&settings_zero, &value, sizeof value); } static void config_set_u32(Config *cfg, const SettingU32 *set, u32 value) { if (value >= set->min && value <= set->max) config_set_setting(cfg, (char*)set->control - (char*)&settings_zero, &value, sizeof value); } static void config_set_float(Config *cfg, const SettingFloat *set, float value) { if (value >= set->min && value <= set->max) config_set_setting(cfg, (char*)set->control - (char*)&settings_zero, &value, sizeof value); } static void config_set_key_combo(Config *cfg, const SettingKeyCombo *set, KeyCombo value) { config_set_setting(cfg, (char *)set->control - (char *)&settings_zero, &value, sizeof value); } static void config_set_string(Config *cfg, const SettingString *set, const char *value) { assert(value); Settings *settings = &cfg->settings; const ptrdiff_t offset = ((char *)set->control - (char*)&settings_zero); RcStr **control = (RcStr **)((char *)settings + offset); if (*control) rc_str_decref(control); RcStr *rc = rc_str_new(value, -1); config_set_setting(cfg, offset, &rc, sizeof (RcStr *)); } static void config_set_color(Config *cfg, ColorSetting setting, u32 color) { config_set_setting(cfg, (char *)&settings_zero.colors[setting] - (char *)&settings_zero, &color, sizeof color); } typedef struct { Ted *ted; const char *filename; FILE *fp; ConfigSection section; u32 line_number; // currently processing this line number bool error; } ConfigReader; static void config_verr(ConfigReader *cfg, const char *fmt, va_list args) { if (cfg->error) return; cfg->error = true; char error[1024] = {0}; strbuf_printf(error, "%s:%u: ", cfg->filename, cfg->line_number); vsnprintf(error + strlen(error), sizeof error - strlen(error) - 1, fmt, args); ted_error(cfg->ted, "%s", error); } static void config_err(ConfigReader *cfg, PRINTF_FORMAT_STRING const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3); static void config_err(ConfigReader *cfg, const char *fmt, ...) { va_list args; va_start(args, fmt); config_verr(cfg, fmt, args); va_end(args); } static void config_debug_err(ConfigReader *cfg, PRINTF_FORMAT_STRING const char *fmt, ...) ATTRIBUTE_PRINTF(2, 3); static void config_debug_err(ConfigReader *cfg, const char *fmt, ...) { va_list args; va_start(args, fmt); #if DEBUG config_verr(cfg, fmt, args); #else ted_vlog(cfg->ted, fmt, args); #endif va_end(args); } static void settings_free_set(Settings *settings, const bool *set) { if (set[offsetof(Settings, language_extensions)]) arr_free(settings->language_extensions); if (set[offsetof(Settings, key_actions)]) { arr_foreach_ptr(settings->key_actions, KeyAction, act) { free((void *)act->argument.string); } arr_free(settings->key_actions); } if (set[offsetof(Settings, bg_shader)]) gl_rc_sab_decref(&settings->bg_shader); if (set[offsetof(Settings, bg_texture)]) gl_rc_texture_decref(&settings->bg_texture); for (size_t i = 0; i < arr_count(settings_string); i++) { const SettingString *s = &settings_string[i]; const ptrdiff_t offset = (char *)s->control - (char *)&settings_zero; if (set[offset]) { RcStr **rc = (RcStr **)((char *)settings + offset); rc_str_decref(rc); } } } void settings_free(Settings *settings) { static bool all_set[sizeof(Settings)]; memset(all_set, 1, sizeof all_set); settings_free_set(settings, all_set); memset(settings, 0, sizeof *settings); } 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); } i32 config_priority(const Config *cfg) { size_t path_len = cfg->path ? strlen(cfg->path_regex) : 0; return (i32)path_len * 2 + (cfg->language != 0); } static KeyAction key_action_copy(const KeyAction *src) { KeyAction cpy = *src; if (cpy.argument.string) cpy.argument.string = str_dup(cpy.argument.string); return cpy; } void config_merge_into(Settings *dest, const Config *src_cfg) { const Settings *src = &src_cfg->settings; char *destc = (char *)dest; const char *srcc = (const char *)src; const bool *set = src_cfg->settings_set; settings_free_set(dest, set); for (size_t i = 0; i < sizeof(Settings); i++) { if (set[i]) destc[i] = srcc[i]; } // increment reference counts for things we've borrowed from src if (set[offsetof(Settings, bg_shader)]) gl_rc_sab_incref(dest->bg_shader); if (set[offsetof(Settings, bg_texture)]) gl_rc_texture_incref(dest->bg_texture); for (size_t i = 0; i < arr_count(settings_string); i++) { const SettingString *s = &settings_string[i]; ptrdiff_t offset = (char *)s->control - (char *)&settings_zero; if (!set[offset]) continue; RcStr *rc = *(RcStr **)((char *)dest + offset); rc_str_incref(rc); } // we should never copy these from src assert(!set[offsetof(Settings, language_extensions)]); assert(!set[offsetof(Settings, key_actions)]); // merge language_extensions and key_actions arr_foreach_ptr(src->language_extensions, LanguageExtension, ext) arr_add(dest->language_extensions, *ext); arr_foreach_ptr(src->key_actions, KeyAction, act) arr_add(dest->key_actions, key_action_copy(act)); } static void config_err_unexpected_eof(ConfigReader *reader) { config_err(reader, "Unexpected EOF (no newline at end of file?)"); } 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) { c = 0; } if (c == '\n') { reader->line_number += 1; } 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; if (c) ungetc(c, reader->fp); } static void config_skip_space(ConfigReader *reader) { char c; while ((c = config_getc(reader))) { if (!isspace(c) || c == '\n') { config_ungetc(reader, c); break; } } } static void config_read_to_eol(ConfigReader *reader, char *buf, size_t bufsz) { assert(bufsz < INT_MAX); if (!fgets(buf, (int)bufsz, reader->fp)) { config_err_unexpected_eof(reader); *buf = '\0'; } if (strchr(buf, '\n')) reader->line_number += 1; buf[strcspn(buf, "\r\n")] = '\0'; } static SDL_Keycode config_parse_key(ConfigReader *cfg, const char *str) { SDL_Keycode keycode = SDL_GetKeyFromName(str); if (keycode != SDLK_UNKNOWN) return keycode; typedef struct { const char *keyname1; const char *keyname2; // alternate key name SDL_Keycode keycode; } KeyName; static KeyName const key_names[] = { {"X1", NULL, KEYCODE_X1}, {"X2", NULL, KEYCODE_X2}, {"Enter", NULL, SDLK_RETURN}, {"Equals", "Equal", SDLK_EQUALS}, }; for (size_t i = 0; i < arr_count(key_names); ++i) { KeyName const *k = &key_names[i]; if (streq_case_insensitive(str, k->keyname1) || (k->keyname2 && streq_case_insensitive(str, k->keyname2))) { keycode = k->keycode; break; } } if (keycode != SDLK_UNKNOWN) return keycode; if (isdigit(str[0])) { // direct keycode numbers, e.g. Ctrl+24 or Ctrl+08 char *endp; long n = strtol(str, &endp, 10); if (*endp == '\0' && n > 0) { return (SDL_Keycode)n; } else { config_err(cfg, "Invalid keycode number: %s", str); return 0; } } else { config_err(cfg, "Unrecognized key name: %s.", str); return 0; } } // Returns the key combination described by str. static KeyCombo config_parse_key_combo(ConfigReader *cfg, const char *str) { u32 modifier = 0; // read modifier while (true) { if (str_has_prefix(str, "Ctrl+")) { if (modifier & KEY_MODIFIER_CTRL) { config_err(cfg, "Ctrl+ written twice"); return (KeyCombo){0}; } modifier |= KEY_MODIFIER_CTRL; str += strlen("Ctrl+"); } else if (str_has_prefix(str, "Shift+")) { if (modifier & KEY_MODIFIER_SHIFT) { config_err(cfg, "Shift+ written twice"); return (KeyCombo){0}; } modifier |= KEY_MODIFIER_SHIFT; str += strlen("Shift+"); } else if (str_has_prefix(str, "Alt+")) { if (modifier & KEY_MODIFIER_ALT) { config_err(cfg, "Alt+ written twice"); return (KeyCombo){0}; } modifier |= KEY_MODIFIER_ALT; str += strlen("Alt+"); } else break; } // read key SDL_Keycode keycode = config_parse_key(cfg, str); if (keycode == SDLK_UNKNOWN) return (KeyCombo){0}; return KEY_COMBO(modifier, keycode); } static bool settings_initialized = false; static SettingAny settings_all[1000] = {0}; static void config_init_settings(void) { if (settings_initialized) return; SettingAny *opt = settings_all; for (size_t i = 0; i < arr_count(settings_bool); ++i) { opt->type = SETTING_BOOL; opt->name = settings_bool[i].name; opt->per_language = settings_bool[i].per_language; opt->u._bool = settings_bool[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_u8); ++i) { opt->type = SETTING_U8; opt->name = settings_u8[i].name; opt->per_language = settings_u8[i].per_language; opt->u._u8 = settings_u8[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_u16); ++i) { opt->type = SETTING_U16; opt->name = settings_u16[i].name; opt->per_language = settings_u16[i].per_language; opt->u._u16 = settings_u16[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_u32); ++i) { opt->type = SETTING_U32; opt->name = settings_u32[i].name; opt->per_language = settings_u32[i].per_language; opt->u._u32 = settings_u32[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_float); ++i) { opt->type = SETTING_FLOAT; opt->name = settings_float[i].name; opt->per_language = settings_float[i].per_language; opt->u._float = settings_float[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_string); ++i) { opt->type = SETTING_STRING; opt->name = settings_string[i].name; opt->per_language = settings_string[i].per_language; opt->u._string = settings_string[i]; ++opt; } for (size_t i = 0; i < arr_count(settings_key_combo); ++i) { opt->type = SETTING_KEY_COMBO; opt->name = settings_key_combo[i].name; opt->per_language = settings_key_combo[i].per_language; opt->u._key = settings_key_combo[i]; ++opt; } settings_initialized = true; } static void get_config_path(Ted *ted, char *expanded, size_t expanded_sz, const char *path) { assert(path != expanded); expanded[0] = '\0'; if (path[0] == '~' && is_path_separator(path[1])) { str_printf(expanded, expanded_sz, "%s%c%s", ted->home, PATH_SEPARATOR, path + 1); } else if (!path_is_absolute(path)) { if (!ted_get_file(ted, path, expanded, expanded_sz)) { str_cpy(expanded, expanded_sz, path); } } else { str_cpy(expanded, expanded_sz, path); } } // only reads fp for multi-line strings // return value should be freed. static char *config_read_string(ConfigReader *reader, char delimiter) { char *str = NULL; while (true) { char c = config_getc(reader); if (c == delimiter) break; switch (c) { case '\\': c = config_getc(reader); switch (c) { case '\\': case '"': case '`': break; case 'n': arr_add(str, '\n'); continue; case 'r': arr_add(str, '\r'); continue; case 't': arr_add(str, '\t'); continue; case '[': arr_add(str, '['); continue; default: config_err(reader, "Unrecognized escape sequence: '\\%c'.", c); arr_clear(str); return NULL; } break; default: arr_add(str, c); break; } } char *s = strn_dup(str, arr_len(str)); arr_free(str); return s; } static void settings_load_bg_shader(Ted *ted, Config *cfg, const char *bg_shader_text) { char vshader[8192] ; strbuf_printf(vshader, "attribute vec2 v_pos;\n\ OUT vec2 t_pos;\n\ void main() { \n\ gl_Position = vec4(v_pos * 2.0 - 1.0, 0.0, 1.0);\n\ t_pos = v_pos;\n\ }"); char fshader[8192]; strbuf_printf(fshader, "IN vec2 t_pos;\n\ uniform float t_time;\n\ uniform float t_save_time;\n\ uniform vec2 t_aspect;\n\ uniform sampler2D t_texture;\n\ #line 1\n\ %s", bg_shader_text); char error[512] = {0}; GLuint shader = gl_compile_and_link_shaders(error, vshader, fshader); if (*error) ted_error(ted, "%s", error); if (!shader) return; GLuint buffer = 0, array = 0; glGenBuffers(1, &buffer); if (gl_version_major >= 3) { glGenVertexArrays(1, &array); glBindVertexArray(array); } float buffer_data[][2] = { {0,0}, {1,0}, {1,1}, {0,0}, {1,1}, {0,1} }; GLuint v_pos = (GLuint)glGetAttribLocation(shader, "v_pos"); glBindBuffer(GL_ARRAY_BUFFER, buffer); glBufferData(GL_ARRAY_BUFFER, sizeof buffer_data, buffer_data, GL_STATIC_DRAW); glVertexAttribPointer(v_pos, 2, GL_FLOAT, 0, 2 * sizeof(float), 0); glEnableVertexAttribArray(v_pos); cfg->settings.bg_shader = gl_rc_sab_new(shader, array, buffer); } static void settings_load_bg_texture(Ted *ted, Config *cfg, const char *path) { char expanded[TED_PATH_MAX]; get_config_path(ted, expanded, sizeof expanded, path); GLuint texture = gl_load_texture_from_image(expanded); if (!texture) { ted_error(ted, "Couldn't load image %s", path); return; } cfg->settings.bg_texture = gl_rc_texture_new(texture); } static void config_parse_line(ConfigReader *reader, Config *cfg) { Ted *ted = reader->ted; if (reader->section == 0) { // there was an error reading this section. don't bother with anything else. return; } char key[128] = {0}; char c; for (size_t i = 0; i < sizeof key - 1; ++i) { c = config_getc(reader); if (!c) break; if (c == '=') break; if (c == '\n') break; key[i] = c; } str_trim(key); if (key[0] == 0) { return; } if (c != '=') { config_err(reader, "Unexpected end-of-line (expected key = value)"); return; } config_skip_space(reader); switch (reader->section) { case SECTION_NONE: config_err(reader, "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) { char value[32] = {0}; config_read_to_eol(reader, value, sizeof value); str_trim(value); u32 color = 0; if (color_from_str(value, &color)) { config_set_color(cfg, setting, color); } else { config_err(reader, "'%s' is not a valid color. Colors should look like #rgb, #rgba, #rrggbb, or #rrggbbaa.", value); } } else { // don't actually produce this error. // we have removed colors in the past and might again in the future. #if 0 config_err(cfg, "No such color setting: %s", key); #endif } } break; case SECTION_KEYBOARD: { // lines like Ctrl+Down = 10 :down KeyCombo key_combo = config_parse_key_combo(reader, key); KeyAction action = {0}; action.key_combo = key_combo; CommandArgument argument = { .number = 1, // default argument = 1 .string = NULL }; c = config_getc(reader); if (isdigit(c)) { // read the argument char num[32] = {c}; for (size_t i = 1; i < sizeof num - 1; i++) { c = config_getc(reader); if (!c || c == ' ') break; num[i] = c; } argument.number = atoll(num); config_skip_space(reader); c = config_getc(reader); } else if (c == '"' || c == '`') { // string argument argument.string = config_read_string(reader, (char)c); config_skip_space(reader); c = config_getc(reader); } if (c == ':') { char cmd_str[64]; config_read_to_eol(reader, cmd_str, sizeof cmd_str); // read the command Command command = command_from_str(cmd_str); if (command != CMD_UNKNOWN) { action.command = command; action.argument = argument; } else { config_err(reader, "Unrecognized command %s", cmd_str); } } else { config_err(reader, "Expected ':' for key action. This line should look something like: %s = :command.", key); } Settings *settings = &cfg->settings; bool have = false; // check if we already have an action for this key combo arr_foreach_ptr(settings->key_actions, KeyAction, act) { if (act->key_combo.value == key_combo.value) { *act = action; have = true; break; } } // if this is a new key combo, add an element to the key_actions array if (!have) arr_add(settings->key_actions, action); } break; case SECTION_EXTENSIONS: { Language lang = language_from_str(key); if (lang == LANG_NONE) { config_err(reader, "Invalid programming language: %s.", key); } else { char exts[2048]; config_read_to_eol(reader, exts, sizeof exts); char *dst = exts; // get rid of whitespace in extension list for (const char *src = exts; *src; ++src) if (!isspace(*src)) *dst++ = *src; *dst = 0; Settings *settings = &cfg->settings; // remove old extensions u32 *indices = NULL; arr_foreach_ptr(settings->language_extensions, LanguageExtension, ext) { if (ext->language == lang) { arr_add(indices, (u32)(ext - settings->language_extensions)); } } for (u32 i = 0; i < arr_len(indices); ++i) arr_remove(settings->language_extensions, indices[i] - i); arr_free(indices); char *p = exts; while (*p) { while (*p == ',') ++p; if (*p == '\0') break; size_t len = strcspn(p, ","); LanguageExtension *ext = arr_addp(settings->language_extensions); ext->language = lang; memcpy(ext->extension, p, len); p += len; } } } break; case SECTION_CORE: { char *needs_freeing = NULL; char *value = NULL; char line_buf[2048]; c = config_getc(reader); const char *endptr; i64 integer = 0; double floating = 0; bool is_integer = false; bool is_floating = false; bool is_bool = false; bool boolean = false; if (c == '"' || c == '`') { char *string = config_read_string(reader, c); if (!string) break; needs_freeing = string; value = string; } else { config_ungetc(reader, c); config_read_to_eol(reader, line_buf, sizeof line_buf); value = line_buf; integer = (i64)strtoll(value, (char **)&endptr, 10); is_integer = *endptr == '\0'; floating = strtod(value, (char **)&endptr); is_floating = *endptr == '\0'; if (streq(value, "yes") || streq(value, "on") || streq(value, "true")) { is_bool = true; boolean = true; } else if (streq(value, "no") || streq(value, "off") || streq(value, "false")) { is_bool = true; boolean = false; } } const SettingAny *setting_any = NULL; for (u32 i = 0; i < arr_count(settings_all); ++i) { SettingAny const *s = &settings_all[i]; if (s->type == 0) break; if (streq(key, s->name)) { setting_any = s; break; } } if (!setting_any) { if (streq(key, "bg-shader")) settings_load_bg_shader(ted, cfg, value); else if (streq(key, "bg-texture")) settings_load_bg_texture(ted, cfg, value); // it's probably a bad idea to error on unrecognized settings // because if we ever remove a setting in the future // everyone will get errors break; } if (cfg->language != 0 && !setting_any->per_language) { config_err(reader, "Setting %s cannot be controlled for individual languages.", key); break; } switch (setting_any->type) { case SETTING_BOOL: { const SettingBool *setting = &setting_any->u._bool; if (is_bool) config_set_bool(cfg, setting, boolean); else config_err(reader, "Invalid %s: %s. This should be yes, no, on, or off.", setting->name, value); } break; case SETTING_U8: { const SettingU8 *setting = &setting_any->u._u8; if (is_integer && integer >= setting->min && integer <= setting->max) config_set_u8(cfg, setting, (u8)integer); else config_err(reader, "Invalid %s: %s. This should be an integer from %u to %u.", setting->name, value, setting->min, setting->max); } break; case SETTING_U16: { const SettingU16 *setting = &setting_any->u._u16; if (is_integer && integer >= setting->min && integer <= setting->max) config_set_u16(cfg, setting, (u16)integer); else config_err(reader, "Invalid %s: %s. This should be an integer from %u to %u.", setting->name, value, setting->min, setting->max); } break; case SETTING_U32: { const SettingU32 *setting = &setting_any->u._u32; if (is_integer && integer >= setting->min && integer <= setting->max) config_set_u32(cfg, setting, (u32)integer); else config_err(reader, "Invalid %s: %s. This should be an integer from %" PRIu32 " to %" PRIu32 ".", setting->name, value, setting->min, setting->max); } break; case SETTING_FLOAT: { const SettingFloat *setting = &setting_any->u._float; if (is_floating && floating >= setting->min && floating <= setting->max) config_set_float(cfg, setting, (float)floating); else config_err(reader, "Invalid %s: %s. This should be a number from %g to %g.", setting->name, value, setting->min, setting->max); } break; case SETTING_STRING: { const SettingString *setting = &setting_any->u._string; config_set_string(cfg, setting, value); } break; case SETTING_KEY_COMBO: { const SettingKeyCombo *setting = &setting_any->u._key; KeyCombo combo = config_parse_key_combo(reader, value); if (combo.value) { config_set_key_combo(cfg, setting, combo); } } break; } free(needs_freeing); } break; } } static int key_action_qsort_cmp_combo(const void *av, const void *bv) { const KeyAction *a = av, *b = bv; if (a->key_combo.value < b->key_combo.value) return -1; if (a->key_combo.value > b->key_combo.value) return 1; return 0; } void settings_finalize(Ted *ted, Settings *settings) { arr_qsort(settings->key_actions, key_action_qsort_cmp_combo); settings->text_size = clamp_u16((u16)roundf((float)settings->text_size_no_dpi * ted_get_ui_scaling(ted)), TEXT_SIZE_MIN, TEXT_SIZE_MAX); #if _WIN32 settings->crlf |= settings->crlf_windows; #endif } static void config_compile_regex(Config *cfg, ConfigReader *reader) { if (cfg->path_regex) { const char *regex = cfg->path_regex; #if _WIN32 char fixed[8192]; // replace forward slashes with backslashes // (but actually double backslashes because this is a regex) for (size_t in = 0, out = 0; out < sizeof fixed - 4 && regex[in]; ++in) { if (regex[in] == '/') { fixed[out++] = '\\'; fixed[out++] = '\\'; } else { fixed[out++] = regex[in]; } fixed[out] = 0; } regex = fixed; #endif // compile regex int error_code = 0; PCRE2_SIZE error_offset = 0; cfg->path = pcre2_compile_8((const u8 *)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, regex); free(cfg->path_regex); cfg->path_regex = NULL; } } } static bool regex_char_needs_escaping(char c) { return strchr("\\^.$|()[]*+?{}-", c); } static void config_read_ted_cfg(Ted *ted, RcStr *cfg_path_rc, const char ***include_stack) { const char *const cfg_path = rc_str(cfg_path_rc, ""); const bool is_local = str_has_suffix(cfg_path, ".ted.cfg") && is_path_separator(cfg_path[strlen(cfg_path) - 9]); // 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)) { char text[1024]; strbuf_cpy(text, "%include loop in config files: "); strbuf_cat(text, (*include_stack)[0]); for (u32 i = 1; i < arr_len(*include_stack); ++i) { if (i > 1) strbuf_cat(text, ", which"); strbuf_catf(text, " includes %s", (*include_stack)[i]); } if (arr_len(*include_stack) > 1) strbuf_cat(text, ", which"); strbuf_catf(text, " includes %s", cfg_path); ted_error(ted, "%s", text); return; } } arr_add(*include_stack, cfg_path); FILE *fp = fopen(cfg_path, "rb"); if (!fp) { return; } ConfigReader reader_data = { .ted = ted, .filename = cfg_path, .line_number = 1, .fp = fp, .error = false }; ConfigReader *reader = &reader_data; Config *cfg = NULL; while (true) { 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]" char header[256]; config_read_to_eol(reader, header, sizeof header); char path[TED_PATH_MAX]; path[0] = '\0'; if (is_local) { // prepend directory char dirname[TED_PATH_MAX]; strbuf_cpy(dirname, cfg_path); path_dirname(dirname); for (size_t i = 0, out = 0; dirname[i] && out < sizeof path - 3; i++) { if (regex_char_needs_escaping(dirname[i])) { path[out++] = '\\'; } path[out++] = dirname[i]; path[out] = '\0'; } } Language language = 0; if (strlen(header) == 0 || header[strlen(header) - 1] != ']') { config_err(reader, "Section header doesn't end with ]\n" SECTION_HEADER_HELP); break; } else { header[strlen(header) - 1] = 0; char *p = header; char *path_end = strstr(p, "//"); if (path_end) { if (is_local) { // important we do this here and not above so that // if we have /foo/.ted.cfg // [colors] // bg = #f00 // ^^ this should apply to the path "/foo" (not just "/foo/something") strbuf_catf(path, "%c", PATH_SEPARATOR); } size_t path_len = (size_t)(path_end - header); path[0] = '\0'; // expand ~ if (p[0] == '~') { str_cpy(path, sizeof path, ted->home); ++p; --path_len; } strn_cat(path, sizeof path, p, path_len); p = path_end + 2; } char *dot = strchr(p, '.'); if (dot) { *dot = '\0'; language = language_from_str(p); if (!language) { config_err(reader, "Unrecognized language: %s.", p); } p = dot + 1; } if (streq(p, "keyboard")) { reader->section = SECTION_KEYBOARD; } else if (streq(p, "colors")) { reader->section = SECTION_COLORS; } else if (streq(p, "core")) { reader->section = SECTION_CORE; } else if (streq(p, "extensions")) { if (language != 0 || *path) { config_err(reader, "Extensions section cannot be language- or path-specific."); break; } reader->section = SECTION_EXTENSIONS; } else { config_err(reader, "Unrecognized section: [%s].", p); } } Config new_cfg = { .language = language, .path_regex = *path ? path : NULL, }; cfg = NULL; // search for config with same context to update arr_foreach_ptr(ted->all_configs, Config, conf) { if (config_definitely_has_same_context(conf, &new_cfg)) { cfg = conf; } } if (!cfg) { // create new config cfg = arr_addp(ted->all_configs); cfg->path_regex = *path ? str_dup(path) : NULL; cfg->language = language; cfg->format = CONFIG_TED_CFG; cfg->source = rc_str_copy(cfg_path_rc); config_compile_regex(cfg, reader); } } else if (c == '%') { char line[2048]; config_read_to_eol(reader, line, sizeof line); if (str_has_prefix(line, "include ")) { char included[TED_PATH_MAX]; char expanded[TED_PATH_MAX]; strbuf_cpy(included, line + strlen("include ")); str_trim(included); get_config_path(ted, expanded, sizeof expanded, included); RcStr *expanded_rc = rc_str_new(expanded, -1); config_read_ted_cfg(ted, expanded_rc, include_stack); rc_str_decref(&expanded_rc); } else { config_err(reader, "Unrecognized directive: %s", line); } } else if (isspace(c)) { // whitespace } else if (c == '#') { // comment char buf[4096]; config_read_to_eol(reader, buf, sizeof buf); } else if (cfg) { config_ungetc(reader, c); config_parse_line(reader, cfg); } else { config_err(reader, "Config has text before first section header."); } } if (ferror(fp)) ted_error(ted, "Error reading %s.", cfg_path); fclose(fp); arr_remove_last(*include_stack); } static void regex_append_literal_char(StrBuilder *b, char c) { if (regex_char_needs_escaping(c)) str_builder_appendf(b, "\\%c", c); else str_builder_appendf(b, "%c", c); } // handles 000-999 as 000|001|002|...|999 for example (which is why we need strings) static void regex_append_number_str_range(StrBuilder *b, const char *s1, const char *s2) { assert(strlen(s1) == strlen(s2)); assert(strcmp(s1, s2) <= 0); if (s1[0] == 0) return; if (s1[0] == s2[0]) { int common_prefix = 1; while (s1[common_prefix] && s1[common_prefix] == s2[common_prefix]) common_prefix += 1; str_builder_appendf(b, "%.*s", common_prefix, s1); if (s1[common_prefix]) { str_builder_append(b, "("); regex_append_number_str_range(b, &s1[common_prefix], &s2[common_prefix]); str_builder_append(b, ")"); } return; } assert(s1[0] < s2[0]); if (!s1[1]) { // single digits str_builder_appendf(b, "[%c-%c]", s1[0], s2[0]); return; } bool first = true; char midstart = s1[0] + 1, midend = s2[0] - 1; char s[24]; strcpy(s, s1); for (size_t i = 1; s[i]; i++) { s[i] = '9'; } if (strspn(s1 + 1, "0") == strlen(s1 + 1)) { // s is 100 or something so we can just do 1[0-9]{2} --midstart; } else { if (!first) str_builder_append(b, "|"); first = false; regex_append_number_str_range(b, s1, s); } strcpy(s, s2); for (size_t i = 1; s[i]; i++) { s[i] = '0'; } if (strspn(s2 + 1, "9") == strlen(s2 + 1)) { ++midend; } else { if (!first) str_builder_append(b, "|"); first = false; regex_append_number_str_range(b, s, s2); } // the middle digits if (midstart <= midend) { unsigned nleft = (unsigned)strlen(s1) - 1; if (!first) str_builder_append(b, "|"); first = false; if (midstart == midend) str_builder_appendf(b, "%c", midstart); else str_builder_appendf(b, "[%c-%c]", midstart, midend); str_builder_append(b, "[0-9]"); if (nleft > 1) { str_builder_appendf(b, "{%u}", nleft); } } } // absolutely crazy code to convert number range to regex static void regex_append_number_range(StrBuilder *b, i64 num1, i64 num2) { if (num1 > num2) { // switcheroo regex_append_number_range(b, num2, num1); return; } if (num1 < 0 && num2 >= 0) { // split into just-positive and just-negative regex_append_number_range(b, num1, -1); str_builder_append(b, "|"); regex_append_number_range(b, 0, num2); return; } if (num1 < 0 && num2 < 0) { // all negatives str_builder_append(b, "-"); str_builder_append(b, "("); regex_append_number_range(b, -num2, -num1); str_builder_append(b, ")"); return; } // hoo ray we don't need to deal with negatives anymore assert(num1 >= 0 && num2 >= 0); if (num2 > 999999999999999999) { // bull shit forget it str_builder_append(b, "//"); // will never match a valid path return; } char s1[24], s2[24]; strbuf_printf(s1, "%" PRId64, num1); strbuf_printf(s2, "%" PRId64, num2); if (strlen(s1) != strlen(s2)) { // e.g. split 45-333 into 45-99 and 100-333 assert(strlen(s1) < strlen(s2)); for (size_t i = 0; s1[i]; i++) s1[i] = '9'; i64 n = (i64)atoll(s1); regex_append_number_range(b, num1, n); str_builder_append(b, "|"); regex_append_number_range(b, n + 1, num2); return; } regex_append_number_str_range(b, s1, s2); } static char *editorconfig_glob_to_regex(ConfigReader *reader, const char *glob) { StrBuilder builder = str_builder_new(), *b = &builder; { // add base path char dirname[4096]; strbuf_cpy(dirname, reader->filename); path_dirname(dirname); if (!dirname[0]) { assert(0); goto error; } if (!is_path_separator(dirname[strlen(dirname) - 1])) { strbuf_catf(dirname, "%c", PATH_SEPARATOR); } for (const char *p = dirname; *p; ++p) regex_append_literal_char(b, *p); } if (!strchr(glob, '/')) { // allow any bull shit directory before the glob str_builder_append(b, "(.*/)?"); } int brace_level = 0; for (size_t i = 0; glob[i]; i++) { assert(brace_level >= 0); switch (glob[i]) { case '\\': if (glob[i+1]) { regex_append_literal_char(b, glob[i+1]); i += 1; } else { regex_append_literal_char(b, '\\'); } break; case '*': if (glob[i+1] == '*') { // ** str_builder_append(b, ".*"); } else { // * str_builder_append(b, "[^/]*"); } break; case '?': str_builder_append(b, "[^/]"); break; case '/': if (str_has_prefix(&glob[i], "/**/")) { // allow just a single slash // (not in spec, but editorconfig library has this) str_builder_append(b, "/(.*/)?"); } else { regex_append_literal_char(b, '/'); } break; case '[': str_builder_append(b, "["); i += 1; if (glob[i] == '!') { str_builder_append(b, "^"); i += 1; } for (; glob[i]; i++) { if (glob[i] == ']') break; if (glob[i] == '\\') { i += 1; if (!glob[i]) break; } regex_append_literal_char(b, glob[i]); } str_builder_append(b, "]"); if (!glob[i]) { // we don't wanna show some error message if editorconfig glob spec changes config_debug_err(reader, "glob has [ with no matching ]"); goto error; } break; case '{': { i64 num1 = 0, num2 = 0; int bytes = 0; if (sscanf(&glob[i], "{%" SCNd64 "..%" SCNd64 "}%n", &num1, &num2, &bytes) == 2 && bytes > 0) { i += (unsigned)bytes - 1; str_builder_append(b, "("); regex_append_number_range(b, num1, num2); str_builder_append(b, ")"); break; } bool has_comma = false; size_t j; for (j = i; glob[j]; j++) { if (glob[j] == '}') break; if (glob[j] == ',') has_comma = true; if (glob[j] == '\\') { j += 1; if (!glob[j]) break; } } if (has_comma && glob[j]) { str_builder_append(b, "("); brace_level += 1; } else { regex_append_literal_char(b, '{'); } } break; case ',': if (brace_level > 0) { str_builder_append(b, "|"); } else { regex_append_literal_char(b, ','); } break; case '}': if (brace_level == 0) { regex_append_literal_char(b, '}'); } else { str_builder_append(b, ")"); brace_level -= 1; } break; default: regex_append_literal_char(b, glob[i]); break; } } if (brace_level) { config_debug_err(reader, "glob has { with no matching }"); goto error; } // make sure end is anchored str_builder_append(b, "$"); char *ret = str_dup(b->str); str_builder_free(&builder); return ret; error: str_builder_free(&builder); return NULL; } static void config_read_editorconfig(Ted *ted, RcStr *path_rc) { const char *const path = rc_str(path_rc, ""); FILE *fp = fopen(path, "r"); if (!fp) return; 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; 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_debug_err(reader, "Unmatched ["); break; } line[strlen(line) - 1] = 0; 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 = editorconfig_glob_to_regex(reader, line); if (!cfg->path_regex) { // regex failed to compile cfg->path_regex = str_dup("//"); // never matches a valid path } config_compile_regex(cfg, reader); break; default: { char key[128] = {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 = (just have %s)", key); break; } str_trim(key); str_ascii_to_lowercase(key); config_read_to_eol(reader, line, sizeof line); str_trim(line); char *value = line; if (streq(key, "root")) { str_ascii_to_lowercase(value); if (cfg) { config_debug_err(reader, "root cannot be set outside of preamble"); break; } if (streq(value, "true")) is_root = true; else is_root = false; break; } const int value_int = atoi(value); if (cfg && streq(key, "indent_style")) { str_ascii_to_lowercase(value); if (streq(value, "tab")) config_set_bool(cfg, &setting_indent_with_spaces, false); else if (streq(value, "space")) config_set_bool(cfg, &setting_indent_with_spaces, true); } else if (cfg && (streq(key, "indent_size") || streq(key, "tab_width")) && value_int > 0 && value_int < 255) { config_set_u8(cfg, &setting_tab_width, (u8)value_int); } else if (cfg && streq(key, "end_of_line")) { str_ascii_to_lowercase(value); if (streq(value, "lf")) { config_set_bool(cfg, &setting_crlf, false); config_set_bool(cfg, &setting_crlf_windows, false); } else if (streq(value, "crlf")) { config_set_bool(cfg, &setting_crlf, true); } // who the fuck uses CR line endings in the 21st century?? } else if (cfg && streq(key, "insert_final_newline")) { str_ascii_to_lowercase(value); if (streq(value, "true")) { config_set_bool(cfg, &setting_auto_add_newline, true); } else if (streq(value, "false")) { config_set_bool(cfg, &setting_auto_add_newline, false); } } else if (cfg && streq(key, "trim_trailing_whitespace")) { str_ascii_to_lowercase(value); if (streq(value, "true")) { config_set_bool(cfg, &setting_remove_trailing_whitespace, true); } else if (streq(value, "false")) { config_set_bool(cfg, &setting_remove_trailing_whitespace, false); } } } break; } } fclose(fp); } void config_free_all(Ted *ted) { arr_foreach_ptr(ted->all_configs, Config, cfg) { config_free(cfg); } arr_clear(ted->all_configs); settings_free(&ted->default_settings); } void config_read(Ted *ted, const char *path, ConfigFormat format) { const char **include_stack = NULL; config_init_settings(); // check if we've already read this arr_foreach_ptr(ted->all_configs, Config, c) { if (streq(rc_str(c->source, ""), path)) return; } RcStr *source_rc = rc_str_new(path, -1); // add dummy config so even if `path` doesn't exist we don't try to read it again Config *dummy = arr_addp(ted->all_configs); dummy->source = rc_str_copy(source_rc); dummy->language = LANG_INVALID; switch (format) { case CONFIG_NONE: assert(0); break; case CONFIG_TED_CFG: config_read_ted_cfg(ted, source_rc, &include_stack); // force recompute default settings strcpy(ted->default_settings_cwd, "//"); break; case CONFIG_EDITORCONFIG: config_read_editorconfig(ted, source_rc); break; } rc_str_decref(&source_rc); } static char *last_separator(char *path) { for (int i = (int)strlen(path) - 1; i >= 0; --i) if (is_path_separator(path[i])) return &path[i]; return NULL; } char *settings_get_root_dir(const Settings *settings, const char *path) { char best_path[TED_PATH_MAX]; *best_path = '\0'; u32 best_path_score = 0; char pathbuf[TED_PATH_MAX]; strbuf_cpy(pathbuf, path); while (1) { FsDirectoryEntry **entries = fs_list_directory(pathbuf); if (entries) { // note: this may actually be NULL on the first iteration if `path` is a file for (int e = 0; entries[e]; ++e) { const char *entry_name = entries[e]->name; const char *root_identifiers = rc_str(settings->root_identifiers, ""); const char *ident_name = root_identifiers; while (*ident_name) { const char *separators = ", \t\n\r\v"; size_t ident_len = strcspn(ident_name, separators); if (strlen(entry_name) == ident_len && strncmp(entry_name, ident_name, ident_len) == 0) { // we found an identifier! u32 score = U32_MAX - (u32)(ident_name - root_identifiers); if (score > best_path_score) { best_path_score = score; strbuf_cpy(best_path, pathbuf); } } ident_name += ident_len; ident_name += strspn(ident_name, separators); } } fs_dir_entries_free(entries); } char *p = last_separator(pathbuf); if (!p) break; *p = '\0'; if (!last_separator(pathbuf)) break; // we made it all the way to / (or c:\ or whatever) } if (*best_path) { return str_dup(best_path); } else { // didn't find any identifiers. // just return // - `path` if it's a directory // - the directory containing path if it's a file if (fs_path_type(path) == FS_DIRECTORY) { return str_dup(path); } strbuf_cpy(pathbuf, path); char *sep = last_separator(pathbuf); *sep = '\0'; return str_dup(pathbuf); } } u32 settings_color(const Settings *settings, ColorSetting color) { if (color >= COLOR_COUNT) { assert(0); return 0xff00ffff; } return settings->colors[color]; } void settings_color_floats(const Settings *settings, ColorSetting color, float f[4]) { color_u32_to_floats(settings_color(settings, color), f); } u16 settings_tab_width(const Settings *settings) { return settings->tab_width; } bool settings_indent_with_spaces(const Settings *settings) { return settings->indent_with_spaces; } bool settings_auto_indent(const Settings *settings) { return settings->auto_indent; } float settings_border_thickness(const Settings *settings) { return settings->border_thickness; } float settings_padding(const Settings *settings) { return settings->padding; } static void editorconfig_glob_test_expect(Ted *ted, const char *pattern, const char *path, bool result) { // fake config reader ConfigReader reader = { .ted = ted, .filename = "/test.editorconfig", .line_number = 1, }; char *regex = editorconfig_glob_to_regex(&reader, pattern); int error_code = 0; PCRE2_SIZE error_offset = 0; pcre2_code_8 *code = pcre2_compile_8((const u8 *)regex, PCRE2_ZERO_TERMINATED, PCRE2_ANCHORED, &error_code, &error_offset, NULL); if (!code) { println("bad regex produced from editorconfig glob (error code %d at offset %u): %s", error_code, (unsigned)error_offset, regex); exit(1); } pcre2_match_data_8 *match_data = pcre2_match_data_create_from_pattern_8(code, NULL); bool match = pcre2_match_8(code, (const u8 *)path, PCRE2_ZERO_TERMINATED, 0, 0, match_data, NULL) > 0; pcre2_match_data_free_8(match_data); if (!match && result) { println("expected editorconfig glob \"%s\" to match \"%s\" but it didn't. regex was: %s", pattern, path, regex); exit(1); } if (match && !result) { println("expected editorconfig glob \"%s\" not to match \"%s\" but it did. regex was: %s", pattern, path, regex); exit(1); } } static void config_test_editorconfig_glob_to_regex(Ted *ted) { static const struct { const char *pattern; const char *path; bool result; } tests[] = { {"foo", "/foo", 1}, {"foo", "/a/foo", 1}, {"food", "/a/foo", 0}, {"*.py", "/a/b/x.py", 1}, {"a/*.py", "/a/x.py", 1}, {"a/*.py", "/b/x.py", 0}, {"a/*.py", "/a/b/x.py", 0}, {"a/**.py", "/a/b/x.py", 1}, {"[xyz]", "/y", 1}, {"[xyz]", "/z", 1}, {"[xyz]", "/a", 0}, {"[!xyz]", "/y", 0}, {"[!xyz]", "/z", 0}, {"[!xyz]", "/a", 1}, {"{x,y,z}", "/x", 1}, {"{x,y,z}", "/a", 0}, {"{foo,bar}", "/foo", 1}, {"{foo,bar}", "/bar", 1}, {"{foo,bar,xylum,plant species}", "/barricade", 0}, {"{foo{,s,t}}", "/foo", 1}, {"{foo{,s,t}}", "/foot", 1}, {"{foo{,s,t}}", "/foos", 1}, {"{foo{,s,t}}", "/foob", 0}, {",", "/,", 1}, {",", "/\\,", 0}, {"\\{", "/{", 1}, {"\\{", "/\\{", 0}, {"[\\[]", "/[", 1}, {"[\\[]", "/]", 0}, {"[\\]]", "/[", 0}, {"[\\]]", "/]", 1}, {"[!\\[]", "/[", 0}, {"[!\\[]", "/]", 1}, {"[!\\]]", "/[", 1}, {"[!\\]]", "/]", 0}, {"\\[\\]", "/[]", 1}, {"\\[\\]", "/.gitignore", 0}, {"{1..100}", "/00", 0}, {"{1..100}", "/11", 1}, {"{1..100}", "/1", 1}, {"{1..100}", "/100", 1}, {"{1..100}", "/90", 1}, {"{1..100}", "/35", 1}, {"{1..100}", "/7", 1}, {"{1..100}", "/101", 0}, {"{-325..-320}", "/-323", 1}, {"{-325..-320}", "/-325", 1}, {"{-325..-320}", "/-320", 1}, {"{-325..-320}", "/-300", 0}, {"{0..99999999}", "/34345", 1}, {"{0..99999999}", "/0", 1}, {"{0..99999999}", "/balls", 0}, {"{0..99999999}", "/99999999999", 0}, {"{0..0}", "/0", 1}, {"x{625..629}", "/x627", 1}, {"x{625..629}", "/x637", 0}, {"{..}", "/{..}", 1}, {"{a\\,b}", "/{a,b}", 1}, {"\\{a,b}", "/{a,b}", 1}, }; for (size_t i = 0; i < arr_count(tests); i++) { editorconfig_glob_test_expect(ted, tests[i].pattern, tests[i].path, tests[i].result); } } void config_test(Ted *ted) { config_test_editorconfig_glob_to_regex(ted); }