diff options
33 files changed, 466 insertions, 208 deletions
diff --git a/.github/workflows/news.yml b/.github/workflows/news.yml index 11807e9b42..c715bf1031 100644 --- a/.github/workflows/news.yml +++ b/.github/workflows/news.yml @@ -1,11 +1,13 @@ name: "news.txt check" on: pull_request: + types: [opened, synchronize, reopened, ready_for_review] branches: - 'master' jobs: check: runs-on: ubuntu-latest + if: github.event.pull_request.draft == false steps: - uses: actions/checkout@v3 with: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6c134b583..22be9bf719 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -241,17 +241,9 @@ jobs: fi # Check that all runtime files were installed - for file in $(git -C runtime ls-files '*.vim' '*.ps' '*.dict' '*.py' '*.tutor'); do + for file in $(git -C runtime ls-files '*.vim' '*.ps' '*.dict' '*.py' '*.tutor' '*.awk' '*.sh' '*.bat'); do if ! test -e "$INSTALL_PREFIX/share/nvim/runtime/$file"; then - printf "%s%s" 'It appears that %s is not installed.' "$file" - exit 1 - fi - done - - # Check that some runtime files are installed and are executables - for file in $(git -C runtime ls-files '*.awk' '*.sh' '*.bat'); do - if ! test -x "$INSTALL_PREFIX/share/nvim/runtime/$file"; then - printf "%s%s" 'It appears that %s is not installed or is not executable.' "$file" + printf "It appears that %s is not installed." "$file" exit 1 fi done diff --git a/cmake/InstallHelpers.cmake b/cmake/InstallHelpers.cmake index 3786c4177f..49d8692aae 100644 --- a/cmake/InstallHelpers.cmake +++ b/cmake/InstallHelpers.cmake @@ -167,12 +167,3 @@ function(glob_wrapper outvar) endif() set(${outvar} ${${outvar}} PARENT_SCOPE) endfunction() - -function(globrecurse_wrapper outvar root) - if(${CMAKE_VERSION} VERSION_LESS 3.12) - file(GLOB_RECURSE ${outvar} RELATIVE ${root} ${ARGN}) - else() - file(GLOB_RECURSE ${outvar} CONFIGURE_DEPENDS RELATIVE ${root} ${ARGN}) - endif() - set(${outvar} ${${outvar}} PARENT_SCOPE) -endfunction() diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 581a4545db..c6447721a0 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -116,23 +116,17 @@ install_helper( FILES ${CMAKE_CURRENT_SOURCE_DIR}/nvim.png DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/128x128/apps) -install_helper( - FILES ${CMAKE_CURRENT_SOURCE_DIR}/neovim.ico - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime) - -globrecurse_wrapper(RUNTIME_PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR} *.awk *.sh *.bat) - -foreach(PROG ${RUNTIME_PROGRAMS}) - get_filename_component(BASEDIR ${PROG} DIRECTORY) - install_helper(PROGRAMS ${PROG} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/${BASEDIR}) +glob_wrapper(RUNTIME_ROOT_FILES *.vim *.lua *.ico) +install_helper(FILES ${RUNTIME_ROOT_FILES} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/) + +glob_wrapper(RUNTIME_DIRS */) +foreach(D ${RUNTIME_DIRS}) + if(IS_DIRECTORY ${D}) + install_helper(DIRECTORY ${D} + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/) + endif() endforeach() -globrecurse_wrapper(RUNTIME_FILES ${CMAKE_CURRENT_SOURCE_DIR} - *.vim *.lua *.scm *.dict *.py *.rb *.ps *.spl *.tutor *.tutor.json) - -foreach(F ${RUNTIME_FILES}) - get_filename_component(BASEDIR ${F} DIRECTORY) - install_helper(FILES ${F} - DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/${BASEDIR}) -endforeach() +# only foo.sh script in runtime/ +install_helper(PROGRAMS ${CMAKE_CURRENT_SOURCE_DIR}/macros/less.sh DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/nvim/runtime/macros/) diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 3ca50dda15..0e1cf9ec3e 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -356,6 +356,7 @@ In-process Lua plugins can receive buffer updates in the form of Lua callbacks. These callbacks are called frequently in various contexts; |textlock| prevents changing buffer contents and window layout (use |vim.schedule()| to defer such operations to the main loop instead). +Moving the cursor is allowed, but it is restored afterwards. |nvim_buf_attach()| will take keyword args for the callbacks. "on_lines" will receive parameters ("lines", {buf}, {changedtick}, {firstline}, {lastline}, @@ -451,6 +452,10 @@ Buffer text can be highlighted by typical mechanisms (syntax highlighting, options from the current window; specify `style=minimal` in |nvim_open_win()| to disable various visual features such as the 'number' column. +Other highlight groups specific to floating windows: +- |hl-FloatBorder| for window's border +- |hl-FloatTitle| for window's title + Currently, floating windows don't support some widgets like scrollbar. The output of |:mksession| does not include commands for restoring floating diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index f5b9f39d93..04ae9360a0 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -196,6 +196,10 @@ The following new APIs or features were added. • Added an omnifunc implementation for lua, |vim.lua_omnifunc()| +• Treesitter injection queries now use the format described at + https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection . + Support for the previous format will be removed in a future release. + ============================================================================== CHANGED FEATURES *news-changes* diff --git a/runtime/doc/syntax.txt b/runtime/doc/syntax.txt index 68d059be82..b22d673eb9 100644 --- a/runtime/doc/syntax.txt +++ b/runtime/doc/syntax.txt @@ -5268,6 +5268,10 @@ NonText '@' at the end of the window, characters from 'showbreak' Normal Normal text. *hl-NormalFloat* NormalFloat Normal text in floating windows. + *hl-FloatBorder* +FloatBorder Border of floating windows. + *hl-FloatTitle* +FloatTitle Title of floating windows. *hl-NormalNC* NormalNC Normal text in non-current windows. *hl-Pmenu* diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index 1f78e4d5d9..3b0936941f 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -441,7 +441,53 @@ individual query pattern manually by setting its `"priority"` metadata attribute: > (super_important_node) @ImportantHighlight (#set! "priority" 105) + +============================================================================== +TREESITTER LANGUAGE INJECTIONS *treesitter-language-injections* < + +Note the following information is adapted from: + https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection + +Some source files contain code written in multiple different languages. +Examples include: + + • HTML files, which can contain JavaScript inside of `<script>` tags and + CSS inside of `<style>` tags + • ERB files, which contain Ruby inside of `<%` `%>` tags, and HTML outside of + those tags + • PHP files, which can contain HTML between the `<php` tags + • JavaScript files, which contain regular expression syntax within regex + literals + • Ruby, which can contain snippets of code inside of heredoc literals, + where the heredoc delimiter often indicates the language + • Lua, which can contain snippets of Vimscript inside |vim.cmd()| calls. + • Vimscript, which can contain snippets of Lua inside |:lua-heredoc| + blocks. + +All of these examples can be modeled in terms of a parent syntax tree and one +or more injected syntax trees, which reside inside of certain nodes in the +parent tree. The language injection query allows you to specify these +“injections” using the following captures: + + • `@injection.content` - indicates that the captured node should have its + contents re-parsed using another language. + • `@injection.language` - indicates that the captured node’s text may + contain the name of a language that should be used to re-parse the + `@injection.content`. + +The language injection behavior can also be configured by some properties +associated with patterns: + + • `injection.language` - can be used to hard-code the name of a specific + language. + • `injection.combined` - indicates that all of the matching nodes in the + tree should have their content parsed as one nested document. + • `injection.include-children` - indicates that the `@injection.content` + node's entire text should be re-parsed, including the text of its child + nodes. By default, child nodes' text will be excluded from the injected + document. + ============================================================================== VIM.TREESITTER *lua-treesitter* diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index 5efe094226..2305f101e2 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -243,6 +243,8 @@ Highlight groups: |highlight-blend| controls blend level for a highlight group |expr-highlight| highlight groups (prefixed with "Nvim") |hl-NormalFloat| highlights floating window + |hl-FloatBorder| highlights border of a floating window + |hl-FloatTitle| highlights title of a floating window |hl-NormalNC| highlights non-current windows |hl-MsgArea| highlights messages/cmdline area |hl-MsgSeparator| highlights separator for scrolled messages diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 65579d3543..8238fec2cc 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -751,6 +751,7 @@ local extension = { mli = 'ocaml', ml = 'ocaml', occ = 'occam', + odin = 'odin', xom = 'omnimark', xin = 'omnimark', opam = 'opam', @@ -1015,6 +1016,7 @@ local extension = { svh = 'systemverilog', sv = 'systemverilog', tak = 'tak', + tal = 'tal', task = 'taskedit', tm = 'tcl', tcl = 'tcl', diff --git a/runtime/lua/vim/lsp/semantic_tokens.lua b/runtime/lua/vim/lsp/semantic_tokens.lua index 9eaccd539f..a5e007a011 100644 --- a/runtime/lua/vim/lsp/semantic_tokens.lua +++ b/runtime/lua/vim/lsp/semantic_tokens.lua @@ -435,7 +435,7 @@ function STHighlighter:on_win(topline, botline) token.marked = true api.nvim_exec_autocmds('LspTokenUpdate', { - pattern = vim.api.nvim_buf_get_name(self.bufnr), + buffer = self.bufnr, modeline = false, data = { token = token, diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index d13824076e..ab9f8968c8 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -191,7 +191,7 @@ end --- ---@return boolean True if the position is in node range function M.is_in_node_range(node, line, col) - return M.node_contains(node, { line, col, line, col }) + return M.node_contains(node, { line, col, line, col + 1 }) end --- Determines if a node contains a range @@ -202,7 +202,8 @@ end ---@return boolean True if the {node} contains the {range} function M.node_contains(node, range) vim.validate({ - node = { node, 'userdata' }, + -- allow a table so nodes can be mocked + node = { node, { 'userdata', 'table' } }, range = { range, Range.validate, 'integer list with 4 or 6 elements' }, }) return Range.contains({ node:range() }, range) diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 731a5ebf9f..ad0854706b 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -14,7 +14,7 @@ ---@field child_count fun(self: TSNode): integer ---@field named_child_count fun(self: TSNode): integer ---@field child fun(self: TSNode, integer): TSNode ----@field name_child fun(self: TSNode, integer): TSNode +---@field named_child fun(self: TSNode, integer): TSNode ---@field descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ---@field named_descendant_for_range fun(self: TSNode, integer, integer, integer, integer): TSNode ---@field parent fun(self: TSNode): TSNode @@ -43,10 +43,10 @@ function TSNode:_rawquery(query, captures, start, end_) end function TSNode:_rawquery(query, captures, start, end_) end ---@class TSParser ----@field parse fun(self: TSParser, tree, source: integer|string): TSTree, integer[] +---@field parse fun(self: TSParser, tree, source: integer|string): TSTree, Range4[] ---@field reset fun(self: TSParser) ----@field included_ranges fun(self: TSParser): integer[] ----@field set_included_ranges fun(self: TSParser, ranges: integer[][]) +---@field included_ranges fun(self: TSParser): Range4[] +---@field set_included_ranges fun(self: TSParser, ranges: Range6[]) ---@field set_timeout fun(self: TSParser, timeout: integer) ---@field timeout fun(self: TSParser): integer diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b8b0dd867e..fbc602486b 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -399,6 +399,169 @@ local function get_range_from_metadata(node, id, metadata) return { node:range() } end +---@private +--- TODO(lewis6991): cleanup of the node_range interface +---@param node TSNode +---@param id integer +---@param metadata TSMetadata +---@return Range4[] +local function get_node_ranges(node, id, metadata, include_children) + local range = get_range_from_metadata(node, id, metadata) + + if include_children then + return { range } + end + + local ranges = {} ---@type Range4[] + + local srow, scol, erow, ecol = range[1], range[2], range[3], range[4] + + for i = 0, node:named_child_count() - 1 do + local child = node:named_child(i) + local child_srow, child_scol, child_erow, child_ecol = child:range() + if child_srow > srow or child_scol > scol then + table.insert(ranges, { srow, scol, child_srow, child_scol }) + end + srow = child_erow + scol = child_ecol + end + + if erow > srow or ecol > scol then + table.insert(ranges, { srow, scol, erow, ecol }) + end + + return ranges +end + +---@alias TSInjection table<string,table<integer,table>> + +---@private +---@param t table<integer,TSInjection> +---@param tree_index integer +---@param pattern integer +---@param lang string +---@param combined boolean +---@param ranges Range4[] +local function add_injection(t, tree_index, pattern, lang, combined, ranges) + assert(type(lang) == 'string') + + -- Each tree index should be isolated from the other nodes. + if not t[tree_index] then + t[tree_index] = {} + end + + if not t[tree_index][lang] then + t[tree_index][lang] = {} + end + + -- Key this by pattern. If combined is set to true all captures of this pattern + -- will be parsed by treesitter as the same "source". + -- If combined is false, each "region" will be parsed as a single source. + if not t[tree_index][lang][pattern] then + t[tree_index][lang][pattern] = { combined = combined, regions = {} } + end + + table.insert(t[tree_index][lang][pattern].regions, ranges) +end + +---@private +---Get node text +--- +---Note: `query.get_node_text` returns string|string[]|nil so use this simple alias function +---to annotate it returns string. +--- +---TODO(lewis6991): use [at]overload annotations on `query.get_node_text` +---@param node TSNode +---@param source integer|string +---@param metadata table +---@return string +local function get_node_text(node, source, metadata) + return query.get_node_text(node, source, { metadata = metadata }) --[[@as string]] +end + +---@private +--- Extract injections according to: +--- https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection +---@param match table<integer,TSNode> +---@param metadata table +---@return string, boolean, Range4[] +function LanguageTree:_get_injection(match, metadata) + local ranges = {} ---@type Range4[] + local combined = metadata['injection.combined'] ~= nil + local lang = metadata['injection.language'] ---@type string + local include_children = metadata['injection.include-children'] ~= nil + + for id, node in pairs(match) do + local name = self._injection_query.captures[id] + + -- Lang should override any other language tag + if name == 'injection.language' then + lang = get_node_text(node, self._source, metadata[id]) + elseif name == 'injection.content' then + ranges = get_node_ranges(node, id, metadata, include_children) + end + end + + return lang, combined, ranges +end + +---@private +---@param match table<integer,TSNode> +---@param metadata table +---@return string, boolean, Range4[] +function LanguageTree:_get_injection_deprecated(match, metadata) + local lang = nil ---@type string + local ranges = {} ---@type Range4[] + local combined = metadata.combined ~= nil + + -- Directives can configure how injections are captured as well as actual node captures. + -- This allows more advanced processing for determining ranges and language resolution. + if metadata.content then + local content = metadata.content ---@type any + + -- Allow for captured nodes to be used + if type(content) == 'number' then + content = { match[content]:range() } + end + + if type(content) == 'table' and #content >= 4 then + vim.list_extend(ranges, content) + end + end + + if metadata.language then + lang = metadata.language ---@type string + end + + -- 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] + + -- Lang should override any other language tag + if name == 'language' and not lang then + lang = get_node_text(node, self._source, metadata[id]) + elseif name == 'combined' then + combined = true + elseif name == 'content' and #ranges == 0 then + table.insert(ranges, get_range_from_metadata(node, id, metadata)) + -- Ignore any tags that start with "_" + -- Allows for other tags to be used in matches + elseif string.sub(name, 1, 1) ~= '_' then + if not lang then + lang = name + end + + if #ranges == 0 then + table.insert(ranges, get_range_from_metadata(node, id, metadata)) + end + end + end + + return lang, combined, ranges +end + --- Gets language injection points by language. --- --- This is where most of the injection processing occurs. @@ -406,13 +569,13 @@ end --- TODO: Allow for an offset predicate to tailor the injection range --- instead of using the entire nodes range. ---@private ----@return table<string, integer[][]> +---@return table<string, Range4[][]> function LanguageTree:_get_injections() if not self._injection_query then return {} end - ---@type table<integer,table<string,table<integer,table>>> + ---@type table<integer,TSInjection> local injections = {} for tree_index, tree in ipairs(self._trees) do @@ -422,75 +585,12 @@ function LanguageTree:_get_injections() for pattern, match, metadata in self._injection_query:iter_matches(root_node, self._source, start_line, end_line + 1) do - local lang = nil ---@type string - local ranges = {} ---@type Range4[] - local combined = metadata.combined ---@type boolean - - -- Directives can configure how injections are captured as well as actual node captures. - -- This allows more advanced processing for determining ranges and language resolution. - if metadata.content then - local content = metadata.content ---@type any - - -- Allow for captured nodes to be used - if type(content) == 'number' then - content = { match[content]:range() } - end - - if type(content) == 'table' and #content >= 4 then - vim.list_extend(ranges, content) - end - end - - if metadata.language then - lang = metadata.language ---@type string - end - - -- 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] - - -- Lang should override any other language tag - if name == 'language' and not lang then - ---@diagnostic disable-next-line - lang = query.get_node_text(node, self._source, { metadata = metadata[id] }) - elseif name == 'combined' then - combined = true - elseif name == 'content' and #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) - -- Ignore any tags that start with "_" - -- Allows for other tags to be used in matches - elseif string.sub(name, 1, 1) ~= '_' then - if not lang then - lang = name - end - - if #ranges == 0 then - table.insert(ranges, get_range_from_metadata(node, id, metadata)) - end - end + local lang, combined, ranges = self:_get_injection(match, metadata) + if not lang then + -- TODO(lewis6991): remove after 0.9 (#20434) + lang, combined, ranges = self:_get_injection_deprecated(match, metadata) end - - assert(type(lang) == 'string') - - -- 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 this by pattern. If combined is set to true all captures of this pattern - -- will be parsed by treesitter as the same "source". - -- If combined is false, each "region" will be parsed as a single source. - if not injections[tree_index][lang][pattern] then - injections[tree_index][lang][pattern] = { combined = combined, regions = {} } - end - - table.insert(injections[tree_index][lang][pattern].regions, ranges) + add_injection(injections, tree_index, pattern, lang, combined, ranges) end end diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua index 22f706585e..59894cc7f5 100644 --- a/runtime/lua/vim/treesitter/query.lua +++ b/runtime/lua/vim/treesitter/query.lua @@ -407,7 +407,7 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@field [string] integer|string ---@field range Range4 ----@alias TSDirective fun(match: TSMatch, _, _, predicate: any[], metadata: TSMetadata) +---@alias TSDirective fun(match: TSMatch, _, _, predicate: (string|integer)[], metadata: TSMetadata) -- Predicate handler receive the following arguments -- (match, pattern, bufnr, predicate) @@ -419,24 +419,17 @@ predicate_handlers['vim-match?'] = predicate_handlers['match?'] ---@type table<string,TSDirective> local directive_handlers = { ['set!'] = function(_, _, _, pred, metadata) - if #pred == 4 then - -- (#set! @capture "key" "value") - ---@diagnostic disable-next-line:no-unknown - local _, capture_id, key, value = unpack(pred) - ---@cast value integer|string - ---@cast capture_id integer - ---@cast key string + if #pred >= 3 and type(pred[2]) == 'number' then + -- (#set! @capture key value) + local capture_id, key, value = pred[2], pred[3], pred[4] if not metadata[capture_id] then metadata[capture_id] = {} end metadata[capture_id][key] = value else - ---@diagnostic disable-next-line:no-unknown - local _, key, value = unpack(pred) - ---@cast value integer|string - ---@cast key string - -- (#set! "key" "value") - metadata[key] = value + -- (#set! key value) + local key, value = pred[2], pred[3] + metadata[key] = value or true end end, -- Shifts the range of a node. diff --git a/runtime/queries/c/injections.scm b/runtime/queries/c/injections.scm index 7e9e73449d..84ae33683e 100644 --- a/runtime/queries/c/injections.scm +++ b/runtime/queries/c/injections.scm @@ -1,3 +1,5 @@ -(preproc_arg) @c +((preproc_arg) @injection.content + (#set! injection.language "c")) -; (comment) @comment +; ((comment) @injection.content +; (#set! injection.language "comment")) diff --git a/runtime/queries/help/injections.scm b/runtime/queries/help/injections.scm index 09bbe44e84..260a05d863 100644 --- a/runtime/queries/help/injections.scm +++ b/runtime/queries/help/injections.scm @@ -1,3 +1,4 @@ -(codeblock - (language) @language - (code) @content) +((codeblock + (language) @injection.language + (code) @injection.content) + (#set! injection.include-children)) diff --git a/runtime/queries/lua/injections.scm b/runtime/queries/lua/injections.scm index 69acbbbe9f..10eb4c4054 100644 --- a/runtime/queries/lua/injections.scm +++ b/runtime/queries/lua/injections.scm @@ -3,20 +3,26 @@ (identifier) @_cdef_identifier (_ _ (identifier) @_cdef_identifier) ] - arguments: (arguments (string content: _ @c))) + arguments: (arguments (string content: _ @injection.content))) + (#set! injection.language "c") (#eq? @_cdef_identifier "cdef")) ((function_call name: (_) @_vimcmd_identifier - arguments: (arguments (string content: _ @vim))) + arguments: (arguments (string content: _ @injection.content))) + (#set! injection.language "vim") (#any-of? @_vimcmd_identifier "vim.cmd" "vim.api.nvim_command" "vim.api.nvim_exec" "vim.api.nvim_cmd")) ((function_call name: (_) @_vimcmd_identifier - arguments: (arguments (string content: _ @query) .)) + arguments: (arguments (string content: _ @injection.content) .)) + (#set! injection.language "query") (#eq? @_vimcmd_identifier "vim.treesitter.query.set_query")) ; ;; highlight string as query if starts with `;; query` -; ((string ("string_content") @query) (#lua-match? @query "^%s*;+%s?query")) +; ((string ("string_content") @injection.content) +; (#set! injection.language "query") +; (#lua-match? @injection.content "^%s*;+%s?query")) -; (comment) @comment +; ((comment) @injection.content +; (#set! injection.language "comment")) diff --git a/runtime/queries/vim/injections.scm b/runtime/queries/vim/injections.scm index b53643dc1a..50f0190112 100644 --- a/runtime/queries/vim/injections.scm +++ b/runtime/queries/vim/injections.scm @@ -1,18 +1,33 @@ -(lua_statement (script (body) @lua)) -(lua_statement (chunk) @lua) -(ruby_statement (script (body) @ruby)) -(ruby_statement (chunk) @ruby) -(python_statement (script (body) @python)) -(python_statement (chunk) @python) +((lua_statement (script (body) @injection.content)) + (#set! injection.language "lua")) + +((lua_statement (chunk) @injection.content) + (#set! injection.language "lua")) + +((ruby_statement (script (body) @injection.content)) + (#set! injection.language "ruby")) + +((ruby_statement (chunk) @injection.content) + (#set! injection.language "ruby")) + +((python_statement (script (body) @injection.content)) + (#set! injection.language "python")) + +((python_statement (chunk) @injection.content) + (#set! injection.language "python")) + ;; If we support perl at some point... -;; (perl_statement (script (body) @perl)) -;; (perl_statement (chunk) @perl) +;; ((perl_statement (script (body) @injection.content)) +;; (#set! injection.language "perl")) +;; ((perl_statement (chunk) @injection.content) +;; (#set! injection.language "perl")) -(autocmd_statement (pattern) @regex) +((autocmd_statement (pattern) @injection.content) + (#set! injection.language "regex")) ((set_item option: (option_name) @_option - value: (set_value) @vim) + value: (set_value) @injection.content) (#any-of? @_option "includeexpr" "inex" "printexpr" "pexpr" @@ -22,7 +37,12 @@ "foldexpr" "fde" "diffexpr" "dex" "patchexpr" "pex" - "charconvert" "ccv")) + "charconvert" "ccv") + (#set! injection.language "vim")) + + +; ((comment) @injection.content +; (#set! injection.language "comment")) -; (comment) @comment -; (line_continuation_comment) @comment +; ((line_continuation_comment) @injection.content +; (#set! injection.language "comment")) diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh index 06ddf0f3b8..20e337afcb 100755 --- a/scripts/vim-patch.sh +++ b/scripts/vim-patch.sh @@ -598,6 +598,7 @@ _set_missing_vimpatches() { # Massage arguments for git-log. declare -A git_log_replacements=( [^\(.*/\)?src/nvim/\(.*\)]="\${BASH_REMATCH[1]}src/\${BASH_REMATCH[2]}" + [^\(.*/\)?test/old/\(.*\)]="\${BASH_REMATCH[1]}src/\${BASH_REMATCH[2]}" [^\(.*/\)?\.vim-src/\(.*\)]="\${BASH_REMATCH[2]}" ) local i j diff --git a/src/nvim/api/private/helpers.h b/src/nvim/api/private/helpers.h index b70452d7cb..1b82aeac34 100644 --- a/src/nvim/api/private/helpers.h +++ b/src/nvim/api/private/helpers.h @@ -155,8 +155,18 @@ typedef struct { msglist_T *private_msg_list; \ msg_list = &private_msg_list; \ private_msg_list = NULL; \ - code \ - msg_list = saved_msg_list; /* Restore the exception context. */ \ + code; \ + msg_list = saved_msg_list; /* Restore the exception context. */ \ + } while (0) + +// Execute code with cursor position saved and restored and textlock active. +#define TEXTLOCK_WRAP(code) \ + do { \ + const pos_T save_cursor = curwin->w_cursor; \ + textlock++; \ + code; \ + textlock--; \ + curwin->w_cursor = save_cursor; \ } while (0) // Useful macro for executing some `code` for each item in an array. diff --git a/src/nvim/buffer_updates.c b/src/nvim/buffer_updates.c index 075ac2adbf..9543731c9b 100644 --- a/src/nvim/buffer_updates.c +++ b/src/nvim/buffer_updates.c @@ -188,9 +188,9 @@ void buf_updates_unload(buf_T *buf, bool can_reload) // the first argument is always the buffer handle args.items[0] = BUFFER_OBJ(buf->handle); - textlock++; - nlua_call_ref(thecb, keep ? "reload" : "detach", args, false, NULL); - textlock--; + TEXTLOCK_WRAP({ + nlua_call_ref(thecb, keep ? "reload" : "detach", args, false, NULL); + }); } if (keep) { @@ -305,9 +305,11 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added, args.items[6] = INTEGER_OBJ((Integer)deleted_codepoints); args.items[7] = INTEGER_OBJ((Integer)deleted_codeunits); } - textlock++; - Object res = nlua_call_ref(cb.on_lines, "lines", args, false, NULL); - textlock--; + + Object res; + TEXTLOCK_WRAP({ + res = nlua_call_ref(cb.on_lines, "lines", args, false, NULL); + }); if (res.type == kObjectTypeBoolean && res.data.boolean == true) { buffer_update_callbacks_free(cb); @@ -354,9 +356,10 @@ void buf_updates_send_splice(buf_T *buf, int start_row, colnr_T start_col, bcoun ADD_C(args, INTEGER_OBJ(new_col)); ADD_C(args, INTEGER_OBJ(new_byte)); - textlock++; - Object res = nlua_call_ref(cb.on_bytes, "bytes", args, false, NULL); - textlock--; + Object res; + TEXTLOCK_WRAP({ + res = nlua_call_ref(cb.on_bytes, "bytes", args, false, NULL); + }); if (res.type == kObjectTypeBoolean && res.data.boolean == true) { buffer_update_callbacks_free(cb); @@ -389,10 +392,10 @@ void buf_updates_changedtick(buf_T *buf) // next argument is b:changedtick ADD_C(args, INTEGER_OBJ(buf_get_changedtick(buf))); - textlock++; - Object res = nlua_call_ref(cb.on_changedtick, "changedtick", - args, false, NULL); - textlock--; + Object res; + TEXTLOCK_WRAP({ + res = nlua_call_ref(cb.on_changedtick, "changedtick", args, false, NULL); + }); if (res.type == kObjectTypeBoolean && res.data.boolean == true) { buffer_update_callbacks_free(cb); diff --git a/src/nvim/eval.c b/src/nvim/eval.c index cbad5d04ff..9b42375120 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -2906,6 +2906,12 @@ static int eval7(char **arg, typval_T *rettv, int evaluate, int want_string) case '8': case '9': ret = get_number_tv(arg, rettv, evaluate, want_string); + + // Apply prefixed "-" and "+" now. Matters especially when + // "->" follows. + if (ret == OK && evaluate && end_leader > start_leader) { + ret = eval7_leader(rettv, true, start_leader, &end_leader); + } break; // String constant: "string". @@ -3011,13 +3017,12 @@ static int eval7(char **arg, typval_T *rettv, int evaluate, int want_string) // Handle following '[', '(' and '.' for expr[expr], expr.name, // expr(expr), expr->name(expr) if (ret == OK) { - ret = handle_subscript((const char **)arg, rettv, evaluate, true, - (char *)start_leader, &end_leader); + ret = handle_subscript((const char **)arg, rettv, evaluate, true); } // Apply logical NOT and unary '-', from right to left, ignore '+'. if (ret == OK && evaluate && end_leader > start_leader) { - ret = eval7_leader(rettv, (char *)start_leader, &end_leader); + ret = eval7_leader(rettv, false, start_leader, &end_leader); } recurse--; @@ -3027,9 +3032,11 @@ static int eval7(char **arg, typval_T *rettv, int evaluate, int want_string) /// Apply the leading "!" and "-" before an eval7 expression to "rettv". /// Adjusts "end_leaderp" until it is at "start_leader". /// +/// @param numeric_only if true only handle "+" and "-". +/// /// @return OK on success, FAIL on failure. -static int eval7_leader(typval_T *const rettv, const char *const start_leader, - const char **const end_leaderp) +static int eval7_leader(typval_T *const rettv, const bool numeric_only, + const char *const start_leader, const char **const end_leaderp) FUNC_ATTR_NONNULL_ALL { const char *end_leader = (char *)(*end_leaderp); @@ -3050,6 +3057,10 @@ static int eval7_leader(typval_T *const rettv, const char *const start_leader, while (end_leader > start_leader) { end_leader--; if (*end_leader == '!') { + if (numeric_only) { + end_leader++; + break; + } if (rettv->v_type == VAR_FLOAT) { f = !(bool)f; } else { @@ -6899,8 +6910,7 @@ int check_luafunc_name(const char *const str, const bool paren) /// @param verbose give error messages /// @param start_leader start of '!' and '-' prefixes /// @param end_leaderp end of '!' and '-' prefixes -int handle_subscript(const char **const arg, typval_T *rettv, int evaluate, int verbose, - const char *const start_leader, const char **const end_leaderp) +int handle_subscript(const char **const arg, typval_T *rettv, int evaluate, int verbose) { int ret = OK; dict_T *selfdict = NULL; @@ -6944,11 +6954,6 @@ int handle_subscript(const char **const arg, typval_T *rettv, int evaluate, int tv_dict_unref(selfdict); selfdict = NULL; } else if (**arg == '-') { - // Expression "-1.0->method()" applies the leader "-" before - // applying ->. - if (evaluate && *end_leaderp > start_leader) { - ret = eval7_leader(rettv, (char *)start_leader, end_leaderp); - } if (ret == OK) { if ((*arg)[2] == '{') { // expr->{lambda}() diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index 3c4cc34464..13779f4173 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -3075,8 +3075,7 @@ void ex_call(exarg_T *eap) } // Handle a function returning a Funcref, Dictionary or List. - if (handle_subscript((const char **)&arg, &rettv, true, true, - (const char *)name, (const char **)&name) + if (handle_subscript((const char **)&arg, &rettv, true, true) == FAIL) { failed = true; break; diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index d80bdc70f6..ed2453bd59 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -508,7 +508,7 @@ static const char *list_arg_vars(exarg_T *eap, const char *arg, int *first) } else { // handle d.key, l[idx], f(expr) const char *const arg_subsc = arg; - if (handle_subscript(&arg, &tv, true, true, name, &name) == FAIL) { + if (handle_subscript(&arg, &tv, true, true) == FAIL) { error = true; } else { if (arg == arg_subsc && len == 2 && name[1] == ':') { @@ -1715,7 +1715,7 @@ bool var_exists(const char *var) n = get_var_tv(name, len, &tv, NULL, false, true) == OK; if (n) { // Handle d.key, l[idx], f(expr). - n = handle_subscript(&var, &tv, true, false, name, &name) == OK; + n = handle_subscript(&var, &tv, true, false) == OK; if (n) { tv_clear(&tv); } diff --git a/src/nvim/getchar.c b/src/nvim/getchar.c index 183fd5e19f..ea541dbca4 100644 --- a/src/nvim/getchar.c +++ b/src/nvim/getchar.c @@ -2345,7 +2345,7 @@ void check_end_reg_executing(bool advance) /// K_SPECIAL may be escaped, need to get two more bytes then. static int vgetorpeek(bool advance) { - int c, c1; + int c; bool timedout = false; // waited for more than 'timeoutlen' // for mapping to complete or // 'ttimeoutlen' for complete key code @@ -2639,7 +2639,7 @@ static int vgetorpeek(bool advance) // input from the user), show the partially matched characters // to the user with showcmd. int showcmd_idx = 0; - c1 = 0; + bool showing_partial = false; if (typebuf.tb_len > 0 && advance && !exmode_active) { if (((State & (MODE_NORMAL | MODE_INSERT)) || State == MODE_LANGMAP) && State != MODE_HITRETURN) { @@ -2648,7 +2648,7 @@ static int vgetorpeek(bool advance) && ptr2cells((char *)typebuf.tb_buf + typebuf.tb_off + typebuf.tb_len - 1) == 1) { edit_putchar(typebuf.tb_buf[typebuf.tb_off + typebuf.tb_len - 1], false); setcursor(); // put cursor back where it belongs - c1 = 1; + showing_partial = true; } // need to use the col and row from above here old_wcol = curwin->w_wcol; @@ -2666,12 +2666,15 @@ static int vgetorpeek(bool advance) curwin->w_wrow = old_wrow; } - // this looks nice when typing a dead character map - if ((State & MODE_CMDLINE) && cmdline_star == 0) { + // This looks nice when typing a dead character map. + // There is no actual command line for get_number(). + if ((State & MODE_CMDLINE) + && get_cmdline_info()->cmdbuff != NULL + && cmdline_star == 0) { char *p = (char *)typebuf.tb_buf + typebuf.tb_off + typebuf.tb_len - 1; if (ptr2cells(p) == 1 && (uint8_t)(*p) < 128) { putcmdline(*p, false); - c1 = 1; + showing_partial = true; } } } @@ -2704,11 +2707,12 @@ static int vgetorpeek(bool advance) if (showcmd_idx != 0) { pop_showcmd(); } - if (c1 == 1) { + if (showing_partial == 1) { if (State & MODE_INSERT) { edit_unputchar(); } - if (State & MODE_CMDLINE) { + if ((State & MODE_CMDLINE) + && get_cmdline_info()->cmdbuff != NULL) { unputcmdline(); } else { setcursor(); // put cursor back where it belongs diff --git a/test/functional/lua/buffer_updates_spec.lua b/test/functional/lua/buffer_updates_spec.lua index 2fd44b8b5f..b1b39501f7 100644 --- a/test/functional/lua/buffer_updates_spec.lua +++ b/test/functional/lua/buffer_updates_spec.lua @@ -317,7 +317,18 @@ describe('lua buffer event callbacks: on_lines', function() feed('1G0') feed('P') eq(meths.get_var('linesev'), { "lines", 1, 6, 0, 3, 3, 9 }) + end) + it('calling nvim_buf_call() from callback does not cause Normal mode CTRL-A to misbehave #16729', function() + exec_lua([[ + vim.api.nvim_buf_attach(0, false, { + on_lines = function(...) + vim.api.nvim_buf_call(0, function() end) + end, + }) + ]]) + feed('itest123<Esc><C-A>') + eq('test124', meths.get_current_line()) end) end) diff --git a/test/functional/treesitter/utils_spec.lua b/test/functional/treesitter/utils_spec.lua index 7f5a864c3d..9c07959098 100644 --- a/test/functional/treesitter/utils_spec.lua +++ b/test/functional/treesitter/utils_spec.lua @@ -28,4 +28,21 @@ describe('treesitter utils', function() eq(true, exec_lua('return vim.treesitter.is_ancestor(ancestor, child)')) eq(false, exec_lua('return vim.treesitter.is_ancestor(child, ancestor)')) end) + + it('can detect if a position is contained in a node', function() + exec_lua([[ + node = { + range = function() + return 0, 4, 0, 8 + end, + } + ]]) + + eq(false, exec_lua('return vim.treesitter.is_in_node_range(node, 0, 3)')) + for i = 4, 7 do + eq(true, exec_lua('return vim.treesitter.is_in_node_range(node, 0, ...)', i)) + end + -- End column exclusive + eq(false, exec_lua('return vim.treesitter.is_in_node_range(node, 0, 8)')) + end) end) diff --git a/test/old/testdir/samples/memfile_test.c b/test/old/memfile_test.c index 73f67fb250..41316bcc2d 100644 --- a/test/old/testdir/samples/memfile_test.c +++ b/test/old/memfile_test.c @@ -1,8 +1,6 @@ // This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com -// uncrustify:off - /* vi:set ts=8 sts=4 sw=4 noet: * * VIM - Vi IMproved by Bram Moolenaar @@ -20,11 +18,11 @@ #undef NDEBUG #include <assert.h> -/* Must include main.c because it contains much more than just main() */ +// Must include main.c because it contains much more than just main() #define NO_VIM_MAIN #include "main.c" -/* This file has to be included because the tested functions are static */ +// This file has to be included because the tested functions are static #include "memfile.c" #define index_to_key(i) ((i) ^ 15167) @@ -39,26 +37,26 @@ test_mf_hash(void) mf_hashtab_T ht; mf_hashitem_T *item; blocknr_T key; - size_t i; - size_t num_buckets; + long_u i; + long_u num_buckets; mf_hash_init(&ht); - /* insert some items and check invariants */ + // insert some items and check invariants for (i = 0; i < TEST_COUNT; i++) { assert(ht.mht_count == i); - /* check that number of buckets is a power of 2 */ + // check that number of buckets is a power of 2 num_buckets = ht.mht_mask + 1; assert(num_buckets > 0 && (num_buckets & (num_buckets - 1)) == 0); - /* check load factor */ + // check load factor assert(ht.mht_count <= (num_buckets << MHT_LOG_LOAD_FACTOR)); if (i < (MHT_INIT_SIZE << MHT_LOG_LOAD_FACTOR)) { - /* first expansion shouldn't have occurred yet */ + // first expansion shouldn't have occurred yet assert(num_buckets == MHT_INIT_SIZE); assert(ht.mht_buckets == ht.mht_small_buckets); } @@ -71,8 +69,8 @@ test_mf_hash(void) key = index_to_key(i); assert(mf_hash_find(&ht, key) == NULL); - /* allocate and add new item */ - item = (mf_hashitem_T *)lalloc_clear(sizeof(*item), FALSE); + // allocate and add new item + item = LALLOC_CLEAR_ONE(mf_hashitem_T); assert(item != NULL); item->mhi_key = key; mf_hash_add_item(&ht, item); @@ -81,13 +79,13 @@ test_mf_hash(void) if (ht.mht_mask + 1 != num_buckets) { - /* hash table was expanded */ + // hash table was expanded assert(ht.mht_mask + 1 == num_buckets * MHT_GROWTH_FACTOR); assert(i + 1 == (num_buckets << MHT_LOG_LOAD_FACTOR)); } } - /* check presence of inserted items */ + // check presence of inserted items for (i = 0; i < TEST_COUNT; i++) { key = index_to_key(i); @@ -96,7 +94,7 @@ test_mf_hash(void) assert(item->mhi_key == key); } - /* delete some items */ + // delete some items for (i = 0; i < TEST_COUNT; i++) { if (i % 100 < 70) @@ -119,7 +117,7 @@ test_mf_hash(void) } } - /* check again */ + // check again for (i = 0; i < TEST_COUNT; i++) { key = index_to_key(i); @@ -136,7 +134,7 @@ test_mf_hash(void) } } - /* free hash table and all remaining items */ + // free hash table and all remaining items mf_hash_free_all(&ht); } diff --git a/test/old/testdir/test_ex_mode.vim b/test/old/testdir/test_ex_mode.vim index 93100732ed..b30dce8630 100644 --- a/test/old/testdir/test_ex_mode.vim +++ b/test/old/testdir/test_ex_mode.vim @@ -123,6 +123,42 @@ func Test_open_command() close! endfunc +func Test_open_command_flush_line() + throw 'Skipped: Nvim does not have :open' + " this was accessing freed memory: the regexp match uses a pointer to the + " current line which becomes invalid when searching for the ') mark. + new + call setline(1, ['one', 'two. three']) + s/one/ONE + try + open /\%')/ + catch /E479/ + endtry + bwipe! +endfunc + +" FIXME: this doesn't fail without the fix but hangs +func Skip_Test_open_command_state() + " Tricky script that failed because State was not set properly + let lines =<< trim END + !ls + 0scìi + so! Xsourced + set t_û0=0 + v/-/o + END + call writefile(lines, 'XopenScript', '') + + let sourced = ["!f\u0083\x02\<Esc>z=0"] + call writefile(sourced, 'Xsourced', 'b') + + CheckRunVimInTerminal + let buf = RunVimInTerminal('-u NONE -i NONE -n -m -X -Z -e -s -S XopenScript -c qa!', #{rows: 6, wait_for_ruler: 0, no_clean: 1}) + sleep 3 + + call StopVimInTerminal(buf) +endfunc + " Test for :g/pat/visual to run vi commands in Ex mode " This used to hang Vim before 8.2.0274. func Test_Ex_global() diff --git a/test/old/testdir/test_expr.vim b/test/old/testdir/test_expr.vim index 47f7f5eb0e..292a504df9 100644 --- a/test/old/testdir/test_expr.vim +++ b/test/old/testdir/test_expr.vim @@ -111,6 +111,13 @@ func Test_special_char() call assert_fails('echo "\<C-">') endfunc +func Test_method_with_prefix() + call assert_equal(1, !range(5)->empty()) + call assert_equal([0, 1, 2], --3->range()) + call assert_equal(0, !-3) + call assert_equal(1, !+-+0) +endfunc + func Test_option_value() " boolean set bri diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index eb00cb98ca..cb16200afc 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -414,6 +414,7 @@ let s:filename_checks = { \ 'ocaml': ['file.ml', 'file.mli', 'file.mll', 'file.mly', '.ocamlinit', 'file.mlt', 'file.mlp', 'file.mlip', 'file.mli.cppo', 'file.ml.cppo'], \ 'occam': ['file.occ'], \ 'octave': ['octaverc', '.octaverc', 'octave.conf'], + \ 'odin': ['file.odin'], \ 'omnimark': ['file.xom', 'file.xin'], \ 'opam': ['opam', 'file.opam', 'file.opam.template'], \ 'openroad': ['file.or'], @@ -581,6 +582,7 @@ let s:filename_checks = { \ 'systemverilog': ['file.sv', 'file.svh'], \ 'tags': ['tags'], \ 'tak': ['file.tak'], + \ 'tal': ['file.tal'], \ 'taskdata': ['pending.data', 'completed.data', 'undo.data'], \ 'taskedit': ['file.task'], \ 'tcl': ['file.tcl', 'file.tm', 'file.tk', 'file.itcl', 'file.itk', 'file.jacl', '.tclshrc', 'tclsh.rc', '.wishrc'], diff --git a/test/old/testdir/test_syntax.vim b/test/old/testdir/test_syntax.vim index 45230c4208..886c23efa7 100644 --- a/test/old/testdir/test_syntax.vim +++ b/test/old/testdir/test_syntax.vim @@ -116,7 +116,7 @@ func Test_syntime() let a = execute('syntime clear') call assert_equal("\nNo Syntax items defined for this buffer", a) - view samples/memfile_test.c + view ../memfile_test.c setfiletype cpp redraw let a = execute('syntime report') |