neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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()