From 30f37caf7d9facf7698d01ef77d7da93e7316103 Mon Sep 17 00:00:00 2001
From: pommicket <pommicket@gmail.com>
Date: Thu, 19 Oct 2023 09:47:57 -0400
Subject: basic editorconfig implementation

---
 buffer.c       |   4 +-
 config.c       | 139 +++++++++++++++++++++++++++++++++++++++------------------
 main.c         |   4 +-
 ted-internal.h |   7 +--
 ted.cfg        |   3 +-
 ted.h          |   2 +
 6 files changed, 108 insertions(+), 51 deletions(-)

diff --git a/buffer.c b/buffer.c
index 6766a5a..44992bc 100644
--- a/buffer.c
+++ b/buffer.c
@@ -3185,10 +3185,8 @@ static bool buffer_write_to_file(TextBuffer *buffer, const char *path) {
 		}
 
 		if (i != buffer->nlines-1) {
-		#if _WIN32
-			if (settings->crlf_windows)
+			if (settings->crlf)
 				putc('\r', out);
-		#endif
 			putc('\n', out);
 		}
 	}
diff --git a/config.c b/config.c
index aa7b705..d6b3eca 100644
--- a/config.c
+++ b/config.c
@@ -88,15 +88,18 @@ typedef struct {
 static const Settings settings_zero = {0};
 static const SettingBool settings_bool[] = {
 	{"auto-indent", &settings_zero.auto_indent, true},
-	{"auto-add-newline", &settings_zero.auto_add_newline, true},
-	{"remove-trailing-whitespace", &settings_zero.remove_trailing_whitespace, 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},
 	{"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},
-	{"indent-with-spaces", &settings_zero.indent_with_spaces, 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},
@@ -109,13 +112,22 @@ static const SettingBool settings_bool[] = {
 	{"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}
 	{"crlf-windows", &settings_zero.crlf_windows, true},
 	{"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[] = {
-	{"tab-width", &settings_zero.tab_width, 1, 100, true},
+#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},
@@ -123,6 +135,7 @@ static const SettingU8 settings_u8[] = {
 	{"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},
@@ -936,6 +949,9 @@ static int key_action_qsort_cmp_combo(const void *av, const void *bv) {
 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) {
@@ -951,7 +967,8 @@ static void config_compile_regex(Config *cfg, ConfigReader *reader) {
 	}
 }
 
-static bool config_read_ted_cfg(Ted *ted, const char *cfg_path, const char ***include_stack) {
+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, "");
 	// 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)) {
@@ -967,17 +984,16 @@ static bool config_read_ted_cfg(Ted *ted, const char *cfg_path, const char ***in
 				strbuf_cat(text, ", which");
 			strbuf_catf(text, " includes %s", cfg_path);
 			ted_error(ted, "%s", text);
-			return false;
+			return;
 		}
 	}
 	arr_add(*include_stack, cfg_path);
 	
 	FILE *fp = fopen(cfg_path, "rb");
 	if (!fp) {
-		return false;
+		return;
 	}
 	
-	RcStr *cfg_path_rc = rc_str_new(cfg_path, -1);
 	ConfigReader reader_data = {
 		.ted = ted,
 		.filename = cfg_path,
@@ -1080,7 +1096,9 @@ static bool config_read_ted_cfg(Ted *ted, const char *cfg_path, const char ***in
 				strbuf_cpy(included, line + strlen("include "));
 				str_trim(included);
 				get_config_path(ted, expanded, sizeof expanded, included);
-				config_read_ted_cfg(ted, expanded, include_stack);
+				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);
 			}
@@ -1097,12 +1115,10 @@ static bool config_read_ted_cfg(Ted *ted, const char *cfg_path, const char ***in
 			config_err(reader, "Config has text before first section header.");
 		}
 	}
-	rc_str_decref(&cfg_path_rc);
 	if (ferror(fp))
 		ted_error(ted, "Error reading %s.", cfg_path);
 	fclose(fp);
 	arr_remove_last(*include_stack);
-	return true;
 }
 
 static void regex_append_literal_char(StrBuilder *b, char c) {
@@ -1365,9 +1381,10 @@ error:
 	return NULL;
 }
 
-static bool config_read_editorconfig(Ted *ted, const char *path) {
+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 false;
+	if (!fp) return;
 	ConfigReader reader_data = {
 		.ted = ted,
 		.filename = path,
@@ -1378,7 +1395,6 @@ static bool config_read_editorconfig(Ted *ted, const char *path) {
 	Config *cfg = NULL;
 	char line[4096];
 	bool is_root = false;
-	RcStr *path_rc = rc_str_new(path, -1);
 	while (true) {
 		char c = config_getc_no_err_on_eof(reader);
 		if (!c) break;
@@ -1393,7 +1409,7 @@ static bool config_read_editorconfig(Ted *ted, const char *path) {
 			config_read_to_eol(reader, line, sizeof line);
 			str_trim(line);
 			if (strlen(line) == 0 || line[strlen(line) - 1] != ']') {
-				config_err(reader, "Unmatched [");
+				config_debug_err(reader, "Unmatched [");
 				break;
 			}
 			line[strlen(line) - 1] = 0;
@@ -1432,20 +1448,49 @@ static bool config_read_editorconfig(Ted *ted, const char *path) {
 			if (streq(key, "root")) {
 				str_ascii_to_lowercase(value);
 				if (cfg) {
-					config_err(reader, "root cannot be set outside of preamble");
+					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")) {
+				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")) {
+				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")) {
+				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")) {
+				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;
 		}
 	}
-	rc_str_decref(&path_rc);
 	fclose(fp);
-	return true;
 }
 
 void config_free_all(Ted *ted) {
@@ -1456,6 +1501,35 @@ void config_free_all(Ted *ted) {
 	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);
+		// recompute default settings
+		ted_compute_settings(ted, "", LANG_NONE, &ted->default_settings);
+		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)
@@ -1521,29 +1595,6 @@ char *settings_get_root_dir(const Settings *settings, const char *path) {
 	}
 }
 
-bool 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 true;
-	}
-	switch (format) {
-	case CONFIG_TED_CFG:
-		if (config_read_ted_cfg(ted, path, &include_stack)) {
-			// recompute default settings
-			ted_compute_settings(ted, "", LANG_NONE, &ted->default_settings);
-			return true;
-		}
-		return false;
-	case CONFIG_EDITORCONFIG:
-		return config_read_editorconfig(ted, path);
-	}
-	assert(0);
-	return false;
-}
-
 u32 settings_color(const Settings *settings, ColorSetting color) {
 	if (color >= COLOR_COUNT) {
 		assert(0);
@@ -1669,9 +1720,11 @@ static void config_test_editorconfig_glob_to_regex(Ted *ted) {
 		{"{0..99999999}", "/balls", 0},
 		{"{0..99999999}", "/99999999999", 0},
 		{"{0..0}", "/0", 1},
-		{"{625..629}", "/627", 1},
-		{"{625..629}", "/637", 0},
+		{"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);
diff --git a/main.c b/main.c
index d9f206a..9ee97e8 100644
--- a/main.c
+++ b/main.c
@@ -1,7 +1,9 @@
 /*
 TODO:
 - .editorconfig (see https://editorconfig.org/)
-   - test number range
+  - handle root = true
+  - reload configs when .editorconfig is saved
+- local .ted.cfg
 FUTURE FEATURES:
 - more tests
 - prepare rename support
diff --git a/ted-internal.h b/ted-internal.h
index 3a9c578..c8a72c3 100644
--- a/ted-internal.h
+++ b/ted-internal.h
@@ -115,6 +115,8 @@ struct Settings {
 	bool document_links;
 	bool vsync;
 	bool save_backup;
+	bool crlf;
+	// this is automatically copied to crlf if compiling for windows
 	bool crlf_windows;
 	bool jump_to_build_error;
 	bool force_monospace;
@@ -154,6 +156,7 @@ struct Settings {
 };
 
 typedef enum {
+	CONFIG_NONE = 0,
 	CONFIG_TED_CFG = 1,
 	CONFIG_EDITORCONFIG = 2,
 } ConfigFormat;
@@ -499,10 +502,8 @@ void command_execute_ex(Ted *ted, Command c, const CommandArgument *argument, co
 // === config.c ===
 /// read a config file.
 ///
-/// returns true on success. if the file at `path` does not exist, this returns false but doesn't show an error message.
-///
 /// if the config with this path has already been read, this does nothing.
-bool config_read(Ted *ted, const char *path, ConfigFormat format);
+void config_read(Ted *ted, const char *path, ConfigFormat format);
 void config_free_all(Ted *ted);
 void config_merge_into(Settings *dest, const Config *src_cfg);
 /// call this after all your calls to \ref config_merge_into
diff --git a/ted.cfg b/ted.cfg
index 6c86fd9..9d3b864 100644
--- a/ted.cfg
+++ b/ted.cfg
@@ -126,7 +126,8 @@ root-identifiers = .ted-root, .ted-root.out, Cargo.toml, make.bat, CMakeLists.tx
 # the backups are deleted immediately after writing.
 save-backup = yes
 # whether to default to \r\n line endings.
-# this setting is ignored on non-Windows operating system.
+crlf = no
+# same as crlf setting, but ignored on non-Windows operating system.
 crlf-windows = no
 
 # you can make your own custom background for ted using a shader.
diff --git a/ted.h b/ted.h
index 6331d5c..1dbacb6 100644
--- a/ted.h
+++ b/ted.h
@@ -70,6 +70,8 @@ enum {
 	/// GDScript
 	LANG_GDSCRIPT = 19,
 	
+	/// this will never be a valid language ID
+	LANG_INVALID = 9999,
 	/// all user-defined languages are greater than this.
 	LANG_USER_MIN = 100000,
 	/// all user-defined languages are less than this.
-- 
cgit v1.2.3