Newer
Older
dotfiles / .config / lite-xl / plugins / lsp_snippets.lua
-- mod-version:3

-- LSP style snippet parser
-- shamelessly 'inspired by' (stolen from) LuaSnip
-- https://github.com/L3MON4D3/LuaSnip/blob/master/lua/luasnip/util/parser/neovim_parser.lua

local core     = require 'core'
local common   = require 'core.common'
local Doc      = require 'core.doc'
local system   = require 'system'
local regex    = require 'regex'
local snippets = require 'plugins.snippets'

local json do
	local ok, j 
	for _, p in ipairs {
		'plugins.json', 'plugins.lsp.json', 'plugins.lintplus.json',
		'libraries.json'
	} do
		ok, j = pcall(require, p)
		if ok then json = j; break end
	end
end


local B = snippets.builder

local LAST_CONVERTED_ID = { }
local THREAD_KEY = { }


-- node factories

local function doc_syntax(doc, k)
	return doc.syntax and doc.syntax[k]
end

local variables = {
	-- LSP
	TM_SELECTED_TEXT         = function(ctx) return ctx.selection end,
	TM_CURRENT_LINE          = function(ctx) return ctx.doc.lines[ctx.line] end,
	TM_CURRENT_WORD          = function(ctx) return ctx.partial end,
	TM_LINE_INDEX            = function(ctx) return ctx.line - 1 end,
	TM_LINE_NUMBER           = function(ctx) return ctx.line end,
	TM_FILENAME              = function(ctx) return ctx.doc.filename:match('[^/%\\]*$') or '' end,
	TM_FILENAME_BASE         = function(ctx) return ctx.doc.filename:match('([^/%\\]*)%.%w*$') or ctx.doc.filename end,
	TM_DIRECTORY             = function(ctx) return ctx.doc.filename:match('([^/%\\]*)[/%\\].*$') or '' end,
	TM_FILEPATH              = function(ctx) return common.dirname(ctx.doc.abs_filename) or '' end,
	-- VSCode
	RELATIVE_FILEPATH        = function(ctx) return core.normalize_to_project_dir(ctx.doc.filename) end,
	CLIPBOARD                = function()    return system.get_clipboard() end,
	-- https://github.com/lite-xl/lite-xl/pull/1455
	WORKSPACE_NAME           = function(ctx) return end,
	WORKSPACE_FOLDER         = function(ctx) return end,
	CURSOR_INDEX             = function(ctx) return ctx.col - 1 end,
	CURSOR_NUMBER            = function(ctx) return ctx.col end,
	CURRENT_YEAR             = function()    return os.date('%G') end,
	CURRENT_YEAR_SHORT       = function()    return os.date('%g') end,
	CURRENT_MONTH            = function()    return os.date('%m') end,
	CURRENT_MONTH_NAME       = function()    return os.date('%B') end,
	CURRENT_MONTH_NAME_SHORT = function()    return os.date('%b') end,
	CURRENT_DATE             = function()    return os.date('%d') end,
	CURRENT_DAY_NAME         = function()    return os.date('%A') end,
	CURRENT_DAY_NAME_SHORT   = function()    return os.date('%a') end,
	CURRENT_HOUR             = function()    return os.date('%H') end,
	CURRENT_MINUTE           = function()    return os.date('%M') end,
	CURRENT_SECOND           = function()    return os.date('%S') end,
	CURRENT_SECONDS_UNIX     = function()    return os.time() end,
	RANDOM                   = function()    return string.format('%06d', math.random(999999)) end,
	RANDOM_HEX               = function()    return string.format('%06x', math.random(0xFFFFFF)) end,
	BLOCK_COMMENT_START      = function(ctx) return (doc_syntax(ctx.doc, 'block_comment') or { })[1] end,
	BLOCK_COMMENT_END        = function(ctx) return (doc_syntax(ctx.doc, 'block_comment') or { })[2] end,
	LINE_COMMENT             = function(ctx) return doc_syntax(ctx.doc, 'comment') end
	-- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables
	-- UUID
}

local formatters; formatters = {
	downcase   = string.lower,
	upcase     = string.upper,
	capitalize = function(str)
		return str:sub(1, 1):upper() .. str:sub(2)
	end,
	pascalcase = function(str)
		local t = { }
		for s in str:gmatch('%w+') do
			table.insert(t, formatters.capitalize(s))
		end
		return table.concat(t)
	end,
	camelcase  = function(str)
		str = formatters.pascalcase(str)
		return str:sub(1, 1):lower() .. str:sub(2)
	end
}

