diff options
| author | Mitja Felicijan <mitja.felicijan@gmail.com> | 2023-11-09 23:19:53 +0100 |
|---|---|---|
| committer | Mitja Felicijan <mitja.felicijan@gmail.com> | 2023-11-09 23:19:53 +0100 |
| commit | 1566b6faa8534118c3566188181367cd0868468f (patch) | |
| tree | 1de8d4b369efb5e592685a31088f798a6b63ffa1 /examples/dte/options.c | |
| parent | 349991bf6efe473ab9a5cbdae0a8114d72b997e3 (diff) | |
| download | crep-1566b6faa8534118c3566188181367cd0868468f.tar.gz | |
Added partial matching and introduced threads
Diffstat (limited to 'examples/dte/options.c')
| -rw-r--r-- | examples/dte/options.c | 987 |
1 files changed, 987 insertions, 0 deletions
diff --git a/examples/dte/options.c b/examples/dte/options.c new file mode 100644 index 0000000..2fd4190 --- /dev/null +++ b/examples/dte/options.c | |||
| @@ -0,0 +1,987 @@ | |||
| 1 | #include <stdint.h> | ||
| 2 | #include <stdlib.h> | ||
| 3 | #include <string.h> | ||
| 4 | #include "options.h" | ||
| 5 | #include "buffer.h" | ||
| 6 | #include "command/serialize.h" | ||
| 7 | #include "editor.h" | ||
| 8 | #include "error.h" | ||
| 9 | #include "file-option.h" | ||
| 10 | #include "filetype.h" | ||
| 11 | #include "screen.h" | ||
| 12 | #include "status.h" | ||
| 13 | #include "terminal/output.h" | ||
| 14 | #include "util/bsearch.h" | ||
| 15 | #include "util/debug.h" | ||
| 16 | #include "util/intern.h" | ||
| 17 | #include "util/numtostr.h" | ||
| 18 | #include "util/str-util.h" | ||
| 19 | #include "util/string-view.h" | ||
| 20 | #include "util/strtonum.h" | ||
| 21 | #include "util/xmalloc.h" | ||
| 22 | |||
| 23 | typedef enum { | ||
| 24 | OPT_STR, | ||
| 25 | OPT_UINT, | ||
| 26 | OPT_ENUM, | ||
| 27 | OPT_BOOL, | ||
| 28 | OPT_FLAG, | ||
| 29 | OPT_REGEX, | ||
| 30 | } OptionType; | ||
| 31 | |||
| 32 | typedef union { | ||
| 33 | const char *str_val; // OPT_STR, OPT_REGEX | ||
| 34 | unsigned int uint_val; // OPT_UINT, OPT_ENUM, OPT_FLAG | ||
| 35 | bool bool_val; // OPT_BOOL | ||
| 36 | } OptionValue; | ||
| 37 | |||
| 38 | typedef union { | ||
| 39 | struct {bool (*validate)(const char *value);} str_opt; // OPT_STR (optional) | ||
| 40 | struct {unsigned int min, max;} uint_opt; // OPT_UINT | ||
| 41 | struct {const char *const *values;} enum_opt; // OPT_ENUM, OPT_FLAG, OPT_BOOL | ||
| 42 | } OptionConstraint; | ||
| 43 | |||
| 44 | typedef struct { | ||
| 45 | const char name[22]; | ||
| 46 | bool local; | ||
| 47 | bool global; | ||
| 48 | unsigned int offset; | ||
| 49 | OptionType type; | ||
| 50 | OptionConstraint u; | ||
| 51 | void (*on_change)(EditorState *e, bool global); // Optional | ||
| 52 | } OptionDesc; | ||
| 53 | |||
| 54 | #define STR_OPT(_name, OLG, _validate, _on_change) { \ | ||
| 55 | OLG \ | ||
| 56 | .name = _name, \ | ||
| 57 | .type = OPT_STR, \ | ||
| 58 | .u = {.str_opt = {.validate = _validate}}, \ | ||
| 59 | .on_change = _on_change, \ | ||
| 60 | } | ||
| 61 | |||
| 62 | #define UINT_OPT(_name, OLG, _min, _max, _on_change) { \ | ||
| 63 | OLG \ | ||
| 64 | .name = _name, \ | ||
| 65 | .type = OPT_UINT, \ | ||
| 66 | .u = {.uint_opt = { \ | ||
| 67 | .min = _min, \ | ||
| 68 | .max = _max, \ | ||
| 69 | }}, \ | ||
| 70 | .on_change = _on_change, \ | ||
| 71 | } | ||
| 72 | |||
| 73 | #define ENUM_OPT(_name, OLG, _values, _on_change) { \ | ||
| 74 | OLG \ | ||
| 75 | .name = _name, \ | ||
| 76 | .type = OPT_ENUM, \ | ||
| 77 | .u = {.enum_opt = {.values = _values}}, \ | ||
| 78 | .on_change = _on_change, \ | ||
| 79 | } | ||
| 80 | |||
| 81 | #define FLAG_OPT(_name, OLG, _values, _on_change) { \ | ||
| 82 | OLG \ | ||
| 83 | .name = _name, \ | ||
| 84 | .type = OPT_FLAG, \ | ||
| 85 | .u = {.enum_opt = {.values = _values}}, \ | ||
| 86 | .on_change = _on_change, \ | ||
| 87 | } | ||
| 88 | |||
| 89 | #define BOOL_OPT(_name, OLG, _on_change) { \ | ||
| 90 | OLG \ | ||
| 91 | .name = _name, \ | ||
| 92 | .type = OPT_BOOL, \ | ||
| 93 | .u = {.enum_opt = {.values = bool_enum}}, \ | ||
| 94 | .on_change = _on_change, \ | ||
| 95 | } | ||
| 96 | |||
| 97 | #define REGEX_OPT(_name, OLG, _on_change) { \ | ||
| 98 | OLG \ | ||
| 99 | .name = _name, \ | ||
| 100 | .type = OPT_REGEX, \ | ||
| 101 | .on_change = _on_change, \ | ||
| 102 | } | ||
| 103 | |||
| 104 | #define OLG(_offset, _local, _global) \ | ||
| 105 | .offset = _offset, \ | ||
| 106 | .local = _local, \ | ||
| 107 | .global = _global, \ | ||
| 108 | |||
| 109 | #define L(member) OLG(offsetof(LocalOptions, member), true, false) | ||
| 110 | #define G(member) OLG(offsetof(GlobalOptions, member), false, true) | ||
| 111 | #define C(member) OLG(offsetof(CommonOptions, member), true, true) | ||
| 112 | |||
| 113 | static void filetype_changed(EditorState *e, bool global) | ||
| 114 | { | ||
| 115 | BUG_ON(!e->buffer); | ||
| 116 | BUG_ON(global); | ||
| 117 | set_file_options(e, e->buffer); | ||
| 118 | buffer_update_syntax(e, e->buffer); | ||
| 119 | } | ||
| 120 | |||
| 121 | static void set_window_title_changed(EditorState *e, bool global) | ||
| 122 | { | ||
| 123 | BUG_ON(!global); | ||
| 124 | Terminal *term = &e->terminal; | ||
| 125 | if (e->options.set_window_title) { | ||
| 126 | if (e->status == EDITOR_RUNNING) { | ||
| 127 | update_term_title(term, e->buffer, e->options.set_window_title); | ||
| 128 | } | ||
| 129 | } else { | ||
| 130 | term_restore_title(term); | ||
| 131 | term_save_title(term); | ||
| 132 | } | ||
| 133 | } | ||
| 134 | |||
| 135 | static void syntax_changed(EditorState *e, bool global) | ||
| 136 | { | ||
| 137 | if (e->buffer && !global) { | ||
| 138 | buffer_update_syntax(e, e->buffer); | ||
| 139 | } | ||
| 140 | } | ||
| 141 | |||
| 142 | static void overwrite_changed(EditorState *e, bool global) | ||
| 143 | { | ||
| 144 | if (!global) { | ||
| 145 | e->cursor_style_changed = true; | ||
| 146 | } | ||
| 147 | } | ||
| 148 | |||
| 149 | static void redraw_buffer(EditorState *e, bool global) | ||
| 150 | { | ||
| 151 | if (e->buffer && !global) { | ||
| 152 | mark_all_lines_changed(e->buffer); | ||
| 153 | } | ||
| 154 | } | ||
| 155 | |||
| 156 | static void redraw_screen(EditorState *e, bool global) | ||
| 157 | { | ||
| 158 | BUG_ON(!global); | ||
| 159 | mark_everything_changed(e); | ||
| 160 | } | ||
| 161 | |||
| 162 | static bool validate_statusline_format(const char *value) | ||
| 163 | { | ||
| 164 | size_t errpos = statusline_format_find_error(value); | ||
| 165 | if (likely(errpos == 0)) { | ||
| 166 | return true; | ||
| 167 | } | ||
| 168 | char ch = value[errpos]; | ||
| 169 | if (ch == '\0') { | ||
| 170 | return error_msg("Format character expected after '%%'"); | ||
| 171 | } | ||
| 172 | return error_msg("Invalid format character '%c'", ch); | ||
| 173 | } | ||
| 174 | |||
| 175 | static bool validate_filetype(const char *value) | ||
| 176 | { | ||
| 177 | if (!is_valid_filetype_name(value)) { | ||
| 178 | return error_msg("Invalid filetype name '%s'", value); | ||
| 179 | } | ||
| 180 | return true; | ||
| 181 | } | ||
| 182 | |||
| 183 | static OptionValue str_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 184 | { | ||
| 185 | const char *const *strp = ptr; | ||
| 186 | return (OptionValue){.str_val = *strp}; | ||
| 187 | } | ||
| 188 | |||
| 189 | static void str_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 190 | { | ||
| 191 | const char **strp = ptr; | ||
| 192 | *strp = str_intern(v.str_val); | ||
| 193 | } | ||
| 194 | |||
| 195 | static bool str_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 196 | { | ||
| 197 | bool valid = !d->u.str_opt.validate || d->u.str_opt.validate(str); | ||
| 198 | v->str_val = valid ? str : NULL; | ||
| 199 | return valid; | ||
| 200 | } | ||
| 201 | |||
| 202 | static const char *str_string(const OptionDesc* UNUSED_ARG(d), OptionValue v) | ||
| 203 | { | ||
| 204 | const char *s = v.str_val; | ||
| 205 | return s ? s : ""; | ||
| 206 | } | ||
| 207 | |||
| 208 | static bool str_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 209 | { | ||
| 210 | const char **strp = ptr; | ||
| 211 | return xstreq(*strp, v.str_val); | ||
| 212 | } | ||
| 213 | |||
| 214 | static OptionValue re_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 215 | { | ||
| 216 | const InternedRegexp *const *irp = ptr; | ||
| 217 | return (OptionValue){.str_val = *irp ? (*irp)->str : NULL}; | ||
| 218 | } | ||
| 219 | |||
| 220 | static void re_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 221 | { | ||
| 222 | const InternedRegexp **irp = ptr; | ||
| 223 | *irp = v.str_val ? regexp_intern(v.str_val) : NULL; | ||
| 224 | } | ||
| 225 | |||
| 226 | static bool re_parse(const OptionDesc* UNUSED_ARG(d), const char *str, OptionValue *v) | ||
| 227 | { | ||
| 228 | if (str[0] == '\0') { | ||
| 229 | v->str_val = NULL; | ||
| 230 | return true; | ||
| 231 | } | ||
| 232 | |||
| 233 | bool valid = regexp_is_interned(str) || regexp_is_valid(str, REG_NEWLINE); | ||
| 234 | v->str_val = valid ? str : NULL; | ||
| 235 | return valid; | ||
| 236 | } | ||
| 237 | |||
| 238 | static bool re_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 239 | { | ||
| 240 | const InternedRegexp **irp = ptr; | ||
| 241 | return *irp ? xstreq((*irp)->str, v.str_val) : !v.str_val; | ||
| 242 | } | ||
| 243 | |||
| 244 | static OptionValue uint_get(const OptionDesc* UNUSED_ARG(desc), void *ptr) | ||
| 245 | { | ||
| 246 | const unsigned int *valp = ptr; | ||
| 247 | return (OptionValue){.uint_val = *valp}; | ||
| 248 | } | ||
| 249 | |||
| 250 | static void uint_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 251 | { | ||
| 252 | unsigned int *valp = ptr; | ||
| 253 | *valp = v.uint_val; | ||
| 254 | } | ||
| 255 | |||
| 256 | static bool uint_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 257 | { | ||
| 258 | unsigned int val; | ||
| 259 | if (!str_to_uint(str, &val)) { | ||
| 260 | return error_msg("Integer value for %s expected", d->name); | ||
| 261 | } | ||
| 262 | |||
| 263 | const unsigned int min = d->u.uint_opt.min; | ||
| 264 | const unsigned int max = d->u.uint_opt.max; | ||
| 265 | if (val < min || val > max) { | ||
| 266 | return error_msg("Value for %s must be in %u-%u range", d->name, min, max); | ||
| 267 | } | ||
| 268 | |||
| 269 | v->uint_val = val; | ||
| 270 | return true; | ||
| 271 | } | ||
| 272 | |||
| 273 | static const char *uint_string(const OptionDesc* UNUSED_ARG(desc), OptionValue value) | ||
| 274 | { | ||
| 275 | return uint_to_str(value.uint_val); | ||
| 276 | } | ||
| 277 | |||
| 278 | static bool uint_equals(const OptionDesc* UNUSED_ARG(desc), void *ptr, OptionValue value) | ||
| 279 | { | ||
| 280 | const unsigned int *valp = ptr; | ||
| 281 | return *valp == value.uint_val; | ||
| 282 | } | ||
| 283 | |||
| 284 | static OptionValue bool_get(const OptionDesc* UNUSED_ARG(d), void *ptr) | ||
| 285 | { | ||
| 286 | const bool *valp = ptr; | ||
| 287 | return (OptionValue){.bool_val = *valp}; | ||
| 288 | } | ||
| 289 | |||
| 290 | static void bool_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 291 | { | ||
| 292 | bool *valp = ptr; | ||
| 293 | *valp = v.bool_val; | ||
| 294 | } | ||
| 295 | |||
| 296 | static bool bool_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 297 | { | ||
| 298 | if (streq(str, "true")) { | ||
| 299 | v->bool_val = true; | ||
| 300 | return true; | ||
| 301 | } else if (streq(str, "false")) { | ||
| 302 | v->bool_val = false; | ||
| 303 | return true; | ||
| 304 | } | ||
| 305 | return error_msg("Invalid value for %s", d->name); | ||
| 306 | } | ||
| 307 | |||
| 308 | static const char *bool_string(const OptionDesc* UNUSED_ARG(d), OptionValue v) | ||
| 309 | { | ||
| 310 | return v.bool_val ? "true" : "false"; | ||
| 311 | } | ||
| 312 | |||
| 313 | static bool bool_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v) | ||
| 314 | { | ||
| 315 | const bool *valp = ptr; | ||
| 316 | return *valp == v.bool_val; | ||
| 317 | } | ||
| 318 | |||
| 319 | static bool enum_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 320 | { | ||
| 321 | const char *const *values = d->u.enum_opt.values; | ||
| 322 | unsigned int i; | ||
| 323 | for (i = 0; values[i]; i++) { | ||
| 324 | if (streq(values[i], str)) { | ||
| 325 | v->uint_val = i; | ||
| 326 | return true; | ||
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | unsigned int val; | ||
| 331 | if (!str_to_uint(str, &val) || val >= i) { | ||
| 332 | return error_msg("Invalid value for %s", d->name); | ||
| 333 | } | ||
| 334 | |||
| 335 | v->uint_val = val; | ||
| 336 | return true; | ||
| 337 | } | ||
| 338 | |||
| 339 | static const char *enum_string(const OptionDesc *desc, OptionValue value) | ||
| 340 | { | ||
| 341 | return desc->u.enum_opt.values[value.uint_val]; | ||
| 342 | } | ||
| 343 | |||
| 344 | static bool flag_parse(const OptionDesc *d, const char *str, OptionValue *v) | ||
| 345 | { | ||
| 346 | // "0" is allowed for compatibility and is the same as "" | ||
| 347 | if (str[0] == '0' && str[1] == '\0') { | ||
| 348 | v->uint_val = 0; | ||
| 349 | return true; | ||
| 350 | } | ||
| 351 | |||
| 352 | const char *const *values = d->u.enum_opt.values; | ||
| 353 | unsigned int flags = 0; | ||
| 354 | |||
| 355 | for (size_t pos = 0, len = strlen(str); pos < len; ) { | ||
| 356 | const StringView flag = get_delim(str, &pos, len, ','); | ||
| 357 | size_t i; | ||
| 358 | for (i = 0; values[i]; i++) { | ||
| 359 | if (strview_equal_cstring(&flag, values[i])) { | ||
| 360 | flags |= 1u << i; | ||
| 361 | break; | ||
| 362 | } | ||
| 363 | } | ||
| 364 | if (unlikely(!values[i])) { | ||
| 365 | int flen = (int)flag.length; | ||
| 366 | return error_msg("Invalid flag '%.*s' for %s", flen, flag.data, d->name); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | |||
| 370 | v->uint_val = flags; | ||
| 371 | return true; | ||
| 372 | } | ||
| 373 | |||
| 374 | static const char *flag_string(const OptionDesc *desc, OptionValue value) | ||
| 375 | { | ||
| 376 | static char buf[128]; | ||
| 377 | unsigned int flags = value.uint_val; | ||
| 378 | if (!flags) { | ||
| 379 | buf[0] = '0'; | ||
| 380 | buf[1] = '\0'; | ||
| 381 | return buf; | ||
| 382 | } | ||
| 383 | |||
| 384 | char *ptr = buf; | ||
| 385 | const char *const *values = desc->u.enum_opt.values; | ||
| 386 | for (size_t i = 0, avail = sizeof(buf); values[i]; i++) { | ||
| 387 | if (flags & (1u << i)) { | ||
| 388 | char *next = memccpy(ptr, values[i], '\0', avail); | ||
| 389 | if (DEBUG >= 1) { | ||
| 390 | BUG_ON(!next); | ||
| 391 | avail -= (size_t)(next - ptr); | ||
| 392 | } | ||
| 393 | ptr = next; | ||
| 394 | ptr[-1] = ','; | ||
| 395 | } | ||
| 396 | } | ||
| 397 | |||
| 398 | BUG_ON(ptr == buf); | ||
| 399 | ptr[-1] = '\0'; | ||
| 400 | return buf; | ||
| 401 | } | ||
| 402 | |||
| 403 | static const struct { | ||
| 404 | OptionValue (*get)(const OptionDesc *desc, void *ptr); | ||
| 405 | void (*set)(const OptionDesc *desc, void *ptr, OptionValue value); | ||
| 406 | bool (*parse)(const OptionDesc *desc, const char *str, OptionValue *value); | ||
| 407 | const char *(*string)(const OptionDesc *desc, OptionValue value); | ||
| 408 | bool (*equals)(const OptionDesc *desc, void *ptr, OptionValue value); | ||
| 409 | } option_ops[] = { | ||
| 410 | [OPT_STR] = {str_get, str_set, str_parse, str_string, str_equals}, | ||
| 411 | [OPT_UINT] = {uint_get, uint_set, uint_parse, uint_string, uint_equals}, | ||
| 412 | [OPT_ENUM] = {uint_get, uint_set, enum_parse, enum_string, uint_equals}, | ||
| 413 | [OPT_BOOL] = {bool_get, bool_set, bool_parse, bool_string, bool_equals}, | ||
| 414 | [OPT_FLAG] = {uint_get, uint_set, flag_parse, flag_string, uint_equals}, | ||
| 415 | [OPT_REGEX] = {re_get, re_set, re_parse, str_string, re_equals}, | ||
| 416 | }; | ||
| 417 | |||
| 418 | static const char *const bool_enum[] = {"false", "true", NULL}; | ||
| 419 | static const char *const newline_enum[] = {"unix", "dos", NULL}; | ||
| 420 | static const char *const tristate_enum[] = {"false", "true", "auto", NULL}; | ||
| 421 | static const char *const save_unmodified_enum[] = {"none", "touch", "full", NULL}; | ||
| 422 | |||
| 423 | static const char *const detect_indent_values[] = { | ||
| 424 | "1", "2", "3", "4", "5", "6", "7", "8", | ||
| 425 | NULL | ||
| 426 | }; | ||
| 427 | |||
| 428 | // Note: this must be kept in sync with WhitespaceErrorFlags | ||
| 429 | static const char *const ws_error_values[] = { | ||
| 430 | "space-indent", | ||
| 431 | "space-align", | ||
| 432 | "tab-indent", | ||
| 433 | "tab-after-indent", | ||
| 434 | "special", | ||
| 435 | "auto-indent", | ||
| 436 | "trailing", | ||
| 437 | "all-trailing", | ||
| 438 | NULL | ||
| 439 | }; | ||
| 440 | |||
| 441 | static const OptionDesc option_desc[] = { | ||
| 442 | BOOL_OPT("auto-indent", C(auto_indent), NULL), | ||
| 443 | BOOL_OPT("brace-indent", L(brace_indent), NULL), | ||
| 444 | ENUM_OPT("case-sensitive-search", G(case_sensitive_search), tristate_enum, NULL), | ||
| 445 | FLAG_OPT("detect-indent", C(detect_indent), detect_indent_values, NULL), | ||
| 446 | BOOL_OPT("display-special", G(display_special), redraw_screen), | ||
| 447 | BOOL_OPT("editorconfig", C(editorconfig), NULL), | ||
| 448 | BOOL_OPT("emulate-tab", C(emulate_tab), NULL), | ||
| 449 | UINT_OPT("esc-timeout", G(esc_timeout), 0, 2000, NULL), | ||
| 450 | BOOL_OPT("expand-tab", C(expand_tab), redraw_buffer), | ||
| 451 | BOOL_OPT("file-history", C(file_history), NULL), | ||
| 452 | UINT_OPT("filesize-limit", G(filesize_limit), 0, 16000, NULL), | ||
| 453 | STR_OPT("filetype", L(filetype), validate_filetype, filetype_changed), | ||
| 454 | BOOL_OPT("fsync", C(fsync), NULL), | ||
| 455 | REGEX_OPT("indent-regex", L(indent_regex), NULL), | ||
| 456 | UINT_OPT("indent-width", C(indent_width), 1, INDENT_WIDTH_MAX, NULL), | ||
| 457 | BOOL_OPT("lock-files", G(lock_files), NULL), | ||
| 458 | ENUM_OPT("newline", G(crlf_newlines), newline_enum, NULL), | ||
| 459 | BOOL_OPT("optimize-true-color", G(optimize_true_color), redraw_screen), | ||
| 460 | BOOL_OPT("overwrite", C(overwrite), overwrite_changed), | ||
| 461 | ENUM_OPT("save-unmodified", C(save_unmodified), save_unmodified_enum, NULL), | ||
| 462 | UINT_OPT("scroll-margin", G(scroll_margin), 0, 100, redraw_screen), | ||
| 463 | BOOL_OPT("select-cursor-char", G(select_cursor_char), redraw_screen), | ||
| 464 | BOOL_OPT("set-window-title", G(set_window_title), set_window_title_changed), | ||
| 465 | BOOL_OPT("show-line-numbers", G(show_line_numbers), redraw_screen), | ||
| 466 | STR_OPT("statusline-left", G(statusline_left), validate_statusline_format, NULL), | ||
| 467 | STR_OPT("statusline-right", G(statusline_right), validate_statusline_format, NULL), | ||
| 468 | BOOL_OPT("syntax", C(syntax), syntax_changed), | ||
| 469 | BOOL_OPT("tab-bar", G(tab_bar), redraw_screen), | ||
| 470 | UINT_OPT("tab-width", C(tab_width), 1, TAB_WIDTH_MAX, redraw_buffer), | ||
| 471 | UINT_OPT("text-width", C(text_width), 1, TEXT_WIDTH_MAX, NULL), | ||
| 472 | BOOL_OPT("utf8-bom", G(utf8_bom), NULL), | ||
| 473 | FLAG_OPT("ws-error", C(ws_error), ws_error_values, redraw_buffer), | ||
| 474 | }; | ||
| 475 | |||
| 476 | static char *local_ptr(const OptionDesc *desc, LocalOptions *opt) | ||
| 477 | { | ||
| 478 | BUG_ON(!desc->local); | ||
| 479 | return (char*)opt + desc->offset; | ||
| 480 | } | ||
| 481 | |||
| 482 | static char *global_ptr(const OptionDesc *desc, GlobalOptions *opt) | ||
| 483 | { | ||
| 484 | BUG_ON(!desc->global); | ||
| 485 | return (char*)opt + desc->offset; | ||
| 486 | } | ||
| 487 | |||
| 488 | static char *get_option_ptr(EditorState *e, const OptionDesc *d, bool global) | ||
| 489 | { | ||
| 490 | return global ? global_ptr(d, &e->options) : local_ptr(d, &e->buffer->options); | ||
| 491 | } | ||
| 492 | |||
| 493 | static inline size_t count_enum_values(const OptionDesc *desc) | ||
| 494 | { | ||
| 495 | OptionType type = desc->type; | ||
| 496 | BUG_ON(type != OPT_ENUM && type != OPT_FLAG && type != OPT_BOOL); | ||
| 497 | const char *const *values = desc->u.enum_opt.values; | ||
| 498 | BUG_ON(!values); | ||
| 499 | return string_array_length((char**)values); | ||
| 500 | } | ||
| 501 | |||
| 502 | UNITTEST { | ||
| 503 | static const struct { | ||
| 504 | size_t alignment; | ||
| 505 | size_t size; | ||
| 506 | } map[] = { | ||
| 507 | [OPT_STR] = {ALIGNOF(const char*), sizeof(const char*)}, | ||
| 508 | [OPT_UINT] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 509 | [OPT_ENUM] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 510 | [OPT_BOOL] = {ALIGNOF(bool), sizeof(bool)}, | ||
| 511 | [OPT_FLAG] = {ALIGNOF(unsigned int), sizeof(unsigned int)}, | ||
| 512 | [OPT_REGEX] = {ALIGNOF(const InternedRegexp*), sizeof(const InternedRegexp*)}, | ||
| 513 | }; | ||
| 514 | |||
| 515 | GlobalOptions gopts = {.tab_bar = true}; | ||
| 516 | LocalOptions lopts = {.filetype = NULL}; | ||
| 517 | |||
| 518 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 519 | const OptionDesc *desc = &option_desc[i]; | ||
| 520 | const OptionType type = desc->type; | ||
| 521 | BUG_ON(type >= ARRAYLEN(map)); | ||
| 522 | size_t alignment = map[type].alignment; | ||
| 523 | size_t end = desc->offset + map[type].size; | ||
| 524 | if (desc->global) { | ||
| 525 | uintptr_t ptr_val = (uintptr_t)global_ptr(desc, &gopts); | ||
| 526 | BUG_ON(ptr_val % alignment != 0); | ||
| 527 | BUG_ON(end > sizeof(GlobalOptions)); | ||
| 528 | } | ||
| 529 | if (desc->local) { | ||
| 530 | uintptr_t ptr_val = (uintptr_t)local_ptr(desc, &lopts); | ||
| 531 | BUG_ON(ptr_val % alignment != 0); | ||
| 532 | BUG_ON(end > sizeof(LocalOptions)); | ||
| 533 | } | ||
| 534 | if (desc->global && desc->local) { | ||
| 535 | BUG_ON(end > sizeof(CommonOptions)); | ||
| 536 | } | ||
| 537 | if (type == OPT_UINT) { | ||
| 538 | BUG_ON(desc->u.uint_opt.max <= desc->u.uint_opt.min); | ||
| 539 | } else if (type == OPT_BOOL) { | ||
| 540 | BUG_ON(desc->u.enum_opt.values != bool_enum); | ||
| 541 | } else if (type == OPT_ENUM) { | ||
| 542 | BUG_ON(count_enum_values(desc) < 2); | ||
| 543 | } else if (type == OPT_FLAG) { | ||
| 544 | size_t nvals = count_enum_values(desc); | ||
| 545 | OptionValue val = {.uint_val = -1}; | ||
| 546 | BUG_ON(nvals < 2); | ||
| 547 | BUG_ON(nvals >= BITSIZE(val.uint_val)); | ||
| 548 | const char *str = flag_string(desc, val); | ||
| 549 | BUG_ON(!str); | ||
| 550 | BUG_ON(str[0] == '\0'); | ||
| 551 | if (!flag_parse(desc, str, &val)) { | ||
| 552 | BUG("flag_parse() failed for string: %s", str); | ||
| 553 | } | ||
| 554 | unsigned int mask = (1u << nvals) - 1; | ||
| 555 | if (val.uint_val != mask) { | ||
| 556 | BUG("values not equal: %u, %u", val.uint_val, mask); | ||
| 557 | } | ||
| 558 | } | ||
| 559 | } | ||
| 560 | |||
| 561 | // Ensure option_desc[] is properly sorted | ||
| 562 | CHECK_BSEARCH_ARRAY(option_desc, name, strcmp); | ||
| 563 | } | ||
| 564 | |||
| 565 | static OptionValue desc_get(const OptionDesc *desc, void *ptr) | ||
| 566 | { | ||
| 567 | return option_ops[desc->type].get(desc, ptr); | ||
| 568 | } | ||
| 569 | |||
| 570 | static void desc_set(EditorState *e, const OptionDesc *desc, void *ptr, bool global, OptionValue value) | ||
| 571 | { | ||
| 572 | option_ops[desc->type].set(desc, ptr, value); | ||
| 573 | if (desc->on_change) { | ||
| 574 | desc->on_change(e, global); | ||
| 575 | } | ||
| 576 | } | ||
| 577 | |||
| 578 | static bool desc_parse(const OptionDesc *desc, const char *str, OptionValue *value) | ||
| 579 | { | ||
| 580 | return option_ops[desc->type].parse(desc, str, value); | ||
| 581 | } | ||
| 582 | |||
| 583 | static const char *desc_string(const OptionDesc *desc, OptionValue value) | ||
| 584 | { | ||
| 585 | return option_ops[desc->type].string(desc, value); | ||
| 586 | } | ||
| 587 | |||
| 588 | static bool desc_equals(const OptionDesc *desc, void *ptr, OptionValue value) | ||
| 589 | { | ||
| 590 | return option_ops[desc->type].equals(desc, ptr, value); | ||
| 591 | } | ||
| 592 | |||
| 593 | static int option_cmp(const void *key, const void *elem) | ||
| 594 | { | ||
| 595 | const char *name = key; | ||
| 596 | const OptionDesc *desc = elem; | ||
| 597 | return strcmp(name, desc->name); | ||
| 598 | } | ||
| 599 | |||
| 600 | static const OptionDesc *find_option(const char *name) | ||
| 601 | { | ||
| 602 | return BSEARCH(name, option_desc, option_cmp); | ||
| 603 | } | ||
| 604 | |||
| 605 | static const OptionDesc *must_find_option(const char *name) | ||
| 606 | { | ||
| 607 | const OptionDesc *desc = find_option(name); | ||
| 608 | if (!desc) { | ||
| 609 | error_msg("No such option %s", name); | ||
| 610 | } | ||
| 611 | return desc; | ||
| 612 | } | ||
| 613 | |||
| 614 | static const OptionDesc *must_find_global_option(const char *name) | ||
| 615 | { | ||
| 616 | const OptionDesc *desc = must_find_option(name); | ||
| 617 | if (desc && !desc->global) { | ||
| 618 | error_msg("Option %s is not global", name); | ||
| 619 | return NULL; | ||
| 620 | } | ||
| 621 | return desc; | ||
| 622 | } | ||
| 623 | |||
| 624 | static bool do_set_option ( | ||
| 625 | EditorState *e, | ||
| 626 | const OptionDesc *desc, | ||
| 627 | const char *value, | ||
| 628 | bool local, | ||
| 629 | bool global | ||
| 630 | ) { | ||
| 631 | if (local && !desc->local) { | ||
| 632 | return error_msg("Option %s is not local", desc->name); | ||
| 633 | } | ||
| 634 | if (global && !desc->global) { | ||
| 635 | return error_msg("Option %s is not global", desc->name); | ||
| 636 | } | ||
| 637 | |||
| 638 | OptionValue val; | ||
| 639 | if (!desc_parse(desc, value, &val)) { | ||
| 640 | return false; | ||
| 641 | } | ||
| 642 | |||
| 643 | if (!local && !global) { | ||
| 644 | // Set both by default | ||
| 645 | local = desc->local; | ||
| 646 | global = desc->global; | ||
| 647 | } | ||
| 648 | |||
| 649 | if (local) { | ||
| 650 | desc_set(e, desc, local_ptr(desc, &e->buffer->options), false, val); | ||
| 651 | } | ||
| 652 | if (global) { | ||
| 653 | desc_set(e, desc, global_ptr(desc, &e->options), true, val); | ||
| 654 | } | ||
| 655 | |||
| 656 | return true; | ||
| 657 | } | ||
| 658 | |||
| 659 | bool set_option(EditorState *e, const char *name, const char *value, bool local, bool global) | ||
| 660 | { | ||
| 661 | const OptionDesc *desc = must_find_option(name); | ||
| 662 | if (!desc) { | ||
| 663 | return false; | ||
| 664 | } | ||
| 665 | return do_set_option(e, desc, value, local, global); | ||
| 666 | } | ||
| 667 | |||
| 668 | bool set_bool_option(EditorState *e, const char *name, bool local, bool global) | ||
| 669 | { | ||
| 670 | const OptionDesc *desc = must_find_option(name); | ||
| 671 | if (!desc) { | ||
| 672 | return false; | ||
| 673 | } | ||
| 674 | if (desc->type != OPT_BOOL) { | ||
| 675 | return error_msg("Option %s is not boolean", desc->name); | ||
| 676 | } | ||
| 677 | return do_set_option(e, desc, "true", local, global); | ||
| 678 | } | ||
| 679 | |||
| 680 | static const OptionDesc *find_toggle_option(const char *name, bool *global) | ||
| 681 | { | ||
| 682 | if (*global) { | ||
| 683 | return must_find_global_option(name); | ||
| 684 | } | ||
| 685 | |||
| 686 | // Toggle local value by default if option has both values | ||
| 687 | const OptionDesc *desc = must_find_option(name); | ||
| 688 | if (desc && !desc->local) { | ||
| 689 | *global = true; | ||
| 690 | } | ||
| 691 | return desc; | ||
| 692 | } | ||
| 693 | |||
| 694 | bool toggle_option(EditorState *e, const char *name, bool global, bool verbose) | ||
| 695 | { | ||
| 696 | const OptionDesc *desc = find_toggle_option(name, &global); | ||
| 697 | if (!desc) { | ||
| 698 | return false; | ||
| 699 | } | ||
| 700 | |||
| 701 | char *ptr = get_option_ptr(e, desc, global); | ||
| 702 | OptionValue value = desc_get(desc, ptr); | ||
| 703 | OptionType type = desc->type; | ||
| 704 | if (type == OPT_ENUM) { | ||
| 705 | if (desc->u.enum_opt.values[value.uint_val + 1]) { | ||
| 706 | value.uint_val++; | ||
| 707 | } else { | ||
| 708 | value.uint_val = 0; | ||
| 709 | } | ||
| 710 | } else if (type == OPT_BOOL) { | ||
| 711 | value.bool_val = !value.bool_val; | ||
| 712 | } else { | ||
| 713 | return error_msg("Toggling %s requires arguments", name); | ||
| 714 | } | ||
| 715 | |||
| 716 | desc_set(e, desc, ptr, global, value); | ||
| 717 | if (verbose) { | ||
| 718 | const char *prefix = (global && desc->local) ? "[global] " : ""; | ||
| 719 | const char *str = desc_string(desc, value); | ||
| 720 | info_msg("%s%s = %s", prefix, desc->name, str); | ||
| 721 | } | ||
| 722 | |||
| 723 | return true; | ||
| 724 | } | ||
| 725 | |||
| 726 | bool toggle_option_values ( | ||
| 727 | EditorState *e, | ||
| 728 | const char *name, | ||
| 729 | bool global, | ||
| 730 | bool verbose, | ||
| 731 | char **values, | ||
| 732 | size_t count | ||
| 733 | ) { | ||
| 734 | const OptionDesc *desc = find_toggle_option(name, &global); | ||
| 735 | if (!desc) { | ||
| 736 | return false; | ||
| 737 | } | ||
| 738 | |||
| 739 | BUG_ON(count == 0); | ||
| 740 | size_t current = 0; | ||
| 741 | bool error = false; | ||
| 742 | char *ptr = get_option_ptr(e, desc, global); | ||
| 743 | OptionValue *parsed_values = xnew(OptionValue, count); | ||
| 744 | |||
| 745 | for (size_t i = 0; i < count; i++) { | ||
| 746 | if (desc_parse(desc, values[i], &parsed_values[i])) { | ||
| 747 | if (desc_equals(desc, ptr, parsed_values[i])) { | ||
| 748 | current = i + 1; | ||
| 749 | } | ||
| 750 | } else { | ||
| 751 | error = true; | ||
| 752 | } | ||
| 753 | } | ||
| 754 | |||
| 755 | if (!error) { | ||
| 756 | size_t i = current % count; | ||
| 757 | desc_set(e, desc, ptr, global, parsed_values[i]); | ||
| 758 | if (verbose) { | ||
| 759 | const char *prefix = (global && desc->local) ? "[global] " : ""; | ||
| 760 | const char *str = desc_string(desc, parsed_values[i]); | ||
| 761 | info_msg("%s%s = %s", prefix, desc->name, str); | ||
| 762 | } | ||
| 763 | } | ||
| 764 | |||
| 765 | free(parsed_values); | ||
| 766 | return !error; | ||
| 767 | } | ||
| 768 | |||
| 769 | bool validate_local_options(char **strs) | ||
| 770 | { | ||
| 771 | size_t invalid = 0; | ||
| 772 | for (size_t i = 0; strs[i]; i += 2) { | ||
| 773 | const char *name = strs[i]; | ||
| 774 | const char *value = strs[i + 1]; | ||
| 775 | const OptionDesc *desc = must_find_option(name); | ||
| 776 | if (unlikely(!desc)) { | ||
| 777 | invalid++; | ||
| 778 | } else if (unlikely(!desc->local)) { | ||
| 779 | error_msg("%s is not local", name); | ||
| 780 | invalid++; | ||
| 781 | } else if (unlikely(desc->on_change == filetype_changed)) { | ||
| 782 | error_msg("filetype cannot be set via option command"); | ||
| 783 | invalid++; | ||
| 784 | } else { | ||
| 785 | OptionValue val; | ||
| 786 | if (unlikely(!desc_parse(desc, value, &val))) { | ||
| 787 | invalid++; | ||
| 788 | } | ||
| 789 | } | ||
| 790 | } | ||
| 791 | return !invalid; | ||
| 792 | } | ||
| 793 | |||
| 794 | #if DEBUG >= 1 | ||
| 795 | static void sanity_check_option_value(const OptionDesc *desc, OptionValue val) | ||
| 796 | { | ||
| 797 | switch (desc->type) { | ||
| 798 | case OPT_STR: | ||
| 799 | BUG_ON(!val.str_val); | ||
| 800 | BUG_ON(val.str_val != str_intern(val.str_val)); | ||
| 801 | if (desc->u.str_opt.validate) { | ||
| 802 | BUG_ON(!desc->u.str_opt.validate(val.str_val)); | ||
| 803 | } | ||
| 804 | return; | ||
| 805 | case OPT_UINT: | ||
| 806 | BUG_ON(val.uint_val < desc->u.uint_opt.min); | ||
| 807 | BUG_ON(val.uint_val > desc->u.uint_opt.max); | ||
| 808 | return; | ||
| 809 | case OPT_ENUM: | ||
| 810 | BUG_ON(val.uint_val >= count_enum_values(desc)); | ||
| 811 | return; | ||
| 812 | case OPT_FLAG: { | ||
| 813 | size_t nvals = count_enum_values(desc); | ||
| 814 | BUG_ON(nvals >= 32); | ||
| 815 | unsigned int mask = (1u << nvals) - 1; | ||
| 816 | unsigned int uint_val = val.uint_val; | ||
| 817 | BUG_ON((uint_val & mask) != uint_val); | ||
| 818 | } | ||
| 819 | return; | ||
| 820 | case OPT_REGEX: | ||
| 821 | BUG_ON(val.str_val && val.str_val[0] == '\0'); | ||
| 822 | BUG_ON(val.str_val && !regexp_is_interned(val.str_val)); | ||
| 823 | return; | ||
| 824 | case OPT_BOOL: | ||
| 825 | return; | ||
| 826 | } | ||
| 827 | |||
| 828 | BUG("unhandled option type"); | ||
| 829 | } | ||
| 830 | |||
| 831 | static void sanity_check_options(const void *opts, bool global) | ||
| 832 | { | ||
| 833 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 834 | const OptionDesc *desc = &option_desc[i]; | ||
| 835 | BUG_ON(desc->type >= ARRAYLEN(option_ops)); | ||
| 836 | if ((desc->global && desc->local) || global == desc->global) { | ||
| 837 | OptionValue val = desc_get(desc, (char*)opts + desc->offset); | ||
| 838 | sanity_check_option_value(desc, val); | ||
| 839 | } | ||
| 840 | } | ||
| 841 | } | ||
| 842 | |||
| 843 | void sanity_check_global_options(const GlobalOptions *gopts) | ||
| 844 | { | ||
| 845 | sanity_check_options(gopts, true); | ||
| 846 | } | ||
| 847 | |||
| 848 | void sanity_check_local_options(const LocalOptions *lopts) | ||
| 849 | { | ||
| 850 | sanity_check_options(lopts, false); | ||
| 851 | } | ||
| 852 | #endif | ||
| 853 | |||
| 854 | void collect_options(PointerArray *a, const char *prefix, bool local, bool global) | ||
| 855 | { | ||
| 856 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 857 | const OptionDesc *desc = &option_desc[i]; | ||
| 858 | if ((local && !desc->local) || (global && !desc->global)) { | ||
| 859 | continue; | ||
| 860 | } | ||
| 861 | if (str_has_prefix(desc->name, prefix)) { | ||
| 862 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 863 | } | ||
| 864 | } | ||
| 865 | } | ||
| 866 | |||
| 867 | // Collect options that can be set via the "option" command | ||
| 868 | void collect_auto_options(PointerArray *a, const char *prefix) | ||
| 869 | { | ||
| 870 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 871 | const OptionDesc *desc = &option_desc[i]; | ||
| 872 | if (!desc->local || desc->on_change == filetype_changed) { | ||
| 873 | continue; | ||
| 874 | } | ||
| 875 | if (str_has_prefix(desc->name, prefix)) { | ||
| 876 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 877 | } | ||
| 878 | } | ||
| 879 | } | ||
| 880 | |||
| 881 | void collect_toggleable_options(PointerArray *a, const char *prefix, bool global) | ||
| 882 | { | ||
| 883 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 884 | const OptionDesc *desc = &option_desc[i]; | ||
| 885 | if (global && !desc->global) { | ||
| 886 | continue; | ||
| 887 | } | ||
| 888 | OptionType type = desc->type; | ||
| 889 | bool toggleable = (type == OPT_ENUM || type == OPT_BOOL); | ||
| 890 | if (toggleable && str_has_prefix(desc->name, prefix)) { | ||
| 891 | ptr_array_append(a, xstrdup(desc->name)); | ||
| 892 | } | ||
| 893 | } | ||
| 894 | } | ||
| 895 | |||
| 896 | void collect_option_values(EditorState *e, PointerArray *a, const char *option, const char *prefix) | ||
| 897 | { | ||
| 898 | const OptionDesc *desc = find_option(option); | ||
| 899 | if (!desc) { | ||
| 900 | return; | ||
| 901 | } | ||
| 902 | |||
| 903 | if (prefix[0] == '\0') { | ||
| 904 | char *ptr = get_option_ptr(e, desc, !desc->local); | ||
| 905 | OptionValue value = desc_get(desc, ptr); | ||
| 906 | ptr_array_append(a, xstrdup(desc_string(desc, value))); | ||
| 907 | return; | ||
| 908 | } | ||
| 909 | |||
| 910 | OptionType type = desc->type; | ||
| 911 | if (type == OPT_STR || type == OPT_UINT || type == OPT_REGEX) { | ||
| 912 | return; | ||
| 913 | } | ||
| 914 | |||
| 915 | const char *const *values = desc->u.enum_opt.values; | ||
| 916 | if (type == OPT_ENUM || type == OPT_BOOL) { | ||
| 917 | for (size_t i = 0; values[i]; i++) { | ||
| 918 | if (str_has_prefix(values[i], prefix)) { | ||
| 919 | ptr_array_append(a, xstrdup(values[i])); | ||
| 920 | } | ||
| 921 | } | ||
| 922 | return; | ||
| 923 | } | ||
| 924 | |||
| 925 | BUG_ON(type != OPT_FLAG); | ||
| 926 | const char *comma = strrchr(prefix, ','); | ||
| 927 | size_t prefix_len = comma ? ++comma - prefix : 0; | ||
| 928 | for (size_t i = 0; values[i]; i++) { | ||
| 929 | const char *str = values[i]; | ||
| 930 | if (str_has_prefix(str, prefix + prefix_len)) { | ||
| 931 | size_t str_len = strlen(str); | ||
| 932 | char *completion = xmalloc(prefix_len + str_len + 1); | ||
| 933 | memcpy(completion, prefix, prefix_len); | ||
| 934 | memcpy(completion + prefix_len, str, str_len + 1); | ||
| 935 | ptr_array_append(a, completion); | ||
| 936 | } | ||
| 937 | } | ||
| 938 | } | ||
| 939 | |||
| 940 | static void append_option(String *s, const OptionDesc *desc, void *ptr) | ||
| 941 | { | ||
| 942 | const OptionValue value = desc_get(desc, ptr); | ||
| 943 | const char *value_str = desc_string(desc, value); | ||
| 944 | if (unlikely(value_str[0] == '-')) { | ||
| 945 | string_append_literal(s, "-- "); | ||
| 946 | } | ||
| 947 | string_append_cstring(s, desc->name); | ||
| 948 | string_append_byte(s, ' '); | ||
| 949 | string_append_escaped_arg(s, value_str, true); | ||
| 950 | string_append_byte(s, '\n'); | ||
| 951 | } | ||
| 952 | |||
| 953 | String dump_options(GlobalOptions *gopts, LocalOptions *lopts) | ||
| 954 | { | ||
| 955 | String buf = string_new(4096); | ||
| 956 | for (size_t i = 0; i < ARRAYLEN(option_desc); i++) { | ||
| 957 | const OptionDesc *desc = &option_desc[i]; | ||
| 958 | void *local = desc->local ? local_ptr(desc, lopts) : NULL; | ||
| 959 | void *global = desc->global ? global_ptr(desc, gopts) : NULL; | ||
| 960 | if (local && global) { | ||
| 961 | const OptionValue global_value = desc_get(desc, global); | ||
| 962 | if (desc_equals(desc, local, global_value)) { | ||
| 963 | string_append_literal(&buf, "set "); | ||
| 964 | append_option(&buf, desc, local); | ||
| 965 | } else { | ||
| 966 | string_append_literal(&buf, "set -g "); | ||
| 967 | append_option(&buf, desc, global); | ||
| 968 | string_append_literal(&buf, "set -l "); | ||
| 969 | append_option(&buf, desc, local); | ||
| 970 | } | ||
| 971 | } else { | ||
| 972 | string_append_literal(&buf, "set "); | ||
| 973 | append_option(&buf, desc, local ? local : global); | ||
| 974 | } | ||
| 975 | } | ||
| 976 | return buf; | ||
| 977 | } | ||
| 978 | |||
| 979 | const char *get_option_value_string(EditorState *e, const char *name) | ||
| 980 | { | ||
| 981 | const OptionDesc *desc = find_option(name); | ||
| 982 | if (!desc) { | ||
| 983 | return NULL; | ||
| 984 | } | ||
| 985 | char *ptr = get_option_ptr(e, desc, !desc->local); | ||
| 986 | return desc_string(desc, desc_get(desc, ptr)); | ||
| 987 | } | ||
