diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/_load_package.lua | 49 | ||||
-rw-r--r-- | runtime/lua/vim/diagnostic.lua | 96 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 1625 | ||||
-rw-r--r-- | runtime/lua/vim/highlight.lua | 114 | ||||
-rw-r--r-- | runtime/lua/vim/keymap.lua | 135 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 253 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 26 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/diagnostic.lua | 46 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 106 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 4 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 5 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/sync.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 153 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 54 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 18 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 28 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 43 | ||||
-rw-r--r-- | runtime/lua/vim/ui.lua | 25 |
20 files changed, 2460 insertions, 325 deletions
diff --git a/runtime/lua/vim/_load_package.lua b/runtime/lua/vim/_load_package.lua new file mode 100644 index 0000000000..525f603438 --- /dev/null +++ b/runtime/lua/vim/_load_package.lua @@ -0,0 +1,49 @@ +-- prevents luacheck from making lints for setting things on vim +local vim = assert(vim) + +local pathtrails = {} +vim._so_trails = {} +for s in (package.cpath..';'):gmatch('[^;]*;') do + s = s:sub(1, -2) -- Strip trailing semicolon + -- Find out path patterns. pathtrail should contain something like + -- /?.so, \?.dll. This allows not to bother determining what correct + -- suffixes are. + local pathtrail = s:match('[/\\][^/\\]*%?.*$') + if pathtrail and not pathtrails[pathtrail] then + pathtrails[pathtrail] = true + table.insert(vim._so_trails, pathtrail) + end +end + +function vim._load_package(name) + local basename = name:gsub('%.', '/') + local paths = {"lua/"..basename..".lua", "lua/"..basename.."/init.lua"} + local found = vim.api.nvim__get_runtime(paths, false, {is_lua=true}) + if #found > 0 then + local f, err = loadfile(found[1]) + return f or error(err) + end + + local so_paths = {} + for _,trail in ipairs(vim._so_trails) do + local path = "lua"..trail:gsub('?', basename) -- so_trails contains a leading slash + table.insert(so_paths, path) + end + + found = vim.api.nvim__get_runtime(so_paths, false, {is_lua=true}) + if #found > 0 then + -- Making function name in Lua 5.1 (see src/loadlib.c:mkfuncname) is + -- a) strip prefix up to and including the first dash, if any + -- b) replace all dots by underscores + -- c) prepend "luaopen_" + -- So "foo-bar.baz" should result in "luaopen_bar_baz" + local dash = name:find("-", 1, true) + local modname = dash and name:sub(dash + 1) or name + local f, err = package.loadlib(found[1], "luaopen_"..modname:gsub("%.", "_")) + return f or error(err) + end + return nil +end + +-- Insert vim._load_package after the preloader at position 2 +table.insert(package.loaders, 2, vim._load_package) diff --git a/runtime/lua/vim/diagnostic.lua b/runtime/lua/vim/diagnostic.lua index 742ebf69b2..fcb1e61764 100644 --- a/runtime/lua/vim/diagnostic.lua +++ b/runtime/lua/vim/diagnostic.lua @@ -30,7 +30,7 @@ M.handlers = setmetatable({}, { __newindex = function(t, name, handler) vim.validate { handler = {handler, "t" } } rawset(t, name, handler) - if not global_diagnostic_options[name] then + if global_diagnostic_options[name] == nil then global_diagnostic_options[name] = true end end, @@ -447,7 +447,7 @@ local function set_list(loclist, opts) vim.fn.setqflist({}, ' ', { title = title, items = items }) end if open then - vim.api.nvim_command(loclist and "lopen" or "copen") + vim.api.nvim_command(loclist and "lopen" or "botright copen") end end @@ -552,17 +552,24 @@ end --- - `table`: Enable this feature with overrides. Use an empty table to use default values. --- - `function`: Function with signature (namespace, bufnr) that returns any of the above. --- ----@param opts table Configuration table with the following keys: +---@param opts table|nil When omitted or "nil", retrieve the current configuration. Otherwise, a +--- configuration table with the following keys: --- - underline: (default true) Use underline for diagnostics. Options: --- * severity: Only underline diagnostics matching the given severity --- |diagnostic-severity| ---- - virtual_text: (default true) Use virtual text for diagnostics. Options: +--- - virtual_text: (default true) Use virtual text for diagnostics. If multiple diagnostics +--- are set for a namespace, one prefix per diagnostic + the last diagnostic +--- message are shown. +--- Options: --- * severity: Only show virtual text for diagnostics matching the given --- severity |diagnostic-severity| --- * source: (boolean or string) Include the diagnostic source in virtual --- text. Use "if_many" to only show sources if there is more than --- one diagnostic source in the buffer. Otherwise, any truthy value --- means to always show the diagnostic source. +--- * spacing: (number) Amount of empty spaces inserted at the beginning +--- of the virtual text. +--- * prefix: (string) Prepend diagnostic message with prefix. --- * format: (function) A function that takes a diagnostic as input and --- returns a string. The return value is the text used to display --- the diagnostic. Example: @@ -593,7 +600,7 @@ end --- global diagnostic options. function M.config(opts, namespace) vim.validate { - opts = { opts, 't' }, + opts = { opts, 't', true }, namespace = { namespace, 'n', true }, } @@ -605,10 +612,13 @@ function M.config(opts, namespace) t = global_diagnostic_options end - for opt in pairs(global_diagnostic_options) do - if opts[opt] ~= nil then - t[opt] = opts[opt] - end + if not opts then + -- Return current config + return vim.deepcopy(t) + end + + for k, v in pairs(opts) do + t[k] = v end if namespace then @@ -638,7 +648,11 @@ function M.set(namespace, bufnr, diagnostics, opts) vim.validate { namespace = {namespace, 'n'}, bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, + diagnostics = { + diagnostics, + vim.tbl_islist, + "a list of diagnostics", + }, opts = {opts, 't', true}, } @@ -804,11 +818,16 @@ M.handlers.signs = { vim.validate { namespace = {namespace, 'n'}, bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, + diagnostics = { + diagnostics, + vim.tbl_islist, + "a list of diagnostics", + }, opts = {opts, 't', true}, } bufnr = get_bufnr(bufnr) + opts = opts or {} if opts.signs and opts.signs.severity then diagnostics = filter_by_severity(opts.signs.severity, diagnostics) @@ -867,11 +886,16 @@ M.handlers.underline = { vim.validate { namespace = {namespace, 'n'}, bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, + diagnostics = { + diagnostics, + vim.tbl_islist, + "a list of diagnostics", + }, opts = {opts, 't', true}, } bufnr = get_bufnr(bufnr) + opts = opts or {} if opts.underline and opts.underline.severity then diagnostics = filter_by_severity(opts.underline.severity, diagnostics) @@ -896,7 +920,8 @@ M.handlers.underline = { underline_ns, higroup, { diagnostic.lnum, diagnostic.col }, - { diagnostic.end_lnum, diagnostic.end_col } + { diagnostic.end_lnum, diagnostic.end_col }, + { priority = vim.highlight.priorities.diagnostics } ) end save_extmarks(underline_ns, bufnr) @@ -915,11 +940,16 @@ M.handlers.virtual_text = { vim.validate { namespace = {namespace, 'n'}, bufnr = {bufnr, 'n'}, - diagnostics = {diagnostics, 't'}, + diagnostics = { + diagnostics, + vim.tbl_islist, + "a list of diagnostics", + }, opts = {opts, 't', true}, } bufnr = get_bufnr(bufnr) + opts = opts or {} local severity if opts.virtual_text then @@ -1075,7 +1105,11 @@ function M.show(namespace, bufnr, diagnostics, opts) vim.validate { namespace = { namespace, 'n', true }, bufnr = { bufnr, 'n', true }, - diagnostics = { diagnostics, 't', true }, + diagnostics = { + diagnostics, + function(v) return v == nil or vim.tbl_islist(v) end, + "a list of diagnostics", + }, opts = { opts, 't', true }, } @@ -1346,16 +1380,16 @@ function M.reset(namespace, bufnr) diagnostic_cache[iter_bufnr][iter_namespace] = nil M.hide(iter_namespace, iter_bufnr) end - end - vim.api.nvim_buf_call(bufnr, function() - vim.api.nvim_command( - string.format( - "doautocmd <nomodeline> DiagnosticChanged %s", - vim.fn.fnameescape(vim.api.nvim_buf_get_name(bufnr)) + vim.api.nvim_buf_call(iter_bufnr, function() + vim.api.nvim_command( + string.format( + "doautocmd <nomodeline> DiagnosticChanged %s", + vim.fn.fnameescape(vim.api.nvim_buf_get_name(iter_bufnr)) + ) ) - ) - end) + end) + end end --- Add all diagnostics to the quickfix list. @@ -1520,7 +1554,13 @@ local errlist_type_map = { ---@param diagnostics table List of diagnostics |diagnostic-structure|. ---@return array of quickfix list items |setqflist-what| function M.toqflist(diagnostics) - vim.validate { diagnostics = {diagnostics, 't'} } + vim.validate { + diagnostics = { + diagnostics, + vim.tbl_islist, + "a list of diagnostics", + }, + } local list = {} for _, v in ipairs(diagnostics) do @@ -1551,7 +1591,13 @@ end --- |getloclist()|. ---@return array of diagnostics |diagnostic-structure| function M.fromqflist(list) - vim.validate { list = {list, 't'} } + vim.validate { + list = { + list, + vim.tbl_islist, + "a list of quickfix items", + }, + } local diagnostics = {} for _, item in ipairs(list) do diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua new file mode 100644 index 0000000000..f5e4dabfb6 --- /dev/null +++ b/runtime/lua/vim/filetype.lua @@ -0,0 +1,1625 @@ +local api = vim.api + +local M = {} + +---@private +local function starsetf(ft) + return {function(path) + if not vim.g.fg_ignore_pat then + return ft + end + + local re = vim.regex(vim.g.fg_ignore_pat) + if re:match_str(path) then + return ft + end + end, { + -- Starset matches should always have lowest priority + priority = -math.huge, + }} +end + +---@private +local function getline(bufnr, lnum) + return api.nvim_buf_get_lines(bufnr, lnum-1, lnum, false)[1] +end + +-- Filetypes based on file extension +-- luacheck: push no unused args +local extension = { + -- BEGIN EXTENSION + ["8th"] = "8th", + ["a65"] = "a65", + aap = "aap", + abap = "abap", + abc = "abc", + abl = "abel", + wrm = "acedb", + ads = "ada", + ada = "ada", + gpr = "ada", + adb = "ada", + tdf = "ahdl", + aidl = "aidl", + aml = "aml", + run = "ampl", + scpt = "applescript", + ino = "arduino", + pde = "arduino", + art = "art", + asciidoc = "asciidoc", + adoc = "asciidoc", + ["asn1"] = "asn", + asn = "asn", + atl = "atlas", + as = "atlas", + ahk = "autohotkey", + ["au3"] = "autoit", + ave = "ave", + gawk = "awk", + awk = "awk", + ref = "b", + imp = "b", + mch = "b", + bc = "bc", + bdf = "bdf", + beancount = "beancount", + bib = "bib", + bicep = "bicep", + bl = "blank", + bsdl = "bsdl", + bst = "bst", + bzl = "bzl", + bazel = "bzl", + BUILD = "bzl", + qc = "c", + cabal = "cabal", + cdl = "cdl", + toc = "cdrtoc", + cfc = "cf", + cfm = "cf", + cfi = "cf", + cfg = "cfg", + hgrc = "cfg", + chf = "ch", + chai = "chaiscript", + chs = "chaskell", + chopro = "chordpro", + crd = "chordpro", + crdpro = "chordpro", + cho = "chordpro", + chordpro = "chordpro", + eni = "cl", + dcl = "clean", + icl = "clean", + cljx = "clojure", + clj = "clojure", + cljc = "clojure", + cljs = "clojure", + cmake = "cmake", + cmod = "cmod", + lib = "cobol", + cob = "cobol", + cbl = "cobol", + atg = "coco", + recipe = "conaryrecipe", + mklx = "context", + mkiv = "context", + mkii = "context", + mkxl = "context", + mkvi = "context", + moc = "cpp", + hh = "cpp", + tlh = "cpp", + inl = "cpp", + ipp = "cpp", + ["c++"] = "cpp", + C = "cpp", + cxx = "cpp", + H = "cpp", + tcc = "cpp", + hxx = "cpp", + hpp = "cpp", + cpp = function(path, bufnr) + if vim.g.cynlib_syntax_for_cc then + return "cynlib" + end + return "cpp" + end, + cc = function(path, bufnr) + if vim.g.cynlib_syntax_for_cc then + return "cynlib" + end + return "cpp" + end, + crm = "crm", + csx = "cs", + cs = "cs", + csc = "csc", + csdl = "csdl", + fdr = "csp", + csp = "csp", + css = "css", + con = "cterm", + feature = "cucumber", + cuh = "cuda", + cu = "cuda", + pld = "cupl", + si = "cuplsim", + cyn = "cynpp", + dart = "dart", + drt = "dart", + ds = "datascript", + dcd = "dcd", + def = "def", + desc = "desc", + directory = "desktop", + desktop = "desktop", + diff = "diff", + rej = "diff", + Dockerfile = "dockerfile", + sys = "dosbatch", + bat = "dosbatch", + wrap = "dosini", + ini = "dosini", + dot = "dot", + gv = "dot", + drac = "dracula", + drc = "dracula", + dtd = "dtd", + dts = "dts", + dtsi = "dts", + dylan = "dylan", + intr = "dylanintr", + lid = "dylanlid", + ecd = "ecd", + eex = "eelixir", + leex = "eelixir", + exs = "elixir", + elm = "elm", + epp = "epuppet", + erl = "erlang", + hrl = "erlang", + yaws = "erlang", + erb = "eruby", + rhtml = "eruby", + ec = "esqlc", + EC = "esqlc", + strl = "esterel", + exp = "expect", + factor = "factor", + fal = "falcon", + fan = "fan", + fwt = "fan", + fnl = "fennel", + ["m4gl"] = "fgl", + ["4gl"] = "fgl", + ["4gh"] = "fgl", + fish = "fish", + focexec = "focexec", + fex = "focexec", + fth = "forth", + ft = "forth", + FOR = "fortran", + ["f77"] = "fortran", + ["f03"] = "fortran", + fortran = "fortran", + ["F95"] = "fortran", + ["f90"] = "fortran", + ["F03"] = "fortran", + fpp = "fortran", + FTN = "fortran", + ftn = "fortran", + ["for"] = "fortran", + ["F90"] = "fortran", + ["F77"] = "fortran", + ["f95"] = "fortran", + FPP = "fortran", + f = "fortran", + F = "fortran", + ["F08"] = "fortran", + ["f08"] = "fortran", + fpc = "fpcmake", + fsl = "framescript", + fb = "freebasic", + fsi = "fsharp", + fsx = "fsharp", + fusion = "fusion", + gdmo = "gdmo", + mo = "gdmo", + tres = "gdresource", + tscn = "gdresource", + gd = "gdscript", + ged = "gedcom", + gmi = "gemtext", + gemini = "gemtext", + gift = "gift", + glsl = "glsl", + gpi = "gnuplot", + gnuplot = "gnuplot", + go = "go", + gp = "gp", + gs = "grads", + gql = "graphql", + graphql = "graphql", + graphqls = "graphql", + gretl = "gretl", + gradle = "groovy", + groovy = "groovy", + gsp = "gsp", + gjs = "javascript.glimmer", + gts = "typescript.glimmer", + hack = "hack", + hackpartial = "hack", + haml = "haml", + hsm = "hamster", + hbs = "handlebars", + ["hs-boot"] = "haskell", + hsig = "haskell", + hsc = "haskell", + hs = "haskell", + ht = "haste", + htpp = "hastepreproc", + hb = "hb", + sum = "hercules", + errsum = "hercules", + ev = "hercules", + vc = "hercules", + hcl = "hcl", + heex = "heex", + hex = "hex", + ["h32"] = "hex", + hjson = "hjson", + hog = "hog", + hws = "hollywood", + htt = "httest", + htb = "httest", + iba = "ibasic", + ibi = "ibasic", + icn = "icon", + inf = "inform", + INF = "inform", + ii = "initng", + iss = "iss", + mst = "ist", + ist = "ist", + ijs = "j", + JAL = "jal", + jal = "jal", + jpr = "jam", + jpl = "jam", + jav = "java", + java = "java", + jj = "javacc", + jjt = "javacc", + es = "javascript", + mjs = "javascript", + javascript = "javascript", + js = "javascript", + cjs = "javascript", + jsx = "javascriptreact", + clp = "jess", + jgr = "jgraph", + ["j73"] = "jovial", + jov = "jovial", + jovial = "jovial", + properties = "jproperties", + slnf = "json", + json = "json", + jsonp = "json", + webmanifest = "json", + ipynb = "json", + ["json-patch"] = "json", + json5 = "json5", + jsonc = "jsonc", + jsp = "jsp", + jl = "julia", + kv = "kivy", + kix = "kix", + kts = "kotlin", + kt = "kotlin", + ktm = "kotlin", + ks = "kscript", + k = "kwt", + ACE = "lace", + ace = "lace", + latte = "latte", + lte = "latte", + ld = "ld", + ldif = "ldif", + journal = "ledger", + ldg = "ledger", + ledger = "ledger", + less = "less", + lex = "lex", + lxx = "lex", + ["l++"] = "lex", + l = "lex", + lhs = "lhaskell", + ll = "lifelines", + liquid = "liquid", + cl = "lisp", + L = "lisp", + lisp = "lisp", + el = "lisp", + lsp = "lisp", + asd = "lisp", + lt = "lite", + lite = "lite", + lgt = "logtalk", + lotos = "lotos", + lot = "lotos", + lout = "lout", + lou = "lout", + ulpc = "lpc", + lpc = "lpc", + sig = "lprolog", + lsl = "lsl", + lss = "lss", + nse = "lua", + rockspec = "lua", + lua = "lua", + quake = "m3quake", + at = "m4", + eml = "mail", + mk = "make", + mak = "make", + dsp = "make", + page = "mallard", + map = "map", + mws = "maple", + mpl = "maple", + mv = "maple", + mkdn = "markdown", + md = "markdown", + mdwn = "markdown", + mkd = "markdown", + markdown = "markdown", + mdown = "markdown", + mhtml = "mason", + comp = "mason", + mason = "mason", + master = "master", + mas = "master", + mel = "mel", + mf = "mf", + mgl = "mgl", + mgp = "mgp", + my = "mib", + mib = "mib", + mix = "mix", + mixal = "mix", + nb = "mma", + mmp = "mmp", + DEF = "modula2", + ["m2"] = "modula2", + MOD = "modula2", + mi = "modula2", + ssc = "monk", + monk = "monk", + tsc = "monk", + isc = "monk", + moo = "moo", + mp = "mp", + mof = "msidl", + odl = "msidl", + msql = "msql", + mu = "mupad", + mush = "mush", + mysql = "mysql", + ["n1ql"] = "n1ql", + nql = "n1ql", + nanorc = "nanorc", + ncf = "ncf", + nginx = "nginx", + ninja = "ninja", + nix = "nix", + nqc = "nqc", + roff = "nroff", + tmac = "nroff", + man = "nroff", + mom = "nroff", + nr = "nroff", + tr = "nroff", + nsi = "nsis", + nsh = "nsis", + obj = "obj", + mlt = "ocaml", + mly = "ocaml", + mll = "ocaml", + mlp = "ocaml", + mlip = "ocaml", + mli = "ocaml", + ml = "ocaml", + occ = "occam", + xom = "omnimark", + xin = "omnimark", + opam = "opam", + ["or"] = "openroad", + ora = "ora", + pxsl = "papp", + papp = "papp", + pxml = "papp", + pas = "pascal", + lpr = "pascal", + dpr = "pascal", + pbtxt = "pbtxt", + g = "pccts", + pcmk = "pcmk", + pdf = "pdf", + plx = "perl", + prisma = "prisma", + psgi = "perl", + al = "perl", + ctp = "php", + php = "php", + phtml = "php", + pike = "pike", + pmod = "pike", + rcp = "pilrc", + pli = "pli", + ["pl1"] = "pli", + ["p36"] = "plm", + plm = "plm", + pac = "plm", + plp = "plp", + pls = "plsql", + plsql = "plsql", + po = "po", + pot = "po", + pod = "pod", + pk = "poke", + ps = "postscr", + epsi = "postscr", + afm = "postscr", + epsf = "postscr", + eps = "postscr", + pfa = "postscr", + ai = "postscr", + pov = "pov", + ppd = "ppd", + it = "ppwiz", + ih = "ppwiz", + action = "privoxy", + pc = "proc", + pdb = "prolog", + pml = "promela", + proto = "proto", + ["psd1"] = "ps1", + ["psm1"] = "ps1", + ["ps1"] = "ps1", + pssc = "ps1", + ["ps1xml"] = "ps1xml", + psf = "psf", + psl = "psl", + pug = "pug", + arr = "pyret", + pxd = "pyrex", + pyx = "pyrex", + pyw = "python", + py = "python", + pyi = "python", + ptl = "python", + ql = "ql", + qll = "ql", + rad = "radiance", + mat = "radiance", + ["pod6"] = "raku", + rakudoc = "raku", + rakutest = "raku", + rakumod = "raku", + ["pm6"] = "raku", + raku = "raku", + ["t6"] = "raku", + ["p6"] = "raku", + raml = "raml", + rbs = "rbs", + rego = "rego", + rem = "remind", + remind = "remind", + res = "rescript", + resi = "rescript", + frt = "reva", + testUnit = "rexx", + rex = "rexx", + orx = "rexx", + rexx = "rexx", + jrexx = "rexx", + rxj = "rexx", + rexxj = "rexx", + testGroup = "rexx", + rxo = "rexx", + Rd = "rhelp", + rd = "rhelp", + rib = "rib", + Rmd = "rmd", + rmd = "rmd", + smd = "rmd", + Smd = "rmd", + rnc = "rnc", + rng = "rng", + rnw = "rnoweb", + snw = "rnoweb", + Rnw = "rnoweb", + Snw = "rnoweb", + rsc = "routeros", + x = "rpcgen", + rpl = "rpl", + Srst = "rrst", + srst = "rrst", + Rrst = "rrst", + rrst = "rrst", + rst = "rst", + rtf = "rtf", + rjs = "ruby", + rxml = "ruby", + rb = "ruby", + rant = "ruby", + ru = "ruby", + rbw = "ruby", + gemspec = "ruby", + builder = "ruby", + rake = "ruby", + rs = "rust", + sas = "sas", + sass = "sass", + sa = "sather", + sbt = "sbt", + scala = "scala", + sc = "scala", + scd = "scdoc", + ss = "scheme", + scm = "scheme", + sld = "scheme", + rkt = "scheme", + rktd = "scheme", + rktl = "scheme", + sce = "scilab", + sci = "scilab", + scss = "scss", + sd = "sd", + sdc = "sdc", + pr = "sdl", + sdl = "sdl", + sed = "sed", + sexp = "sexplib", + sieve = "sieve", + siv = "sieve", + sil = "sil", + sim = "simula", + ["s85"] = "sinda", + sin = "sinda", + ssm = "sisu", + sst = "sisu", + ssi = "sisu", + ["_sst"] = "sisu", + ["-sst"] = "sisu", + il = "skill", + ils = "skill", + cdf = "skill", + sl = "slang", + ice = "slice", + score = "slrnsc", + sol = "solidity", + tpl = "smarty", + ihlp = "smcl", + smcl = "smcl", + hlp = "smcl", + smith = "smith", + smt = "smith", + sml = "sml", + spt = "snobol4", + sno = "snobol4", + sln = "solution", + sparql = "sparql", + rq = "sparql", + spec = "spec", + spice = "spice", + sp = "spice", + spd = "spup", + spdata = "spup", + speedup = "spup", + spi = "spyce", + spy = "spyce", + tyc = "sql", + typ = "sql", + pkb = "sql", + tyb = "sql", + pks = "sql", + sqlj = "sqlj", + sqi = "sqr", + sqr = "sqr", + nut = "squirrel", + ["s28"] = "srec", + ["s37"] = "srec", + srec = "srec", + mot = "srec", + ["s19"] = "srec", + st = "st", + imata = "stata", + ["do"] = "stata", + mata = "stata", + ado = "stata", + stp = "stp", + sface = "surface", + svelte = "svelte", + svg = "svg", + swift = "swift", + svh = "systemverilog", + sv = "systemverilog", + tak = "tak", + task = "taskedit", + tm = "tcl", + tcl = "tcl", + itk = "tcl", + itcl = "tcl", + tk = "tcl", + jacl = "tcl", + tl = "teal", + tmpl = "template", + ti = "terminfo", + dtx = "tex", + ltx = "tex", + bbl = "tex", + latex = "tex", + sty = "tex", + texi = "texinfo", + txi = "texinfo", + texinfo = "texinfo", + text = "text", + tfvars = "terraform", + tla = "tla", + tli = "tli", + toml = "toml", + tpp = "tpp", + treetop = "treetop", + slt = "tsalt", + tsscl = "tsscl", + tssgm = "tssgm", + tssop = "tssop", + tutor = "tutor", + twig = "twig", + ts = function(path, bufnr) + if getline(bufnr, 1):find("<%?xml") then + return "xml" + else + return "typescript" + end + end, + tsx = "typescriptreact", + uc = "uc", + uit = "uil", + uil = "uil", + sba = "vb", + vb = "vb", + dsm = "vb", + ctl = "vb", + vbs = "vb", + vr = "vera", + vri = "vera", + vrh = "vera", + v = "verilog", + va = "verilogams", + vams = "verilogams", + vhdl = "vhdl", + vst = "vhdl", + vhd = "vhdl", + hdl = "vhdl", + vho = "vhdl", + vbe = "vhdl", + vim = "vim", + vba = "vim", + mar = "vmasm", + cm = "voscm", + wrl = "vrml", + vroom = "vroom", + vue = "vue", + wat = "wast", + wast = "wast", + wm = "webmacro", + wbt = "winbatch", + wml = "wml", + wsml = "wsml", + ad = "xdefaults", + xhtml = "xhtml", + xht = "xhtml", + msc = "xmath", + msf = "xmath", + ["psc1"] = "xml", + tpm = "xml", + xliff = "xml", + atom = "xml", + xul = "xml", + cdxml = "xml", + mpd = "xml", + rss = "xml", + fsproj = "xml", + ui = "xml", + vbproj = "xml", + xlf = "xml", + wsdl = "xml", + csproj = "xml", + wpl = "xml", + xmi = "xml", + ["xpm2"] = "xpm2", + xqy = "xquery", + xqm = "xquery", + xquery = "xquery", + xq = "xquery", + xql = "xquery", + xs = "xs", + xsd = "xsd", + xsl = "xslt", + xslt = "xslt", + yy = "yacc", + ["y++"] = "yacc", + yxx = "yacc", + yml = "yaml", + yaml = "yaml", + yang = "yang", + ["z8a"] = "z8a", + zig = "zig", + zu = "zimbu", + zut = "zimbutempl", + zsh = "zsh", + vala = "vala", + E = function() vim.fn["dist#ft#FTe"]() end, + EU = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + EW = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + EX = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + EXU = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + EXW = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + PL = function() vim.fn["dist#ft#FTpl"]() end, + R = function() vim.fn["dist#ft#FTr"]() end, + asm = function() vim.fn["dist#ft#FTasm"]() end, + bas = function() vim.fn["dist#ft#FTbas"]() end, + bi = function() vim.fn["dist#ft#FTbas"]() end, + bm = function() vim.fn["dist#ft#FTbas"]() end, + bash = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + btm = function() vim.fn["dist#ft#FTbtm"]() end, + c = function() vim.fn["dist#ft#FTlpc"]() end, + ch = function() vim.fn["dist#ft#FTchange"]() end, + com = function() vim.fn["dist#ft#BindzoneCheck"]('dcl') end, + cpt = function() vim.fn["dist#ft#FThtml"]() end, + csh = function() vim.fn["dist#ft#CSH"]() end, + d = function() vim.fn["dist#ft#DtraceCheck"]() end, + db = function() vim.fn["dist#ft#BindzoneCheck"]('') end, + dtml = function() vim.fn["dist#ft#FThtml"]() end, + e = function() vim.fn["dist#ft#FTe"]() end, + ebuild = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + eclass = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ent = function() vim.fn["dist#ft#FTent"]() end, + env = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + eu = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + ew = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + ex = function() vim.fn["dist#ft#ExCheck"]() end, + exu = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + exw = function() vim.fn["dist#ft#EuphoriaCheck"]() end, + frm = function() vim.fn["dist#ft#FTfrm"]() end, + fs = function() vim.fn["dist#ft#FTfs"]() end, + h = function() vim.fn["dist#ft#FTheader"]() end, + htm = function() vim.fn["dist#ft#FThtml"]() end, + html = function() vim.fn["dist#ft#FThtml"]() end, + i = function() vim.fn["dist#ft#FTprogress_asm"]() end, + idl = function() vim.fn["dist#ft#FTidl"]() end, + inc = function() vim.fn["dist#ft#FTinc"]() end, + inp = function() vim.fn["dist#ft#Check_inp"]() end, + ksh = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end, + lst = function() vim.fn["dist#ft#FTasm"]() end, + m = function() vim.fn["dist#ft#FTm"]() end, + mac = function() vim.fn["dist#ft#FTasm"]() end, + mc = function() vim.fn["dist#ft#McSetf"]() end, + mm = function() vim.fn["dist#ft#FTmm"]() end, + mms = function() vim.fn["dist#ft#FTmms"]() end, + p = function() vim.fn["dist#ft#FTprogress_pascal"]() end, + patch = function(path, bufnr) + local firstline = getline(bufnr, 1) + if string.find(firstline, "^From " .. string.rep("%x", 40) .. "+ Mon Sep 17 00:00:00 2001$") then + return "gitsendemail" + else + return "diff" + end + end, + pl = function() vim.fn["dist#ft#FTpl"]() end, + pp = function() vim.fn["dist#ft#FTpp"]() end, + pro = function() vim.fn["dist#ft#ProtoCheck"]('idlang') end, + pt = function() vim.fn["dist#ft#FThtml"]() end, + r = function() vim.fn["dist#ft#FTr"]() end, + rdf = function() vim.fn["dist#ft#Redif"]() end, + rules = function() vim.fn["dist#ft#FTRules"]() end, + sh = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + shtml = function() vim.fn["dist#ft#FThtml"]() end, + sql = function() vim.fn["dist#ft#SQL"]() end, + stm = function() vim.fn["dist#ft#FThtml"]() end, + tcsh = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end, + tex = function() vim.fn["dist#ft#FTtex"]() end, + tf = function() vim.fn["dist#ft#FTtf"]() end, + w = function() vim.fn["dist#ft#FTprogress_cweb"]() end, + xml = function() vim.fn["dist#ft#FTxml"]() end, + y = function() vim.fn["dist#ft#FTy"]() end, + zsql = function() vim.fn["dist#ft#SQL"]() end, + txt = function(path, bufnr) + --helpfiles match *.txt, but should have a modeline as last line + if not getline(bufnr, -1):match("vim:.*ft=help") then + return "text" + end + end, + -- END EXTENSION +} + +local filename = { + -- BEGIN FILENAME + ["a2psrc"] = "a2ps", + ["/etc/a2ps.cfg"] = "a2ps", + [".a2psrc"] = "a2ps", + [".asoundrc"] = "alsaconf", + ["/usr/share/alsa/alsa.conf"] = "alsaconf", + ["/etc/asound.conf"] = "alsaconf", + ["build.xml"] = "ant", + [".htaccess"] = "apache", + ["apt.conf"] = "aptconf", + ["/.aptitude/config"] = "aptconf", + ["=tagging-method"] = "arch", + [".arch-inventory"] = "arch", + ["GNUmakefile.am"] = "automake", + ["named.root"] = "bindzone", + WORKSPACE = "bzl", + BUILD = "bzl", + ["cabal.config"] = "cabalconfig", + ["cabal.project"] = "cabalproject", + calendar = "calendar", + catalog = "catalog", + ["/etc/cdrdao.conf"] = "cdrdaoconf", + [".cdrdao"] = "cdrdaoconf", + ["/etc/default/cdrdao"] = "cdrdaoconf", + ["/etc/defaults/cdrdao"] = "cdrdaoconf", + ["cfengine.conf"] = "cfengine", + ["CMakeLists.txt"] = "cmake", + ["auto.master"] = "conf", + ["configure.in"] = "config", + ["configure.ac"] = "config", + [".cvsrc"] = "cvsrc", + ["/debian/changelog"] = "debchangelog", + ["changelog.dch"] = "debchangelog", + ["changelog.Debian"] = "debchangelog", + ["NEWS.dch"] = "debchangelog", + ["NEWS.Debian"] = "debchangelog", + ["/debian/control"] = "debcontrol", + ["/debian/copyright"] = "debcopyright", + ["/etc/apt/sources.list"] = "debsources", + ["denyhosts.conf"] = "denyhosts", + ["dict.conf"] = "dictconf", + [".dictrc"] = "dictconf", + ["/etc/DIR_COLORS"] = "dircolors", + [".dir_colors"] = "dircolors", + [".dircolors"] = "dircolors", + ["/etc/dnsmasq.conf"] = "dnsmasq", + Containerfile = "dockerfile", + Dockerfile = "dockerfile", + npmrc = "dosini", + ["/etc/yum.conf"] = "dosini", + ["/etc/pacman.conf"] = "dosini", + [".npmrc"] = "dosini", + [".editorconfig"] = "dosini", + dune = "dune", + jbuild = "dune", + ["dune-workspace"] = "dune", + ["dune-project"] = "dune", + ["elinks.conf"] = "elinks", + ["mix.lock"] = "elixir", + ["filter-rules"] = "elmfilt", + ["exim.conf"] = "exim", + exports = "exports", + [".fetchmailrc"] = "fetchmail", + fvSchemes = function() vim.fn["dist#ft#FTfoam"]() end, + fvSolution = function() vim.fn["dist#ft#FTfoam"]() end, + fvConstraints = function() vim.fn["dist#ft#FTfoam"]() end, + fvModels = function() vim.fn["dist#ft#FTfoam"]() end, + fstab = "fstab", + mtab = "fstab", + [".gdbinit"] = "gdb", + gdbinit = "gdb", + ["lltxxxxx.txt"] = "gedcom", + ["TAG_EDITMSG"] = "gitcommit", + ["MERGE_MSG"] = "gitcommit", + ["COMMIT_EDITMSG"] = "gitcommit", + ["NOTES_EDITMSG"] = "gitcommit", + ["EDIT_DESCRIPTION"] = "gitcommit", + [".gitconfig"] = "gitconfig", + [".gitmodules"] = "gitconfig", + ["gitolite.conf"] = "gitolite", + ["git-rebase-todo"] = "gitrebase", + gkrellmrc = "gkrellmrc", + [".gnashrc"] = "gnash", + [".gnashpluginrc"] = "gnash", + gnashpluginrc = "gnash", + gnashrc = "gnash", + ["go.work"] = "gowork", + [".gprc"] = "gp", + ["/.gnupg/gpg.conf"] = "gpg", + ["/.gnupg/options"] = "gpg", + ["/var/backups/gshadow.bak"] = "group", + ["/etc/gshadow"] = "group", + ["/etc/group-"] = "group", + ["/etc/gshadow.edit"] = "group", + ["/etc/gshadow-"] = "group", + ["/etc/group"] = "group", + ["/var/backups/group.bak"] = "group", + ["/etc/group.edit"] = "group", + ["/boot/grub/menu.lst"] = "grub", + ["/etc/grub.conf"] = "grub", + ["/boot/grub/grub.conf"] = "grub", + [".gtkrc"] = "gtkrc", + gtkrc = "gtkrc", + ["snort.conf"] = "hog", + ["vision.conf"] = "hog", + ["/etc/host.conf"] = "hostconf", + ["/etc/hosts.allow"] = "hostsaccess", + ["/etc/hosts.deny"] = "hostsaccess", + ["/i3/config"] = "i3config", + ["/sway/config"] = "i3config", + ["/.sway/config"] = "i3config", + ["/.i3/config"] = "i3config", + ["/.icewm/menu"] = "icemenu", + [".indent.pro"] = "indent", + indentrc = "indent", + inittab = "inittab", + ["ipf.conf"] = "ipfilter", + ["ipf6.conf"] = "ipfilter", + ["ipf.rules"] = "ipfilter", + [".eslintrc"] = "json", + [".babelrc"] = "json", + ["Pipfile.lock"] = "json", + [".firebaserc"] = "json", + [".prettierrc"] = "json", + Kconfig = "kconfig", + ["Kconfig.debug"] = "kconfig", + ["lftp.conf"] = "lftp", + [".lftprc"] = "lftp", + ["/.libao"] = "libao", + ["/etc/libao.conf"] = "libao", + ["lilo.conf"] = "lilo", + ["/etc/limits"] = "limits", + [".emacs"] = "lisp", + sbclrc = "lisp", + [".sbclrc"] = "lisp", + [".sawfishrc"] = "lisp", + ["/etc/login.access"] = "loginaccess", + ["/etc/login.defs"] = "logindefs", + ["lynx.cfg"] = "lynx", + ["m3overrides"] = "m3build", + ["m3makefile"] = "m3build", + ["cm3.cfg"] = "m3quake", + [".followup"] = "mail", + [".article"] = "mail", + [".letter"] = "mail", + ["/etc/aliases"] = "mailaliases", + ["/etc/mail/aliases"] = "mailaliases", + mailcap = "mailcap", + [".mailcap"] = "mailcap", + ["/etc/man.conf"] = "manconf", + ["man.config"] = "manconf", + ["meson.build"] = "meson", + ["meson_options.txt"] = "meson", + ["/etc/conf.modules"] = "modconf", + ["/etc/modules"] = "modconf", + ["/etc/modules.conf"] = "modconf", + ["/.mplayer/config"] = "mplayerconf", + ["mplayer.conf"] = "mplayerconf", + mrxvtrc = "mrxvtrc", + [".mrxvtrc"] = "mrxvtrc", + ["/etc/nanorc"] = "nanorc", + Neomuttrc = "neomuttrc", + [".netrc"] = "netrc", + [".ocamlinit"] = "ocaml", + [".octaverc"] = "octave", + octaverc = "octave", + ["octave.conf"] = "octave", + opam = "opam", + ["/etc/pam.conf"] = "pamconf", + ["pam_env.conf"] = "pamenv", + [".pam_environment"] = "pamenv", + ["/var/backups/passwd.bak"] = "passwd", + ["/var/backups/shadow.bak"] = "passwd", + ["/etc/passwd"] = "passwd", + ["/etc/passwd-"] = "passwd", + ["/etc/shadow.edit"] = "passwd", + ["/etc/shadow-"] = "passwd", + ["/etc/shadow"] = "passwd", + ["/etc/passwd.edit"] = "passwd", + ["pf.conf"] = "pf", + ["main.cf"] = "pfmain", + pinerc = "pine", + [".pinercex"] = "pine", + [".pinerc"] = "pine", + pinercex = "pine", + ["/etc/pinforc"] = "pinfo", + ["/.pinforc"] = "pinfo", + [".povrayrc"] = "povini", + [".procmailrc"] = "procmail", + [".procmail"] = "procmail", + ["/etc/protocols"] = "protocols", + [".pythonstartup"] = "python", + [".pythonrc"] = "python", + SConstruct = "python", + ratpoisonrc = "ratpoison", + [".ratpoisonrc"] = "ratpoison", + v = "rcs", + inputrc = "readline", + [".inputrc"] = "readline", + [".reminders"] = "remind", + ["resolv.conf"] = "resolv", + ["robots.txt"] = "robots", + Gemfile = "ruby", + Puppetfile = "ruby", + [".irbrc"] = "ruby", + irbrc = "ruby", + ["smb.conf"] = "samba", + screenrc = "screen", + [".screenrc"] = "screen", + ["/etc/sensors3.conf"] = "sensors", + ["/etc/sensors.conf"] = "sensors", + ["/etc/services"] = "services", + ["/etc/serial.conf"] = "setserial", + ["/etc/udev/cdsymlinks.conf"] = "sh", + ["/etc/slp.conf"] = "slpconf", + ["/etc/slp.reg"] = "slpreg", + ["/etc/slp.spi"] = "slpspi", + [".slrnrc"] = "slrnrc", + ["sendmail.cf"] = "sm", + ["squid.conf"] = "squid", + ["/.ssh/config"] = "sshconfig", + ["ssh_config"] = "sshconfig", + ["sshd_config"] = "sshdconfig", + ["/etc/sudoers"] = "sudoers", + ["sudoers.tmp"] = "sudoers", + ["/etc/sysctl.conf"] = "sysctl", + tags = "tags", + [".tclshrc"] = "tcl", + [".wishrc"] = "tcl", + ["tclsh.rc"] = "tcl", + ["texmf.cnf"] = "texmf", + COPYING = "text", + README = "text", + LICENSE = "text", + AUTHORS = "text", + tfrc = "tf", + [".tfrc"] = "tf", + ["tidy.conf"] = "tidy", + tidyrc = "tidy", + [".tidyrc"] = "tidy", + [".tmux.conf"] = "tmux", + ["/.cargo/config"] = "toml", + Pipfile = "toml", + ["Gopkg.lock"] = "toml", + ["/.cargo/credentials"] = "toml", + ["Cargo.lock"] = "toml", + ["trustees.conf"] = "trustees", + ["/etc/udev/udev.conf"] = "udevconf", + ["/etc/updatedb.conf"] = "updatedb", + ["fdrupstream.log"] = "upstreamlog", + vgrindefs = "vgrindefs", + [".exrc"] = "vim", + ["_exrc"] = "vim", + ["_viminfo"] = "viminfo", + [".viminfo"] = "viminfo", + [".wgetrc"] = "wget", + wgetrc = "wget", + [".wvdialrc"] = "wvdial", + ["wvdial.conf"] = "wvdial", + [".Xresources"] = "xdefaults", + [".Xpdefaults"] = "xdefaults", + ["xdm-config"] = "xdefaults", + [".Xdefaults"] = "xdefaults", + ["/etc/xinetd.conf"] = "xinetd", + fglrxrc = "xml", + ["/etc/blkid.tab"] = "xml", + ["/etc/blkid.tab.old"] = "xml", + ["/etc/zprofile"] = "zsh", + [".zlogin"] = "zsh", + [".zlogout"] = "zsh", + [".zshrc"] = "zsh", + [".zprofile"] = "zsh", + [".zcompdump"] = "zsh", + [".zshenv"] = "zsh", + [".zfbfmarks"] = "zsh", + [".alias"] = function() vim.fn["dist#ft#CSH"]() end, + [".bashrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + [".cshrc"] = function() vim.fn["dist#ft#CSH"]() end, + [".env"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + [".kshrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end, + [".login"] = function() vim.fn["dist#ft#CSH"]() end, + [".profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + [".tcshrc"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end, + ["/etc/profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + APKBUILD = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + PKGBUILD = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ["bash.bashrc"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + bashrc = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + crontab = starsetf('crontab'), + ["csh.cshrc"] = function() vim.fn["dist#ft#CSH"]() end, + ["csh.login"] = function() vim.fn["dist#ft#CSH"]() end, + ["csh.logout"] = function() vim.fn["dist#ft#CSH"]() end, + ["indent.pro"] = function() vim.fn["dist#ft#ProtoCheck"]('indent') end, + ["tcsh.login"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end, + ["tcsh.tcshrc"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end, + -- END FILENAME +} + +local pattern = { + -- BEGIN PATTERN + [".*/etc/a2ps/.*%.cfg"] = "a2ps", + [".*/etc/a2ps%.cfg"] = "a2ps", + [".*/usr/share/alsa/alsa%.conf"] = "alsaconf", + [".*/etc/asound%.conf"] = "alsaconf", + [".*/etc/apache2/sites%-.*/.*%.com"] = "apache", + [".*/etc/httpd/.*%.conf"] = "apache", + [".*/%.aptitude/config"] = "aptconf", + ["[mM]akefile%.am"] = "automake", + [".*bsd"] = "bsdl", + ["bzr_log%..*"] = "bzr", + [".*enlightenment/.*%.cfg"] = "c", + [".*/etc/defaults/cdrdao"] = "cdrdaoconf", + [".*/etc/cdrdao%.conf"] = "cdrdaoconf", + [".*/etc/default/cdrdao"] = "cdrdaoconf", + [".*hgrc"] = "cfg", + [".*%.%.ch"] = "chill", + [".*%.cmake%.in"] = "cmake", + [".*/debian/changelog"] = "debchangelog", + [".*/debian/control"] = "debcontrol", + [".*/debian/copyright"] = "debcopyright", + [".*/etc/apt/sources%.list%.d/.*%.list"] = "debsources", + [".*/etc/apt/sources%.list"] = "debsources", + ["dictd.*%.conf"] = "dictdconf", + [".*/etc/DIR_COLORS"] = "dircolors", + [".*/etc/dnsmasq%.conf"] = "dnsmasq", + ["php%.ini%-.*"] = "dosini", + [".*/etc/pacman%.conf"] = "dosini", + [".*/etc/yum%.conf"] = "dosini", + [".*lvs"] = "dracula", + [".*lpe"] = "dracula", + [".*/dtrace/.*%.d"] = "dtrace", + [".*esmtprc"] = "esmtprc", + [".*Eterm/.*%.cfg"] = "eterm", + [".*%.git/modules/.*/config"] = "gitconfig", + [".*%.git/config"] = "gitconfig", + [".*/etc/gitconfig"] = "gitconfig", + [".*/%.config/git/config"] = "gitconfig", + [".*%.git/config%.worktree"] = "gitconfig", + [".*%.git/worktrees/.*/config%.worktree"] = "gitconfig", + ["%.gitsendemail%.msg%......."] = "gitsendemail", + ["gkrellmrc_."] = "gkrellmrc", + [".*/usr/.*/gnupg/options%.skel"] = "gpg", + [".*/%.gnupg/options"] = "gpg", + [".*/%.gnupg/gpg%.conf"] = "gpg", + [".*/etc/group"] = "group", + [".*/etc/gshadow"] = "group", + [".*/etc/group%.edit"] = "group", + [".*/var/backups/gshadow%.bak"] = "group", + [".*/etc/group-"] = "group", + [".*/etc/gshadow-"] = "group", + [".*/var/backups/group%.bak"] = "group", + [".*/etc/gshadow%.edit"] = "group", + [".*/boot/grub/grub%.conf"] = "grub", + [".*/boot/grub/menu%.lst"] = "grub", + [".*/etc/grub%.conf"] = "grub", + ["hg%-editor%-.*%.txt"] = "hgcommit", + [".*/etc/host%.conf"] = "hostconf", + [".*/etc/hosts%.deny"] = "hostsaccess", + [".*/etc/hosts%.allow"] = "hostsaccess", + [".*%.html%.m4"] = "htmlm4", + [".*/%.i3/config"] = "i3config", + [".*/sway/config"] = "i3config", + [".*/i3/config"] = "i3config", + [".*/%.sway/config"] = "i3config", + [".*/%.icewm/menu"] = "icemenu", + [".*/etc/initng/.*/.*%.i"] = "initng", + [".*%.properties_.."] = "jproperties", + [".*%.properties_.._.."] = "jproperties", + [".*lftp/rc"] = "lftp", + [".*/%.libao"] = "libao", + [".*/etc/libao%.conf"] = "libao", + [".*/etc/.*limits%.conf"] = "limits", + [".*/etc/limits"] = "limits", + [".*/etc/.*limits%.d/.*%.conf"] = "limits", + [".*/LiteStep/.*/.*%.rc"] = "litestep", + [".*/etc/login%.access"] = "loginaccess", + [".*/etc/login%.defs"] = "logindefs", + [".*/etc/mail/aliases"] = "mailaliases", + [".*/etc/aliases"] = "mailaliases", + [".*[mM]akefile"] = "make", + [".*/etc/man%.conf"] = "manconf", + [".*/etc/modules%.conf"] = "modconf", + [".*/etc/conf%.modules"] = "modconf", + [".*/etc/modules"] = "modconf", + [".*%.[mi][3g]"] = "modula3", + [".*/%.mplayer/config"] = "mplayerconf", + ["rndc.*%.conf"] = "named", + ["rndc.*%.key"] = "named", + ["named.*%.conf"] = "named", + [".*/etc/nanorc"] = "nanorc", + [".*%.NS[ACGLMNPS]"] = "natural", + ["nginx.*%.conf"] = "nginx", + [".*/etc/nginx/.*"] = "nginx", + [".*nginx%.conf"] = "nginx", + [".*/nginx/.*%.conf"] = "nginx", + [".*/usr/local/nginx/conf/.*"] = "nginx", + [".*%.ml%.cppo"] = "ocaml", + [".*%.mli%.cppo"] = "ocaml", + [".*%.opam%.template"] = "opam", + [".*%.[Oo][Pp][Ll]"] = "opl", + [".*/etc/pam%.conf"] = "pamconf", + [".*/etc/passwd-"] = "passwd", + [".*/etc/shadow"] = "passwd", + [".*/etc/shadow%.edit"] = "passwd", + [".*/var/backups/shadow%.bak"] = "passwd", + [".*/var/backups/passwd%.bak"] = "passwd", + [".*/etc/passwd"] = "passwd", + [".*/etc/passwd%.edit"] = "passwd", + [".*/etc/shadow-"] = "passwd", + [".*/%.pinforc"] = "pinfo", + [".*/etc/pinforc"] = "pinfo", + [".*/etc/protocols"] = "protocols", + [".*baseq[2-3]/.*%.cfg"] = "quake", + [".*quake[1-3]/.*%.cfg"] = "quake", + [".*id1/.*%.cfg"] = "quake", + ["[rR]antfile"] = "ruby", + ["[rR]akefile"] = "ruby", + [".*/etc/sensors%.conf"] = "sensors", + [".*/etc/sensors3%.conf"] = "sensors", + [".*/etc/services"] = "services", + [".*/etc/serial%.conf"] = "setserial", + [".*/etc/udev/cdsymlinks%.conf"] = "sh", + [".*%._sst%.meta"] = "sisu", + [".*%.%-sst%.meta"] = "sisu", + [".*%.sst%.meta"] = "sisu", + [".*/etc/slp%.conf"] = "slpconf", + [".*/etc/slp%.reg"] = "slpreg", + [".*/etc/slp%.spi"] = "slpspi", + [".*/etc/ssh/ssh_config%.d/.*%.conf"] = "sshconfig", + [".*/%.ssh/config"] = "sshconfig", + [".*/etc/ssh/sshd_config%.d/.*%.conf"] = "sshdconfig", + [".*/etc/sudoers"] = "sudoers", + ["svn%-commit.*%.tmp"] = "svn", + [".*%.swift%.gyb"] = "swiftgyb", + [".*/etc/sysctl%.conf"] = "sysctl", + [".*/etc/sysctl%.d/.*%.conf"] = "sysctl", + [".*/etc/systemd/.*%.conf%.d/.*%.conf"] = "systemd", + [".*/%.config/systemd/user/.*%.d/.*%.conf"] = "systemd", + [".*/etc/systemd/system/.*%.d/.*%.conf"] = "systemd", + [".*%.t%.html"] = "tilde", + ["%.?tmux.*%.conf"] = "tmux", + ["%.?tmux.*%.conf.*"] = { "tmux", { priority = -1 } }, + [".*/%.cargo/config"] = "toml", + [".*/%.cargo/credentials"] = "toml", + [".*/etc/udev/udev%.conf"] = "udevconf", + [".*/etc/udev/permissions%.d/.*%.permissions"] = "udevperm", + [".*/etc/updatedb%.conf"] = "updatedb", + [".*/%.init/.*%.override"] = "upstart", + [".*/usr/share/upstart/.*%.conf"] = "upstart", + [".*/%.config/upstart/.*%.override"] = "upstart", + [".*/etc/init/.*%.conf"] = "upstart", + [".*/etc/init/.*%.override"] = "upstart", + [".*/%.config/upstart/.*%.conf"] = "upstart", + [".*/%.init/.*%.conf"] = "upstart", + [".*/usr/share/upstart/.*%.override"] = "upstart", + [".*%.ws[fc]"] = "wsh", + [".*/etc/xinetd%.conf"] = "xinetd", + [".*/etc/blkid%.tab"] = "xml", + [".*/etc/blkid%.tab%.old"] = "xml", + [".*%.vbproj%.user"] = "xml", + [".*%.fsproj%.user"] = "xml", + [".*%.csproj%.user"] = "xml", + [".*/etc/xdg/menus/.*%.menu"] = "xml", + [".*Xmodmap"] = "xmodmap", + [".*/etc/zprofile"] = "zsh", + ["%.bash[_-]aliases"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ["%.bash[_-]logout"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ["%.bash[_-]profile"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ["%.cshrc.*"] = function() vim.fn["dist#ft#CSH"]() end, + ["%.gtkrc.*"] = starsetf('gtkrc'), + ["%.kshrc.*"] = function() vim.fn["dist#ft#SetFileTypeSH"]("ksh") end, + ["%.login.*"] = function() vim.fn["dist#ft#CSH"]() end, + ["%.neomuttrc.*"] = starsetf('neomuttrc'), + ["%.profile.*"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + ["%.reminders.*"] = starsetf('remind'), + ["%.tcshrc.*"] = function() vim.fn["dist#ft#SetFileTypeShell"]("tcsh") end, + ["%.zcompdump.*"] = starsetf('zsh'), + ["%.zlog.*"] = starsetf('zsh'), + ["%.zsh.*"] = starsetf('zsh'), + [".*%.[1-9]"] = function() vim.fn["dist#ft#FTnroff"]() end, + [".*%.[aA]"] = function() vim.fn["dist#ft#FTasm"]() end, + [".*%.[sS]"] = function() vim.fn["dist#ft#FTasm"]() end, + [".*%.properties_.._.._.*"] = starsetf('jproperties'), + [".*%.vhdl_[0-9].*"] = starsetf('vhdl'), + [".*/%.fvwm/.*"] = starsetf('fvwm'), + [".*/%.gitconfig%.d/.*"] = starsetf('gitconfig'), + [".*/%.neomutt/neomuttrc.*"] = starsetf('neomuttrc'), + [".*/Xresources/.*"] = starsetf('xdefaults'), + [".*/app%-defaults/.*"] = starsetf('xdefaults'), + [".*/bind/db%..*"] = starsetf('bindzone'), + [".*/debian/patches/.*"] = function() vim.fn["dist#ft#Dep3patch"]() end, + [".*/etc/Muttrc%.d/.*"] = starsetf('muttrc'), + [".*/etc/apache2/.*%.conf.*"] = starsetf('apache'), + [".*/etc/apache2/conf%..*/.*"] = starsetf('apache'), + [".*/etc/apache2/mods%-.*/.*"] = starsetf('apache'), + [".*/etc/apache2/sites%-.*/.*"] = starsetf('apache'), + [".*/etc/cron%.d/.*"] = starsetf('crontab'), + [".*/etc/dnsmasq%.d/.*"] = starsetf('dnsmasq'), + [".*/etc/httpd/conf%..*/.*"] = starsetf('apache'), + [".*/etc/httpd/conf%.d/.*%.conf.*"] = starsetf('apache'), + [".*/etc/httpd/mods%-.*/.*"] = starsetf('apache'), + [".*/etc/httpd/sites%-.*/.*"] = starsetf('apache'), + [".*/etc/logcheck/.*%.d.*/.*"] = starsetf('logcheck'), + [".*/etc/modprobe%..*"] = starsetf('modconf'), + [".*/etc/pam%.d/.*"] = starsetf('pamconf'), + [".*/etc/profile"] = function() vim.fn["dist#ft#SetFileTypeSH"](vim.fn.getline(1)) end, + [".*/etc/proftpd/.*%.conf.*"] = starsetf('apachestyle'), + [".*/etc/proftpd/conf%..*/.*"] = starsetf('apachestyle'), + [".*/etc/sudoers%.d/.*"] = starsetf('sudoers'), + [".*/etc/xinetd%.d/.*"] = starsetf('xinetd'), + [".*/etc/yum%.repos%.d/.*"] = starsetf('dosini'), + [".*/gitolite%-admin/conf/.*"] = starsetf('gitolite'), + [".*/named/db%..*"] = starsetf('bindzone'), + [".*/tmp/lltmp.*"] = starsetf('gedcom'), + [".*asterisk.*/.*voicemail%.conf.*"] = starsetf('asteriskvm'), + [".*asterisk/.*%.conf.*"] = starsetf('asterisk'), + [".*vimrc.*"] = starsetf('vim'), + [".*xmodmap.*"] = starsetf('xmodmap'), + ["/etc/gitconfig%.d/.*"] = starsetf('gitconfig'), + ["/etc/hostname%..*"] = starsetf('config'), + ["Containerfile%..*"] = starsetf('dockerfile'), + ["Dockerfile%..*"] = starsetf('dockerfile'), + ["JAM.*%..*"] = starsetf('jam'), + ["Kconfig%..*"] = starsetf('kconfig'), + ["Neomuttrc.*"] = starsetf('neomuttrc'), + ["Prl.*%..*"] = starsetf('jam'), + ["Xresources.*"] = starsetf('xdefaults'), + ["[mM]akefile.*"] = starsetf('make'), + ["[rR]akefile.*"] = starsetf('ruby'), + ["access%.conf.*"] = starsetf('apache'), + ["apache%.conf.*"] = starsetf('apache'), + ["apache2%.conf.*"] = starsetf('apache'), + ["bash%-fc[-%.]"] = function() vim.fn["dist#ft#SetFileTypeSH"]("bash") end, + ["cabal%.project%..*"] = starsetf('cabalproject'), + ["crontab%..*"] = starsetf('crontab'), + ["drac%..*"] = starsetf('dracula'), + ["gtkrc.*"] = starsetf('gtkrc'), + ["httpd%.conf.*"] = starsetf('apache'), + ["lilo%.conf.*"] = starsetf('lilo'), + ["neomuttrc.*"] = starsetf('neomuttrc'), + ["proftpd%.conf.*"] = starsetf('apachestyle'), + ["reportbug%-.*"] = starsetf('mail'), + ["sgml%.catalog.*"] = starsetf('catalog'), + ["srm%.conf.*"] = starsetf('apache'), + ["tmac%..*"] = starsetf('nroff'), + ["zlog.*"] = starsetf('zsh'), + ["zsh.*"] = starsetf('zsh'), + ["ae%d+%.txt"] = 'mail', + ["snd%.%d+"] = "mail", + ["%.letter%.%d+"] = "mail", + ["%.article%.%d+"] = "mail", + ["pico%.%d+"] = "mail", + ["mutt%-.*%-%w+"] = "mail", + ["neomutt%-.*%-%w+"] = "mail", + ["muttng%-.*%-%w+"] = "mail", + ["mutt" .. string.rep("[%w_-]", 6)] = "mail", + ["neomutt" .. string.rep("[%w_-]", 6)] = "mail", + ["/tmp/SLRN[0-9A-Z.]+"] = "mail", + ["[a-zA-Z0-9].*Dict"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z0-9].*Dict%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z].*Properties"] = function() vim.fn["dist#ft#FTfoam"]() end, + ["[a-zA-Z].*Properties%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*Transport%..*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/constant/g"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/0/.*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/0%.orig/.*"] = function() vim.fn["dist#ft#FTfoam"]() end, + [".*/etc/sensors%.d/[^.].*"] = starsetf('sensors'), + [".*%.git/.*"] = function(path, bufnr) + local firstline = getline(bufnr, 1) + if firstline:find("^" .. string.rep("%x", 40) .. "+ ") or firstline:sub(1, 5) == "ref: " then + return "git" + end + end, + -- END PATTERN +} +-- luacheck: pop + +---@private +local function sort_by_priority(t) + local sorted = {} + for k, v in pairs(t) do + local ft = type(v) == "table" and v[1] or v + assert(type(ft) == "string" or type(ft) == "function", "Expected string or function for filetype") + + local opts = (type(v) == "table" and type(v[2]) == "table") and v[2] or {} + if not opts.priority then + opts.priority = 0 + end + table.insert(sorted, { [k] = { ft, opts } }) + end + table.sort(sorted, function(a, b) + return a[next(a)][2].priority > b[next(b)][2].priority + end) + return sorted +end + +local pattern_sorted = sort_by_priority(pattern) + +---@private +local function normalize_path(path) + return (path:gsub("\\", "/"):gsub("^~", vim.env.HOME)) +end + +--- Add new filetype mappings. +--- +--- Filetype mappings can be added either by extension or by filename (either +--- the "tail" or the full file path). The full file path is checked first, +--- followed by the file name. If a match is not found using the filename, then +--- the filename is matched against the list of patterns (sorted by priority) +--- until a match is found. Lastly, if pattern matching does not find a +--- filetype, then the file extension is used. +--- +--- The filetype can be either a string (in which case it is used as the +--- filetype directly) or a function. If a function, it takes the full path and +--- buffer number of the file as arguments (along with captures from the matched +--- pattern, if any) and should return a string that will be used as the +--- buffer's filetype. +--- +--- Filename patterns can specify an optional priority to resolve cases when a +--- file path matches multiple patterns. Higher priorities are matched first. +--- When omitted, the priority defaults to 0. +--- +--- See $VIMRUNTIME/lua/vim/filetype.lua for more examples. +--- +--- Note that Lua filetype detection is only enabled when |g:do_filetype_lua| is +--- set to 1. +--- +--- Example: +--- <pre> +--- vim.filetype.add({ +--- extension = { +--- foo = "fooscript", +--- bar = function(path, bufnr) +--- if some_condition() then +--- return "barscript" +--- end +--- return "bar" +--- end, +--- }, +--- filename = { +--- [".foorc"] = "toml", +--- ["/etc/foo/config"] = "toml", +--- }, +--- pattern = { +--- [".*/etc/foo/.*"] = "fooscript", +--- -- Using an optional priority +--- [".*/etc/foo/.*%.conf"] = { "dosini", { priority = 10 } }, +--- ["README.(%a+)$"] = function(path, bufnr, ext) +--- if ext == "md" then +--- return "markdown" +--- elseif ext == "rst" then +--- return "rst" +--- end +--- end, +--- }, +--- }) +--- </pre> +--- +---@param filetypes table A table containing new filetype maps (see example). +function M.add(filetypes) + for k, v in pairs(filetypes.extension or {}) do + extension[k] = v + end + + for k, v in pairs(filetypes.filename or {}) do + filename[normalize_path(k)] = v + end + + for k, v in pairs(filetypes.pattern or {}) do + pattern[normalize_path(k)] = v + end + + if filetypes.pattern then + pattern_sorted = sort_by_priority(pattern) + end +end + +---@private +local function dispatch(ft, path, bufnr, ...) + if type(ft) == "function" then + ft = ft(path, bufnr, ...) + end + + if type(ft) == "string" then + api.nvim_buf_set_option(bufnr, "filetype", ft) + return true + end + + -- Any non-falsey value (that is, anything other than 'nil' or 'false') will + -- end filetype matching. This is useful for e.g. the dist#ft functions that + -- return 0, but set the buffer's filetype themselves + return ft +end + +---@private +local function match_pattern(name, path, tail, pat) + -- If the pattern contains a / match against the full path, otherwise just the tail + local fullpat = "^" .. pat .. "$" + local matches + if pat:find("/") then + -- Similar to |autocmd-pattern|, if the pattern contains a '/' then check for a match against + -- both the short file name (as typed) and the full file name (after expanding to full path + -- and resolving symlinks) + matches = name:match(fullpat) or path:match(fullpat) + else + matches = tail:match(fullpat) + end + return matches +end + +--- Set the filetype for the given buffer from a file name. +--- +---@param name string File name (can be an absolute or relative path) +---@param bufnr number|nil The buffer to set the filetype for. Defaults to the current buffer. +function M.match(name, bufnr) + -- When fired from the main filetypedetect autocommand the {bufnr} argument is omitted, so we use + -- the current buffer. The {bufnr} argument is provided to allow extensibility in case callers + -- wish to perform filetype detection on buffers other than the current one. + bufnr = bufnr or api.nvim_get_current_buf() + + name = normalize_path(name) + + -- First check for the simple case where the full path exists as a key + local path = vim.fn.resolve(vim.fn.fnamemodify(name, ":p")) + if dispatch(filename[path], path, bufnr) then + return + end + + -- Next check against just the file name + local tail = vim.fn.fnamemodify(name, ":t") + if dispatch(filename[tail], path, bufnr) then + return + end + + -- Next, check the file path against available patterns with non-negative priority + local j = 1 + for i, v in ipairs(pattern_sorted) do + local k = next(v) + local opts = v[k][2] + if opts.priority < 0 then + j = i + break + end + + local ft = v[k][1] + local matches = match_pattern(name, path, tail, k) + if matches then + if dispatch(ft, path, bufnr, matches) then + return + end + end + end + + -- Next, check file extension + local ext = vim.fn.fnamemodify(name, ":e") + if dispatch(extension[ext], path, bufnr) then + return + end + + -- Finally, check patterns with negative priority + for i = j, #pattern_sorted do + local v = pattern_sorted[i] + local k = next(v) + + local ft = v[k][1] + local matches = match_pattern(name, path, tail, k) + if matches then + if dispatch(ft, path, bufnr, matches) then + return + end + end + end +end + +return M diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 236f3165f2..4105ef0675 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -1,9 +1,16 @@ local api = vim.api -local highlight = {} +local M = {} + +M.priorities = { + syntax = 50, + treesitter = 100, + diagnostics = 150, + user = 200, +} ---@private -function highlight.create(higroup, hi_info, default) +function M.create(higroup, hi_info, default) local options = {} -- TODO: Add validation for k, v in pairs(hi_info) do @@ -13,33 +20,50 @@ function highlight.create(higroup, hi_info, default) end ---@private -function highlight.link(higroup, link_to, force) +function M.link(higroup, link_to, force) vim.cmd(string.format([[highlight%s link %s %s]], force and "!" or " default", higroup, link_to)) end - --- Highlight range between two positions --- ---@param bufnr number of buffer to apply highlighting to ---@param ns namespace to add highlight to ---@param higroup highlight group to use for highlighting ----@param rtype type of range (:help setreg, default charwise) ----@param inclusive boolean indicating whether the range is end-inclusive (default false) -function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive) - rtype = rtype or 'v' - inclusive = inclusive or false +---@param start first position (tuple {line,col}) +---@param finish second position (tuple {line,col}) +---@param opts table with options: +-- - regtype type of range (:help setreg, default charwise) +-- - inclusive boolean indicating whether the range is end-inclusive (default false) +-- - priority number indicating priority of highlight (default priorities.user) +function M.range(bufnr, ns, higroup, start, finish, opts) + opts = opts or {} + local regtype = opts.regtype or "v" + local inclusive = opts.inclusive or false + local priority = opts.priority or M.priorities.user -- sanity check - if start[2] < 0 or finish[1] < start[1] then return end + if start[2] < 0 or finish[1] < start[1] then + return + end - local region = vim.region(bufnr, start, finish, rtype, inclusive) + local region = vim.region(bufnr, start, finish, regtype, inclusive) for linenr, cols in pairs(region) do - api.nvim_buf_add_highlight(bufnr, ns, higroup, linenr, cols[1], cols[2]) + local end_row + if cols[2] == -1 then + end_row = linenr + 1 + cols[2] = 0 + end + api.nvim_buf_set_extmark(bufnr, ns, linenr, cols[1], { + hl_group = higroup, + end_row = end_row, + end_col = cols[2], + priority = priority, + strict = false, + }) end - end -local yank_ns = api.nvim_create_namespace('hlyank') +local yank_ns = api.nvim_create_namespace("hlyank") --- Highlight the yanked region --- --- use from init.vim via @@ -49,26 +73,40 @@ local yank_ns = api.nvim_create_namespace('hlyank') --- customize conditions (here: do not highlight a visual selection) via --- au TextYankPost * lua vim.highlight.on_yank {on_visual=false} --- --- @param opts dictionary with options controlling the highlight: +-- @param opts table with options controlling the highlight: -- - higroup highlight group for yanked region (default "IncSearch") -- - timeout time in ms before highlight is cleared (default 150) -- - on_macro highlight when executing macro (default false) -- - on_visual highlight when yanking visual selection (default true) -- - event event structure (default vim.v.event) -function highlight.on_yank(opts) - vim.validate { - opts = { opts, - function(t) if t == nil then return true else return type(t) == 'table' end end, - 'a table or nil to configure options (see `:h highlight.on_yank`)', - }} +function M.on_yank(opts) + vim.validate({ + opts = { + opts, + function(t) + if t == nil then + return true + else + return type(t) == "table" + end + end, + "a table or nil to configure options (see `:h highlight.on_yank`)", + }, + }) opts = opts or {} local event = opts.event or vim.v.event local on_macro = opts.on_macro or false local on_visual = (opts.on_visual ~= false) - if (not on_macro) and vim.fn.reg_executing() ~= '' then return end - if event.operator ~= 'y' or event.regtype == '' then return end - if (not on_visual) and event.visual then return end + if not on_macro and vim.fn.reg_executing() ~= "" then + return + end + if event.operator ~= "y" or event.regtype == "" then + return + end + if not on_visual and event.visual then + return + end local higroup = opts.higroup or "IncSearch" local timeout = opts.timeout or 150 @@ -79,19 +117,23 @@ function highlight.on_yank(opts) local pos1 = vim.fn.getpos("'[") local pos2 = vim.fn.getpos("']") - pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} - pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} + pos1 = { pos1[2] - 1, pos1[3] - 1 + pos1[4] } + pos2 = { pos2[2] - 1, pos2[3] - 1 + pos2[4] } - highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive) - - vim.defer_fn( - function() - if api.nvim_buf_is_valid(bufnr) then - api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) - end - end, - timeout + M.range( + bufnr, + yank_ns, + higroup, + pos1, + pos2, + { regtype = event.regtype, inclusive = event.inclusive, priority = M.priorities.user } ) + + vim.defer_fn(function() + if api.nvim_buf_is_valid(bufnr) then + api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) + end + end, timeout) end -return highlight +return M diff --git a/runtime/lua/vim/keymap.lua b/runtime/lua/vim/keymap.lua new file mode 100644 index 0000000000..df49eff4b6 --- /dev/null +++ b/runtime/lua/vim/keymap.lua @@ -0,0 +1,135 @@ +local keymap = {} + +--- Add a new |mapping|. +--- Examples: +--- <pre> +--- -- Can add mapping to Lua functions +--- vim.keymap.set('n', 'lhs', function() print("real lua function") end) +--- +--- -- Can use it to map multiple modes +--- vim.keymap.set({'n', 'v'}, '<leader>lr', vim.lsp.buf.references, { buffer=true }) +--- +--- -- Can add mapping for specific buffer +--- vim.keymap.set('n', '<leader>w', "<cmd>w<cr>", { silent = true, buffer = 5 }) +--- +--- -- Expr mappings +--- vim.keymap.set('i', '<Tab>', function() +--- return vim.fn.pumvisible() == 1 and "<C-n>" or "<Tab>" +--- end, { expr = true }) +--- -- <Plug> mappings +--- vim.keymap.set('n', '[%%', '<Plug>(MatchitNormalMultiBackward)') +--- </pre> +--- +--- Note that in a mapping like: +--- <pre> +--- vim.keymap.set('n', 'asdf', require('jkl').my_fun) +--- </pre> +--- +--- the require('jkl') gets evaluated during this call in order to access the function. If you want to +--- avoid this cost at startup you can wrap it in a function, for example: +--- <pre> +--- vim.keymap.set('n', 'asdf', function() return require('jkl').my_fun() end) +--- </pre> +--- +---@param mode string|table Same mode short names as |nvim_set_keymap()|. +--- Can also be list of modes to create mapping on multiple modes. +---@param lhs string Left-hand side |{lhs}| of the mapping. +---@param rhs string|function Right-hand side |{rhs}| of the mapping. Can also be a Lua function. +-- +---@param opts table A table of |:map-arguments| such as "silent". In addition to the options +--- listed in |nvim_set_keymap()|, this table also accepts the following keys: +--- - replace_keycodes: (boolean, default true) When both this and expr is "true", +--- |nvim_replace_termcodes()| is applied to the result of Lua expr maps. +--- - remap: (boolean) Make the mapping recursive. This is the +--- inverse of the "noremap" option from |nvim_set_keymap()|. +--- Default `false`. +---@see |nvim_set_keymap()| +function keymap.set(mode, lhs, rhs, opts) + vim.validate { + mode = {mode, {'s', 't'}}, + lhs = {lhs, 's'}, + rhs = {rhs, {'s', 'f'}}, + opts = {opts, 't', true} + } + + opts = vim.deepcopy(opts) or {} + local is_rhs_luaref = type(rhs) == "function" + mode = type(mode) == 'string' and {mode} or mode + + if is_rhs_luaref and opts.expr and opts.replace_keycodes ~= false then + local user_rhs = rhs + rhs = function () + return vim.api.nvim_replace_termcodes(user_rhs(), true, true, true) + end + end + -- clear replace_keycodes from opts table + opts.replace_keycodes = nil + + if opts.remap == nil then + -- default remap value is false + opts.noremap = true + else + -- remaps behavior is opposite of noremap option. + opts.noremap = not opts.remap + opts.remap = nil + end + + if is_rhs_luaref then + opts.callback = rhs + rhs = '' + end + + if opts.buffer then + local bufnr = opts.buffer == true and 0 or opts.buffer + opts.buffer = nil + for _, m in ipairs(mode) do + vim.api.nvim_buf_set_keymap(bufnr, m, lhs, rhs, opts) + end + else + opts.buffer = nil + for _, m in ipairs(mode) do + vim.api.nvim_set_keymap(m, lhs, rhs, opts) + end + end +end + +--- Remove an existing mapping. +--- Examples: +--- <pre> +--- vim.keymap.del('n', 'lhs') +--- +--- vim.keymap.del({'n', 'i', 'v'}, '<leader>w', { buffer = 5 }) +--- </pre> +---@param opts table A table of optional arguments: +--- - buffer: (number or boolean) Remove a mapping from the given buffer. +--- When "true" or 0, use the current buffer. +---@see |vim.keymap.set()| +--- +function keymap.del(modes, lhs, opts) + vim.validate { + mode = {modes, {'s', 't'}}, + lhs = {lhs, 's'}, + opts = {opts, 't', true} + } + + opts = opts or {} + modes = type(modes) == 'string' and {modes} or modes + + local buffer = false + if opts.buffer ~= nil then + buffer = opts.buffer == true and 0 or opts.buffer + opts.buffer = nil + end + + if buffer == false then + for _, mode in ipairs(modes) do + vim.api.nvim_del_keymap(mode, lhs) + end + else + for _, mode in ipairs(modes) do + vim.api.nvim_buf_del_keymap(buffer, mode, lhs) + end + end +end + +return keymap diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 00839ec181..8d11b4621c 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -256,7 +256,7 @@ local function validate_client_config(config) (not config.flags or not config.flags.debounce_text_changes or type(config.flags.debounce_text_changes) == 'number'), - "flags.debounce_text_changes must be nil or a number with the debounce time in milliseconds" + "flags.debounce_text_changes must be a number with the debounce time in milliseconds" ) local cmd, cmd_args = lsp._cmd_parts(config.cmd) @@ -290,7 +290,7 @@ end --- Memoizes a function. On first run, the function return value is saved and --- immediately returned on subsequent runs. If the function returns a multival, --- only the first returned value will be memoized and returned. The function will only be run once, ---- even if it has side-effects. +--- even if it has side effects. --- ---@param fn (function) Function to run ---@returns (function) Memoized function @@ -306,70 +306,138 @@ local function once(fn) end end - local changetracking = {} do --@private --- client_id → state --- --- state + --- use_incremental_sync: bool + --- buffers: bufnr -> buffer_state + --- + --- buffer_state --- pending_change?: function that the timer starts to trigger didChange --- pending_changes: table (uri -> list of pending changeset tables)); - -- Only set if incremental_sync is used - --- use_incremental_sync: bool - --- buffers?: table (bufnr → lines); for incremental sync only + --- Only set if incremental_sync is used + --- --- timer?: uv_timer + --- lines: table local state_by_client = {} ---@private function changetracking.init(client, bufnr) + local use_incremental_sync = ( + if_nil(client.config.flags.allow_incremental_sync, true) + and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental + ) local state = state_by_client[client.id] if not state then state = { - pending_changes = {}; - use_incremental_sync = ( - if_nil(client.config.flags.allow_incremental_sync, true) - and client.resolved_capabilities.text_document_did_change == protocol.TextDocumentSyncKind.Incremental - ); + buffers = {}; + debounce = client.config.flags.debounce_text_changes or 150, + use_incremental_sync = use_incremental_sync; } state_by_client[client.id] = state end - if not state.use_incremental_sync then - return - end - if not state.buffers then - state.buffers = {} + if not state.buffers[bufnr] then + local buf_state = {} + state.buffers[bufnr] = buf_state + if use_incremental_sync then + buf_state.lines = nvim_buf_get_lines(bufnr, 0, -1, true) + buf_state.lines_tmp = {} + buf_state.pending_changes = {} + end end - state.buffers[bufnr] = nvim_buf_get_lines(bufnr, 0, -1, true) end ---@private function changetracking.reset_buf(client, bufnr) - changetracking.flush(client) + changetracking.flush(client, bufnr) local state = state_by_client[client.id] if state and state.buffers then + local buf_state = state.buffers[bufnr] state.buffers[bufnr] = nil + if buf_state and buf_state.timer then + buf_state.timer:stop() + buf_state.timer:close() + buf_state.timer = nil + end end end ---@private function changetracking.reset(client_id) local state = state_by_client[client_id] - if state then - state_by_client[client_id] = nil - changetracking._reset_timer(state) + if not state then + return end + for _, buf_state in pairs(state.buffers) do + if buf_state.timer then + buf_state.timer:stop() + buf_state.timer:close() + buf_state.timer = nil + end + end + state.buffers = {} + end + + ---@private + -- + -- Adjust debounce time by taking time of last didChange notification into + -- consideration. If the last didChange happened more than `debounce` time ago, + -- debounce can be skipped and otherwise maybe reduced. + -- + -- This turns the debounce into a kind of client rate limiting + local function next_debounce(debounce, buf_state) + if debounce == 0 then + return 0 + end + local ns_to_ms = 0.000001 + if not buf_state.last_flush then + return debounce + end + local now = uv.hrtime() + local ms_since_last_flush = (now - buf_state.last_flush) * ns_to_ms + return math.max(debounce - ms_since_last_flush, 0) end ---@private function changetracking.prepare(bufnr, firstline, lastline, new_lastline) - local incremental_changes = function(client) - local cached_buffers = state_by_client[client.id].buffers - local curr_lines = nvim_buf_get_lines(bufnr, 0, -1, true) + local incremental_changes = function(client, buf_state) + + local prev_lines = buf_state.lines + local curr_lines = buf_state.lines_tmp + + local changed_lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true) + for i = 1, firstline do + curr_lines[i] = prev_lines[i] + end + for i = firstline + 1, new_lastline do + curr_lines[i] = changed_lines[i - firstline] + end + for i = lastline + 1, #prev_lines do + curr_lines[i - lastline + new_lastline] = prev_lines[i] + end + if tbl_isempty(curr_lines) then + -- Can happen when deleting the entire contents of a buffer, see https://github.com/neovim/neovim/issues/16259. + curr_lines[1] = '' + end + local line_ending = buf_get_line_ending(bufnr) local incremental_change = sync.compute_diff( - cached_buffers[bufnr], curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) - cached_buffers[bufnr] = curr_lines + buf_state.lines, curr_lines, firstline, lastline, new_lastline, client.offset_encoding or 'utf-16', line_ending) + + -- Double-buffering of lines tables is used to reduce the load on the garbage collector. + -- At this point the prev_lines table is useless, but its internal storage has already been allocated, + -- so let's keep it around for the next didChange event, in which it will become the next + -- curr_lines table. Note that setting elements to nil doesn't actually deallocate slots in the + -- internal storage - it merely marks them as free, for the GC to deallocate them. + for i in ipairs(prev_lines) do + prev_lines[i] = nil + end + buf_state.lines = curr_lines + buf_state.lines_tmp = prev_lines + return incremental_change end local full_changes = once(function() @@ -383,75 +451,68 @@ do return end local state = state_by_client[client.id] - local debounce = client.config.flags.debounce_text_changes - if not debounce then - local changes = state.use_incremental_sync and incremental_changes(client) or full_changes() - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = util.buf_versions[bufnr]; - }; - contentChanges = { changes, } - }) - return - end - changetracking._reset_timer(state) + local buf_state = state.buffers[bufnr] + changetracking._reset_timer(buf_state) + local debounce = next_debounce(state.debounce, buf_state) if state.use_incremental_sync then -- This must be done immediately and cannot be delayed -- The contents would further change and startline/endline may no longer fit - if not state.pending_changes[uri] then - state.pending_changes[uri] = {} - end - table.insert(state.pending_changes[uri], incremental_changes(client)) + table.insert(buf_state.pending_changes, incremental_changes(client, buf_state)) end - state.pending_change = function() - state.pending_change = nil + buf_state.pending_change = function() + buf_state.pending_change = nil + buf_state.last_flush = uv.hrtime() if client.is_stopped() or not vim.api.nvim_buf_is_valid(bufnr) then return end - if state.use_incremental_sync then - for change_uri, content_changes in pairs(state.pending_changes) do - client.notify("textDocument/didChange", { - textDocument = { - uri = change_uri; - version = util.buf_versions[vim.uri_to_bufnr(change_uri)]; - }; - contentChanges = content_changes, - }) - end - state.pending_changes = {} - else - client.notify("textDocument/didChange", { - textDocument = { - uri = uri; - version = util.buf_versions[bufnr]; - }; - contentChanges = { full_changes() }, - }) - end + local changes = state.use_incremental_sync and buf_state.pending_changes or { full_changes() } + client.notify("textDocument/didChange", { + textDocument = { + uri = uri, + version = util.buf_versions[bufnr], + }, + contentChanges = changes, + }) + buf_state.pending_changes = {} + end + if debounce == 0 then + buf_state.pending_change() + else + local timer = vim.loop.new_timer() + buf_state.timer = timer + -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines + timer:start(debounce, 0, vim.schedule_wrap(buf_state.pending_change)) end - state.timer = vim.loop.new_timer() - -- Must use schedule_wrap because `full_changes()` calls nvim_buf_get_lines - state.timer:start(debounce, 0, vim.schedule_wrap(state.pending_change)) end end - function changetracking._reset_timer(state) - if state.timer then - state.timer:stop() - state.timer:close() - state.timer = nil + function changetracking._reset_timer(buf_state) + if buf_state.timer then + buf_state.timer:stop() + buf_state.timer:close() + buf_state.timer = nil end end --- Flushes any outstanding change notification. ---@private - function changetracking.flush(client) + function changetracking.flush(client, bufnr) local state = state_by_client[client.id] - if state then - changetracking._reset_timer(state) - if state.pending_change then - state.pending_change() + if not state then + return + end + if bufnr then + local buf_state = state.buffers[bufnr] or {} + changetracking._reset_timer(buf_state) + if buf_state.pending_change then + buf_state.pending_change() + end + else + for _, buf_state in pairs(state.buffers) do + changetracking._reset_timer(buf_state) + if buf_state.pending_change then + buf_state.pending_change() + end end end end @@ -645,8 +706,8 @@ end ---@param on_error Callback with parameters (code, ...), invoked --- when the client operation throws an error. `code` is a number describing --- the error. Other arguments may be passed depending on the error kind. See ---- |vim.lsp.client_errors| for possible errors. ---- Use `vim.lsp.client_errors[code]` to get human-friendly name. +--- |vim.lsp.rpc.client_errors| for possible errors. +--- Use `vim.lsp.rpc.client_errors[code]` to get human-friendly name. --- ---@param before_init Callback with parameters (initialize_params, config) --- invoked before the LSP "initialize" phase, where `params` contains the @@ -757,8 +818,8 @@ function lsp.start_client(config) --- ---@param code (number) Error code ---@param err (...) Other arguments may be passed depending on the error kind - ---@see |vim.lsp.client_errors| for possible errors. Use - ---`vim.lsp.client_errors[code]` to get a human-friendly name. + ---@see |vim.lsp.rpc.client_errors| for possible errors. Use + ---`vim.lsp.rpc.client_errors[code]` to get a human-friendly name. function dispatch.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) @@ -897,7 +958,7 @@ function lsp.start_client(config) client.initialized = true uninitialized_clients[client_id] = nil client.workspace_folders = workspace_folders - -- TODO(mjlbach): Backwards compatbility, to be removed in 0.7 + -- TODO(mjlbach): Backwards compatibility, to be removed in 0.7 client.workspaceFolders = client.workspace_folders client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") -- These are the cleaned up capabilities we use for dynamically deciding @@ -957,7 +1018,7 @@ function lsp.start_client(config) or error(string.format("not found: %q request handler for client %q.", method, client.name)) end -- Ensure pending didChange notifications are sent so that the server doesn't operate on a stale state - changetracking.flush(client) + changetracking.flush(client, bufnr) bufnr = resolve_bufnr(bufnr) local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, handler, bufnr) local success, request_id = rpc.request(method, params, function(err, result) @@ -1014,14 +1075,16 @@ function lsp.start_client(config) ---@private --- Sends a notification to an LSP server. --- - ---@param method (string) LSP method name. - ---@param params (optional, table) LSP request params. - ---@param bufnr (number) Buffer handle, or 0 for current. + ---@param method string LSP method name. + ---@param params table|nil LSP request params. ---@returns {status} (bool) true if the notification was successful. ---If it is false, then it will always be false ---(the client has shutdown). - function client.notify(...) - return rpc.notify(...) + function client.notify(method, params) + if method ~= 'textDocument/didChange' then + changetracking.flush(client) + end + return rpc.notify(method, params) end ---@private @@ -1131,7 +1194,7 @@ function lsp._text_document_did_save_handler(bufnr) if client.resolved_capabilities.text_document_save then local included_text if client.resolved_capabilities.text_document_save_include_text then - included_text = text() + included_text = text(bufnr) end client.notify('textDocument/didSave', { textDocument = { @@ -1535,7 +1598,7 @@ end local function adjust_start_col(lnum, line, items, encoding) local min_start_char = nil for _, item in pairs(items) do - if item.textEdit and item.textEdit.range.start.line == lnum - 1 then + if item.filterText == nil and item.textEdit and item.textEdit.range.start.line == lnum - 1 then if min_start_char and min_start_char ~= item.textEdit.range.start.character then return nil end @@ -1708,14 +1771,14 @@ end -- -- Can be used to lookup the number from the name or the -- name from the number. --- Levels by name: "trace", "debug", "info", "warn", "error" --- Level numbers begin with "trace" at 0 +-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR" +-- Level numbers begin with "TRACE" at 0 lsp.log_levels = log.levels --- Sets the global log level for LSP logging. --- ---- Levels by name: "trace", "debug", "info", "warn", "error" ---- Level numbers begin with "trace" at 0 +--- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR" +--- Level numbers begin with "TRACE" at 0 --- --- Use `lsp.log_levels` for reverse lookup. --- diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 8e3ed9b002..eb7ec579f1 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -184,7 +184,7 @@ function M.formatting_sync(options, timeout_ms) local result, err = client.request_sync('textDocument/formatting', params, timeout_ms, bufnr) if result and result.result then - util.apply_text_edits(result.result, bufnr) + util.apply_text_edits(result.result, bufnr, client.offset_encoding) elseif err then vim.notify('vim.lsp.buf.formatting_sync: ' .. err, vim.log.levels.WARN) end @@ -228,7 +228,7 @@ function M.formatting_seq_sync(options, timeout_ms, order) local params = util.make_formatting_params(options) local result, err = client.request_sync("textDocument/formatting", params, timeout_ms, vim.api.nvim_get_current_buf()) if result and result.result then - util.apply_text_edits(result.result, bufnr) + util.apply_text_edits(result.result, bufnr, client.offset_encoding) elseif err then vim.notify(string.format("vim.lsp.buf.formatting_seq_sync: (%s) %s", client.name, err), vim.log.levels.WARN) end @@ -447,13 +447,16 @@ end ---@param query (string, optional) function M.workspace_symbol(query) query = query or npcall(vfn.input, "Query: ") + if query == nil then + return + end local params = {query = query} request('workspace/symbol', params) end --- Send request to the server to resolve document highlights for the current --- text document position. This request can be triggered by a key mapping or ---- by events such as `CursorHold`, eg: +--- by events such as `CursorHold`, e.g.: --- --- <pre> --- autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() @@ -503,7 +506,7 @@ local function on_code_action_results(results, ctx) ---@private local function apply_action(action, client) if action.edit then - util.apply_workspace_edit(action.edit) + util.apply_workspace_edit(action.edit, client.offset_encoding) end if action.command then local command = type(action.command) == 'table' and action.command or action @@ -627,14 +630,19 @@ end --- Executes an LSP server command. --- ----@param command A valid `ExecuteCommandParams` object +---@param command_params table A valid `ExecuteCommandParams` object ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand -function M.execute_command(command) +function M.execute_command(command_params) validate { - command = { command.command, 's' }, - arguments = { command.arguments, 't', true } + command = { command_params.command, 's' }, + arguments = { command_params.arguments, 't', true } + } + command_params = { + command=command_params.command, + arguments=command_params.arguments, + workDoneToken=command_params.workDoneToken, } - request('workspace/executeCommand', command) + request('workspace/executeCommand', command_params ) end return M diff --git a/runtime/lua/vim/lsp/diagnostic.lua b/runtime/lua/vim/lsp/diagnostic.lua index 8850d25233..68942ae11a 100644 --- a/runtime/lua/vim/lsp/diagnostic.lua +++ b/runtime/lua/vim/lsp/diagnostic.lua @@ -168,8 +168,8 @@ end --- }, --- -- Use a function to dynamically turn signs off --- -- and on, using buffer local variables ---- signs = function(bufnr, client_id) ---- return vim.bo[bufnr].show_signs == false +--- signs = function(namespace, bufnr) +--- return vim.b[bufnr].show_signs == true --- end, --- -- Disable a feature --- update_in_insert = false, @@ -243,7 +243,7 @@ end ---@param client_id number ---@private function M.save(diagnostics, bufnr, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.save is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.save is deprecated. See :h deprecated', vim.log.levels.WARN) local namespace = M.get_namespace(client_id) vim.diagnostic.set(namespace, bufnr, diagnostic_lsp_to_vim(diagnostics, bufnr, client_id)) end @@ -257,7 +257,7 @@ end --- If nil, diagnostics of all clients are included. ---@return table with diagnostics grouped by bufnr (bufnr: Diagnostic[]) function M.get_all(client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_all is deprecated. See :h deprecated', vim.log.levels.WARN) local result = {} local namespace if client_id then @@ -279,7 +279,7 @@ end --- Else, return just the diagnostics associated with the client_id. ---@param predicate function|nil Optional function for filtering diagnostics function M.get(bufnr, client_id, predicate) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get is deprecated. See :h deprecated', vim.log.levels.WARN) predicate = predicate or function() return true end if client_id == nil then local all_diagnostics = {} @@ -341,7 +341,7 @@ end ---@param severity DiagnosticSeverity ---@param client_id number the client id function M.get_count(bufnr, severity, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_count is deprecated. See :h deprecated', vim.log.levels.WARN) severity = severity_lsp_to_vim(severity) local opts = { severity = severity } if client_id ~= nil then @@ -358,7 +358,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Previous diagnostic function M.get_prev(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_prev is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -376,7 +376,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Previous diagnostic position function M.get_prev_pos(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_prev_pos is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -393,7 +393,7 @@ end --- ---@param opts table See |vim.lsp.diagnostic.goto_next()| function M.goto_prev(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.goto_prev is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -411,7 +411,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Next diagnostic function M.get_next(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_next is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -429,7 +429,7 @@ end ---@param opts table See |vim.lsp.diagnostic.goto_next()| ---@return table Next diagnostic position function M.get_next_pos(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_next_pos is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -444,7 +444,7 @@ end --- ---@deprecated Prefer |vim.diagnostic.goto_next()| function M.goto_next(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.goto_next is deprecated. See :h deprecated', vim.log.levels.WARN) if opts then if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -468,7 +468,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_signs(diagnostics, bufnr, client_id, _, opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.set_signs is deprecated. See :h deprecated', vim.log.levels.WARN) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -489,7 +489,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_underline(diagnostics, bufnr, client_id, _, opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.set_underline is deprecated. See :h deprecated', vim.log.levels.WARN) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -511,7 +511,7 @@ end --- - severity_limit (DiagnosticSeverity): --- - Limit severity of diagnostics found. E.g. "Warning" means { "Error", "Warning" } will be valid. function M.set_virtual_text(diagnostics, bufnr, client_id, _, opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.set_virtual_text is deprecated. See :h deprecated', vim.log.levels.WARN) local namespace = M.get_namespace(client_id) if opts and not opts.severity and opts.severity_limit then opts.severity = {min=severity_lsp_to_vim(opts.severity_limit)} @@ -530,7 +530,7 @@ end ---@return an array of [text, hl_group] arrays. This can be passed directly to --- the {virt_text} option of |nvim_buf_set_extmark()|. function M.get_virtual_text_chunks_for_line(bufnr, _, line_diags, opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.get_virtual_text_chunks_for_line is deprecated. See :h deprecated', vim.log.levels.WARN) return vim.diagnostic._get_virt_text_chunks(diagnostic_lsp_to_vim(line_diags, bufnr), opts) end @@ -548,7 +548,7 @@ end ---@param position table|nil The (0,0)-indexed position ---@return table {popup_bufnr, win_id} function M.show_position_diagnostics(opts, buf_nr, position) - vim.api.nvim_echo({{'vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.show_position_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN) opts = opts or {} opts.scope = "cursor" opts.pos = position @@ -572,7 +572,7 @@ end ---@param client_id number|nil the client id ---@return table {popup_bufnr, win_id} function M.show_line_diagnostics(opts, buf_nr, line_nr, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.show_line_diagnostics is deprecated. See :h deprecated', vim.log.levels.WARN) opts = opts or {} opts.scope = "line" opts.pos = line_nr @@ -596,7 +596,7 @@ end --- client. The default is to redraw diagnostics for all attached --- clients. function M.redraw(bufnr, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.redraw is deprecated. See :h deprecated', vim.log.levels.WARN) bufnr = get_bufnr(bufnr) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) @@ -624,7 +624,7 @@ end --- - {workspace}: (boolean, default true) --- - Set the list with workspace diagnostics function M.set_qflist(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.set_qflist is deprecated. See :h deprecated', vim.log.levels.WARN) opts = opts or {} if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -656,7 +656,7 @@ end --- - {workspace}: (boolean, default false) --- - Set the list with workspace diagnostics function M.set_loclist(opts) - vim.api.nvim_echo({{'vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.set_loclist is deprecated. See :h deprecated', vim.log.levels.WARN) opts = opts or {} if opts.severity then opts.severity = severity_lsp_to_vim(opts.severity) @@ -684,7 +684,7 @@ end -- send diagnostic information and the client will still process it. The -- diagnostics are simply not displayed to the user. function M.disable(bufnr, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.disable is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.disable is deprecated. See :h deprecated', vim.log.levels.WARN) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) M.disable(bufnr, client.id) @@ -705,7 +705,7 @@ end --- client. The default is to enable diagnostics for all attached --- clients. function M.enable(bufnr, client_id) - vim.api.nvim_echo({{'vim.lsp.diagnostic.enable is deprecated. See :h deprecated', 'WarningMsg'}}, true, {}) + vim.notify_once('vim.lsp.diagnostic.enable is deprecated. See :h deprecated', vim.log.levels.WARN) if not client_id then return vim.lsp.for_each_buffer_client(bufnr, function(client) M.enable(bufnr, client.id) diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index c974d1a6b4..f5aefd4402 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -111,13 +111,15 @@ M['client/registerCapability'] = function(_, _, ctx) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -M['workspace/applyEdit'] = function(_, workspace_edit) +M['workspace/applyEdit'] = function(_, workspace_edit, ctx) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) if workspace_edit.label then print("Workspace edit", workspace_edit.label) end - local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit) + local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit, client.offset_encoding) return { applied = status; failureReason = result; @@ -150,6 +152,17 @@ M['workspace/configuration'] = function(_, result, ctx) return response end +--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_workspaceFolders +M['workspace/workspaceFolders'] = function(_, _, ctx) + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) + if not client then + err_message("LSP[id=", client_id, "] client has shut down after sending the message") + return + end + return client.workspace_folders or vim.NIL +end + M['textDocument/publishDiagnostics'] = function(...) return require('vim.lsp.diagnostic').on_publish_diagnostics(...) end @@ -159,6 +172,31 @@ M['textDocument/codeLens'] = function(...) end +--see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references +M['textDocument/references'] = function(_, result, ctx, config) + if not result or vim.tbl_isempty(result) then + vim.notify('No references found') + else + local client = vim.lsp.get_client_by_id(ctx.client_id) + config = config or {} + if config.loclist then + vim.fn.setloclist(0, {}, ' ', { + title = 'References'; + items = util.locations_to_items(result, client.offset_encoding); + context = ctx; + }) + api.nvim_command("lopen") + else + vim.fn.setqflist({}, ' ', { + title = 'References'; + items = util.locations_to_items(result, client.offset_encoding); + context = ctx; + }) + api.nvim_command("botright copen") + end + end +end + ---@private --- Return a function that converts LSP responses to list items and opens the list @@ -169,23 +207,26 @@ end --- loclist: (boolean) use the location list (default is to use the quickfix list) --- ---@param map_result function `((resp, bufnr) -> list)` to convert the response ----@param entity name of the resource used in a `not found` error message -local function response_to_list(map_result, entity) - return function(_,result, ctx, config) +---@param entity string name of the resource used in a `not found` error message +---@param title_fn function Function to call to generate list title +local function response_to_list(map_result, entity, title_fn) + return function(_, result, ctx, config) if not result or vim.tbl_isempty(result) then vim.notify('No ' .. entity .. ' found') else config = config or {} if config.loclist then vim.fn.setloclist(0, {}, ' ', { - title = 'Language Server'; + title = title_fn(ctx); items = map_result(result, ctx.bufnr); + context = ctx; }) api.nvim_command("lopen") else vim.fn.setqflist({}, ' ', { - title = 'Language Server'; + title = title_fn(ctx); items = map_result(result, ctx.bufnr); + context = ctx; }) api.nvim_command("botright copen") end @@ -194,31 +235,36 @@ local function response_to_list(map_result, entity) end ---see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references -M['textDocument/references'] = response_to_list(util.locations_to_items, 'references') - --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol -M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols') +M['textDocument/documentSymbol'] = response_to_list(util.symbols_to_items, 'document symbols', function(ctx) + local fname = vim.fn.fnamemodify(vim.uri_to_fname(ctx.params.textDocument.uri), ":.") + return string.format('Symbols in %s', fname) +end) --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol -M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols') +M['workspace/symbol'] = response_to_list(util.symbols_to_items, 'symbols', function(ctx) + return string.format("Symbols matching '%s'", ctx.params.query) +end) --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename -M['textDocument/rename'] = function(_, result, _) +M['textDocument/rename'] = function(_, result, ctx, _) if not result then return end - util.apply_workspace_edit(result) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_workspace_edit(result, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting M['textDocument/rangeFormatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, ctx.bufnr) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting M['textDocument/formatting'] = function(_, result, ctx, _) if not result then return end - util.apply_text_edits(result, ctx.bufnr) + local client = vim.lsp.get_client_by_id(ctx.client_id) + util.apply_text_edits(result, ctx.bufnr, client.offset_encoding) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion @@ -246,7 +292,7 @@ end ---@param config table Configuration table. --- - border: (default=nil) --- - Add borders to the floating window ---- - See |vim.api.nvim_open_win()| +--- - See |nvim_open_win()| function M.hover(_, result, ctx, config) config = config or {} config.focus_id = ctx.method @@ -277,19 +323,23 @@ local function location_handler(_, result, ctx, _) local _ = log.info() and log.info(ctx.method, 'No location found') return nil end + local client = vim.lsp.get_client_by_id(ctx.client_id) -- textDocument/definition can return Location or Location[] -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition if vim.tbl_islist(result) then - util.jump_to_location(result[1]) + util.jump_to_location(result[1], client.offset_encoding) if #result > 1 then - vim.fn.setqflist({}, ' ', {title = 'LSP locations', items = util.locations_to_items(result)}) - api.nvim_command("copen") + vim.fn.setqflist({}, ' ', { + title = 'LSP locations', + items = util.locations_to_items(result, client.offset_encoding) + }) + api.nvim_command("botright copen") end else - util.jump_to_location(result) + util.jump_to_location(result, client.offset_encoding) end end @@ -380,7 +430,7 @@ local make_call_hierarchy_handler = function(direction) end end vim.fn.setqflist({}, ' ', {title = 'LSP call hierarchy', items = items}) - api.nvim_command("copen") + api.nvim_command("botright copen") end end @@ -439,14 +489,20 @@ for k, fn in pairs(M) do }) if err then - local client = vim.lsp.get_client_by_id(ctx.client_id) - local client_name = client and client.name or string.format("client_id=%d", ctx.client_id) -- LSP spec: -- interface ResponseError: -- code: integer; -- message: string; -- data?: string | number | boolean | array | object | null; - return err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message) + + -- Per LSP, don't show ContentModified error to the user. + if err.code ~= protocol.ErrorCodes.ContentModified then + local client = vim.lsp.get_client_by_id(ctx.client_id) + local client_name = client and client.name or string.format("client_id=%d", ctx.client_id) + + err_message(client_name .. ': ' .. tostring(err.code) .. ': ' .. err.message) + end + return end return fn(err, result, ctx, config) diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index dbc473b52c..e0b5653587 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -8,8 +8,8 @@ local log = {} -- Log level dictionary with reverse lookup as well. -- -- Can be used to lookup the number from the name or the name from the number. --- Levels by name: 'trace', 'debug', 'info', 'warn', 'error' --- Level numbers begin with 'trace' at 0 +-- Levels by name: "TRACE", "DEBUG", "INFO", "WARN", "ERROR" +-- Level numbers begin with "TRACE" at 0 log.levels = vim.deepcopy(vim.log.levels) -- Default log level is warn. diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 1fb75ddeb7..1ecac50df4 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -133,7 +133,8 @@ local function request_parser_loop() end end -local client_errors = vim.tbl_add_reverse_lookup { +--- Mapping of error codes used by the client +local client_errors = { INVALID_SERVER_MESSAGE = 1; INVALID_SERVER_JSON = 2; NO_RESULT_CALLBACK_FOUND = 3; @@ -143,6 +144,8 @@ local client_errors = vim.tbl_add_reverse_lookup { SERVER_RESULT_CALLBACK_ERROR = 7; } +client_errors = vim.tbl_add_reverse_lookup(client_errors) + --- Constructs an error message from an LSP error object. --- ---@param err (table) The error object diff --git a/runtime/lua/vim/lsp/sync.lua b/runtime/lua/vim/lsp/sync.lua index d01f45ad8f..0f4e5b572b 100644 --- a/runtime/lua/vim/lsp/sync.lua +++ b/runtime/lua/vim/lsp/sync.lua @@ -298,7 +298,7 @@ end ---@private -- rangelength depends on the offset encoding --- bytes for utf-8 (clangd with extenion) +-- bytes for utf-8 (clangd with extension) -- codepoints for utf-16 -- codeunits for utf-32 -- Line endings count here as 2 chars for \r\n (dos), 1 char for \n (unix), and 1 char for \r (mac) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5921eb5bf0..655c3a4679 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -10,14 +10,6 @@ local uv = vim.loop local npcall = vim.F.npcall local split = vim.split -local _warned = {} -local warn_once = function(message) - if not _warned[message] then - vim.api.nvim_err_writeln(message) - _warned[message] = true - end -end - local M = {} local default_border = { @@ -201,6 +193,11 @@ end local function get_lines(bufnr, rows) rows = type(rows) == "table" and rows or { rows } + -- This is needed for bufload and bufloaded + if bufnr == 0 then + bufnr = vim.api.nvim_get_current_buf() + end + ---@private local function buf_lines() local lines = {} @@ -280,7 +277,7 @@ end ---@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to ----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to utf-16 +---@param offset_encoding string utf-8|utf-16|utf-32 --- 1-indexed local function get_line_byte_from_position(bufnr, position, offset_encoding) -- LSP's line and characters are 0-indexed @@ -289,7 +286,7 @@ local function get_line_byte_from_position(bufnr, position, offset_encoding) -- When on the first character, we can ignore the difference between byte and -- character if col > 0 then - local line = get_line(bufnr, position.line) + local line = get_line(bufnr, position.line) or '' local ok, result ok, result = pcall(_str_byteindex_enc, line, col, offset_encoding) if ok then @@ -363,15 +360,14 @@ end --- Applies a list of text edits to a buffer. ---@param text_edits table list of `TextEdit` objects ---@param bufnr number Buffer id ----@param offset_encoding string utf-8|utf-16|utf-32|nil defaults to encoding of first client of `bufnr` +---@param offset_encoding string utf-8|utf-16|utf-32 defaults to encoding of first client of `bufnr` ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textEdit function M.apply_text_edits(text_edits, bufnr, offset_encoding) validate { text_edits = { text_edits, 't', false }; bufnr = { bufnr, 'number', false }; - offset_encoding = { offset_encoding, 'string', true }; + offset_encoding = { offset_encoding, 'string', false }; } - offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then vim.fn.bufload(bufnr) @@ -405,25 +401,6 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end) - -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. - local has_eol_text_edit = false - local max = vim.api.nvim_buf_line_count(bufnr) - local len = _str_utfindex_enc(vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '', nil, offset_encoding) - text_edits = vim.tbl_map(function(text_edit) - if max <= text_edit.range.start.line then - text_edit.range.start.line = max - 1 - text_edit.range.start.character = len - text_edit.newText = '\n' .. text_edit.newText - has_eol_text_edit = true - end - if max <= text_edit.range['end'].line then - text_edit.range['end'].line = max - 1 - text_edit.range['end'].character = len - has_eol_text_edit = true - end - return text_edit - end, text_edits) - -- Some LSP servers are depending on the VSCode behavior. -- The VSCode will re-locate the cursor position after applying TextEdit so we also do it. local is_current_buf = vim.api.nvim_get_current_buf() == bufnr @@ -443,16 +420,38 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- Apply text edits. local is_cursor_fixed = false + local has_eol_text_edit = false for _, text_edit in ipairs(text_edits) do + -- Normalize line ending + text_edit.newText, _ = string.gsub(text_edit.newText, '\r\n?', '\n') + + -- Convert from LSP style ranges to Neovim style ranges. local e = { start_row = text_edit.range.start.line, - start_col = get_line_byte_from_position(bufnr, text_edit.range.start), + start_col = get_line_byte_from_position(bufnr, text_edit.range.start, offset_encoding), end_row = text_edit.range['end'].line, - end_col = get_line_byte_from_position(bufnr, text_edit.range['end']), + end_col = get_line_byte_from_position(bufnr, text_edit.range['end'], offset_encoding), text = vim.split(text_edit.newText, '\n', true), } + + -- Some LSP servers may return +1 range of the buffer content but nvim_buf_set_text can't accept it so we should fix it here. + local max = vim.api.nvim_buf_line_count(bufnr) + if max <= e.start_row or max <= e.end_row then + local len = #(get_line(bufnr, max - 1) or '') + if max <= e.start_row then + e.start_row = max - 1 + e.start_col = len + table.insert(e.text, 1, '') + end + if max <= e.end_row then + e.end_row = max - 1 + e.end_col = len + end + has_eol_text_edit = true + end vim.api.nvim_buf_set_text(bufnr, e.start_row, e.start_col, e.end_row, e.end_col, e.text) + -- Fix cursor position. local row_count = (e.end_row - e.start_row) + 1 if e.end_row < cursor.row then cursor.row = cursor.row + (#e.text - row_count) @@ -467,10 +466,13 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) end end + local max = vim.api.nvim_buf_line_count(bufnr) + + -- Apply fixed cursor position. if is_cursor_fixed then local is_valid_cursor = true - is_valid_cursor = is_valid_cursor and cursor.row < vim.api.nvim_buf_line_count(bufnr) - is_valid_cursor = is_valid_cursor and cursor.col <= #(vim.api.nvim_buf_get_lines(bufnr, cursor.row, cursor.row + 1, false)[1] or '') + is_valid_cursor = is_valid_cursor and cursor.row < max + is_valid_cursor = is_valid_cursor and cursor.col <= #(get_line(bufnr, max - 1) or '') if is_valid_cursor then vim.api.nvim_win_set_cursor(0, { cursor.row + 1, cursor.col }) end @@ -479,7 +481,7 @@ function M.apply_text_edits(text_edits, bufnr, offset_encoding) -- Remove final line if needed local fix_eol = has_eol_text_edit fix_eol = fix_eol and api.nvim_buf_get_option(bufnr, 'fixeol') - fix_eol = fix_eol and (vim.api.nvim_buf_get_lines(bufnr, -2, -1, false)[1] or '') == '' + fix_eol = fix_eol and get_line(bufnr, max - 1) == '' if fix_eol then vim.api.nvim_buf_set_lines(bufnr, -2, -1, false, {}) end @@ -517,9 +519,12 @@ end ---@param text_document_edit table: a `TextDocumentEdit` object ---@param index number: Optional index of the edit, if from a list of edits (or nil, if not from a list) ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit -function M.apply_text_document_edit(text_document_edit, index) +function M.apply_text_document_edit(text_document_edit, index, offset_encoding) local text_document = text_document_edit.textDocument local bufnr = vim.uri_to_bufnr(text_document.uri) + if offset_encoding == nil then + vim.notify_once("apply_text_document_edit must be called with valid offset encoding", vim.log.levels.WARN) + end -- For lists of text document edits, -- do not check the version after the first edit. @@ -538,7 +543,7 @@ function M.apply_text_document_edit(text_document_edit, index) return end - M.apply_text_edits(text_document_edit.edits, bufnr) + M.apply_text_edits(text_document_edit.edits, bufnr, offset_encoding) end --- Parses snippets in a completion entry. @@ -735,9 +740,13 @@ end --- Applies a `WorkspaceEdit`. --- ----@param workspace_edit (table) `WorkspaceEdit` +---@param workspace_edit table `WorkspaceEdit` +---@param offset_encoding string utf-8|utf-16|utf-32 (required) --see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit -function M.apply_workspace_edit(workspace_edit) +function M.apply_workspace_edit(workspace_edit, offset_encoding) + if offset_encoding == nil then + vim.notify_once("apply_workspace_edit must be called with valid offset encoding", vim.log.levels.WARN) + end if workspace_edit.documentChanges then for idx, change in ipairs(workspace_edit.documentChanges) do if change.kind == "rename" then @@ -753,7 +762,7 @@ function M.apply_workspace_edit(workspace_edit) elseif change.kind then error(string.format("Unsupported change: %q", vim.inspect(change))) else - M.apply_text_document_edit(change, idx) + M.apply_text_document_edit(change, idx, offset_encoding) end end return @@ -766,7 +775,7 @@ function M.apply_workspace_edit(workspace_edit) for uri, changes in pairs(all_changes) do local bufnr = vim.uri_to_bufnr(uri) - M.apply_text_edits(changes, bufnr) + M.apply_text_edits(changes, bufnr, offset_encoding) end end @@ -842,7 +851,8 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers local active_hl local active_signature = signature_help.activeSignature or 0 -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #signature_help.signatures then + -- In 3.15 of the protocol, activeSignature was allowed to be negative + if active_signature >= #signature_help.signatures or active_signature < 0 then active_signature = 0 end local signature = signature_help.signatures[active_signature + 1] @@ -983,12 +993,16 @@ end --- Jumps to a location. --- ----@param location (`Location`|`LocationLink`) +---@param location table (`Location`|`LocationLink`) +---@param offset_encoding string utf-8|utf-16|utf-32 (required) ---@returns `true` if the jump succeeded -function M.jump_to_location(location) +function M.jump_to_location(location, offset_encoding) -- location may be Location or LocationLink local uri = location.uri or location.targetUri if uri == nil then return end + if offset_encoding == nil then + vim.notify_once("jump_to_location must be called with valid offset encoding", vim.log.levels.WARN) + end local bufnr = vim.uri_to_bufnr(uri) -- Save position in jumplist vim.cmd "normal! m'" @@ -1000,10 +1014,10 @@ function M.jump_to_location(location) --- Jump to new location (adjusting for UTF-16 encoding of characters) api.nvim_set_current_buf(bufnr) - api.nvim_buf_set_option(0, 'buflisted', true) + api.nvim_buf_set_option(bufnr, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line - local col = get_line_byte_from_position(0, range.start) + local col = get_line_byte_from_position(bufnr, range.start, offset_encoding) api.nvim_win_set_cursor(0, {row + 1, col}) -- Open folds under the cursor vim.cmd("normal! zv") @@ -1505,18 +1519,20 @@ do --[[ References ]] ---@param bufnr number Buffer id function M.buf_clear_references(bufnr) validate { bufnr = {bufnr, 'n', true} } - api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) + api.nvim_buf_clear_namespace(bufnr or 0, reference_ns, 0, -1) end --- Shows a list of document highlights for a certain buffer. --- ---@param bufnr number Buffer id ---@param references table List of `DocumentHighlight` objects to highlight - ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32", or nil. Defaults to `offset_encoding` of first client of `bufnr` + ---@param offset_encoding string One of "utf-8", "utf-16", "utf-32". ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#documentHighlight function M.buf_highlight_references(bufnr, references, offset_encoding) - validate { bufnr = {bufnr, 'n', true} } - offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) + validate { + bufnr = {bufnr, 'n', true}, + offset_encoding = { offset_encoding, 'string', false }; + } for _, reference in ipairs(references) do local start_line, start_char = reference["range"]["start"]["line"], reference["range"]["start"]["character"] local end_line, end_char = reference["range"]["end"]["line"], reference["range"]["end"]["character"] @@ -1534,7 +1550,8 @@ do --[[ References ]] reference_ns, document_highlight_kind[kind], { start_line, start_idx }, - { end_line, end_idx }) + { end_line, end_idx }, + { priority = vim.highlight.priorities.user }) end end end @@ -1549,9 +1566,14 @@ end) --- The result can be passed to the {list} argument of |setqflist()| or --- |setloclist()|. --- ----@param locations (table) list of `Location`s or `LocationLink`s +---@param locations table list of `Location`s or `LocationLink`s +---@param offset_encoding string offset_encoding for locations utf-8|utf-16|utf-32 ---@returns (table) list of items -function M.locations_to_items(locations) +function M.locations_to_items(locations, offset_encoding) + if offset_encoding == nil then + vim.notify_once("locations_to_items must be called with valid offset encoding", vim.log.levels.WARN) + end + local items = {} local grouped = setmetatable({}, { __index = function(t, k) @@ -1591,7 +1613,7 @@ function M.locations_to_items(locations) local pos = temp.start local row = pos.line local line = lines[row] or "" - local col = pos.character + local col = M._str_byteindex_enc(line, pos.character, offset_encoding) table.insert(items, { filename = filename, lnum = row + 1, @@ -1677,7 +1699,7 @@ function M.symbols_to_items(symbols, bufnr) end return _items end - return _symbols_to_items(symbols, {}, bufnr) + return _symbols_to_items(symbols, {}, bufnr or 0) end --- Removes empty lines from the beginning and end. @@ -1775,7 +1797,13 @@ function M._get_offset_encoding(bufnr) local offset_encoding for _, client in pairs(vim.lsp.buf_get_clients(bufnr)) do - local this_offset_encoding = client.offset_encoding or "utf-16" + if client.offset_encoding == nil then + vim.notify_once( + string.format("Client (id: %s) offset_encoding is nil. Do not unset offset_encoding.", client.id), + vim.log.levels.ERROR + ) + end + local this_offset_encoding = client.offset_encoding if not offset_encoding then offset_encoding = this_offset_encoding elseif offset_encoding ~= this_offset_encoding then @@ -1796,7 +1824,7 @@ end ---@returns { textDocument = { uri = `current_file_uri` }, range = { start = ---`current_position`, end = `current_position` } } function M.make_range_params(window, offset_encoding) - local buf = vim.api.nvim_win_get_buf(window) + local buf = vim.api.nvim_win_get_buf(window or 0) offset_encoding = offset_encoding or M._get_offset_encoding(buf) local position = make_position_param(window, offset_encoding) return { @@ -1822,7 +1850,7 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, offset_encoding) end_pos = {end_pos, 't', true}; offset_encoding = {offset_encoding, 's', true}; } - bufnr = bufnr or 0 + bufnr = bufnr or vim.api.nvim_get_current_buf() offset_encoding = offset_encoding or M._get_offset_encoding(bufnr) local A = list_extend({}, start_pos or api.nvim_buf_get_mark(bufnr, '<')) local B = list_extend({}, end_pos or api.nvim_buf_get_mark(bufnr, '>')) @@ -1904,7 +1932,9 @@ end ---@returns (number, number) `offset_encoding` index of the character in line {row} column {col} in buffer {buf} function M.character_offset(buf, row, col, offset_encoding) local line = get_line(buf, row) - offset_encoding = offset_encoding or M._get_offset_encoding(buf) + if offset_encoding == nil then + vim.notify_once("character_offset must be called with valid offset encoding", vim.log.levels.WARN) + end -- If the col is past the EOL, use the line length. if col > #line then return _str_utfindex_enc(line, nil, offset_encoding) @@ -1928,7 +1958,6 @@ function M.lookup_section(settings, section) end M._get_line_byte_from_position = get_line_byte_from_position -M._warn_once = warn_once M.buf_versions = {} diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 1cf618725d..e170befa4c 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -526,13 +526,23 @@ end --- => error('arg1: expected even number, got 3') --- </pre> --- ----@param opt Map of parameter names to validations. Each key is a parameter +--- If multiple types are valid they can be given as a list. +--- <pre> +--- vim.validate{arg1={{'foo'}, {'table', 'string'}}, arg2={'foo', {'table', 'string'}}} +--- => NOP (success) +--- +--- vim.validate{arg1={1, {'string', table'}}} +--- => error('arg1: expected string|table, got number') +--- +--- </pre> +--- +---@param opt table of parameter names to validations. Each key is a parameter --- name; each value is a tuple in one of these forms: --- 1. (arg_value, type_name, optional) --- - arg_value: argument value ---- - type_name: string type name, one of: ("table", "t", "string", +--- - type_name: string|table type name, one of: ("table", "t", "string", --- "s", "number", "n", "boolean", "b", "function", "f", "nil", ---- "thread", "userdata") +--- "thread", "userdata") or list of them. --- - optional: (optional) boolean, if true, `nil` is valid --- 2. (arg_value, fn, msg) --- - arg_value: argument value @@ -571,31 +581,43 @@ do end local val = spec[1] -- Argument value. - local t = spec[2] -- Type name, or callable. + local types = spec[2] -- Type name, or callable. local optional = (true == spec[3]) - if type(t) == 'string' then - local t_name = type_names[t] - if not t_name then - return false, string.format('invalid type name: %s', t) - end + if type(types) == 'string' then + types = {types} + end - if (not optional or val ~= nil) and not _is_type(val, t_name) then - return false, string.format("%s: expected %s, got %s", param_name, t_name, type(val)) - end - elseif vim.is_callable(t) then + if vim.is_callable(types) then -- Check user-provided validation function. - local valid, optional_message = t(val) + local valid, optional_message = types(val) if not valid then - local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val) + local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), tostring(val)) if optional_message ~= nil then error_message = error_message .. string.format(". Info: %s", optional_message) end return false, error_message end + elseif type(types) == 'table' then + local success = false + for i, t in ipairs(types) do + local t_name = type_names[t] + if not t_name then + return false, string.format('invalid type name: %s', t) + end + types[i] = t_name + + if (optional and val == nil) or _is_type(val, t_name) then + success = true + break + end + end + if not success then + return false, string.format("%s: expected %s, got %s", param_name, table.concat(types, '|'), type(val)) + end else - return false, string.format("invalid type name: %s", tostring(t)) + return false, string.format("invalid type name: %s", tostring(types)) end end diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 07f6418c0c..f9d539f028 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -11,6 +11,7 @@ local parsers = {} local M = vim.tbl_extend("error", query, language) M.language_version = vim._ts_get_language_version() +M.minimum_language_version = vim._ts_get_minimum_language_version() setmetatable(M, { __index = function (t, k) diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 22b528838c..b6f61cfb2e 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -22,7 +22,21 @@ local _link_default_highlight_once = function(from, to) return from end -TSHighlighter.hl_map = { +-- If @definition.special does not exist use @definition instead +local subcapture_fallback = { + __index = function(self, capture) + local rtn + local shortened = capture + while not rtn and shortened do + shortened = shortened:match('(.*)%.') + rtn = shortened and rawget(self, shortened) + end + rawset(self, capture, rtn or "__notfound") + return rtn + end +} + +TSHighlighter.hl_map = setmetatable({ ["error"] = "Error", -- Miscs @@ -66,7 +80,7 @@ TSHighlighter.hl_map = { ["type.builtin"] = "Type", ["structure"] = "Structure", ["include"] = "Include", -} +}, subcapture_fallback) ---@private local function is_highlight_name(capture_name) diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua index 6f347ff25f..8b106108df 100644 --- a/runtime/lua/vim/treesitter/language.lua +++ b/runtime/lua/vim/treesitter/language.lua @@ -14,7 +14,7 @@ function M.require_language(lang, path, silent) return true end if path == nil then - local fname = 'parser/' .. lang .. '.*' + local fname = 'parser/' .. vim.fn.fnameescape(lang) .. '.*' local paths = a.nvim_get_runtime_file(fname, false) if #paths == 0 then if silent then diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index 594765761d..b83df65151 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -76,8 +76,8 @@ function LanguageTree:lang() end --- Determines whether this tree is valid. ---- If the tree is invalid, `parse()` must be called ---- to get the an updated tree. +--- If the tree is invalid, call `parse()`. +--- This will return the updated tree. function LanguageTree:is_valid() return self._valid end @@ -234,7 +234,9 @@ end --- Destroys this language tree and all its children. --- --- Any cleanup logic should be performed here. ---- Note, this DOES NOT remove this tree from a parent. +--- +--- Note: +--- This DOES NOT remove this tree from a parent. Instead, --- `remove_child` must be called on the parent to remove it. function LanguageTree:destroy() -- Cleanup here @@ -448,14 +450,14 @@ function LanguageTree:_on_detach(...) self:_do_callback('detach', ...) end ---- Registers callbacks for the parser ----@param cbs An `nvim_buf_attach`-like table argument with the following keys : ---- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. ---- `on_changedtree` : a callback that will be called every time the tree has syntactical changes. ---- it will only be passed one argument, that is a table of the ranges (as node ranges) that ---- changed. ---- `on_child_added` : emitted when a child is added to the tree. ---- `on_child_removed` : emitted when a child is removed from the tree. +--- Registers callbacks for the parser. +---@param cbs table An |nvim_buf_attach()|-like table argument with the following keys : +--- - `on_bytes` : see |nvim_buf_attach()|, but this will be called _after_ the parsers callback. +--- - `on_changedtree` : a callback that will be called every time the tree has syntactical changes. +--- It will only be passed one argument, which is a table of the ranges (as node ranges) that +--- changed. +--- - `on_child_added` : emitted when a child is added to the tree. +--- - `on_child_removed` : emitted when a child is removed from the tree. function LanguageTree:register_cbs(cbs) if not cbs then return end @@ -493,7 +495,7 @@ local function tree_contains(tree, range) return false end ---- Determines whether @param range is contained in this language tree +--- Determines whether {range} is contained in this language tree --- --- This goes down the tree to recursively check children. --- @@ -508,7 +510,7 @@ function LanguageTree:contains(range) return false end ---- Gets the appropriate language that contains @param range +--- Gets the appropriate language that contains {range} --- ---@param range A text range, see |LanguageTree:contains| function LanguageTree:language_for_range(range) diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index ebed502c92..8383551b5f 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -138,30 +138,43 @@ function M.get_query(lang, query_name) end end +local query_cache = setmetatable({}, { + __index = function(tbl, key) + rawset(tbl, key, {}) + return rawget(tbl, key) + end +}) + --- Parse {query} as a string. (If the query is in a file, the caller ---- should read the contents into a string before calling). +--- should read the contents into a string before calling). --- --- Returns a `Query` (see |lua-treesitter-query|) object which can be used to --- search nodes in the syntax tree for the patterns defined in {query} --- using `iter_*` methods below. --- ---- Exposes `info` and `captures` with additional information about the {query}. +--- Exposes `info` and `captures` with additional context about {query}. --- - `captures` contains the list of unique capture names defined in --- {query}. --- -` info.captures` also points to `captures`. --- - `info.patterns` contains information about predicates. --- ----@param lang The language ----@param query A string containing the query (s-expr syntax) +---@param lang string The language +---@param query string A string containing the query (s-expr syntax) --- ---@returns The query function M.parse_query(lang, query) language.require_language(lang) - local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, query) - self.info = self.query:inspect() - self.captures = self.info.captures - return self + local cached = query_cache[lang][query] + if cached then + return cached + else + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, query) + self.info = self.query:inspect() + self.captures = self.info.captures + query_cache[lang][query] = self + return self + end end --- Gets the text corresponding to a given node @@ -186,11 +199,13 @@ function M.get_node_text(node, source) lines = a.nvim_buf_get_lines(source, start_row, end_row + 1, true) end - if #lines == 1 then - lines[1] = string.sub(lines[1], start_col+1, end_col) - else - lines[1] = string.sub(lines[1], start_col+1) - lines[#lines] = string.sub(lines[#lines], 1, end_col) + if #lines > 0 then + if #lines == 1 then + lines[1] = string.sub(lines[1], start_col+1, end_col) + else + lines[1] = string.sub(lines[1], start_col+1) + lines[#lines] = string.sub(lines[#lines], 1, end_col) + end end return table.concat(lines, "\n") diff --git a/runtime/lua/vim/ui.lua b/runtime/lua/vim/ui.lua index 9568b60fd0..9d4b38f08a 100644 --- a/runtime/lua/vim/ui.lua +++ b/runtime/lua/vim/ui.lua @@ -18,6 +18,24 @@ local M = {} --- Called once the user made a choice. --- `idx` is the 1-based index of `item` within `item`. --- `nil` if the user aborted the dialog. +--- +--- +--- Example: +--- <pre> +--- vim.ui.select({ 'tabs', 'spaces' }, { +--- prompt = 'Select tabs or spaces:', +--- format_item = function(item) +--- return "I'd like to choose " .. item +--- end, +--- }, function(choice) +--- if choice == 'spaces' then +--- vim.o.expandtab = true +--- else +--- vim.o.expandtab = false +--- end +--- end) +--- </pre> + function M.select(items, opts, on_choice) vim.validate { items = { items, 'table', false }, @@ -57,6 +75,13 @@ end --- Called once the user confirms or abort the input. --- `input` is what the user typed. --- `nil` if the user aborted the dialog. +--- +--- Example: +--- <pre> +--- vim.ui.input({ prompt = 'Enter value for shiftwidth: ' }, function(input) +--- vim.o.shiftwidth = tonumber(input) +--- end) +--- </pre> function M.input(opts, on_confirm) vim.validate { on_confirm = { on_confirm, 'function', false }, |