mirror of
				https://github.com/neovim/neovim.git
				synced 2025-10-26 12:27:24 +00:00 
			
		
		
		
	 9912a4c81b
			
		
	
	9912a4c81b
	
	
	
		
			
			Problem:
Besides being redundant with vim.iter():flatten(), `tbl_flatten` has
these problems:
- Has `tbl_` prefix but only accepts lists.
- Discards some results! Compare the following:
  - iter.flatten():
    ```
    vim.iter({1, { { a = 2 } }, { 3 } }):flatten():totable()
    ```
  - tbl_flatten:
    ```
    vim.tbl_flatten({1, { { a = 2 } }, { 3 } })
    ```
Solution:
Deprecate tbl_flatten.
Note:
iter:flatten() currently fails ("flatten() requires a list-like table")
on this code from gen_lsp.lua:
    local anonym = vim.iter({ -- remove nil
      anonymous_num > 1 and '' or nil,
      '---@class ' .. anonymous_classname,
    }):flatten():totable()
Should we enhance :flatten() to work for arrays?
		
	
		
			
				
	
	
		
			460 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
			
		
		
	
	
			460 lines
		
	
	
		
			14 KiB
		
	
	
	
		
			Lua
		
	
	
	
	
	
| -- Generates lua-ls annotations for lsp.
 | |
| 
 | |
| local USAGE = [[
 | |
| Generates lua-ls annotations for lsp.
 | |
| 
 | |
| USAGE:
 | |
| nvim -l scripts/gen_lsp.lua gen  # by default, this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua
 | |
| nvim -l scripts/gen_lsp.lua gen --version 3.18 --out runtime/lua/vim/lsp/_meta/protocol.lua
 | |
| nvim -l scripts/gen_lsp.lua gen --version 3.18 --methods
 | |
| ]]
 | |
| 
 | |
| local DEFAULT_LSP_VERSION = '3.18'
 | |
| 
 | |
| local M = {}
 | |
| 
 | |
| local function tofile(fname, text)
 | |
|   local f = io.open(fname, 'w')
 | |
|   if not f then
 | |
|     error(('failed to write: %s'):format(f))
 | |
|   else
 | |
|     print(('Written to: %s'):format(fname))
 | |
|     f:write(text)
 | |
|     f:close()
 | |
|   end
 | |
| end
 | |
| 
 | |
