summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2022-11-02 13:02:38 -0400
committerpommicket <pommicket@gmail.com>2022-11-02 13:02:38 -0400
commit26d34216da04a2b91e65a0eeee9200ad808d48ce (patch)
treee9781ff976f16f1f5249ca679aa2c941a35eb03c
parentb63bd066bf06317e6637aa03369a75c018c1939f (diff)
(insufficiently tested) per-path settings
also fixed memory bug in path_full, yet again
-rw-r--r--buffer.c30
-rw-r--r--config.c830
-rw-r--r--main.c34
-rw-r--r--ted.c21
-rw-r--r--ted.cfg2
-rw-r--r--ted.h33
-rw-r--r--util.c3
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);
}