local function to_text(v, _s)
	return v.esc
end

local function format_fn(v, _s)
	local id = tonumber(v[2])

	-- $1 | ${1}
	if #v < 4 then
		return function(captures)
			return captures[id] or ''
		end
	end

	-- ${1:...}
	local t = v[3][2][1] -- token after the ':' | (else when no token)
	local i = v[3][2][2] -- formatter | if | (else when no if)
	local e = v[3][2][4] -- (else when if)

	if t == '/' then
		local f = formatters[i]
		return function(captures)
			local c = captures[id]
			return c and f(c) or ''
		end
	elseif t == '+' then
		return function(captures)
			return captures[id] and i or ''
		end
	elseif t == '?' then
		return function(captures)
			return captures[id] and i or e
		end
	elseif t == '-' then
		return function(captures)
			return captures[id] or i
		end
	else
		return function(captures)
			return captures[id] or t
		end
	end
end

local function transform_fn(v, _s)
	local reg = regex.compile(v[2], v[#v])
	local fmt = v[4]

	if type(fmt) ~= 'table' then
		return function(str)
			return reg:gsub(str, '')
		end
	end

	local t = { }
	for _, f in ipairs(fmt) do
		if type(f) == 'string' then
			table.insert(t, f)
		else
			break
		end
	end

	if #t == #fmt then
		t = table.concat(t)
		return function(str)
			return reg:gsub(str, t)
		end
	end

	return function(str)
		local captures = { reg:match(str) }
		for k, v in ipairs(captures) do
			if type(v) ~= 'string' then
				captures[k] = nil
			end
		end
		local t = { }
		for _, f in ipairs(fmt) do
			if type(f) == 'string' then
				table.insert(t, f)
			else
				table.insert(t, f(captures))
			end
		end
		return table.concat(t)
	end
end

local function text_node(v, _s)
	return B.static(v.esc)
end

local function variable_node(v, _s)
	local name = v[2]
	local var = variables[name]

	local id
	if not var then
		if not _s._converted_variables then
			id = os.time()
			_s._converted_variables = { [name] = id, [LAST_CONVERTED_ID] = id }
		else
			id = _s._converted_variables[name]
			if not id then
				id = _s._converted_variables[LAST_CONVERTED_ID] + 1
				_s._converted_variables[name] = id
				_s._converted_variables[LAST_CONVERTED_ID] = id
			end
		end
	end

	if #v ~= 4 then
		return var and B.static(var) or B.user(id, name)
	end

	if type(v[3]) == 'table' then
		-- vscode accepts empty default -> var name
		return var and B.static(var) or B.user(id, v[3][2] or name)
	end

	if not var then
		return B.user(id, nil, v[3])
	end

	return type(var) ~= 'function' and B.static(var) or B.static(function(ctx)
		return v[3](var(ctx))
	end)
end

local function tabstop_node(v, _s)
	local t = v[3] and v[3] ~= '}' and v[3] or nil
	return B.user(tonumber(v[2]), nil, t)
end

local function choice_node(v, _s)
	local id = tonumber(v[2])
	local c = { [v[4]] = true }
	if #v == 6 then
		for _, _c in ipairs(v[5]) do
			c[_c[2]] = true
		end
	end
	_s:choice(id, c)
	return B.user(id)
end

local function placeholder_node(v, _s)
	local id = tonumber(v[2])
	if #v > 4 then _s:default(id, v[4]) end
	return B.user(id)
end

local function build_snippet(v, _s)
	for _, n in ipairs(v) do _s:add(n) end
	return _s:ok()
end


-- parser metatable

local P do
	local mt = {
		__call = function(mt, parser, converter)
			return setmetatable({ parser = parser, converter = converter }, mt)
		end,
		-- allows 'lazy arguments'
		-- i.e can use a yet to be defined rule in a previous rule
		__index = function(t, k)
			return function(...) return t[k](...) end
		end
	}

	P = setmetatable({
		__call = function(t, str, at, _s)
			local r = t.parser(str, at, _s)
			if r.ok and t.converter then
				r.value = t.converter(r.value, _s)
			end
			return r
		end
	}, mt)
end


-- utils

local function toset(t)
	local r = { }
	for _, v in pairs(t or { }) do
		r[v] = true
	end
	return r
end

local function fail(at)
	return { at = at }
end

local function ok(at, v)
	return { ok = true, at = at, value = v }
end


-- base + combinators

local function token(t)
	return function(str, at)
		local to = at + #t
		return t == str:sub(at, to - 1) and ok(to, t) or fail(at)
	end
end

local function consume(stops, escapes)
	stops, escapes = toset(stops), toset(escapes)
	return function(str, at)
		local to = at
		local raw, esc = { }, { }
		local c = str:sub(to, to)
		while to <= #str and not stops[c] do
			if c == '\\' then
				table.insert(raw, c)
				to = to + 1
				c = str:sub(to, to)
				if not stops[c] and not escapes[c] then
					table.insert(esc, '\\')
				end
			end
			table.insert(raw, c)
			table.insert(esc, c)
			to = to + 1
			c = str:sub(to, to)
		end
		return to ~= at
			and ok(to, { raw = table.concat(raw), esc = table.concat(esc) })
			or fail(at)
	end
end

local function pattern(p)
	return function(str, at)
		local r = str:match('^' .. p, at)
		return r and ok(at + #r, r) or fail(at)
	end
end

local function maybe(p)
	return function(str, at, ...)
		local r = p(str, at, ...)
		return ok(r.at, r.value)
	end
end

local function rep(p)
	return function(str, at, ...)
		local v, to, r = { }, at, ok(at)
		while to <= #str and r.ok do
			table.insert(v, r.value)
			to = r.at
			r = p(str, to, ...)
		end
		return #v > 0 and ok(to, v) or fail(at)
	end
end

local function any(...)
	local t = { ... }
	return function(str, at, ...)
		for _, p in ipairs(t) do
			local r = p(str, at, ...)
			if r.ok then return r end
		end
		return fail(at)
	end
end

local function seq(...)
	local t = { ... }
	return function(str, at, ...)
		local v, to = { }, at
		for _, p in ipairs(t) do
			local r = p(str, to, ...)
			if r.ok then
				table.insert(v, r.value)
				to = r.at
			else
				return fail(at)
			end
		end
		return ok(to, v)
	end
end


-- grammar rules

-- token cache
local t = setmetatable({ },
	{
		__index = function(t, k)
			local fn = token(k)
			rawset(t, k, fn)
			return fn
		end
	}
)

P.int = pattern('%d+')

P.var = pattern('[%a_][%w_]*')

-- '}' needs to be escaped in normal text (i.e #0)
local __text0 = consume({ '$' },      { '\\', '}' })
local __text1 = consume({ '}' },      { '\\' })
local __text2 = consume({ ':' },      { '\\' })
local __text3 = consume({ '/' },      { '\\' })
local __text4 = consume({ '$', '}' }, { '\\' })
local __text5 = consume({ ',', '|' }, { '\\' })
local __text6 = consume({ "$", "/" }, { "\\" })

P._if1  = P(__text1, to_text)
P._if2  = P(__text2, to_text)
P._else = P(__text1, to_text)

P.options = pattern('%l*')

P.regex = P(__text3, to_text)

P.format = P(any(
	seq(t['$'],  P.int),
	seq(t['${'], P.int, maybe(seq(t[':'], any(
		seq(t['/'], any(t['upcase'], t['downcase'], t['capitalize'], t['pascalcase'], t['camelcase'])),
		seq(t['+'], P._if1),
		seq(t['?'], P._if2, t[':'], P._else),
		seq(t['-'], P._else),
		P._else
	))), t['}'])
), format_fn)

P.transform_text = P(__text6, to_text)
P.transform = P(
	seq(t['/'], P.regex, t['/'], rep(any(P.format, P.transform_text)), t['/'], P.options),
	transform_fn
)

P.variable_text = P(__text4, text_node)
P.variable = P(any(
	seq(t['$'],  P.var),
	seq(t['${'], P.var, maybe(any(
		-- grammar says a single mandatory 'any' for default, vscode seems to accept any*
		seq(t[':'], maybe(rep(any(P.dollars, P.variable_text)))),
		P.transform
	)), t['}'])
), variable_node)

P.choice_text = P(__text5, to_text)
P.choice = P(
	seq(t['${'], P.int, t['|'], P.choice_text, maybe(rep(seq(t[','], P.choice_text))), t['|}']),
	choice_node
)

P.placeholder_text = P(__text4, text_node)
P.placeholder = P(
	seq(t['${'], P.int, t[':'], maybe(rep(any(P.dollars, P.placeholder_text))), t['}']),
	placeholder_node
)

P.tabstop = P(any(
	seq(t['$'],  P.int),
	-- transform isnt specified in the grammar but seems to be supported by vscode
	seq(t['${'], P.int, maybe(P.transform), t['}'])
), tabstop_node)


P.dollars = any(P.tabstop, P.placeholder, P.choice, P.variable)

P.text = P(__text0, text_node)
P.any = any(P.dollars, P.text)

P.snippet = P(rep(P.any), build_snippet)


-- JSON files

-- defined at the end of the file
local extensions

local fstate = { NOT_DONE = 'not done', QUEUED = 'queued', DONE = 'done' }
local queue = { }
local files = { }
local files2exts = { }
local exts2files = { }

local function parse_file(file)
	if files[file] == fstate.DONE then return end
	files[file] = fstate.DONE

	local _f = io.open(file)
	if not _f then
		core.error('[LSP snippets] Could not open \'%s\'', file)
		return
	end
	local ok, r = pcall(json.decode, _f:read('a'))
	_f:close()
	if not ok then
		core.error('[LSP snippets] %s: %s', file, r:match('%d+:%s+(.*)'))
		return false
	end

	local exts = file:match('%.json$') and files2exts[file]
	for i, s in pairs(r) do
		-- apparently body can be a single string
		local template = type(s.body) == 'table'
			and table.concat(s.body, '\n')
			or s.body
		if not template or template == '' then
			core.warn('[LSP snippets] missing \'body\' for %s (%s)', i, file)
			goto continue
		end

		-- https://code.visualstudio.com/docs/editor/userdefinedsnippets#_language-snippet-scope
		local scope
		if not exts and s.scope then
			local tmp = { }
			for _, l in ipairs(s.scope) do
				for _, e in ipairs(extensions[l:lower()]) do
					tmp[e] = true
				end
			end
			scope = { }
			for l in pairs(tmp) do
				table.insert(scope, l)
			end
		end

		-- prefix may be an array
		local triggers = type(s.prefix) ~= 'table' and { s.prefix } or s.prefix
		if #triggers == 0 then
			core.warn('[LSP snippets] missing \'prefix\' for %s (%s)', i, file)
			goto continue
		end

		for _, t in ipairs(triggers) do
			snippets.add {
				trigger = t,
				format = 'lsp',
				files = exts or scope,
				info = i,
				desc = s.description,
				template = template
			}
		end

		::continue::
	end

	return true
end

local function pop()
	while #queue > 0 do
		repeat until parse_file(table.remove(queue)) ~= nil
		if #queue > 0 then coroutine.yield() end
	end
end

local function enqueue(filename)
	if not core.threads[THREAD_KEY] then
		core.add_thread(pop, THREAD_KEY)
	end
	files[filename] = fstate.QUEUED
	table.insert(queue, filename)
end

local function add_file(filename, exts)
	if files[filename] then return end

	if filename:match('%.code%-snippets$') then
		enqueue(filename)
		return
	end

	if not filename:match('%.json$') then return end

	if not exts then
		local lang_name = filename:match('([^/%\\]*)%.%w*$'):lower()
		exts = extensions[lang_name]
		if not exts then return end
	end

	files[filename] = fstate.NOT_DONE
	exts = type(exts) == 'string' and { exts } or exts
	for _, e in ipairs(exts) do
		files2exts[filename] = files2exts[filename] or { }
		table.insert(files2exts[filename], '%.' .. e .. '$')
		exts2files[e] = exts2files[e] or { }
		table.insert(exts2files[e], filename)
	end
end

local function for_filename(name)
	if not name then return end
	local ext = name:match('%.(.*)$')
	if not ext then return end
	local _files = exts2files[ext]
	if not _files then return end
	for _, f in ipairs(_files) do
		if files[f] == fstate.NOT_DONE then
			enqueue(f)
		end
	end
end

local doc_new = Doc.new
function Doc:new(filename, ...)
	doc_new(self, filename, ...)
	for_filename(filename)
end

local doc_set_filename = Doc.set_filename
function Doc:set_filename(filename, ...)
	doc_set_filename(self, filename, ...)
	for_filename(filename)
end


-- API

local M = { }

function M.parse(template)
	local _s = B.new()
	local r = P.snippet(template, 1, _s)
	if not r.ok then
		return B.new():s(template):ok()
	elseif r.at == #template + 1 then
		return r.value
	else
		return _s:s(template:sub(r.at + 1)):ok()
	end
end

snippets.parsers.lsp = M.parse

local warned = false
function M.add_paths(paths)
	if not json then
		if not warned then
			core.error(
				'[LSP snippets] Could not add snippet file(s):' ..
				'JSON plugin not found'
			)
			warned = true
		end
		return
	end

	paths = type(paths) ~= 'table' and { paths } or paths

	for _, p in ipairs(paths) do
		-- non absolute paths are treated as relative from USERDIR
		p = not common.is_absolute_path(p) and (USERDIR .. PATHSEP .. p) or p
		local finfo = system.get_file_info(p)

		-- if path of a directory, add every file it contains and directories
		-- whose name is that of a lang
		if finfo and finfo.type == 'dir' then
			for _, f in ipairs(system.list_dir(p)) do
				f = p .. PATHSEP .. f
				finfo = system.get_file_info(f)
				if not finfo or finfo.type == 'file' then
					add_file(f)
				else
					-- only if the directory's name matches a language
					local lang_name = f:match('[^/%\\]*$'):lower()
					local exts = extensions[lang_name]
					for _, f2 in ipairs(system.list_dir(f)) do
						f2 = f .. PATHSEP .. f2
						finfo = system.get_file_info(f2)
						if not finfo or finfo.type == 'file' then
							add_file(f2, exts)
						end
					end
				end
			end
		-- if path of a file, add the file
		else
			add_file(p)
		end
	end
end


-- arbitrarily cleaned up extension dump from https://gist.github.com/ppisarczyk/43962d06686722d26d176fad46879d41
-- nothing after this

-- 90% of these are still useless but cba

extensions = {
	['ats'] = { 'dats', 'hats', 'sats', },
	['ada'] = { 'adb', 'ada', 'ads', },
	['agda'] = { 'agda', },
	['asciidoc'] = { 'asciidoc', 'adoc', 'asc', },
	['assembly'] = { 'asm', 'nasm', },
	['autohotkey'] = { 'ahk', 'ahkl', },
	['awk'] = { 'awk', 'auk', 'gawk', 'mawk', 'nawk', },
	['batchfile'] = { 'bat', 'cmd', },
	['c'] = { 'c', 'h', },
	['c#'] = { 'cs', 'cake', 'cshtml', 'csx', },
	['c++'] = { 'cpp', 'c++', 'cc', 'cp', 'cxx', 'h', 'h++', 'hh', 'hpp', 'hxx', },
	['cmake'] = { 'cmake', 'cmake.in', },
	['cobol'] = { 'cob', 'cbl', 'ccp', 'cobol', 'cpy', },
	['css'] = { 'css', },
	['clean'] = { 'icl', 'dcl', },
	['clojure'] = { 'clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic', },
	['common lisp'] = { 'lisp', 'asd', 'cl', 'l', 'lsp', 'ny', 'podsl', 'sexp', },
	['component pascal'] = { 'cp', 'cps', },
	['coq'] = { 'coq', 'v', },
	['crystal'] = { 'cr', },
	['cuda'] = { 'cu', 'cuh', },
	['d'] = { 'd', 'di', },
	['dart'] = { 'dart', },
	['dockerfile'] = { 'dockerfile', },
	['eiffel'] = { 'e', },
	['elixir'] = { 'ex', 'exs', },
	['elm'] = { 'elm', },
	['emacs lisp'] = { 'el', 'emacs', 'emacs.desktop', },
	['erlang'] = { 'erl', 'es', 'escript', 'hrl', 'xrl', 'yrl', },
	['f#'] = { 'fs', 'fsi', 'fsx', },
	['fortran'] = { 'f90', 'f', 'f03', 'f08', 'f77', 'f95', 'for', 'fpp', },
	['factor'] = { 'factor', },
	['forth'] = { 'fth', '4th', 'f', 'for', 'forth', 'fr', 'frt', 'fs', },
	['go'] = { 'go', },
	['groff'] = { 'man', '1', '1in', '1m', '1x', '2', '3', '3in', '3m', '3qt', '3x', '4', '5', '6', '7', '8', '9', 'l', 'me', 'ms', 'n', 'rno', 'roff', },
	['groovy'] = { 'groovy', 'grt', 'gtpl', 'gvy', },
	['html'] = { 'html', 'htm', 'html.hl', 'xht', 'xhtml', },
	['haskell'] = { 'hs', 'hsc', },
	['idris'] = { 'idr', 'lidr', },
	['jsx'] = { 'jsx', },
	['java'] = { 'java', },
	['javascript'] = { 'js', },
	['julia'] = { 'jl', },
	['jupyter notebook'] = { 'ipynb', },
	['kotlin'] = { 'kt', 'ktm', 'kts', },
	['lean'] = { 'lean', 'hlean', },
	['less'] = { 'less', },
	['lua'] = { 'lua', 'fcgi', 'nse', 'pd_lua', 'rbxs', 'wlua', },
	['markdown'] = { 'md', 'markdown', 'mkd', 'mkdn', 'mkdown', 'ron', },
	['modula-2'] = { 'mod', },
	['moonscript'] = { 'moon', },
	['ocaml'] = { 'ml', 'eliom', 'eliomi', 'ml4', 'mli', 'mll', 'mly', },
	['objective-c'] = { 'm', 'h', },
	['objective-c++'] = { 'mm', },
	['oz'] = { 'oz', },
	['php'] = { 'php', 'aw', 'ctp', 'fcgi', 'inc', 'php3', 'php4', 'php5', 'phps', 'phpt', },
	['plsql'] = { 'pls', 'pck', 'pkb', 'pks', 'plb', 'plsql', 'sql', },
	['plpgsql'] = { 'sql', },
	['pascal'] = { 'pas', 'dfm', 'dpr', 'inc', 'lpr', 'pp', },
	['perl'] = { 'pl', 'al', 'cgi', 'fcgi', 'perl', 'ph', 'plx', 'pm', 'pod', 'psgi', 't', },
	['perl6'] = { '6pl', '6pm', 'nqp', 'p6', 'p6l', 'p6m', 'pl', 'pl6', 'pm', 'pm6', 't', },
	['picolisp'] = { 'l', },
	['pike'] = { 'pike', 'pmod', },
	['pony'] = { 'pony', },
	['postscript'] = { 'ps', 'eps', },
	['powershell'] = { 'ps1', 'psd1', 'psm1', },
	['prolog'] = { 'pl', 'pro', 'prolog', 'yap', },
	['python'] = { 'py', 'bzl', 'cgi', 'fcgi', 'gyp', 'lmi', 'pyde', 'pyp', 'pyt', 'pyw', 'rpy', 'tac', 'wsgi', 'xpy', },
	['racket'] = { 'rkt', 'rktd', 'rktl', 'scrbl', },
	['rebol'] = { 'reb', 'r', 'r2', 'r3', 'rebol', },
	['ruby'] = { 'rb', 'builder', 'fcgi', 'gemspec', 'god', 'irbrc', 'jbuilder', 'mspec', 'pluginspec', 'podspec', 'rabl', 'rake', 'rbuild', 'rbw', 'rbx', 'ru', 'ruby', 'thor', 'watchr', },
	['rust'] = { 'rs', 'rs.in', },
	['scss'] = { 'scss', },
	['sql'] = { 'sql', 'cql', 'ddl', 'inc', 'prc', 'tab', 'udf', 'viw', },
	['sqlpl'] = { 'sql', 'db2', },
	['scala'] = { 'scala', 'sbt', 'sc', },
	['scheme'] = { 'scm', 'sld', 'sls', 'sps', 'ss', },
	['self'] = { 'self', },
	['shell'] = { 'sh', 'bash', 'bats', 'cgi', 'command', 'fcgi', 'ksh', 'sh.in', 'tmux', 'tool', 'zsh', },
	['smalltalk'] = { 'st', 'cs', },
	['standard ml'] = { 'ML', 'fun', 'sig', 'sml', },
	['swift'] = { 'swift', },
	['tcl'] = { 'tcl', 'adp', 'tm', },
	['tex'] = { 'tex', 'aux', 'bbx', 'bib', 'cbx', 'cls', 'dtx', 'ins', 'lbx', 'ltx', 'mkii', 'mkiv', 'mkvi', 'sty', 'toc', },
	['typescript'] = { 'ts', 'tsx', },
	['vala'] = { 'vala', 'vapi', },
	['verilog'] = { 'v', 'veo', },
	['visual basic'] = { 'vb', 'bas', 'cls', 'frm', 'frx', 'vba', 'vbhtml', 'vbs', },
}

extensions.cpp    = extensions['c++']
extensions.csharp = extensions['c#']
extensions.latex  = extensions.tex
extensions.objc   = extensions['objective-c']

M.extensions = extensions

return M