aboutsummaryrefslogtreecommitdiff
path: root/scripts/gen_lsp.lua
blob: 6ff8dcb3f4f61b761ec33abf16dadc695e032db0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
--[[
Generates lua-ls annotations for lsp
USAGE:
nvim -l scripts/gen_lsp.lua gen  # this will overwrite runtime/lua/vim/lsp/_meta/protocol.lua
nvim -l scripts/gen_lsp.lua gen --version 3.18 --build/new_lsp_types.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 M = {}

local function tofile(fname, text)
  local f = io.open(fname, 'w')
  if not f then
    error(('failed to write: %s'):format(f))
  else
    f:write(text)
    f:close()
  end
end

local function read_json(opt)
  local uri = 'https://raw.githubusercontent.com/microsoft/language-server-protocol/gh-pages/_specifications/lsp/'
    .. opt.version
    .. '/metaModel/metaModel.json'

  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 name(s)
  -- "$/" prefix is special: https://microsoft.github.io/language-server-protocol/specification/#dollarRequests
  return s:gsub('^%$', 'dollar'):gsub('/', '_')
end

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)

  local all = vim.list_extend(protocol.requests, protocol.notifications)
  table.sort(all, function(a, b)
    return name(a.method) < name(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, name(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

function M.gen(opt)
  local protocol = read_json(opt)

  if opt.methods then
    gen_methods(protocol)
  end

  local output = {
    '--[[',
    'This file is autogenerated from scripts/gen_lsp.lua',
    'Regenerate:',
    [=[nvim -l scripts/gen_lsp.lua gen --version 3.18 --runtime/lua/vim/lsp/_meta/protocol.lua]=],
    '--]]',
    '',
    '---@alias lsp.null nil',
    '---@alias uinteger integer',
    '---@alias lsp.decimal number',
    '---@alias lsp.DocumentUri string',
    '---@alias lsp.URI string',
    '---@alias lsp.LSPObject table<string, lsp.LSPAny>',
    '---@alias lsp.LSPArray lsp.LSPAny[]',
    '---@alias lsp.LSPAny lsp.LSPObject|lsp.LSPArray|string|number|boolean|nil',
    '',
  }

  local anonymous_num = 0

  local anonym_classes = {}

  local simple_types = {
    'string',
    'boolean',
    'integer',
    'uinteger',
    'decimal',
  }

  local function parse_type(type)
    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
    elseif type.kind == 'array' then
      return parse_type(type.element) .. '[]'
    elseif type.kind == 'or' then
      local val = ''
      for _, item in ipairs(type.items) do
        val = val .. parse_type(item) .. '|'
      end
      val = val:sub(0, -2)
      return val
    elseif type.kind == 'stringLiteral' then
      return '"' .. type.value .. '"'
    elseif type.kind == 'map' then
      return 'table<' .. parse_type(type.key) .. ', ' .. parse_type(type.value) .. '>'
    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 anonym = { '---@class anonym' .. anonymous_num }
      for _, field in ipairs(type.value.properties) do
        if field.documentation then
          field.documentation = field.documentation:gsub('\n', '\n---')
          anonym[#anonym + 1] = '---' .. field.documentation
        end
        anonym[#anonym + 1] = '---@field '
          .. field.name
          .. (field.optional and '?' or '')
          .. ' '
          .. parse_type(field.type)
      end
      anonym[#anonym + 1] = ''
      for _, line in ipairs(anonym) do
        anonym_classes[#anonym_classes + 1] = line
      end
      return 'anonym' .. anonymous_num
    elseif type.kind == 'tuple' then
      local tuple = '{ '
      for i, value in ipairs(type.items) do
        tuple = tuple .. '[' .. i .. ']: ' .. parse_type(value) .. ', '
      end
      -- remove , at the end
      tuple = tuple:sub(0, -3)
      return tuple .. ' }'
    end
    vim.print(type)
    return ''
  end

  for _, structure in ipairs(protocol.structures) do
    if structure.documentation then
      structure.documentation = structure.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. structure.documentation
    end
    if structure.extends then
      local class_string = '---@class lsp.'
        .. structure.name
        .. ': '
        .. parse_type(structure.extends[1])
      for _, mixin in ipairs(structure.mixins or {}) do
        class_string = class_string .. ', ' .. parse_type(mixin)
      end
      output[#output + 1] = class_string
    else
      output[#output + 1] = '---@class lsp.' .. structure.name
    end
    for _, field in ipairs(structure.properties or {}) do
      if field.documentation then
        field.documentation = field.documentation:gsub('\n', '\n---')
        output[#output + 1] = '---' .. field.documentation
      end
      output[#output + 1] = '---@field '
        .. field.name
        .. (field.optional and '?' or '')
        .. ' '
        .. parse_type(field.type)
    end
    output[#output + 1] = ''
  end

  for _, enum in ipairs(protocol.enumerations) do
    if enum.documentation then
      enum.documentation = enum.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. 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

  for _, alias in ipairs(protocol.typeAliases) do
    if alias.documentation then
      alias.documentation = alias.documentation:gsub('\n', '\n---')
      output[#output + 1] = '---' .. 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) .. '|'
      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)
    end
    output[#output + 1] = ''
  end

  for _, line in ipairs(anonym_classes) do
    output[#output + 1] = line
  end

  tofile(opt.output_file, table.concat(output, '\n'))
end

local opt = {
  output_file = 'runtime/lua/vim/lsp/_meta/protocol.lua',
  version = nil,
  methods = nil,
}

for i = 1, #_G.arg do
  if _G.arg[i] == '--out' then
    opt.output_file = _G.arg[i + 1]
  elseif _G.arg[i] == '--version' then
    opt.version = _G.arg[i + 1]
  elseif _G.arg[i] == '--methods' then
    opt.methods = true
  elseif vim.startswith(_G.arg[i], '--') then
    opt.output_file = _G.arg[i]:sub(3)
  end
end

for _, a in ipairs(arg) do
  if M[a] then
    M[a](opt)
  end
end

return M