nvim/lsp/util: add Servers, Server, Filter

These objects provide a nice abstraction over the underlying
complexities of configuring language servers in native Neovim.

The idea is to allow users of the module to define Server/s that can
just be included somewhere and have them activate with fuss.

That said, we *do* want ways of configuring the underlying systems, and
this module provides two, one for direct consumers of this module
(Spec.setup), and one for users *of predefined Servers*, in the form of
Server.with/1.
This commit is contained in:
Paul Stemmet 2022-09-10 12:27:42 +00:00
parent 2fca27a5ab
commit 7ff228ae64
Signed by: Paul Stemmet
GPG Key ID: EDEA539F594E7E75
1 changed files with 266 additions and 0 deletions

View File

@ -0,0 +1,266 @@
local util = require 'psoxizsh.util'
-- Utilities for interacting with *lspconfig* servers
local M = {}
-- Server
--
-- A Server is a representation of a LSP server, that can be
-- used to configure lspconfig servers.
--
-- Each server has the following properties:
--
-- {
-- name = 'lspconfig server identifier',
-- filetypes = { 'List of filetypes supported by this Server' },
-- with = 'Function that takes a list of config overrides for this Server'
-- }
--
-- Use `Server.new/1` to create a new Server
local Server = { mt = {} }
-- Create a new Server from the provided Spec object
--
-- Spec = {
-- name = [[ REQUIRED|STRING
-- The lspconfig server name of this Server.
-- See :h lspconfig-all for a list
-- ]]
-- ft = { 'OPTIONAL|ARRAY|STRING',
-- 'A list of filetypes to use this server with',
-- 'Leave empty to use the lspconfig defaults'
-- }
-- setup = { 'OPTIONAL|MAP|FUNC',
-- [[ The setup for this Server
--
-- ## Map
-- Can be either a map of key/values that are
-- passed to lspconfig[Server].setup, after
-- being merged with the defaults; see
-- :h lspconfig-setup for some primer documentation
-- on the subject
--
-- ## Func
-- Or, a function that takes three arguments:
--
-- 1. server | lspconfig[Server]
-- 2. settings | default settings
-- 3. override | user override fn
--
-- This gives you complete control over how the server
-- is initialized, however you must also *guarantee* that
-- some version of `server.setup(settings)` is called before
-- returning from the function.
-- ]]
-- }
-- }
--
-- Note: you may pass an existing Server without harm, as a convenience
function Server.new(spec)
if spec and spec.__is_server then
return spec
else
return Server.from_spec(spec)
end
end
-- Convert a Spec object into a Server
--
-- Callers should prefer Server.new/1
function Server.from_spec(spec)
local spec = spec or {}
local ft = spec.ft and (type(spec.ft) == 'table' and spec.ft or { spec.ft })
local setup, handler = spec.setup, nil
local t = type(setup)
-- Defined setup function, just use it as the handler
if t == 'function' then handler = setup
-- Config map, construct handler glue around it
elseif t == 'table' then handler = Server.make_handler(setup)
-- Unknown (user error), just create default handler
else handler = Server.make_handler(false)
end
local server = {
name = spec.name,
filetypes = ft,
_handler = handler,
_override = function (defaults) return defaults end,
__is_server = true
}
server.with = Server.with(server)
server = setmetatable(server, Server.mt)
return server
end
-- Setup wrapper for Servers
-- @private
--
-- Responsible for merging lspconfig defaults into our
-- Server defined settings, before passing them through
-- to the Server._handler
function Server.setup(self, server, settings)
local merged = vim.tbl_deep_extend('keep', { filetypes = self.filetypes }, settings)
return self._handler(server, merged, self._override)
end
-- Server.with handler
-- @private
--
-- Allows consumers of existing Servers to override
-- certain settings
--
-- This is the implementation of the 'override' arg
-- in a Spec.setup
function Server.with(self)
return function(override)
local this = vim.deepcopy(self)
this._override = function(defaults)
return util.mconfig(override, defaults or {})
end
return this
end
end
-- Create a new Server._handler
-- @private
--
-- Creates a handler function for the given setup *map*
-- (not function)
function Server.make_handler(setup)
local merge = setup and true
return merge
and function(server, settings, override)
return server.setup(override(vim.tbl_deep_extend('keep', setup, settings)))
end
or function(server, settings, override)
return server.setup(override(settings))
end
end
Server.mt.__call = function(self, server, settings) return Server.setup(self, server, settings) end
-- Filter
--
-- A meta object for defining search queries on a `Servers` list
--
-- See `Servers.by` for details on usage
local Filter = { mt = {} }
-- Create a new Filter with the given `context` and `term`
--
-- When calling `Filter.filter/2` on this Filter, the `term`
-- will be used as the identifier search for the given needle
-- with
function Filter.new(cxt, term)
return setmetatable({ _context = cxt, _term = term }, Filter.mt)
end
-- Return a list of objects that match the needle, from this
-- Filter's context.
--
-- `needle` will be matched against the context's `term`,
-- and if `needle` is a list, results that match *any* needle
-- will be returned
function Filter.filter(self, needle)
local results = {}
for _, entry in pairs(self._context) do
local target, t = entry[self._term], type(entry[self._term])
if t == 'string' and target == needle or
vim.tbl_islist(target) and vim.tbl_contains(target, needle)
then
table.insert(results, entry)
end
end
return Servers.new(results)
end
Filter.mt.__call = function(self, args)
local new, t = Servers.new(), type(args)
if t == 'string' then
new:merge(Filter.filter(self, args))
elseif vim.tbl_islist(args) then
for _, needle in ipairs(args) do
new:merge(Filter.filter(self, needle))
end
end
return new
end
-- Servers
--
-- A smart list of `Server` objects, allowing callers
-- to add, extend and filter the stored `Server`s
local Servers = { by = {}, mt = {} }
-- Create a new, empty `Servers` object
--
-- If `init` is a list of `Server` or `Spec`s, add them
-- to the new `Servers`
function Servers.new(init)
local this = setmetatable({ _servers = {} }, Servers.mt)
return this:extend(init)
end
-- Add the provided `Server` or `Spec`s to this `Servers`
--
-- This is the variadic version of Servers.extend/1
function Servers.with(self, ...)
return self:extend({ ... })
end
-- Add the provided `Server` or `Spec` `list` to this `Servers`
function Servers.extend(self, list)
for _, srv in ipairs(list or {}) do
self:insert(srv)
end
return self
end
-- Merge this `Servers` with another `Servers`
--
-- Note, `Server`s in `other` have precedence in case
-- of conflicts
function Servers.merge(self, other)
for _, srv in pairs(other:get()) do
self:insert(srv)
end
return self
end
-- Add the given `Server` to this `Servers`
function Servers.insert(self, server)
local s = Server.new(server)
self._servers[s.name] = s
return self
end
-- Return the set of managed `Server`s
function Servers.get(self)
return self._servers
end
setmetatable(Servers.by, {
__index = function(self, dimension) return Filter.new(self._servers, dimension) end
})
Servers.mt.__index = function(self, key) return Servers[key] end
M.Servers = Servers
M.Server = Server
return M