diff options
Diffstat (limited to 'runtime')
-rw-r--r-- | runtime/autoload/man.vim | 2 | ||||
-rw-r--r-- | runtime/autoload/xmlformat.vim | 164 | ||||
-rw-r--r-- | runtime/compiler/xmllint.vim | 18 | ||||
-rw-r--r-- | runtime/doc/options.txt | 1 | ||||
-rw-r--r-- | runtime/doc/treesitter.txt | 13 | ||||
-rw-r--r-- | runtime/filetype.vim | 15 | ||||
-rw-r--r-- | runtime/indent/xml.vim | 15 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 141 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 239 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 454 | ||||
-rw-r--r-- | runtime/syntax/gitconfig.vim | 12 | ||||
-rw-r--r-- | runtime/syntax/xml.vim | 43 |
12 files changed, 822 insertions, 295 deletions
diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index dab88fde23..5008b8cfaf 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -13,8 +13,6 @@ function! man#init() abort try " Check for -l support. call s:get_page(s:get_path('', 'man')) - catch /E145:/ - " Ignore the error in restricted mode catch /command error .*/ let s:localfile_arg = v:false endtry diff --git a/runtime/autoload/xmlformat.vim b/runtime/autoload/xmlformat.vim index ea89401977..712e6f7351 100644 --- a/runtime/autoload/xmlformat.vim +++ b/runtime/autoload/xmlformat.vim @@ -1,6 +1,6 @@ " Vim plugin for formatting XML -" Last Change: Thu, 07 Dec 2018 -" Version: 0.1 +" Last Change: 2020 Jan 06 +" Version: 0.3 " Author: Christian Brabandt <cb@256bit.org> " Repository: https://github.com/chrisbra/vim-xml-ftplugin " License: VIM License @@ -15,51 +15,92 @@ let s:keepcpo = &cpo set cpo&vim " Main function: Format the input {{{1 -func! xmlformat#Format() +func! xmlformat#Format() abort " only allow reformatting through the gq command " (e.g. Vim is in normal mode) if mode() != 'n' " do not fall back to internal formatting return 0 endif + let count_orig = v:count let sw = shiftwidth() let prev = prevnonblank(v:lnum-1) let s:indent = indent(prev)/sw let result = [] let lastitem = prev ? getline(prev) : '' let is_xml_decl = 0 - " split on `<`, but don't split on very first opening < - for item in split(join(getline(v:lnum, (v:lnum + v:count - 1))), '.\@<=[>]\zs') - if s:EndTag(item) - let s:indent = s:DecreaseIndent() - call add(result, s:Indent(item)) - elseif s:EmptyTag(lastitem) - call add(result, s:Indent(item)) - elseif s:StartTag(lastitem) && s:IsTag(item) - let s:indent += 1 - call add(result, s:Indent(item)) - else - if !s:IsTag(item) - " Simply split on '<' - let t=split(item, '.<\@=\zs') - let s:indent+=1 - call add(result, s:Indent(t[0])) - let s:indent = s:DecreaseIndent() - call add(result, s:Indent(t[1])) - else + " go through every line, but don't join all content together and join it + " back. We might lose empty lines + let list = getline(v:lnum, (v:lnum + count_orig - 1)) + let current = 0 + for line in list + " Keep empty input lines? + if empty(line) + call add(result, '') + continue + elseif line !~# '<[/]\?[^>]*>' + let nextmatch = match(list, '<[/]\?[^>]*>', current) + if nextmatch > -1 + let line .= ' '. join(list[(current + 1):(nextmatch-1)], " ") + call remove(list, current+1, nextmatch-1) + endif + endif + " split on `>`, but don't split on very first opening < + " this means, items can be like ['<tag>', 'tag content</tag>'] + for item in split(line, '.\@<=[>]\zs') + if s:EndTag(item) + call s:DecreaseIndent() + call add(result, s:Indent(item)) + elseif s:EmptyTag(lastitem) call add(result, s:Indent(item)) + elseif s:StartTag(lastitem) && s:IsTag(item) + let s:indent += 1 + call add(result, s:Indent(item)) + else + if !s:IsTag(item) + " Simply split on '<', if there is one, + " but reformat according to &textwidth + let t=split(item, '.<\@=\zs') + + " if the content fits well within a single line, add it there + " so that the output looks like this: + " + " <foobar>1</foobar> + if s:TagContent(lastitem) is# s:TagContent(t[1]) && strlen(result[-1]) + strlen(item) <= s:Textwidth() + let result[-1] .= item + let lastitem = t[1] + continue + endif + " t should only contain 2 items, but just be safe here + if s:IsTag(lastitem) + let s:indent+=1 + endif + let result+=s:FormatContent([t[0]]) + if s:EndTag(t[1]) + call s:DecreaseIndent() + endif + "for y in t[1:] + let result+=s:FormatContent(t[1:]) + "endfor + else + call add(result, s:Indent(item)) + endif endif - endif - let lastitem = item - endfor + let lastitem = item + endfor + let current += 1 + endfor - if !empty(result) - exe v:lnum. ",". (v:lnum + v:count - 1). 'd' + if !empty(result) + let lastprevline = getline(v:lnum + count_orig) + let delete_lastline = v:lnum + count_orig - 1 == line('$') + exe v:lnum. ",". (v:lnum + count_orig - 1). 'd' call append(v:lnum - 1, result) " Might need to remove the last line, if it became empty because of the " append() call let last = v:lnum + len(result) - if getline(last) is '' + " do not use empty(), it returns true for `empty(0)` + if getline(last) is '' && lastprevline is '' && delete_lastline exe last. 'd' endif endif @@ -68,15 +109,15 @@ func! xmlformat#Format() return 0 endfunc " Check if given tag is XML Declaration header {{{1 -func! s:IsXMLDecl(tag) +func! s:IsXMLDecl(tag) abort return a:tag =~? '^\s*<?xml\s\?\%(version="[^"]*"\)\?\s\?\%(encoding="[^"]*"\)\? ?>\s*$' endfunc " Return tag indented by current level {{{1 -func! s:Indent(item) +func! s:Indent(item) abort return repeat(' ', shiftwidth()*s:indent). s:Trim(a:item) endfu " Return item trimmed from leading whitespace {{{1 -func! s:Trim(item) +func! s:Trim(item) abort if exists('*trim') return trim(a:item) else @@ -84,30 +125,77 @@ func! s:Trim(item) endif endfunc " Check if tag is a new opening tag <tag> {{{1 -func! s:StartTag(tag) +func! s:StartTag(tag) abort let is_comment = s:IsComment(a:tag) return a:tag =~? '^\s*<[^/?]' && !is_comment endfunc -func! s:IsComment(tag) +" Check if tag is a Comment start {{{1 +func! s:IsComment(tag) abort return a:tag =~? '<!--' endfunc " Remove one level of indentation {{{1 -func! s:DecreaseIndent() - return (s:indent > 0 ? s:indent - 1 : 0) +func! s:DecreaseIndent() abort + let s:indent = (s:indent > 0 ? s:indent - 1 : 0) endfunc " Check if tag is a closing tag </tag> {{{1 -func! s:EndTag(tag) +func! s:EndTag(tag) abort return a:tag =~? '^\s*</' endfunc " Check that the tag is actually a tag and not {{{1 " something like "foobar</foobar>" -func! s:IsTag(tag) +func! s:IsTag(tag) abort return s:Trim(a:tag)[0] == '<' endfunc " Check if tag is empty <tag/> {{{1 -func! s:EmptyTag(tag) +func! s:EmptyTag(tag) abort return a:tag =~ '/>\s*$' endfunc +func! s:TagContent(tag) abort "{{{1 + " Return content of a tag + return substitute(a:tag, '^\s*<[/]\?\([^>]*\)>\s*$', '\1', '') +endfunc +func! s:Textwidth() abort "{{{1 + " return textwidth (or 80 if not set) + return &textwidth == 0 ? 80 : &textwidth +endfunc +" Format input line according to textwidth {{{1 +func! s:FormatContent(list) abort + let result=[] + let limit = s:Textwidth() + let column=0 + let idx = -1 + let add_indent = 0 + let cnt = 0 + for item in a:list + for word in split(item, '\s\+\S\+\zs') + if match(word, '^\s\+$') > -1 + " skip empty words + continue + endif + let column += strdisplaywidth(word, column) + if match(word, "^\\s*\n\\+\\s*$") > -1 + call add(result, '') + let idx += 1 + let column = 0 + let add_indent = 1 + elseif column > limit || cnt == 0 + let add = s:Indent(s:Trim(word)) + call add(result, add) + let column = strdisplaywidth(add) + let idx += 1 + else + if add_indent + let result[idx] = s:Indent(s:Trim(word)) + else + let result[idx] .= ' '. s:Trim(word) + endif + let add_indent = 0 + endif + let cnt += 1 + endfor + endfor + return result +endfunc " Restoration And Modelines: {{{1 let &cpo= s:keepcpo unlet s:keepcpo diff --git a/runtime/compiler/xmllint.vim b/runtime/compiler/xmllint.vim index 96cfa55383..79d38b4d14 100644 --- a/runtime/compiler/xmllint.vim +++ b/runtime/compiler/xmllint.vim @@ -1,7 +1,7 @@ " Vim compiler file -" Compiler: xmllint +" Compiler: Libxml2 Command-Line Tool " Maintainer: Doug Kearns <dougkearns@gmail.com> -" Last Change: 2013 Jul 8 +" Last Change: 2020 Jul 30 if exists("current_compiler") finish @@ -13,14 +13,16 @@ if exists(":CompilerSet") != 2 " older Vim always used :setlocal endif let s:cpo_save = &cpo -set cpo-=C +set cpo&vim CompilerSet makeprg=xmllint\ --valid\ --noout - -CompilerSet errorformat=%+E%f:%l:\ %.%#\ error\ :\ %m, - \%+W%f:%l:\ %.%#\ warning\ :\ %m, - \%-Z%p^, - \%-G%.%# +CompilerSet errorformat=%E%f:%l:\ %.%#\ error\ :\ %m, + \%W%f:%l:\ %.%#\ warning\ :\ %m, + \%-Z%p^, + \%C%.%#, + \%terror:\ %m, + \%tarning:\ %m, + \%-G%.%# let &cpo = s:cpo_save unlet s:cpo_save diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index b83d2c4484..6c42dd6739 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5166,6 +5166,7 @@ A jump table for the options with a short description can be found at |Q_op|. It is allowed to give an argument to the command, e.g. "csh -f". See |option-backslash| about including spaces and backslashes. Environment variables are expanded |:set_env|. + If the name of the shell contains a space, you might need to enclose it in quotes. Example: > :set shell=\"c:\program\ files\unix\sh.exe\"\ -f diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 58cd535e98..b6a238f158 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -59,15 +59,16 @@ shouldn't be done directly in the change callback anyway as they will be very frequent. Rather a plugin that does any kind of analysis on a tree should use a timer to throttle too frequent updates. -tsparser:set_included_ranges({ranges}) *tsparser:set_included_ranges()* - Changes the ranges the parser should consider. This is used for - language injection. {ranges} should be of the form (all zero-based): > +tsparser:set_included_regions({region_list}) *tsparser:set_included_regions()* + Changes the regions the parser should consider. This is used for + language injection. {region_list} should be of the form (all zero-based): > { - {start_node, end_node}, + {node1, node2}, ... } < - NOTE: `start_node` and `end_node` are both inclusive. + `node1` and `node2` are both considered part of the same region and + will be parsed together with the parser in the same context. Tree methods *lua-treesitter-tree* @@ -253,7 +254,7 @@ Here is a list of built-in predicates : `lua-match?` *ts-predicate-lua-match?* This will match the same way than |match?| but using lua regexes. - + `contains?` *ts-predicate-contains?* Will check if any of the following arguments appears in the text corresponding to the node : > diff --git a/runtime/filetype.vim b/runtime/filetype.vim index 4e617052a9..4e54bcaefd 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -692,12 +692,21 @@ au BufNewFile,BufRead .gtkrc,gtkrc setf gtkrc au BufNewFile,BufRead *.haml setf haml " Hamster Classic | Playground files -au BufNewFile,BufRead *.hsc,*.hsm setf hamster +au BufNewFile,BufRead *.hsm setf hamster +au BufNewFile,BufRead *.hsc + \ if match(join(getline(1,10), "\n"), '\%(^\|\n\)\s*\%({-#\_s*LANGUAGE\>\|\<module\>\)') != -1 | + \ setf haskell | + \ else | + \ setf hamster | + \ endif " Haskell au BufNewFile,BufRead *.hs,*.hs-boot setf haskell au BufNewFile,BufRead *.lhs setf lhaskell au BufNewFile,BufRead *.chs setf chaskell +au BufNewFile,BufRead cabal.project setf cabalproject +au BufNewFile,BufRead $HOME/.cabal/config setf cabalconfig +au BufNewFile,BufRead cabal.config setf cabalconfig " Haste au BufNewFile,BufRead *.ht setf haste @@ -2042,12 +2051,14 @@ au BufNewFile,BufRead bzr_log.* setf bzr " Bazel build file if !has("fname_case") - au BufNewFile,BufRead *.BUILD,BUILD setf bzl + au BufNewFile,BufRead *.BUILD,BUILD setf bzl endif " BIND zone au BufNewFile,BufRead */named/db.*,*/bind/db.* call s:StarSetf('bindzone') +au BufNewFile,BufRead cabal.project.* call s:StarSetf('cabalproject') + " Calendar au BufNewFile,BufRead */.calendar/*, \*/share/calendar/*/calendar.*,*/share/calendar/calendar.* diff --git a/runtime/indent/xml.vim b/runtime/indent/xml.vim index 883af98563..413a3ddb53 100644 --- a/runtime/indent/xml.vim +++ b/runtime/indent/xml.vim @@ -1,9 +1,10 @@ -" Language: xml -" Repository: https://github.com/chrisbra/vim-xml-ftplugin -" Last Changed: July 27, 2019 -" Maintainer: Christian Brabandt <cb@256bit.org> -" Previous Maintainer: Johannes Zellner <johannes@zellner.org> +" Language: XML +" Maintainer: Christian Brabandt <cb@256bit.org> +" Repository: https://github.com/chrisbra/vim-xml-ftplugin +" Previous Maintainer: Johannes Zellner <johannes@zellner.org> +" Last Changed: 2019 Dec 02 " Last Change: +" 20191202 - Handle docbk filetype " 20190726 - Correctly handle non-tagged data " 20190204 - correctly handle wrap tags " https://github.com/chrisbra/vim-xml-ftplugin/issues/5 @@ -134,7 +135,7 @@ fun! XmlIndentGet(lnum, use_syntax_check) if syn_name_end =~ 'Comment' && syn_name_start =~ 'Comment' return <SID>XmlIndentComment(a:lnum) - elseif empty(syn_name_start) && empty(syn_name_end) + elseif empty(syn_name_start) && empty(syn_name_end) && a:use_syntax_check " non-xml tag content: use indent from 'autoindent' return pind + shiftwidth() endif @@ -148,7 +149,7 @@ endfun func! <SID>IsXMLContinuation(line) " Checks, whether or not the line matches a start-of-tag - return a:line !~ '^\s*<' + return a:line !~ '^\s*<' && &ft is# 'xml' endfunc func! <SID>HasNoTagEnd(line) diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 19ef148afc..6886f0c178 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,98 +1,13 @@ local a = vim.api local query = require'vim.treesitter.query' local language = require'vim.treesitter.language' +local LanguageTree = require'vim.treesitter.languagetree' -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with -- it. local parsers = {} -local Parser = {} -Parser.__index = Parser - ---- Parses the buffer if needed and returns a tree. --- --- Calling this will call the on_changedtree callbacks if the tree has changed. --- --- @returns An up to date tree --- @returns If the tree changed with this call, the changed ranges -function Parser:parse() - if self.valid then - return self._tree_immutable - end - local changes - - self._tree, changes = self._parser:parse(self._tree, self:input_source()) - - self._tree_immutable = self._tree:copy() - - self.valid = true - - if not vim.tbl_isempty(changes) then - for _, cb in ipairs(self.changedtree_cbs) do - cb(changes) - end - end - - return self._tree_immutable, changes -end - -function Parser:input_source() - return self.bufnr or self.str -end - -function Parser:_on_bytes(bufnr, changed_tick, - start_row, start_col, start_byte, - old_row, old_col, old_byte, - new_row, new_col, new_byte) - local old_end_col = old_col + ((old_row == 0) and start_col or 0) - local new_end_col = new_col + ((new_row == 0) and start_col or 0) - self._tree:edit(start_byte,start_byte+old_byte,start_byte+new_byte, - start_row, start_col, - start_row+old_row, old_end_col, - start_row+new_row, new_end_col) - self.valid = false - - for _, cb in ipairs(self.bytes_cbs) do - cb(bufnr, changed_tick, - start_row, start_col, start_byte, - old_row, old_col, old_byte, - new_row, new_col, new_byte) - end -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 everytime the tree has syntactical changes. --- it will only be passed one argument, that is a table of the ranges (as node ranges) that --- changed. -function Parser:register_cbs(cbs) - if not cbs then return end - - if cbs.on_changedtree then - table.insert(self.changedtree_cbs, cbs.on_changedtree) - end - - if cbs.on_bytes then - table.insert(self.bytes_cbs, cbs.on_bytes) - end -end - ---- Sets the included ranges for the current parser --- --- @param ranges A table of nodes that will be used as the ranges the parser should include. -function Parser:set_included_ranges(ranges) - self._parser:set_included_ranges(ranges) - -- The buffer will need to be parsed again later - self.valid = false -end - ---- Gets the included ranges for the parsers -function Parser:included_ranges() - return self._parser:included_ranges() -end - local M = vim.tbl_extend("error", query, language) setmetatable(M, { @@ -113,9 +28,9 @@ setmetatable(M, { -- It is not recommended to use this, use vim.treesitter.get_parser() instead. -- -- @param bufnr The buffer the parser will be tied to --- @param lang The language of the parser. --- @param id The id the parser will have -function M._create_parser(bufnr, lang, id) +-- @param lang The language of the parser +-- @param opts Options to pass to the language tree +function M._create_parser(bufnr, lang, opts) language.require_language(lang) if bufnr == 0 then bufnr = a.nvim_get_current_buf() @@ -123,25 +38,22 @@ function M._create_parser(bufnr, lang, id) vim.fn.bufload(bufnr) - local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) - self._parser = vim._create_ts_parser(lang) - self.changedtree_cbs = {} - self.bytes_cbs = {} - self:parse() - -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is - -- using it. + local self = LanguageTree.new(bufnr, lang, opts) + local function bytes_cb(_, ...) - return self:_on_bytes(...) + self:_on_bytes(...) end - local detach_cb = nil - if id ~= nil then - detach_cb = function() - if parsers[id] == self then - parsers[id] = nil - end + + local function detach_cb() + if parsers[bufnr] == self then + parsers[bufnr] = nil end end + a.nvim_buf_attach(self.bufnr, false, {on_bytes=bytes_cb, on_detach=detach_cb}) + + self:parse() + return self end @@ -152,39 +64,36 @@ end -- -- @param bufnr The buffer the parser should be tied to -- @param ft The filetype of this parser --- @param buf_attach_cbs See Parser:register_cbs +-- @param opts Options object to pass to the parser -- -- @returns The parser -function M.get_parser(bufnr, lang, buf_attach_cbs) +function M.get_parser(bufnr, lang, opts) + opts = opts or {} + if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end if lang == nil then lang = a.nvim_buf_get_option(bufnr, "filetype") end - local id = tostring(bufnr)..'_'..lang - if parsers[id] == nil then - parsers[id] = M._create_parser(bufnr, lang, id) + if parsers[bufnr] == nil then + parsers[bufnr] = M._create_parser(bufnr, lang, opts) end - parsers[id]:register_cbs(buf_attach_cbs) + parsers[bufnr]:register_cbs(opts.buf_attach_cbs) - return parsers[id] + return parsers[bufnr] end -function M.get_string_parser(str, lang) +function M.get_string_parser(str, lang, opts) vim.validate { str = { str, 'string' }, lang = { lang, 'string' } } language.require_language(lang) - local self = setmetatable({str=str, lang=lang, valid=false}, Parser) - self._parser = vim._create_ts_parser(lang) - self:parse() - - return self + return LanguageTree.new(str, lang, opts) end return M diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua index 6714bb6354..60db7f24cf 100644 --- a/runtime/lua/vim/treesitter/highlighter.lua +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -1,4 +1,5 @@ local a = vim.api +local query = require"vim.treesitter.query" -- support reload for quick experimentation local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} @@ -6,6 +7,9 @@ TSHighlighter.__index = TSHighlighter TSHighlighter.active = TSHighlighter.active or {} +local TSHighlighterQuery = {} +TSHighlighterQuery.__index = TSHighlighterQuery + local ns = a.nvim_create_namespace("treesitter/highlighter") -- These are conventions defined by nvim-treesitter, though it @@ -56,27 +60,83 @@ TSHighlighter.hl_map = { ["include"] = "Include", } -function TSHighlighter.new(parser, query) +local function is_highlight_name(capture_name) + local firstc = string.sub(capture_name, 1, 1) + return firstc ~= string.lower(firstc) +end + +function TSHighlighterQuery.new(lang, query_string) + local self = setmetatable({}, { __index = TSHighlighterQuery }) + + self.hl_cache = setmetatable({}, { + __index = function(table, capture) + local hl = self:get_hl_from_capture(capture) + rawset(table, capture, hl) + + return hl + end + }) + + if query_string then + self._query = query.parse_query(lang, query_string) + else + self._query = query.get_query(lang, "highlights") + end + + return self +end + +function TSHighlighterQuery:query() + return self._query +end + +function TSHighlighterQuery:get_hl_from_capture(capture) + local name = self._query.captures[capture] + + if is_highlight_name(name) then + -- From "Normal.left" only keep "Normal" + return vim.split(name, '.', true)[1] + else + -- Default to false to avoid recomputing + local hl = TSHighlighter.hl_map[name] + return hl and a.nvim_get_hl_id_by_name(hl) or 0 + end +end + +function TSHighlighter.new(tree, opts) local self = setmetatable({}, TSHighlighter) - self.parser = parser - parser:register_cbs { - on_changedtree = function(...) self:on_changedtree(...) end + if type(tree:source()) ~= "number" then + error("TSHighlighter can not be used with a string parser source.") + end + + opts = opts or {} + self.tree = tree + tree:register_cbs { + on_changedtree = function(...) self:on_changedtree(...) end, + on_bytes = function(...) self:on_bytes(...) end } - self:set_query(query) + self.bufnr = tree:source() self.edit_count = 0 self.redraw_count = 0 self.line_count = {} - self.root = self.parser:parse():root() - a.nvim_buf_set_option(self.buf, "syntax", "") - - -- TODO(bfredl): can has multiple highlighters per buffer???? - if not TSHighlighter.active[parser.bufnr] then - TSHighlighter.active[parser.bufnr] = {} + -- A map of highlight states. + -- This state is kept during rendering across each line update. + self._highlight_states = {} + self._queries = {} + + -- Queries for a specific language can be overridden by a custom + -- string query... if one is not provided it will be looked up by file. + if opts.queries then + for lang, query_string in pairs(opts.queries) do + self._queries[lang] = TSHighlighterQuery.new(lang, query_string) + end end - TSHighlighter.active[parser.bufnr][parser.lang] = self + a.nvim_buf_set_option(self.bufnr, "syntax", "") + + TSHighlighter.active[self.bufnr] = self -- Tricky: if syntax hasn't been enabled, we need to reload color scheme -- but use synload.vim rather than syntax.vim to not enable @@ -85,119 +145,112 @@ function TSHighlighter.new(parser, query) if vim.g.syntax_on ~= 1 then vim.api.nvim_command("runtime! syntax/synload.vim") end + + self.tree:parse() + return self end -local function is_highlight_name(capture_name) - local firstc = string.sub(capture_name, 1, 1) - return firstc ~= string.lower(firstc) +function TSHighlighter:destroy() + if TSHighlighter.active[self.bufnr] then + TSHighlighter.active[self.bufnr] = nil + end end -function TSHighlighter:get_hl_from_capture(capture) +function TSHighlighter:get_highlight_state(tstree) + if not self._highlight_states[tstree] then + self._highlight_states[tstree] = { + next_row = 0, + iter = nil + } + end - local name = self.query.captures[capture] + return self._highlight_states[tstree] +end - if is_highlight_name(name) then - -- From "Normal.left" only keep "Normal" - return vim.split(name, '.', true)[1] - else - -- Default to false to avoid recomputing - local hl = TSHighlighter.hl_map[name] - return hl and a.nvim_get_hl_id_by_name(hl) or 0 - end +function TSHighlighter:reset_highlight_state() + self._highlight_states = {} +end + +function TSHighlighter:on_bytes(_, _, start_row, _, _, _, _, _, new_end) + a.nvim__buf_redraw_range(self.bufnr, start_row, start_row + new_end + 1) end function TSHighlighter:on_changedtree(changes) for _, ch in ipairs(changes or {}) do - a.nvim__buf_redraw_range(self.buf, ch[1], ch[3]+1) + a.nvim__buf_redraw_range(self.bufnr, ch[1], ch[3]+1) end end -function TSHighlighter:set_query(query) - if type(query) == "string" then - query = vim.treesitter.parse_query(self.parser.lang, query) +function TSHighlighter:get_query(lang) + if not self._queries[lang] then + self._queries[lang] = TSHighlighterQuery.new(lang) end - self.query = query + return self._queries[lang] +end - self.hl_cache = setmetatable({}, { - __index = function(table, capture) - local hl = self:get_hl_from_capture(capture) - rawset(table, capture, hl) +local function on_line_impl(self, buf, line) + self.tree:for_each_tree(function(tstree, tree) + if not tstree then return end - return hl + local root_node = tstree:root() + local root_start_row, _, root_end_row, _ = root_node:range() + + -- Only worry about trees within the line range + if root_start_row > line or root_end_row < line then return end + + local state = self:get_highlight_state(tstree) + local highlighter_query = self:get_query(tree:lang()) + + if state.iter == nil then + state.iter = highlighter_query:query():iter_captures(root_node, self.bufnr, line, root_end_row + 1) end - }) - a.nvim__buf_redraw_range(self.parser.bufnr, 0, a.nvim_buf_line_count(self.parser.bufnr)) -end + while line >= state.next_row do + local capture, node = state.iter() -local function iter_active_tshl(buf, fn) - for _, hl in pairs(TSHighlighter.active[buf] or {}) do - fn(hl) - end -end + if capture == nil then break end -local function on_line_impl(self, buf, line) - if self.root == nil then - return -- parser bought the farm already - end + local start_row, start_col, end_row, end_col = node:range() + local hl = highlighter_query.hl_cache[capture] - if self.iter == nil then - self.iter = self.query:iter_captures(self.root,buf,line,self.botline) - end - while line >= self.nextrow do - local capture, node = self.iter() - if capture == nil then - break + if hl and end_row >= line then + a.nvim_buf_set_extmark(buf, ns, start_row, start_col, + { end_line = end_row, end_col = end_col, + hl_group = hl, + ephemeral = true + }) + end + if start_row > line then + state.next_row = start_row + end end - local start_row, start_col, end_row, end_col = node:range() - local hl = self.hl_cache[capture] - if hl and end_row >= line then - a.nvim_buf_set_extmark(buf, ns, start_row, start_col, - { end_line = end_row, end_col = end_col, - hl_group = hl, - ephemeral = true, - }) - end - if start_row > line then - self.nextrow = start_row - end - end + end, true) end -function TSHighlighter._on_line(_, _win, buf, line, highlighter) - -- on_line is only called when this is non-nil - if highlighter then - on_line_impl(highlighter, buf, line) - else - iter_active_tshl(buf, function(self) - on_line_impl(self, buf, line) - end) - end +function TSHighlighter._on_line(_, _win, buf, line, _) + local self = TSHighlighter.active[buf] + if not self then return end + + on_line_impl(self, buf, line) end function TSHighlighter._on_buf(_, buf) - iter_active_tshl(buf, function(self) - if self then - local tree = self.parser:parse() - self.root = (tree and tree:root()) or nil - end - end) + local self = TSHighlighter.active[buf] + if self then + self.tree:parse() + end end -function TSHighlighter._on_win(_, _win, buf, _topline, botline) - iter_active_tshl(buf, function(self) - if not self then - return false - end +function TSHighlighter._on_win(_, _win, buf, _topline) + local self = TSHighlighter.active[buf] + if not self then + return false + end - self.iter = nil - self.nextrow = 0 - self.botline = botline - self.redraw_count = self.redraw_count + 1 - return true - end) + self:reset_highlight_state() + self.redraw_count = self.redraw_count + 1 return true end diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua new file mode 100644 index 0000000000..70e2ac4c62 --- /dev/null +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -0,0 +1,454 @@ +local a = vim.api +local query = require'vim.treesitter.query' +local language = require'vim.treesitter.language' + +local LanguageTree = {} +LanguageTree.__index = LanguageTree + +-- Represents a single treesitter parser for a language. +-- The language can contain child languages with in it's range, +-- hence the tree. +-- +-- @param source Can be a bufnr or a string of text to parse +-- @param lang The language this tree represents +-- @param opts Options table +-- @param opts.queries A table of language to injection query strings +-- This is useful for overridding the built in runtime file +-- searching for the injection language query per language. +function LanguageTree.new(source, lang, opts) + language.require_language(lang) + opts = opts or {} + + local custom_queries = opts.queries or {} + local self = setmetatable({ + _source=source, + _lang=lang, + _children = {}, + _regions = {}, + _trees = {}, + _opts = opts, + _injection_query = custom_queries[lang] + and query.parse_query(lang, custom_queries[lang]) + or query.get_query(lang, "injections"), + _valid = false, + _parser = vim._create_ts_parser(lang), + _callbacks = { + changedtree = {}, + bytes = {}, + child_added = {}, + child_removed = {} + }, + }, LanguageTree) + + + return self +end + +-- Invalidates this parser and all it's children +function LanguageTree:invalidate() + self._valid = false + + for _, child in ipairs(self._children) do + child:invalidate() + end +end + +-- Returns all trees this language tree contains. +-- Does not include child languages. +function LanguageTree:trees() + return self._trees +end + +-- Gets the language of this tree layer. +function LanguageTree:lang() + return self._lang +end + +-- Determines whether this tree is valid. +-- If the tree is invalid, `parse()` must be called +-- to get the an updated tree. +function LanguageTree:is_valid() + return self._valid +end + +-- Returns a map of language to child tree. +function LanguageTree:children() + return self._children +end + +-- Returns the source content of the language tree (bufnr or string). +function LanguageTree:source() + return self._source +end + +-- Parses all defined regions using a treesitter parser +-- for the language this tree represents. +-- This will run the injection query for this language to +-- determine if any child languages should be created. +function LanguageTree:parse() + if self._valid then + return self._trees + end + + local parser = self._parser + local changes = {} + + local old_trees = self._trees + self._trees = {} + + -- If there are no ranges, set to an empty list + -- so the included ranges in the parser ar cleared. + if self._regions and #self._regions > 0 then + for i, ranges in ipairs(self._regions) do + local old_tree = old_trees[i] + parser:set_included_ranges(ranges) + + local tree, tree_changes = parser:parse(old_tree, self._source) + + table.insert(self._trees, tree) + vim.list_extend(changes, tree_changes) + end + else + local tree, tree_changes = parser:parse(old_trees[1], self._source) + + table.insert(self._trees, tree) + vim.list_extend(changes, tree_changes) + end + + local injections_by_lang = self:_get_injections() + local seen_langs = {} + + for lang, injection_ranges in pairs(injections_by_lang) do + local child = self._children[lang] + + if not child then + child = self:add_child(lang) + end + + child:set_included_regions(injection_ranges) + + local _, child_changes = child:parse() + + -- Propagate any child changes so they are included in the + -- the change list for the callback. + if child_changes then + vim.list_extend(changes, child_changes) + end + + seen_langs[lang] = true + end + + for lang, _ in pairs(self._children) do + if not seen_langs[lang] then + self:remove_child(lang) + end + end + + self._valid = true + + self:_do_callback('changedtree', changes) + return self._trees, changes +end + +-- Invokes the callback for each LanguageTree and it's children recursively +-- @param fn The function to invoke. This is invoked with arguments (tree: LanguageTree, lang: string) +-- @param include_self Whether to include the invoking tree in the results. +function LanguageTree:for_each_child(fn, include_self) + if include_self then + fn(self, self._lang) + end + + for _, child in pairs(self._children) do + child:for_each_child(fn, true) + end +end + +-- Invokes the callback for each treesitter trees recursively. +-- Note, this includes the invoking language tree's trees as well. +-- @param fn The callback to invoke. The callback is invoked with arguments +-- (tree: TSTree, languageTree: LanguageTree) +function LanguageTree:for_each_tree(fn) + for _, tree in ipairs(self._trees) do + fn(tree, self) + end + + for _, child in pairs(self._children) do + child:for_each_tree(fn) + end +end + +-- Adds a child language to this tree. +-- If the language already exists as a child, it will first be removed. +-- @param lang The language to add. +function LanguageTree:add_child(lang) + if self._children[lang] then + self:remove_child(lang) + end + + self._children[lang] = LanguageTree.new(self._source, lang, self._opts) + + self:invalidate() + self:_do_callback('child_added', self._children[lang]) + + return self._children[lang] +end + +-- Removes a child language from this tree. +-- @param lang The language to remove. +function LanguageTree:remove_child(lang) + local child = self._children[lang] + + if child then + self._children[lang] = nil + child:destroy() + self:invalidate() + self:_do_callback('child_removed', child) + end +end + +-- Destroys this language tree and all it's children. +-- Any cleanup logic should be performed here. +-- Note, this DOES NOT remove this tree from a parent. +-- `remove_child` must be called on the parent to remove it. +function LanguageTree:destroy() + -- Cleanup here + for _, child in ipairs(self._children) do + child:destroy() + end +end + +-- Sets the included regions that should be parsed by this parser. +-- A region is a set of nodes and/or ranges that will be parsed in the same context. +-- +-- For example, `{ { node1 }, { node2} }` is two separate regions. +-- This will be parsed by the parser in two different contexts... thus resulting +-- in two separate trees. +-- +-- `{ { node1, node2 } }` is a single region consisting of two nodes. +-- This will be parsed by the parser in a single context... thus resulting +-- in a single tree. +-- +-- This allows for embedded languages to be parsed together across different +-- nodes, which is useful for templating languages like ERB and EJS. +-- +-- Note, this call invalidates the tree and requires it to be parsed again. +-- +-- @param regions A list of regions this tree should manange and parse. +function LanguageTree:set_included_regions(regions) + -- Transform the tables from 4 element long to 6 element long (with byte offset) + for _, region in ipairs(regions) do + for i, range in ipairs(region) do + if type(range) == "table" and #range == 4 then + -- TODO(vigoux): I don't think string parsers are useful for now + if type(self._source) == "number" then + local start_row, start_col, end_row, end_col = unpack(range) + -- Easy case, this is a buffer parser + -- TODO(vigoux): proper byte computation here, and account for EOL ? + local start_byte = a.nvim_buf_get_offset(self.bufnr, start_row) + start_col + local end_byte = a.nvim_buf_get_offset(self.bufnr, end_row) + end_col + + region[i] = { start_row, start_col, start_byte, end_row, end_col, end_byte } + end + end + end + end + + self._regions = regions + -- Trees are no longer valid now that we have changed regions. + -- TODO(vigoux,steelsojka): Look into doing this smarter so we can use some of the + -- old trees for incremental parsing. Currently, this only + -- effects injected languages. + self._trees = {} + self:invalidate() +end + +-- Gets the set of included regions +function LanguageTree:included_regions() + return self._regions +end + +-- Gets language injection points by language. +-- This is where most of the injection processing occurs. +-- TODO: Allow for an offset predicate to tailor the injection range +-- instead of using the entire nodes range. +-- @private +function LanguageTree:_get_injections() + if not self._injection_query then return {} end + + local injections = {} + + for tree_index, tree in ipairs(self._trees) do + local root_node = tree:root() + local start_line, _, end_line, _ = root_node:range() + + for pattern, match in self._injection_query:iter_matches(root_node, self._source, start_line, end_line+1) do + local lang = nil + local injection_node = nil + local combined = false + + -- You can specify the content and language together + -- using a tag with the language, for example + -- @javascript + for id, node in pairs(match) do + local name = self._injection_query.captures[id] + -- TODO add a way to offset the content passed to the parser. + -- Needed to shave off leading quotes and things of that nature. + + -- Lang should override any other language tag + if name == "language" then + lang = query.get_node_text(node, self._source) + elseif name == "combined" then + combined = true + elseif name == "content" then + injection_node = node + -- Ignore any tags that start with "_" + -- Allows for other tags to be used in matches + elseif string.sub(name, 1, 1) ~= "_" then + if lang == nil then + lang = name + end + + if not injection_node then + injection_node = node + end + end + end + + -- Each tree index should be isolated from the other nodes. + if not injections[tree_index] then + injections[tree_index] = {} + end + + if not injections[tree_index][lang] then + injections[tree_index][lang] = {} + end + + -- Key by pattern so we can either combine each node to parse in the same + -- context or treat each node independently. + if not injections[tree_index][lang][pattern] then + injections[tree_index][lang][pattern] = { combined = combined, nodes = {} } + end + + table.insert(injections[tree_index][lang][pattern].nodes, injection_node) + end + end + + local result = {} + + -- Generate a map by lang of node lists. + -- Each list is a set of ranges that should be parsed + -- together. + for _, lang_map in ipairs(injections) do + for lang, patterns in pairs(lang_map) do + if not result[lang] then + result[lang] = {} + end + + for _, entry in pairs(patterns) do + if entry.combined then + table.insert(result[lang], entry.nodes) + else + for _, node in ipairs(entry.nodes) do + table.insert(result[lang], {node}) + end + end + end + end + end + + return result +end + +function LanguageTree:_do_callback(cb_name, ...) + for _, cb in ipairs(self._callbacks[cb_name]) do + cb(...) + end +end + +function LanguageTree:_on_bytes(bufnr, changed_tick, + start_row, start_col, start_byte, + old_row, old_col, old_byte, + new_row, new_col, new_byte) + self:invalidate() + + local old_end_col = old_col + ((old_row == 0) and start_col or 0) + local new_end_col = new_col + ((new_row == 0) and start_col or 0) + + -- Edit all trees recursively, together BEFORE emitting a bytes callback. + -- In most cases this callback should only be called from the root tree. + self:for_each_tree(function(tree) + tree:edit(start_byte,start_byte+old_byte,start_byte+new_byte, + start_row, start_col, + start_row+old_row, old_end_col, + start_row+new_row, new_end_col) + end) + + self:_do_callback('bytes', bufnr, changed_tick, + start_row, start_col, start_byte, + old_row, old_col, old_byte, + new_row, new_col, new_byte) +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 everytime 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 remvoed from the tree. +function LanguageTree:register_cbs(cbs) + if not cbs then return end + + if cbs.on_changedtree then + table.insert(self._callbacks.changedtree, cbs.on_changedtree) + end + + if cbs.on_bytes then + table.insert(self._callbacks.bytes, cbs.on_bytes) + end + + if cbs.on_child_added then + table.insert(self._callbacks.child_added, cbs.on_child_added) + end + + if cbs.on_child_removed then + table.insert(self._callbacks.child_removed, cbs.on_child_removed) + end +end + +local function region_contains(region, range) + for _, node in ipairs(region) do + local start_row, start_col, end_row, end_col = node:range() + local start_fits = start_row < range[1] or (start_row == range[1] and start_col <= range[2]) + local end_fits = end_row > range[3] or (end_row == range[3] and end_col >= range[4]) + + if start_fits and end_fits then + return true + end + end + + return false +end + +function LanguageTree:contains(range) + for _, region in pairs(self._region) do + if region_contains(region, range) then + return true + end + end + + return false +end + +function LanguageTree:language_for_range(range) + for _, child in pairs(self._children) do + if child:contains(range) then + return child:node_for_range(range) + end + end + + return self +end + +return LanguageTree diff --git a/runtime/syntax/gitconfig.vim b/runtime/syntax/gitconfig.vim index c6c56f77b6..e5eaf10f94 100644 --- a/runtime/syntax/gitconfig.vim +++ b/runtime/syntax/gitconfig.vim @@ -2,24 +2,22 @@ " Language: git config file " Maintainer: Tim Pope <vimNOSPAM@tpope.org> " Filenames: gitconfig, .gitconfig, *.git/config -" Last Change: 2010 May 21 +" Last Change: 2019 Dec 05 if exists("b:current_syntax") finish endif -setlocal iskeyword+=- -setlocal iskeyword-=_ syn case ignore syn sync minlines=10 -syn match gitconfigComment "[#;].*" +syn match gitconfigComment "[#;].*" contains=@Spell syn match gitconfigSection "\%(^\s*\)\@<=\[[a-z0-9.-]\+\]" syn match gitconfigSection '\%(^\s*\)\@<=\[[a-z0-9.-]\+ \+\"\%([^\\"]\|\\.\)*"\]' -syn match gitconfigVariable "\%(^\s*\)\@<=\a\k*\%(\s*\%([=#;]\|$\)\)\@=" nextgroup=gitconfigAssignment skipwhite +syn match gitconfigVariable "\%(^\s*\)\@<=\a[a-z0-9-]*\%(\s*\%([=#;]\|$\)\)\@=" nextgroup=gitconfigAssignment skipwhite syn region gitconfigAssignment matchgroup=gitconfigNone start=+=\s*+ skip=+\\+ end=+\s*$+ contained contains=gitconfigBoolean,gitconfigNumber,gitConfigString,gitConfigEscape,gitConfigError,gitconfigComment keepend syn keyword gitconfigBoolean true false yes no contained -syn match gitconfigNumber "\d\+" contained +syn match gitconfigNumber "\<\d\+\>" contained syn region gitconfigString matchgroup=gitconfigDelim start=+"+ skip=+\\+ end=+"+ matchgroup=gitconfigError end=+[^\\"]\%#\@!$+ contained contains=gitconfigEscape,gitconfigEscapeError syn match gitconfigError +\\.+ contained syn match gitconfigEscape +\\[\\"ntb]+ contained @@ -32,7 +30,7 @@ hi def link gitconfigBoolean Boolean hi def link gitconfigNumber Number hi def link gitconfigString String hi def link gitconfigDelim Delimiter -hi def link gitconfigEscape Delimiter +hi def link gitconfigEscape Special hi def link gitconfigError Error let b:current_syntax = "gitconfig" diff --git a/runtime/syntax/xml.vim b/runtime/syntax/xml.vim index 7b503abf49..7c9791a7cc 100644 --- a/runtime/syntax/xml.vim +++ b/runtime/syntax/xml.vim @@ -1,11 +1,15 @@ " Vim syntax file -" Language: XML -" Maintainer: Johannes Zellner <johannes@zellner.org> -" Author and previous maintainer: -" Paul Siegmann <pauls@euronet.nl> -" Last Change: 2013 Jun 07 +" Language: XML +" Maintainer: Christian Brabandt <cb@256bit.org> +" Repository: https://github.com/chrisbra/vim-xml-ftplugin +" Previous Maintainer: Johannes Zellner <johannes@zellner.org> +" Author: Paul Siegmann <pauls@euronet.nl> +" Last Changed: Nov 03, 2019 " Filenames: *.xml -" $Id: xml.vim,v 1.3 2006/04/11 21:32:00 vimboss Exp $ +" Last Change: +" 20190923 - Fix xmlEndTag to match xmlTag (vim/vim#884) +" 20190924 - Fix xmlAttribute property (amadeus/vim-xml@d8ce1c946) +" 20191103 - Enable spell checking globally " CONFIGURATION: " syntax folding can be turned on by @@ -49,6 +53,12 @@ set cpo&vim syn case match +" Allow spell checking in tag values, +" there is no syntax region for that, +" so enable spell checking in top-level elements +" <tag>This text is spell checked</tag> +syn spell toplevel + " mark illegal characters syn match xmlError "[<&]" @@ -81,7 +91,7 @@ syn match xmlEqual +=+ display " ^^^^^^^^^^^^^ " syn match xmlAttrib - \ +[-'"<]\@1<!\<[a-zA-Z:_][-.0-9a-zA-Z:_]*\>\%(['">]\@!\|$\)+ + \ +[-'"<]\@1<!\<[a-zA-Z:_][-.0-9a-zA-Z:_]*\>\%(['"]\@!\|$\)+ \ contained \ contains=xmlAttribPunct,@xmlAttribHook \ display @@ -122,7 +132,7 @@ endif " ^^^ " syn match xmlTagName - \ +<\@1<=[^ /!?<>"']\++ + \ +\%(<\|</\)\@2<=[^ /!?<>"']\++ \ contained \ contains=xmlNamespace,xmlAttribPunct,@xmlTagHook \ display @@ -157,11 +167,11 @@ if exists('g:xml_syntax_folding') " </tag> " ^^^^^^ " - syn match xmlEndTag - \ +</[^ /!?<>"']\+>+ + syn region xmlEndTag + \ matchgroup=xmlTag start=+</[^ /!?<>"']\@=+ + \ matchgroup=xmlTag end=+>+ \ contained - \ contains=xmlNamespace,xmlAttribPunct,@xmlTagHook - + \ contains=xmlTagName,xmlNamespace,xmlAttribPunct,@xmlTagHook " tag elements with syntax-folding. " NOTE: NO HIGHLIGHTING -- highlighting is done by contained elements @@ -181,7 +191,7 @@ if exists('g:xml_syntax_folding') \ start=+<\z([^ /!?<>"']\+\)+ \ skip=+<!--\_.\{-}-->+ \ end=+</\z1\_\s\{-}>+ - \ matchgroup=xmlEndTag end=+/>+ + \ end=+/>+ \ fold \ contains=xmlTag,xmlEndTag,xmlCdata,xmlRegion,xmlComment,xmlEntity,xmlProcessing,@xmlRegionHook,@Spell \ keepend @@ -198,9 +208,10 @@ else \ matchgroup=xmlTag end=+>+ \ contains=xmlError,xmlTagName,xmlAttrib,xmlEqual,xmlString,@xmlStartTagHook - syn match xmlEndTag - \ +</[^ /!?<>"']\+>+ - \ contains=xmlNamespace,xmlAttribPunct,@xmlTagHook + syn region xmlEndTag + \ matchgroup=xmlTag start=+</[^ /!?<>"']\@=+ + \ matchgroup=xmlTag end=+>+ + \ contains=xmlTagName,xmlNamespace,xmlAttribPunct,@xmlTagHook endif |