| --- The LSP protocol JSON data (it's partial, non-exhaustive).
 | |
| --- https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/3.18/metaModel/metaModel.schema.json
 | |
| --- @class vim._gen_lsp.Protocol
 | |
| --- @field requests vim._gen_lsp.Request[]
 | |
| --- @field notifications vim._gen_lsp.Notification[]
 | |
| --- @field structures vim._gen_lsp.Structure[]
 | |
| --- @field enumerations vim._gen_lsp.Enumeration[]
 | |
| --- @field typeAliases vim._gen_lsp.TypeAlias[]
 | |
| 
 | |
| ---@param opt vim._gen_lsp.opt
 | |
| ---@return vim._gen_lsp.Protocol
 | |
| local function read_json(opt)
 | |
|   local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/'
 | |
|     .. opt.version
 | |
|     .. '/metaModel/metaModel.json'
 | |
|   print('Reading ' .. uri)
 | |
| 
 | |
|   local res = vim.system({ 'curl', '--no-progress-meter', uri, '-o', '-' }):wait()
 | |
|   if res.code ~= 0 or (res.stdout or ''):len() < 999 then
 | |
|     print(('URL failed: %s'):format(uri))
 | |
|     vim.print(res)
 | |
|     error(res.stdout)
 | |
|   end
 | |
|   return vim.json.decode(res.stdout)
 | |
| end
 | |
| 
 | |
| -- Gets the Lua symbol for a given fully-qualified LSP method name.
 | |
| local function to_luaname(s)
 | |
|   -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests
 | |
|   return s:gsub('^%$', 'dollar'):gsub('/', '_')
 | |
| end
 | |
| 
 | |
| ---@param protocol vim._gen_lsp.Protocol
 | |
| local function gen_methods(protocol)
 | |
|   local output = {
 | |
|     '-- Generated by gen_lsp.lua, keep at end of file.',
 | |
|     '--- LSP method names.',
 | |
|     '---',
 | |
|     '---@see https://microsoft.github.io/language-server-protocol/specification/#metaModel',
 | |
|     'protocol.Methods = {',
 | |
|   }
 | |
|   local indent = (' '):rep(2)
 | |
| 
 | |
|   --- @class vim._gen_lsp.Request
 | |
|   --- @field deprecated? string
 | |
|   --- @field documentation? string
 | |
|   --- @field messageDirection string
 | |
|   --- @field method string
 | |
|   --- @field params? any
 | |
|   --- @field proposed? boolean
 | |
|   --- @field registrationMethod? string
 | |
|   --- @field registrationOptions? any
 | |
|   --- @field since? string
 | |
| 
 | |
|   --- @class vim._gen_lsp.Notification
 | |
|   --- @field deprecated? string
 | |
|   --- @field documentation? string
 | |
|   --- @field errorData? any
 | |
|   --- @field messageDirection string
 | |
|   --- @field method string
 | |
|   --- @field params? any[]
 | |
|   --- @field partialResult? any
 | |
|   --- @field proposed? boolean
 | |
|   --- @field registrationMethod? string
 | |
|   --- @field registrationOptions? any
 | |
|   --- @field result any
 | |
|   --- @field since? string
 | |
| 
 | |
|   ---@type (vim._gen_lsp.Request|vim._gen_lsp.Notification)[]
 | |
|   local all = vim.list_extend(protocol.requests, protocol.notifications)
 | |
|   table.sort(all, function(a, b)
 | |
|     return to_luaname(a.method) < to_luaname(b.method)
 | |
|   end)
 | |
|   for _, item in ipairs(all) do
 | |
|     if item.method then
 | |
|       if item.documentation then
 | |
|         local document = vim.split(item.documentation, '\n?\n', { trimempty = true })
 | |
|         for _, docstring in ipairs(document) do
 | |
|           output[#output + 1] = indent .. '--- ' .. docstring
 | |
|         end
 | |
|       end
 | |
|       output[#output + 1] = ("%s%s = '%s',"):format(indent, to_luaname(item.method), item.method)
 | |
|     end
 | |
|   end
 | |
|   output[#output + 1] = '}'
 | |
|   output = vim.list_extend(
 | |
|     output,
 | |
|     vim.split(
 | |
|       [[
 | |
| local function freeze(t)
 | |
|   return setmetatable({}, {
 | |
|     __index = t,
 | |
|     __newindex = function()
 | |
|       error('cannot modify immutable table')
 | |
|     end,
 | |
|   })
 | |
| end
 | |
| protocol.Methods = freeze(protocol.Methods)
 | |
| 
 | |
| return protocol
 | |
| ]],
 | |
|       '\n',
 | |
|       { trimempty = true }
 | |
|     )
 | |
|   )
 | |
| 
 | |
|   local fname = './runtime/lua/vim/lsp/protocol.lua'
 | |
|   local bufnr = vim.fn.bufadd(fname)
 | |
|   vim.fn.bufload(bufnr)
 | |
|   vim.api.nvim_set_current_buf(bufnr)
 | |
|   local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
 | |
|   local index = vim.iter(ipairs(lines)):find(function(key, item)
 | |
|     return vim.startswith(item, '-- Generated by') and key or nil
 | |
|   end)
 | |
|   index = index and index - 1 or vim.api.nvim_buf_line_count(bufnr) - 1
 | |
|   vim.api.nvim_buf_set_lines(bufnr, index, -1, true, output)
 | |
|   vim.cmd.write()
 | |
| end
 | |
| 
 | |
| ---@class vim._gen_lsp.opt
 | |
| ---@field output_file string
 | |
| ---@field version string
 | |
| ---@field methods boolean
 | |
| 
 | |
| ---@param opt vim._gen_lsp.opt
 | |
| function M.gen(opt)
 | |
|   --- @type vim._gen_lsp.Protocol
 | |
|   local protocol = read_json(opt)
 | |
| 
 | |
|   if opt.methods then
 | |
|     gen_methods(protocol)
 | |
|   end
 | |
| 
 | |
|   local output = {
 | |
|     '--' .. '[[',
 | |
|     'THIS FILE IS GENERATED by scripts/gen_lsp.lua',
 | |
|     'DO NOT EDIT MANUALLY',
 | |
|     '',
 | |
|     'Based on LSP protocol ' .. opt.version,
 | |
|     '',
 | |
|     'Regenerate:',
 | |
|     ([=[nvim -l scripts/gen_lsp.lua gen --version %s]=]):format(DEFAULT_LSP_VERSION),
 | |
|     '--' .. ']]',
 | |
|     '',
 | |
|     '---@meta',
 | |
|     "error('Cannot require a meta file')",
 | |
|     '',
 | |
|     '---@alias lsp.null nil',
 | |
|     '---@alias uinteger integer',
 | |
|     '---@alias decimal number',
 | |
|     '---@alias lsp.DocumentUri string',
 | |
|     '---@alias lsp.URI string',
 | |
|     '',
 | |
|   }
 | |
| 
 | |
|   local anonymous_num = 0
 | |
| 
 | |
|   ---@type string[]
 | |
|   local anonym_classes = {}
 | |
| 
 | |
|   local simple_types = {
 | |
|     'string',
 | |
|     'boolean',
 | |
|     'integer',
 | |
|     'uinteger',
 | |
|     'decimal',
 | |
|   }
 | |
| 
 | |
|   ---@param documentation string
 | |
|   local _process_documentation = function(documentation)
 | |
|     documentation = documentation:gsub('\n', '\n---')
 | |
|     -- Remove <200b> (zero-width space) unicode characters: e.g., `**/<200b>*`
 | |
|     documentation = documentation:gsub('\226\128\139', '')
 | |
|     -- Escape annotations that are not recognized by lua-ls
 | |
|     documentation = documentation:gsub('%^---@sample', '---\\@sample')
 | |
|     return '---' .. documentation
 | |
|   end
 | |
| 
 | |
|   --- @class vim._gen_lsp.Type
 | |
|   --- @field kind string a common field for all Types.
 | |
|   --- @field name? string for ReferenceType, BaseType
 | |
|   --- @field element? any for ArrayType
 | |
|   --- @field items? vim._gen_lsp.Type[] for OrType, AndType
 | |
|   --- @field key? vim._gen_lsp.Type for MapType
 | |
|   --- @field value? string|vim._gen_lsp.Type for StringLiteralType, MapType, StructureLiteralType
 | |
| 
 | |
|   ---@param type vim._gen_lsp.Type
 | |
|   ---@param prefix? string Optional prefix associated with the this type, made of (nested) field name.
 | |
|   ---             Used to generate class name for structure literal types.
 | |
|   ---@return string
 | |
|   local function parse_type(type, prefix)
 | |
|     -- ReferenceType | BaseType
 | |
|     if type.kind == 'reference' or type.kind == 'base' then
 | |
|       if vim.tbl_contains(simple_types, type.name) then
 | |
|         return type.name
 | |
|       end
 | |
|       return 'lsp.' .. type.name
 | |
| 
 | |
|     -- ArrayType
 | |
|     elseif type.kind == 'array' then
 | |
|       local parsed_items = parse_type(type.element, prefix)
 | |
|       if type.element.items and #type.element.items > 1 then
 | |
|         parsed_items = '(' .. parsed_items .. ')'
 | |
|       end
 | |
|       return parsed_items .. '[]'
 | |
| 
 | |
|     -- OrType
 | |
|     elseif type.kind == 'or' then
 | |
|       local val = ''
 | |
|       for _, item in ipairs(type.items) do
 | |
|         val = val .. parse_type(item, prefix) .. '|' --[[ @as string ]]
 | |
|       end
 | |
|       val = val:sub(0, -2)
 | |
|       return val
 | |
| 
 | |
|     -- StringLiteralType
 | |
|     elseif type.kind == 'stringLiteral' then
 | |
|       return '"' .. type.value .. '"'
 | |
| 
 | |
|     -- MapType
 | |
|     elseif type.kind == 'map' then
 | |
|       local key = assert(type.key)
 | |
|       local value = type.value --[[ @as vim._gen_lsp.Type ]]
 | |
|       return 'table<' .. parse_type(key, prefix) .. ', ' .. parse_type(value, prefix) .. '>'
 | |
| 
 | |
|     -- StructureLiteralType
 | |
|     elseif type.kind == 'literal' then
 | |
|       -- can I use ---@param disabled? {reason: string}
 | |
|       -- use | to continue the inline class to be able to add docs
 | |
|       -- https://github.com/LuaLS/lua-language-server/issues/2128
 | |
|       anonymous_num = anonymous_num + 1
 | |
|       local anonymous_classname = 'lsp._anonym' .. anonymous_num
 | |
|       if prefix then
 | |
|         anonymous_classname = anonymous_classname .. '.' .. prefix
 | |
|       end
 | |
|       local anonym = vim
 | |
|         .iter({
 | |
|           (anonymous_num > 1 and { '' } or {}),
 | |
|           { '---@class ' .. anonymous_classname },
 | |
|         })
 | |
|         :flatten()
 | |
|         :totable()
 | |
| 
 | |
|       --- @class vim._gen_lsp.StructureLiteral translated to anonymous @class.
 | |
|       --- @field deprecated? string
 | |
|       --- @field description? string
 | |
|       --- @field properties vim._gen_lsp.Property[]
 | |
|       --- @field proposed? boolean
 | |
|       --- @field since? string
 | |
| 
 | |
|       ---@type vim._gen_lsp.StructureLiteral
 | |
|       local structural_literal = assert(type.value) --[[ @as vim._gen_lsp.StructureLiteral ]]
 | |
|       for _, field in ipairs(structural_literal.properties) do
 | |
|         anonym[#anonym + 1] = '---'
 | |
|         if field.documentation then
 | |
|           anonym[#anonym + 1] = _process_documentation(field.documentation)
 | |
|         end
 | |
|         anonym[#anonym + 1] = '---@field '
 | |
|           .. field.name
 | |
|           .. (field.optional and '?' or '')
 | |
|           .. ' '
 | |
|           .. parse_type(field.type, prefix .. '.' .. field.name)
 | |
|       end
 | |
|       -- anonym[#anonym + 1] = ''
 | |
|       for _, line in ipairs(anonym) do
 | |
|         if line then
 | |
|           anonym_classes[#anonym_classes + 1] = line
 | |
|         end
 | |
|       end
 | |
|       return anonymous_classname
 | |
| 
 | |
|     -- TupleType
 | |
|     elseif type.kind == 'tuple' then
 | |
|       local tuple = '{ '
 | |
|       for i, value in ipairs(type.items) do
 | |
|         tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value, prefix) .. ', '
 | |
|       end
 | |
|       -- remove , at the end
 | |
|       tuple = tuple:sub(0, -3)
 | |
|       return tuple .. ' }'
 | |
|     end
 | |
| 
 | |
|     vim.print('WARNING: Unknown type ', type)
 | |
|     return ''
 | |
|   end
 | |
| 
 | |
|   --- @class vim._gen_lsp.Structure translated to @class
 | |
|   --- @field deprecated? string
 | |
|   --- @field documentation? string
 | |
|   --- @field extends? { kind: string, name: string }[]
 | |
|   --- @field mixins? { kind: string, name: string }[]
 | |
|   --- @field name string
 | |
|   --- @field properties? vim._gen_lsp.Property[]  members, translated to @field
 | |
|   --- @field proposed? boolean
 | |
|   --- @field since? string
 | |
|   for _, structure in ipairs(protocol.structures) do
 | |
|     -- output[#output + 1] = ''
 | |
|     if structure.documentation then
 | |
|       output[#output + 1] = _process_documentation(structure.documentation)
 | |
|     end
 | |
|     local class_string = ('---@class lsp.%s'):format(structure.name)
 | |
|     if structure.extends or structure.mixins then
 | |
|       local inherits_from = table.concat(
 | |
|         vim.list_extend(
 | |
|           vim.tbl_map(parse_type, structure.extends or {}),
 | |
|           vim.tbl_map(parse_type, structure.mixins or {})
 | |
|         ),
 | |
|         ', '
 | |
|       )
 | |
|       class_string = class_string .. ': ' .. inherits_from
 | |
|     end
 | |
|     output[#output + 1] = class_string
 | |
| 
 | |
|     --- @class vim._gen_lsp.Property translated to @field
 | |
|     --- @field deprecated? string
 | |
|     --- @field documentation? string
 | |
|     --- @field name string
 | |
|     --- @field optional? boolean
 | |
|     --- @field proposed? boolean
 | |
|     --- @field since? string
 | |
|     --- @field type { kind: string, name: string }
 | |
|     for _, field in ipairs(structure.properties or {}) do
 | |
|       output[#output + 1] = '---' -- Insert a single newline between @fields (and after @class)
 | |
|       if field.documentation then
 | |
|         output[#output + 1] = _process_documentation(field.documentation)
 | |
|       end
 | |
|       output[#output + 1] = '---@field '
 | |
|         .. field.name
 | |
|         .. (field.optional and '?' or '')
 | |
|         .. ' '
 | |
|         .. parse_type(field.type, field.name)
 | |
|     end
 | |
|     output[#output + 1] = ''
 | |
|   end
 | |
| 
 | |
|   --- @class vim._gen_lsp.Enumeration translated to @enum
 | |
|   --- @field deprecated string?
 | |
|   --- @field documentation string?
 | |
|   --- @field name string?
 | |
|   --- @field proposed boolean?
 | |
|   --- @field since string?
 | |
|   --- @field suportsCustomValues boolean?
 | |
|   --- @field values { name: string, value: string, documentation?: string, since?: string }[]
 | |
|   for _, enum in ipairs(protocol.enumerations) do
 | |
|     if enum.documentation then
 | |
|       output[#output + 1] = _process_documentation(enum.documentation)
 | |
|     end
 | |
|     local enum_type = '---@alias lsp.' .. enum.name
 | |
|     for _, value in ipairs(enum.values) do
 | |
|       enum_type = enum_type
 | |
|         .. '\n---| '
 | |
|         .. (type(value.value) == 'string' and '"' .. value.value .. '"' or value.value)
 | |
|         .. ' # '
 | |
|         .. value.name
 | |
|     end
 | |
|     output[#output + 1] = enum_type
 | |
|     output[#output + 1] = ''
 | |
|   end
 | |
| 
 | |
|   --- @class vim._gen_lsp.TypeAlias translated to @alias
 | |
|   --- @field deprecated? string?
 | |
|   --- @field documentation? string
 | |
|   --- @field name string
 | |
|   --- @field proposed? boolean
 | |
|   --- @field since? string
 | |
|   --- @field type vim._gen_lsp.Type
 | |
|   for _, alias in ipairs(protocol.typeAliases) do
 | |
|     if alias.documentation then
 | |
|       output[#output + 1] = _process_documentation(alias.documentation)
 | |
|     end
 | |
|     if alias.type.kind == 'or' then
 | |
|       local alias_type = '---@alias lsp.' .. alias.name .. ' '
 | |
|       for _, item in ipairs(alias.type.items) do
 | |
|         alias_type = alias_type .. parse_type(item, alias.name) .. '|'
 | |
|       end
 | |
|       alias_type = alias_type:sub(0, -2)
 | |
|       output[#output + 1] = alias_type
 | |
|     else
 | |
|       output[#output + 1] = '---@alias lsp.'
 | |
|         .. alias.name
 | |
|         .. ' '
 | |
|         .. parse_type(alias.type, alias.name)
 | |
|     end
 | |
|     output[#output + 1] = ''
 | |
|   end
 | |
| 
 | |
|   -- anonymous classes
 | |
|   for _, line in ipairs(anonym_classes) do
 | |
|     output[#output + 1] = line
 | |
|   end
 | |
| 
 | |
|   tofile(opt.output_file, table.concat(output, '\n') .. '\n')
 | |
| end
 | |
| 
 | |
| ---@type vim._gen_lsp.opt
 | |
| local opt = {
 | |
|   output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua',
 | |
|   version = DEFAULT_LSP_VERSION,
 | |
|   methods = false,
 | |
| }
 | |
| 
 | |
| local command = nil
 | |
| local i = 1
 | |
| while i <= #_G.arg do
 | |
|   if _G.arg[i] == '--out' then
 | |
|     opt.output_file = assert(_G.arg[i + 1], '--out <outfile> needed')
 | |
|     i = i + 1
 | |
|   elseif _G.arg[i] == '--version' then
 | |
|     opt.version = assert(_G.arg[i + 1], '--version <version> needed')
 | |
|     i = i + 1
 | |
|   elseif _G.arg[i] == '--methods' then
 | |
|     opt.methods = true
 | |
|   elseif vim.startswith(_G.arg[i], '-') then
 | |
|     error('Unrecognized args: ' .. _G.arg[i])
 | |
|   else
 | |
|     if command then
 | |
|       error('More than one command was given: ' .. _G.arg[i])
 | |
|     else
 | |
|       command = _G.arg[i]
 | |
|     end
 | |
|   end
 | |
|   i = i + 1
 | |
| end
 | |
| 
 | |
| if not command then
 | |
|   print(USAGE)
 | |
| elseif M[command] then
 | |
|   M[command](opt) -- see M.gen()
 | |
| else
 | |
|   error('Unknown command: ' .. command)
 | |
| end
 | |
| 
 | |
| return M
 |