gen_options.lua (14249B)
1 local options_input_file = arg[5] 2 3 --- @module 'nvim.options' 4 local options = loadfile(options_input_file)() 5 local options_meta = options.options 6 local cstr = options.cstr 7 local valid_scopes = options.valid_scopes 8 9 --- @param o vim.option_meta 10 --- @return string 11 local function get_values_var(o) 12 return ('opt_%s_values'):format(o.abbreviation or o.full_name) 13 end 14 15 --- @param s string 16 --- @return string 17 local function lowercase_to_titlecase(s) 18 return table.concat(vim.tbl_map(function(word) --- @param word string 19 return word:sub(1, 1):upper() .. word:sub(2) 20 end, vim.split(s, '[-_]'))) 21 end 22 23 --- @param scope string 24 --- @param option_name string 25 --- @return string 26 local function get_scope_option(scope, option_name) 27 return ('k%sOpt%s'):format(lowercase_to_titlecase(scope), lowercase_to_titlecase(option_name)) 28 end 29 30 local redraw_flags = { 31 ui_option = 'kOptFlagUIOption', 32 tabline = 'kOptFlagRedrTabl', 33 statuslines = 'kOptFlagRedrStat', 34 current_window = 'kOptFlagRedrWin', 35 current_buffer = 'kOptFlagRedrBuf', 36 all_windows = 'kOptFlagRedrAll', 37 curswant = 'kOptFlagCurswant', 38 highlight_only = 'kOptFlagHLOnly', 39 } 40 41 local list_flags = { 42 comma = 'kOptFlagComma', 43 onecomma = 'kOptFlagOneComma', 44 commacolon = 'kOptFlagComma|kOptFlagColon', 45 onecommacolon = 'kOptFlagOneComma|kOptFlagColon', 46 flags = 'kOptFlagFlagList', 47 flagscomma = 'kOptFlagComma|kOptFlagFlagList', 48 } 49 50 --- @param o vim.option_meta 51 --- @return string 52 local function get_flags(o) 53 --- @type string[] 54 local flags = { '0' } 55 56 --- @param f string 57 local function add_flag(f) 58 table.insert(flags, f) 59 end 60 61 if o.list then 62 add_flag(list_flags[o.list]) 63 end 64 65 for _, r_flag in ipairs(o.redraw or {}) do 66 add_flag(redraw_flags[r_flag]) 67 end 68 69 if o.expand then 70 add_flag('kOptFlagExpand') 71 if o.expand == 'nodefault' then 72 add_flag('kOptFlagNoDefExp') 73 end 74 end 75 76 for _, flag_desc in ipairs({ 77 { 'nodefault', 'NoDefault' }, 78 { 'no_mkrc', 'NoMkrc' }, 79 { 'secure' }, 80 { 'gettext' }, 81 { 'noglob', 'NoGlob' }, 82 { 'normal_fname_chars', 'NFname' }, 83 { 'normal_dname_chars', 'NDname' }, 84 { 'pri_mkrc', 'PriMkrc' }, 85 { 'deny_in_modelines', 'NoML' }, 86 { 'deny_duplicates', 'NoDup' }, 87 { 'modelineexpr', 'MLE' }, 88 { 'func' }, 89 }) do 90 local key_name, flag_suffix = flag_desc[1], flag_desc[2] 91 if o[key_name] then 92 local def_name = 'kOptFlag' .. (flag_suffix or lowercase_to_titlecase(key_name)) 93 add_flag(def_name) 94 end 95 end 96 97 return table.concat(flags, '|') 98 end 99 100 --- @param opt_type vim.option_type 101 --- @return string 102 local function opt_type_enum(opt_type) 103 return ('kOptValType%s'):format(lowercase_to_titlecase(opt_type)) 104 end 105 106 --- @param scope vim.option_scope 107 --- @return string 108 local function opt_scope_enum(scope) 109 return ('kOptScope%s'):format(lowercase_to_titlecase(scope)) 110 end 111 112 --- @param o vim.option_meta 113 --- @return string 114 local function get_scope_flags(o) 115 local scope_flags = '0' 116 117 for _, scope in ipairs(o.scope) do 118 scope_flags = ('%s | (1 << %s)'):format(scope_flags, opt_scope_enum(scope)) 119 end 120 121 return scope_flags 122 end 123 124 --- @param o vim.option_meta 125 --- @return string 126 local function get_scope_idx(o) 127 --- @type string[] 128 local strs = {} 129 130 for _, scope in pairs(valid_scopes) do 131 local has_scope = vim.tbl_contains(o.scope, scope) 132 strs[#strs + 1] = (' [%s] = %s'):format( 133 opt_scope_enum(scope), 134 get_scope_option(scope, has_scope and o.full_name or 'Invalid') 135 ) 136 end 137 138 return ('{\n%s\n }'):format(table.concat(strs, ',\n')) 139 end 140 141 --- @param s string 142 --- @return string 143 local function static_cstr_as_string(s) 144 return ('{ .data = %s, .size = sizeof(%s) - 1 }'):format(s, s) 145 end 146 147 --- @param v vim.option_value|function 148 --- @return string 149 local function get_opt_val(v) 150 --- @type vim.option_type 151 local v_type 152 153 if type(v) == 'function' then 154 v, v_type = v() --[[ @as string, vim.option_type ]] 155 156 if v_type == 'string' then 157 v = static_cstr_as_string(v) 158 end 159 else 160 v_type = type(v) --[[ @as vim.option_type ]] 161 162 if v_type == 'boolean' then 163 v = v and 'true' or 'false' 164 elseif v_type == 'number' then 165 v = ('%iL'):format(v) 166 elseif v_type == 'string' then 167 --- @cast v string 168 v = static_cstr_as_string(cstr(v)) 169 end 170 end 171 172 return ('{ .type = %s, .data.%s = %s }'):format(opt_type_enum(v_type), v_type, v) 173 end 174 175 --- @param d vim.option_value|function 176 --- @param n string 177 --- @return string 178 local function get_defaults(d, n) 179 if d == nil then 180 error("option '" .. n .. "' should have a default value") 181 end 182 return get_opt_val(d) 183 end 184 185 --- @param i integer 186 --- @param o vim.option_meta 187 --- @param write fun(...: string) 188 local function dump_option(i, o, write) 189 write(' [', ('%u'):format(i - 1) .. ']={') 190 write(' .fullname=', cstr(o.full_name)) 191 if o.abbreviation then 192 write(' .shortname=', cstr(o.abbreviation)) 193 end 194 write(' .type=', opt_type_enum(o.type)) 195 write(' .flags=', get_flags(o)) 196 write(' .scope_flags=', get_scope_flags(o)) 197 write(' .scope_idx=', get_scope_idx(o)) 198 write(' .values=', (o.values and get_values_var(o) or 'NULL')) 199 write(' .values_len=', (o.values and #o.values or '0')) 200 write(' .flags_var=', (o.flags_varname and ('&%s'):format(o.flags_varname) or 'NULL')) 201 if o.enable_if then 202 write(('#if defined(%s)'):format(o.enable_if)) 203 end 204 205 local is_window_local = #o.scope == 1 and o.scope[1] == 'win' 206 207 if is_window_local then 208 write(' .var=NULL') 209 elseif o.varname then 210 write(' .var=&', o.varname) 211 elseif o.immutable then 212 -- Immutable options can directly point to the default value. 213 write((' .var=&options[%u].def_val.data'):format(i - 1)) 214 else 215 error('Option must be immutable or have a variable.') 216 end 217 218 write(' .immutable=', (o.immutable and 'true' or 'false')) 219 write(' .opt_did_set_cb=', o.cb or 'NULL') 220 write(' .opt_expand_cb=', o.expand_cb or 'NULL') 221 222 if o.enable_if then 223 write('#else') 224 -- Hidden option directly points to default value. 225 write((' .var=&options[%u].def_val.data'):format(i - 1)) 226 -- Option is always immutable on the false branch of `enable_if`. 227 write(' .immutable=true') 228 write('#endif') 229 end 230 231 if not o.defaults then 232 write(' .def_val=NIL_OPTVAL') 233 elseif o.defaults.condition then 234 write(('#if defined(%s)'):format(o.defaults.condition)) 235 write(' .def_val=', get_defaults(o.defaults.if_true, o.full_name)) 236 if o.defaults.if_false then 237 write('#else') 238 write(' .def_val=', get_defaults(o.defaults.if_false, o.full_name)) 239 end 240 write('#endif') 241 else 242 write(' .def_val=', get_defaults(o.defaults.if_true, o.full_name)) 243 end 244 245 write(' },') 246 end 247 248 --- @param prefix string 249 --- @param values vim.option_valid_values 250 local function preorder_traversal(prefix, values) 251 local out = {} --- @type string[] 252 253 local function add(s) 254 table.insert(out, s) 255 end 256 257 add('') 258 add(('EXTERN const char *(%s_values[%s]) INIT( = {'):format(prefix, #vim.tbl_keys(values) + 1)) 259 260 --- @type [string,vim.option_valid_values][] 261 local children = {} 262 263 for _, value in ipairs(values) do 264 if type(value) == 'string' then 265 add((' "%s",'):format(value)) 266 else 267 assert(type(value) == 'table' and type(value[1]) == 'string' and type(value[2]) == 'table') 268 add((' "%s",'):format(value[1])) 269 table.insert(children, value) 270 end 271 end 272 273 add(' NULL') 274 add('});') 275 276 for _, value in pairs(children) do 277 -- Remove trailing colon from the added prefix to prevent syntax errors. 278 add(preorder_traversal(prefix .. '_' .. value[1]:gsub(':$', ''), value[2])) 279 end 280 281 return table.concat(out, '\n') 282 end 283 284 --- @param o vim.option_meta 285 --- @return string 286 local function gen_opt_enum(o) 287 local out = {} --- @type string[] 288 289 local function add(s) 290 table.insert(out, s) 291 end 292 293 add('') 294 add('typedef enum {') 295 296 local opt_name = lowercase_to_titlecase(o.abbreviation or o.full_name) 297 --- @type table<string,integer> 298 local enum_values 299 300 if type(o.flags) == 'table' then 301 enum_values = o.flags --[[ @as table<string,integer> ]] 302 else 303 enum_values = {} 304 for i, flag_name in ipairs(o.values) do 305 assert(type(flag_name) == 'string') 306 enum_values[flag_name] = math.pow(2, i - 1) 307 end 308 end 309 310 -- Sort the keys by the flag value so that the enum can be generated in order. 311 --- @type string[] 312 local flag_names = vim.tbl_keys(enum_values) 313 table.sort(flag_names, function(a, b) 314 return enum_values[a] < enum_values[b] 315 end) 316 317 for _, flag_name in pairs(flag_names) do 318 add( 319 (' kOpt%sFlag%s = 0x%02x,'):format( 320 opt_name, 321 lowercase_to_titlecase(flag_name:gsub(':$', '')), 322 enum_values[flag_name] 323 ) 324 ) 325 end 326 327 add(('} Opt%sFlags;'):format(opt_name)) 328 329 return table.concat(out, '\n') 330 end 331 332 --- @param output_file string 333 --- @return table<string,string> options_index Map of option name to option index 334 local function gen_enums(output_file) 335 --- Options for each scope. 336 --- @type table<string, vim.option_meta[]> 337 local scope_options = {} 338 for _, scope in ipairs(valid_scopes) do 339 scope_options[scope] = {} 340 end 341 342 local fd = assert(io.open(output_file, 'w')) 343 344 --- @param s string 345 local function write(s) 346 fd:write(s) 347 fd:write('\n') 348 end 349 350 -- Generate options enum file 351 write('// IWYU pragma: private, include "nvim/option_defs.h"') 352 write('') 353 354 --- Map of option name to option index 355 --- @type table<string, string> 356 local option_index = {} 357 358 -- Generate option index enum and populate the `option_index` and `scope_option` dicts. 359 write('typedef enum {') 360 write(' kOptInvalid = -1,') 361 362 for i, o in ipairs(options_meta) do 363 local enum_val_name = 'kOpt' .. lowercase_to_titlecase(o.full_name) 364 write((' %s = %u,'):format(enum_val_name, i - 1)) 365 366 option_index[o.full_name] = enum_val_name 367 368 if o.abbreviation then 369 option_index[o.abbreviation] = enum_val_name 370 end 371 372 local alias = o.alias or {} --[[@as string[] ]] 373 for _, v in ipairs(alias) do 374 option_index[v] = enum_val_name 375 end 376 377 for _, scope in ipairs(o.scope) do 378 table.insert(scope_options[scope], o) 379 end 380 end 381 382 write(' // Option count') 383 write('#define kOptCount ' .. tostring(#options_meta)) 384 write('} OptIndex;') 385 386 -- Generate option index enum for each scope 387 for _, scope in ipairs(valid_scopes) do 388 write('') 389 390 local scope_name = lowercase_to_titlecase(scope) 391 write('typedef enum {') 392 write((' %s = -1,'):format(get_scope_option(scope, 'Invalid'))) 393 394 for idx, option in ipairs(scope_options[scope]) do 395 write((' %s = %u,'):format(get_scope_option(scope, option.full_name), idx - 1)) 396 end 397 398 write((' // %s option count'):format(scope_name)) 399 write(('#define %s %d'):format(get_scope_option(scope, 'Count'), #scope_options[scope])) 400 write(('} %sOptIndex;'):format(scope_name)) 401 end 402 403 -- Generate reverse lookup from option scope index to option index for each scope. 404 for _, scope in ipairs(valid_scopes) do 405 write('') 406 write(('EXTERN const OptIndex %s_opt_idx[] INIT( = {'):format(scope)) 407 for _, option in ipairs(scope_options[scope]) do 408 local idx = option_index[option.full_name] 409 write((' [%s] = %s,'):format(get_scope_option(scope, option.full_name), idx)) 410 end 411 write('});') 412 end 413 414 fd:close() 415 416 return option_index 417 end 418 419 --- @param output_file string 420 --- @param option_index table<string,string> 421 local function gen_map(output_file, option_index) 422 -- Generate option index map. 423 local hashy = require('gen.hashy') 424 425 local neworder, hashfun = hashy.hashy_hash( 426 'find_option', 427 vim.tbl_keys(option_index), 428 function(idx) 429 return ('option_hash_elems[%s].name'):format(idx) 430 end 431 ) 432 433 local fd = assert(io.open(output_file, 'w')) 434 435 --- @param s string 436 local function write(s) 437 fd:write(s) 438 fd:write('\n') 439 end 440 441 write('static const struct { const char *name; OptIndex opt_idx; } option_hash_elems[] = {') 442 443 for _, name in ipairs(neworder) do 444 assert(option_index[name] ~= nil) 445 write((' { .name = "%s", .opt_idx = %s },'):format(name, option_index[name])) 446 end 447 448 write('};') 449 write('') 450 write('static ' .. hashfun) 451 452 fd:close() 453 end 454 455 --- @param output_file string 456 local function gen_vars(output_file) 457 local fd = assert(io.open(output_file, 'w')) 458 459 --- @param s string 460 local function write(s) 461 fd:write(s) 462 fd:write('\n') 463 end 464 465 write('// IWYU pragma: private, include "nvim/option_vars.h"') 466 467 -- Generate enums for option flags. 468 for _, o in ipairs(options_meta) do 469 if o.flags and (type(o.flags) == 'table' or o.values) then 470 write(gen_opt_enum(o)) 471 end 472 end 473 474 -- Generate valid values for each option. 475 for _, option in ipairs(options_meta) do 476 -- Since option values can be nested, we need to do preorder traversal to generate the values. 477 if option.values then 478 local values_var = ('opt_%s'):format(option.abbreviation or option.full_name) 479 write(preorder_traversal(values_var, option.values)) 480 end 481 end 482 483 fd:close() 484 end 485 486 --- @param output_file string 487 local function gen_options(output_file) 488 local fd = assert(io.open(output_file, 'w')) 489 490 --- @param ... string 491 local function write(...) 492 local s = table.concat({ ... }, '') 493 fd:write(s) 494 if s:match('^ %.') then 495 fd:write(',') 496 end 497 fd:write('\n') 498 end 499 500 -- Generate options[] array. 501 write([[ 502 #include "nvim/ex_docmd.h" 503 #include "nvim/ex_getln.h" 504 #include "nvim/insexpand.h" 505 #include "nvim/mapping.h" 506 #include "nvim/ops.h" 507 #include "nvim/option.h" 508 #include "nvim/optionstr.h" 509 #include "nvim/quickfix.h" 510 #include "nvim/runtime.h" 511 #include "nvim/tag.h" 512 #include "nvim/window.h" 513 514 static vimoption_T options[] = {]]) 515 516 for i, o in ipairs(options_meta) do 517 dump_option(i, o, write) 518 end 519 520 write('};') 521 522 fd:close() 523 end 524 525 local function main() 526 local options_file = arg[1] 527 local options_enum_file = arg[2] 528 local options_map_file = arg[3] 529 local option_vars_file = arg[4] 530 531 local option_index = gen_enums(options_enum_file) 532 gen_map(options_map_file, option_index) 533 gen_vars(option_vars_file) 534 gen_options(options_file) 535 end 536 537 main()