diff options
61 files changed, 1224 insertions, 348 deletions
diff --git a/runtime/autoload/netrw.vim b/runtime/autoload/netrw.vim index be170d8aec..2dd5cf45bf 100644 --- a/runtime/autoload/netrw.vim +++ b/runtime/autoload/netrw.vim @@ -2951,7 +2951,7 @@ fun! s:NetrwGetFile(readcmd, tfile, method) " to process this detection correctly. " call Decho("detect filetype of local version of remote file<".rfile.">",'~'.expand("<slnum>")) " call Decho("..did_filetype()=".did_filetype()) - setl ft= +" setl ft= " call Decho("..initial filetype<".&ft."> for buf#".bufnr()."<".bufname().">") let iskkeep= &isk setl isk-=/ diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index ffd90ec3d7..f04fe9bb87 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -3177,6 +3177,8 @@ nvim_open_win({buffer}, {enter}, {*config}) *nvim_open_win()* • noautocmd: If true then no buffer-related autocommand events such as |BufEnter|, |BufLeave| or |BufWinEnter| may fire from calling this function. + • fixed: If true when anchor is NW or SW, the float window + would be kept fixed even if the window would be truncated. Return: ~ Window handle, or 0 on error diff --git a/runtime/doc/change.txt b/runtime/doc/change.txt index 894557818b..2c47421b02 100644 --- a/runtime/doc/change.txt +++ b/runtime/doc/change.txt @@ -196,6 +196,7 @@ gR Enter Virtual Replace mode: Each character you type *v_r* {Visual}r{char} Replace all selected characters by {char}. + CTRL-C will be inserted literally. *v_C* {Visual}["x]C Delete the highlighted lines [into register x] and diff --git a/runtime/doc/deprecated.txt b/runtime/doc/deprecated.txt index 2e9312cf74..407d7ae9fb 100644 --- a/runtime/doc/deprecated.txt +++ b/runtime/doc/deprecated.txt @@ -141,6 +141,7 @@ LSP FUNCTIONS `progress` of |vim.lsp.client| - *vim.lsp.get_active_clients()* Use |vim.lsp.get_clients()| - *vim.lsp.for_each_buffer_client()* Use |vim.lsp.get_clients()| +- *vim.lsp.util.trim_empty_lines()* Use |vim.split()| with `trimempty` instead. TREESITTER FUNCTIONS - *vim.treesitter.language.require_language()* Use |vim.treesitter.language.add()| diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 5103cc223f..15292cd7cf 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -1678,20 +1678,24 @@ convert_input_to_markdown_lines({input}, {contents}) window for `textDocument/hover`, for parsing the result of `textDocument/signatureHelp`, and potentially others. + Note that if the input is of type `MarkupContent` and its kind is + `plaintext`, then the corresponding value is returned without further + modifications. + Parameters: ~ • {input} (`MarkedString` | `MarkedString[]` | `MarkupContent`) • {contents} (table|nil) List of strings to extend with converted lines. Defaults to {}. Return: ~ - (table) {contents} extended with lines of converted markdown. + string[] extended with lines of converted markdown. See also: ~ • https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover *vim.lsp.util.convert_signature_help_to_markdown_lines()* convert_signature_help_to_markdown_lines({signature_help}, {ft}, {triggers}) - Converts `textDocument/SignatureHelp` response to markdown lines. + Converts `textDocument/signatureHelp` response to markdown lines. Parameters: ~ • {signature_help} (table) Response of `textDocument/SignatureHelp` @@ -1908,10 +1912,6 @@ open_floating_preview({contents}, {syntax}, {opts}) height when wrap is enabled • max_width: (integer) maximal width of floating window • max_height: (integer) maximal height of floating window - • pad_top: (integer) number of lines to pad contents at - top - • pad_bottom: (integer) number of lines to pad contents at - bottom • focus_id: (string) if a popup with this id is opened, then focus it • close_events: (table) list of events that closes the @@ -2005,8 +2005,6 @@ stylize_markdown({bufnr}, {contents}, {opts}) • wrap_at character to wrap at for computing height • max_width maximal width of floating window • max_height maximal height of floating window - • pad_top number of lines to pad contents at top - • pad_bottom number of lines to pad contents at bottom • separator insert separator after code block Return: ~ @@ -2035,15 +2033,6 @@ text_document_completion_list_to_complete_items({result}, {prefix}) See also: ~ • complete-items -trim_empty_lines({lines}) *vim.lsp.util.trim_empty_lines()* - Removes empty lines from the beginning and end. - - Parameters: ~ - • {lines} (table) list of lines to trim - - Return: ~ - (table) trimmed list of lines - *vim.lsp.util.try_trim_markdown_code_blocks()* try_trim_markdown_code_blocks({lines}) Accepts markdown lines and tries to reduce them to a filetype if they diff --git a/runtime/doc/motion.txt b/runtime/doc/motion.txt index 05244cde91..dc92601bfc 100644 --- a/runtime/doc/motion.txt +++ b/runtime/doc/motion.txt @@ -660,6 +660,7 @@ i` *v_i`* *i`* Special case: With a count of 2 the quotes are included, but no extra white space as with a"/a'/a`. + *o_object-select* When used after an operator: For non-block objects: For the "a" commands: The operator applies to the object and the white @@ -675,6 +676,7 @@ For a block object: the surrounding braces are excluded. For the "a" commands, the braces are included. + *v_object-select* When used in Visual mode: When start and end of the Visual area are the same (just after typing "v"): One object is selected, the same as for using an operator. diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 7c971097fb..240eb152d7 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -232,6 +232,11 @@ The following changes to existing APIs or features add new behavior. In addition, |nvim_buf_get_extmarks()| has gained an "overlap" option to return such ranges even if they started before the specified position. +• LSP hover and signature help now use Treesitter for highlighting of Markdown + content. + Note that syntax highlighting of code examples requires a matching parser + and may be affected by custom queries. + ============================================================================== REMOVED FEATURES *news-removed* @@ -267,6 +272,7 @@ release. - |vim.lsp.util.get_progress_messages()| Use |vim.lsp.status()| instead. - |vim.lsp.get_active_clients()| Use |vim.lsp.get_clients()| instead. - |vim.lsp.for_each_buffer_client()| Use |vim.lsp.get_clients()| instead. + - |vim.lsp.util.trim_empty_lines()| Use |vim.split()| with `trimempty` instead. • `vim.loop` has been renamed to `vim.uv`. diff --git a/runtime/doc/treesitter.txt b/runtime/doc/treesitter.txt index e1e280c4db..10f689bad7 100644 --- a/runtime/doc/treesitter.txt +++ b/runtime/doc/treesitter.txt @@ -1101,10 +1101,12 @@ LanguageTree:for_each_tree({fn}) *LanguageTree:for_each_tree()* • {fn} fun(tree: TSTree, ltree: LanguageTree) LanguageTree:included_regions() *LanguageTree:included_regions()* - Gets the set of included regions + Gets the set of included regions managed by this LanguageTree . This can be different from the regions set by injection query, because a + partial |LanguageTree:parse()| drops the regions outside the requested + range. Return: ~ - Range6[][] + table<integer, Range6[]> LanguageTree:invalidate({reload}) *LanguageTree:invalidate()* Invalidates this parser and all its children @@ -1113,10 +1115,12 @@ LanguageTree:invalidate({reload}) *LanguageTree:invalidate()* • {reload} (boolean|nil) LanguageTree:is_valid({exclude_children}) *LanguageTree:is_valid()* - Determines whether this tree is valid. If the tree is invalid, call `parse()` . This will return the updated tree. + Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest state of the + source. If invalid, user should call |LanguageTree:parse()|. Parameters: ~ - • {exclude_children} (boolean|nil) + • {exclude_children} (boolean|nil) whether to ignore the validity of + children (default `false`) Return: ~ (boolean) @@ -1165,7 +1169,7 @@ LanguageTree:parse({range}) *LanguageTree:parse()* injections). Return: ~ - TSTree[] + table<integer, TSTree> *LanguageTree:register_cbs()* LanguageTree:register_cbs({cbs}, {recursive}) @@ -1207,7 +1211,12 @@ LanguageTree:tree_for_range({range}, {opts}) TSTree|nil LanguageTree:trees() *LanguageTree:trees()* - Returns all trees this language tree contains. Does not include child - languages. + Returns all trees of the regions parsed by this parser. Does not include + child languages. The result is list-like if + • this LanguageTree is the root, in which case the result is empty or a singleton list; or + • the root LanguageTree is fully parsed. + + Return: ~ + table<integer, TSTree> vim:tw=78:ts=8:sw=4:sts=4:et:ft=help:norl: diff --git a/runtime/doc/visual.txt b/runtime/doc/visual.txt index 6ca486e8cf..0d1ea937c0 100644 --- a/runtime/doc/visual.txt +++ b/runtime/doc/visual.txt @@ -169,6 +169,7 @@ If you want to highlight exactly the same area as the last time, you can use CTRL-C In Visual mode: Stop Visual mode. When insert mode is pending (the mode message shows "-- (insert) VISUAL --"), it is also stopped. + On MS-Windows, you may need to press CTRL-Break. ============================================================================== 3. Changing the Visual area *visual-change* diff --git a/runtime/ftplugin/kotlin.vim b/runtime/ftplugin/kotlin.vim new file mode 100644 index 0000000000..b21de603ea --- /dev/null +++ b/runtime/ftplugin/kotlin.vim @@ -0,0 +1,33 @@ +" Vim filetype plugin file +" Language: Kotlin +" Maintainer: Alexander Udalov +" URL: https://github.com/udalov/kotlin-vim +" Last Change: 7 November 2021 +" 2023 Sep 17 by Vim Project (browsefilter) + +if exists('b:did_ftplugin') | finish | endif +let b:did_ftplugin = 1 + +let s:save_cpo = &cpo +set cpo&vim + +setlocal comments=sO:*\ -,mO:*\ \ ,exO:*/,s1:/*,mb:*,ex:*/,:// +setlocal commentstring=//\ %s + +setlocal formatoptions-=t formatoptions+=croqnl +silent! setlocal formatoptions+=j + +setlocal includeexpr=substitute(v:fname,'\\.','/','g') +setlocal suffixesadd=.kt + +let b:undo_ftplugin = "setlocal comments< commentstring< ". + \ "formatoptions< includeexpr< suffixesadd<" + +if (has("gui_win32") || has("gui_gtk")) && !exists("b:browsefilter") + let b:browsefilter = "Kotlin Source Files (*.kt, *kts)\t*.kt;*.kts\n" . + \ "All Files (*.*)\t*.*\n" + let b:undo_ftplugin .= " | unlet! b:browsefilter" +endif + +let &cpo = s:save_cpo +unlet s:save_cpo diff --git a/runtime/indent/kotlin.vim b/runtime/indent/kotlin.vim new file mode 100644 index 0000000000..590a5074d1 --- /dev/null +++ b/runtime/indent/kotlin.vim @@ -0,0 +1,60 @@ +" Vim indent file +" Language: Kotlin +" Maintainer: Alexander Udalov +" URL: https://github.com/udalov/kotlin-vim +" Last Change: 7 November 2021 +" 2023 Sep 17 by Vim Project (undo_indent) + +if exists('b:did_indent') + finish +endif +let b:did_indent = 1 + +setlocal cinoptions& cinoptions+=j1,L0 +setlocal indentexpr=GetKotlinIndent() +setlocal indentkeys=0},0),!^F,o,O,e,<CR> +setlocal autoindent " TODO ? + +let b:undo_indent = "setlocal autoindent< cinoptions< indentexpr< indentkeys<" + +" TODO teach it to count bracket balance, etc. +function! GetKotlinIndent() + if v:lnum == 0 + return 0 + endif + + let prev_num = prevnonblank(v:lnum - 1) + let prev = getline(prev_num) + let prev_indent = indent(prev_num) + let cur = getline(v:lnum) + + if cur =~ '^\s*\*' + return cindent(v:lnum) + endif + + if prev =~ '^\s*\*/' + let st = prev + while st > 1 + if getline(st) =~ '^\s*/\*' + break + endif + let st = st - 1 + endwhile + return indent(st) + endif + + let prev_open_paren = prev =~ '^.*(\s*$' + let cur_close_paren = cur =~ '^\s*).*$' + let prev_open_brace = prev =~ '^.*\({\|->\)\s*$' + let cur_close_brace = cur =~ '^\s*}.*$' + + if prev_open_paren && !cur_close_paren || prev_open_brace && !cur_close_brace + return prev_indent + shiftwidth() + endif + + if cur_close_paren && !prev_open_paren || cur_close_brace && !prev_open_brace + return prev_indent - shiftwidth() + endif + + return prev_indent +endfunction diff --git a/runtime/lua/vim/_meta/api.lua b/runtime/lua/vim/_meta/api.lua index 6573c68493..41bb5636b8 100644 --- a/runtime/lua/vim/_meta/api.lua +++ b/runtime/lua/vim/_meta/api.lua @@ -80,6 +80,9 @@ function vim.api.nvim__id_float(flt) end function vim.api.nvim__inspect_cell(grid, row, col) end --- @private +function vim.api.nvim__invalidate_glyph_cache() end + +--- @private --- @return any[] function vim.api.nvim__runtime_inspect() end @@ -1608,6 +1611,8 @@ function vim.api.nvim_open_term(buffer, opts) end --- • noautocmd: If true then no buffer-related autocommand --- events such as `BufEnter`, `BufLeave` or `BufWinEnter` may --- fire from calling this function. +--- • fixed: If true when anchor is NW or SW, the float window +--- would be kept fixed even if the window would be truncated. --- @return integer function vim.api.nvim_open_win(buffer, enter, config) end diff --git a/runtime/lua/vim/_meta/api_keysets.lua b/runtime/lua/vim/_meta/api_keysets.lua index 4d08563ce2..b249f6629f 100644 --- a/runtime/lua/vim/_meta/api_keysets.lua +++ b/runtime/lua/vim/_meta/api_keysets.lua @@ -112,6 +112,7 @@ error('Cannot require a meta file') --- @field footer_pos? string --- @field style? string --- @field noautocmd? boolean +--- @field fixed? boolean --- @class vim.api.keyset.get_autocmds --- @field event? any diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index d847c28f5c..c7f025f9e7 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -592,6 +592,7 @@ local extension = { ly = 'lilypond', ily = 'lilypond', liquid = 'liquid', + liq = 'liquidsoap', cl = 'lisp', L = 'lisp', lisp = 'lisp', diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 4ea3dde81c..d43d9a7cfa 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -371,15 +371,21 @@ function M.hover(_, result, ctx, config) end return end - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - markdown_lines = util.trim_empty_lines(markdown_lines) - if vim.tbl_isempty(markdown_lines) then + local format = 'markdown' + local contents ---@type string[] + if type(result.contents) == 'table' and result.contents.kind == 'plaintext' then + format = 'plaintext' + contents = vim.split(result.contents.value or '', '\n', { trimempty = true }) + else + contents = util.convert_input_to_markdown_lines(result.contents) + end + if vim.tbl_isempty(contents) then if config.silent ~= true then vim.notify('No information available') end return end - return util.open_floating_preview(markdown_lines, 'markdown', config) + return util.open_floating_preview(contents, format, config) end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover @@ -470,7 +476,6 @@ function M.signature_help(_, result, ctx, config) vim.tbl_get(client.server_capabilities, 'signatureHelpProvider', 'triggerCharacters') local ft = vim.bo[ctx.bufnr].filetype local lines, hl = util.convert_signature_help_to_markdown_lines(result, ft, triggers) - lines = util.trim_empty_lines(lines) if vim.tbl_isempty(lines) then if config.silent ~= true then print('No signature help available') diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 54721865b7..988057f5f9 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -102,7 +102,7 @@ end local function split_lines(value) value = string.gsub(value, '\r\n?', '\n') - return split(value, '\n', { plain = true }) + return split(value, '\n', { plain = true, trimempty = true }) end local function create_window_without_focus() @@ -877,9 +877,12 @@ end --- window for `textDocument/hover`, for parsing the result of --- `textDocument/signatureHelp`, and potentially others. --- +--- Note that if the input is of type `MarkupContent` and its kind is `plaintext`, +--- then the corresponding value is returned without further modifications. +--- ---@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) ---@param contents (table|nil) List of strings to extend with converted lines. Defaults to {}. ----@return table {contents} extended with lines of converted markdown. +---@return string[] extended with lines of converted markdown. ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover function M.convert_input_to_markdown_lines(input, contents) contents = contents or {} @@ -887,27 +890,13 @@ function M.convert_input_to_markdown_lines(input, contents) if type(input) == 'string' then list_extend(contents, split_lines(input)) else - assert(type(input) == 'table', 'Expected a table for Hover.contents') + assert(type(input) == 'table', 'Expected a table for LSP input') -- MarkupContent if input.kind then - -- The kind can be either plaintext or markdown. - -- If it's plaintext, then wrap it in a <text></text> block - - -- Some servers send input.value as empty, so let's ignore this :( local value = input.value or '' - - if input.kind == 'plaintext' then - -- wrap this in a <text></text> block so that stylize_markdown - -- can properly process it as plaintext - value = string.format('<text>\n%s\n</text>', value) - end - - -- assert(type(value) == 'string') list_extend(contents, split_lines(value)) -- MarkupString variation 2 elseif input.language then - -- Some servers send input.value as empty, so let's ignore this :( - -- assert(type(input.value) == 'string') table.insert(contents, '```' .. input.language) list_extend(contents, split_lines(input.value or '')) table.insert(contents, '```') @@ -925,7 +914,7 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end ---- Converts `textDocument/SignatureHelp` response to markdown lines. +--- Converts `textDocument/signatureHelp` response to markdown lines. --- ---@param signature_help table Response of `textDocument/SignatureHelp` ---@param ft string|nil filetype that will be use as the `lang` for the label markdown code block @@ -955,10 +944,10 @@ function M.convert_signature_help_to_markdown_lines(signature_help, ft, triggers end local label = signature.label if ft then - -- wrap inside a code block so stylize_markdown can render it properly + -- wrap inside a code block for proper rendering label = ('```%s\n%s\n```'):format(ft, label) end - list_extend(contents, split(label, '\n', { plain = true })) + list_extend(contents, split(label, '\n', { plain = true, trimempty = true })) if signature.documentation then -- if LSP returns plain string, we treat it as plaintext. This avoids -- special characters like underscore or similar from being interpreted @@ -1223,7 +1212,7 @@ function M.preview_location(location, opts) local syntax = vim.bo[bufnr].syntax if syntax == '' then -- When no syntax is set, we use filetype as fallback. This might not result - -- in a valid syntax definition. See also ft detection in stylize_markdown. + -- in a valid syntax definition. -- An empty syntax is more common now with TreeSitter, since TS disables syntax. syntax = vim.bo[bufnr].filetype end @@ -1240,36 +1229,65 @@ local function find_window_by_var(name, value) end end ---- Trims empty lines from input and pad top and bottom with empty lines ---- ----@param contents table of lines to trim and pad ----@param opts table with optional fields ---- - pad_top number of lines to pad contents at top (default 0) ---- - pad_bottom number of lines to pad contents at bottom (default 0) ----@return table table of trimmed and padded lines -function M._trim(contents, opts) - validate({ - contents = { contents, 't' }, - opts = { opts, 't', true }, - }) - opts = opts or {} - contents = M.trim_empty_lines(contents) - if opts.pad_top then - for _ = 1, opts.pad_top do - table.insert(contents, 1, '') +---Returns true if the line is empty or only contains whitespace. +---@param line string +---@return boolean +local function is_blank_line(line) + return line and line:match('^%s*$') +end + +---Returns true if the line corresponds to a Markdown thematic break. +---@param line string +---@return boolean +local function is_separator_line(line) + return line and line:match('^ ? ? ?%-%-%-+%s*$') +end + +---Replaces separator lines by the given divider and removing surrounding blank lines. +---@param contents string[] +---@param divider string +---@return string[] +local function replace_separators(contents, divider) + local trimmed = {} + local l = 1 + while l <= #contents do + local line = contents[l] + if is_separator_line(line) then + if l > 1 and is_blank_line(contents[l - 1]) then + table.remove(trimmed) + end + table.insert(trimmed, divider) + if is_blank_line(contents[l + 1]) then + l = l + 1 + end + else + table.insert(trimmed, line) end + l = l + 1 end - if opts.pad_bottom then - for _ = 1, opts.pad_bottom do - table.insert(contents, '') + + return trimmed +end + +---Collapses successive blank lines in the input table into a single one. +---@param contents string[] +---@return string[] +local function collapse_blank_lines(contents) + local collapsed = {} + local l = 1 + while l <= #contents do + local line = contents[l] + if is_blank_line(line) then + while is_blank_line(contents[l + 1]) do + l = l + 1 + end end + table.insert(collapsed, line) + l = l + 1 end - return contents + return collapsed end ---- Generates a table mapping markdown code block lang to vim syntax, ---- based on g:markdown_fenced_languages ----@return table table of lang -> syntax mappings local function get_markdown_fences() local fences = {} for _, fence in pairs(vim.g.markdown_fenced_languages or {}) do @@ -1297,8 +1315,6 @@ end --- - wrap_at character to wrap at for computing height --- - max_width maximal width of floating window --- - max_height maximal height of floating window ---- - pad_top number of lines to pad contents at top ---- - pad_bottom number of lines to pad contents at bottom --- - separator insert separator after code block ---@return table stripped content function M.stylize_markdown(bufnr, contents, opts) @@ -1335,7 +1351,7 @@ function M.stylize_markdown(bufnr, contents, opts) end -- Clean up - contents = M._trim(contents, opts) + contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) local stripped = {} local highlights = {} @@ -1484,6 +1500,45 @@ function M.stylize_markdown(bufnr, contents, opts) return stripped end +--- @class lsp.util.NormalizeMarkdownOptions +--- @field width integer Thematic breaks are expanded to this size. Defaults to 80. + +--- Normalizes Markdown input to a canonical form. +--- +--- The returned Markdown adheres to the GitHub Flavored Markdown (GFM) +--- specification. +--- +--- The following transformations are made: +--- +--- 1. Carriage returns ('\r') and empty lines at the beginning and end are removed +--- 2. Successive empty lines are collapsed into a single empty line +--- 3. Thematic breaks are expanded to the given width +--- +---@private +---@param contents string[] +---@param opts? lsp.util.NormalizeMarkdownOptions +---@return string[] table of lines containing normalized Markdown +---@see https://github.github.com/gfm +function M._normalize_markdown(contents, opts) + validate({ + contents = { contents, 't' }, + opts = { opts, 't', true }, + }) + opts = opts or {} + + -- 1. Carriage returns are removed + contents = vim.split(table.concat(contents, '\n'):gsub('\r', ''), '\n', { trimempty = true }) + + -- 2. Successive empty lines are collapsed into a single empty line + contents = collapse_blank_lines(contents) + + -- 3. Thematic breaks are expanded to the given width + local divider = string.rep('─', opts.width or 80) + contents = replace_separators(contents, divider) + + return contents +end + --- Closes the preview window --- ---@param winnr integer window id of preview window @@ -1620,8 +1675,6 @@ end --- - wrap_at: (integer) character to wrap at for computing height when wrap is enabled --- - max_width: (integer) maximal width of floating window --- - max_height: (integer) maximal height of floating window ---- - pad_top: (integer) number of lines to pad contents at top ---- - pad_bottom: (integer) number of lines to pad contents at bottom --- - focus_id: (string) if a popup with this id is opened, then focus it --- - close_events: (table) list of events that closes the floating window --- - focusable: (boolean, default true) Make float focusable @@ -1629,8 +1682,7 @@ end --- is also `true`, focus an existing floating window with the same --- {focus_id} ---@return integer bufnr of newly created float window ----@return integer winid of newly created float window ----preview window +---@return integer winid of newly created float window preview window function M.open_floating_preview(contents, syntax, opts) validate({ contents = { contents, 't' }, @@ -1639,7 +1691,6 @@ function M.open_floating_preview(contents, syntax, opts) }) opts = opts or {} opts.wrap = opts.wrap ~= false -- wrapping by default - opts.stylize_markdown = opts.stylize_markdown ~= false and vim.g.syntax_on ~= nil opts.focus = opts.focus ~= false opts.close_events = opts.close_events or { 'CursorMoved', 'CursorMovedI', 'InsertCharPre' } @@ -1671,16 +1722,21 @@ function M.open_floating_preview(contents, syntax, opts) api.nvim_win_close(existing_float, true) end + -- Create the buffer local floating_bufnr = api.nvim_create_buf(false, true) - local do_stylize = syntax == 'markdown' and opts.stylize_markdown - - -- Clean up input: trim empty lines from the end, pad - contents = M._trim(contents, opts) + -- Set up the contents, using treesitter for markdown + local do_stylize = syntax == 'markdown' and vim.g.syntax_on ~= nil if do_stylize then - -- applies the syntax and sets the lines to the buffer - contents = M.stylize_markdown(floating_bufnr, contents, opts) + local width = M._make_floating_popup_size(contents, opts) + contents = M._normalize_markdown(contents, { width = width }) + vim.bo[floating_bufnr].filetype = 'markdown' + vim.treesitter.start(floating_bufnr) + api.nvim_buf_set_lines(floating_bufnr, 0, -1, false, contents) else + -- Clean up input: trim empty lines + contents = vim.split(table.concat(contents, '\n'), '\n', { trimempty = true }) + if syntax then vim.bo[floating_bufnr].syntax = syntax end @@ -1697,9 +1753,9 @@ function M.open_floating_preview(contents, syntax, opts) local float_option = M.make_floating_popup_options(width, height, opts) local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option) + if do_stylize then vim.wo[floating_winnr].conceallevel = 2 - vim.wo[floating_winnr].concealcursor = 'n' end -- disable folding vim.wo[floating_winnr].foldenable = false @@ -1708,6 +1764,7 @@ function M.open_floating_preview(contents, syntax, opts) vim.bo[floating_bufnr].modifiable = false vim.bo[floating_bufnr].bufhidden = 'wipe' + api.nvim_buf_set_keymap( floating_bufnr, 'n', @@ -1908,6 +1965,7 @@ function M.symbols_to_items(symbols, bufnr) end --- Removes empty lines from the beginning and end. +---@deprecated use `vim.split()` with `trimempty` instead ---@param lines table list of lines to trim ---@return table trimmed list of lines function M.trim_empty_lines(lines) diff --git a/runtime/lua/vim/treesitter/_meta.lua b/runtime/lua/vim/treesitter/_meta.lua index 9a94f12c16..d01b7be3b0 100644 --- a/runtime/lua/vim/treesitter/_meta.lua +++ b/runtime/lua/vim/treesitter/_meta.lua @@ -50,7 +50,8 @@ function TSNode:_rawquery(query, captures, start, end_, opts) end ---@alias TSLoggerCallback fun(logtype: 'parse'|'lex', msg: string) ---@class TSParser ----@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: boolean?): TSTree, integer[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: true): TSTree, Range6[] +---@field parse fun(self: TSParser, tree: TSTree?, source: integer|string, include_bytes: false|nil): TSTree, Range4[] ---@field reset fun(self: TSParser) ---@field included_ranges fun(self: TSParser, include_bytes: boolean?): integer[] ---@field set_included_ranges fun(self: TSParser, ranges: (Range6|TSNode)[]) diff --git a/runtime/lua/vim/treesitter/languagetree.lua b/runtime/lua/vim/treesitter/languagetree.lua index b555ee231b..f931291ed7 100644 --- a/runtime/lua/vim/treesitter/languagetree.lua +++ b/runtime/lua/vim/treesitter/languagetree.lua @@ -78,13 +78,14 @@ local TSCallbackNames = { ---@field private _opts table Options ---@field private _parser TSParser Parser for language ---@field private _has_regions boolean ----@field private _regions Range6[][]? +---@field private _regions table<integer, Range6[]>? ---List of regions this tree should manage and parse. If nil then regions are ---taken from _trees. This is mostly a short-lived cache for included_regions() ---@field private _lang string Language name ---@field private _parent_lang? string Parent language name ---@field private _source (integer|string) Buffer or string to parse ----@field private _trees TSTree[] Reference to parsed tree (one for each language) +---@field private _trees table<integer, TSTree> Reference to parsed tree (one for each language). +---Each key is the index of region, which is synced with _regions and _valid. ---@field private _valid boolean|table<integer,boolean> If the parsed tree is valid ---@field private _logger? fun(logtype: string, msg: string) ---@field private _logfile? file* @@ -211,7 +212,7 @@ function LanguageTree:_log(...) end local info = debug.getinfo(2, 'nl') - local nregions = #self:included_regions() + local nregions = vim.tbl_count(self:included_regions()) local prefix = string.format('%s:%d: (#regions=%d) ', info.name or '???', info.currentline or 0, nregions) @@ -244,8 +245,13 @@ function LanguageTree:invalidate(reload) end end ---- Returns all trees this language tree contains. +--- Returns all trees of the regions parsed by this parser. --- Does not include child languages. +--- The result is list-like if +--- * this LanguageTree is the root, in which case the result is empty or a singleton list; or +--- * the root LanguageTree is fully parsed. +--- +---@return table<integer, TSTree> function LanguageTree:trees() return self._trees end @@ -255,16 +261,15 @@ function LanguageTree:lang() return self._lang end ---- Determines whether this tree is valid. ---- If the tree is invalid, call `parse()`. ---- This will return the updated tree. ----@param exclude_children boolean|nil +--- Returns whether this LanguageTree is valid, i.e., |LanguageTree:trees()| reflects the latest +--- state of the source. If invalid, user should call |LanguageTree:parse()|. +---@param exclude_children boolean|nil whether to ignore the validity of children (default `false`) ---@return boolean function LanguageTree:is_valid(exclude_children) local valid = self._valid if type(valid) == 'table' then - for i = 1, #self:included_regions() do + for i, _ in pairs(self:included_regions()) do if not valid[i] then return false end @@ -328,7 +333,7 @@ end --- @private --- @param range boolean|Range? ---- @return integer[] changes +--- @return Range6[] changes --- @return integer no_regions_parsed --- @return number total_parse_time function LanguageTree:_parse_regions(range) @@ -370,7 +375,7 @@ function LanguageTree:_add_injections() local seen_langs = {} ---@type table<string,boolean> local query_time, injections_by_lang = tcall(self._get_injections, self) - for lang, injection_ranges in pairs(injections_by_lang) do + for lang, injection_regions in pairs(injections_by_lang) do local has_lang = pcall(language.add, lang) -- Child language trees should just be ignored if not found, since @@ -383,7 +388,7 @@ function LanguageTree:_add_injections() child = self:add_child(lang) end - child:set_included_regions(injection_ranges) + child:set_included_regions(injection_regions) seen_langs[lang] = true end end @@ -408,14 +413,14 @@ end --- Set to `true` to run a complete parse of the source (Note: Can be slow!) --- Set to `false|nil` to only parse regions with empty ranges (typically --- only the root tree without injections). ---- @return TSTree[] +--- @return table<integer, TSTree> function LanguageTree:parse(range) if self:is_valid() then self:_log('valid') return self._trees end - local changes --- @type Range6? + local changes --- @type Range6[]? -- Collect some stats local no_regions_parsed = 0 @@ -565,7 +570,7 @@ function LanguageTree:_iter_regions(fn) local all_valid = true - for i, region in ipairs(self:included_regions()) do + for i, region in pairs(self:included_regions()) do if was_valid or self._valid[i] then self._valid[i] = fn(i, region) if not self._valid[i] then @@ -601,7 +606,7 @@ end --- nodes, which is useful for templating languages like ERB and EJS. --- ---@private ----@param new_regions Range6[][] List of regions this tree should manage and parse. +---@param new_regions (Range4|Range6|TSNode)[][] List of regions this tree should manage and parse. function LanguageTree:set_included_regions(new_regions) self._has_regions = true @@ -609,16 +614,20 @@ function LanguageTree:set_included_regions(new_regions) for _, region in ipairs(new_regions) do for i, range in ipairs(region) do if type(range) == 'table' and #range == 4 then - region[i] = Range.add_bytes(self._source, range) + region[i] = Range.add_bytes(self._source, range --[[@as Range4]]) elseif type(range) == 'userdata' then region[i] = { range:range(true) } end end end + -- included_regions is not guaranteed to be list-like, but this is still sound, i.e. if + -- new_regions is different from included_regions, then outdated regions in included_regions are + -- invalidated. For example, if included_regions = new_regions ++ hole ++ outdated_regions, then + -- outdated_regions is invalidated by _iter_regions in else branch. if #self:included_regions() ~= #new_regions then -- TODO(lewis6991): inefficient; invalidate trees incrementally - for _, t in ipairs(self._trees) do + for _, t in pairs(self._trees) do self:_do_callback('changedtree', t:included_ranges(true), t) end self._trees = {} @@ -632,20 +641,22 @@ function LanguageTree:set_included_regions(new_regions) self._regions = new_regions end ----Gets the set of included regions ----@return Range6[][] +---Gets the set of included regions managed by this LanguageTree. This can be different from the +---regions set by injection query, because a partial |LanguageTree:parse()| drops the regions +---outside the requested range. +---@return table<integer, Range6[]> function LanguageTree:included_regions() if self._regions then return self._regions end - if not self._has_regions or #self._trees == 0 then + if not self._has_regions or next(self._trees) == nil then -- treesitter.c will default empty ranges to { -1, -1, -1, -1, -1, -1} return { {} } end local regions = {} ---@type Range6[][] - for i, _ in ipairs(self._trees) do + for i, _ in pairs(self._trees) do regions[i] = self._trees[i]:included_ranges(true) end @@ -801,7 +812,7 @@ local function combine_regions(regions) return result end ---- Gets language injection points by language. +--- Gets language injection regions by language. --- --- This is where most of the injection processing occurs. --- diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm index e78d233cc6..2cc5546bac 100644 --- a/runtime/queries/markdown/highlights.scm +++ b/runtime/queries/markdown/highlights.scm @@ -61,3 +61,11 @@ ] @string.escape (inline) @spell + +;; Conceal backticks +(fenced_code_block + (fenced_code_block_delimiter) @conceal + (#set! conceal "")) +(fenced_code_block + (info_string (language) @conceal + (#set! conceal ""))) diff --git a/runtime/queries/markdown_inline/highlights.scm b/runtime/queries/markdown_inline/highlights.scm index cd5da530d7..c75da478af 100644 --- a/runtime/queries/markdown_inline/highlights.scm +++ b/runtime/queries/markdown_inline/highlights.scm @@ -92,3 +92,11 @@ "]" ] @conceal (#set! conceal "")) + +;; Replace common HTML entities. +((entity_reference) @conceal (#eq? @conceal " ") (#set! conceal "")) +((entity_reference) @conceal (#eq? @conceal "<") (#set! conceal "<")) +((entity_reference) @conceal (#eq? @conceal ">") (#set! conceal ">")) +((entity_reference) @conceal (#eq? @conceal "&") (#set! conceal "&")) +((entity_reference) @conceal (#eq? @conceal """) (#set! conceal "\"")) +((entity_reference) @conceal (#any-of? @conceal " " " ") (#set! conceal " ")) diff --git a/runtime/syntax/kotlin.vim b/runtime/syntax/kotlin.vim new file mode 100644 index 0000000000..9b85b8ef5c --- /dev/null +++ b/runtime/syntax/kotlin.vim @@ -0,0 +1,157 @@ +" Vim syntax file +" Language: Kotlin +" Maintainer: Alexander Udalov +" URL: https://github.com/udalov/kotlin-vim +" Last Change: 30 December 2022 + +if exists('b:current_syntax') + finish +endif + +syn keyword ktStatement break continue return +syn keyword ktConditional if else when +syn keyword ktRepeat do for while +syn keyword ktOperator in is by +syn keyword ktKeyword get set out super this where +syn keyword ktException try catch finally throw + +syn keyword ktInclude import package + +" Generated stdlib class names {{{ +" The following is generated by https://github.com/udalov/kotlin-vim/blob/master/extra/generate-stdlib-class-names.main.kts +syn keyword ktType AbstractCollection AbstractCoroutineContextElement AbstractCoroutineContextKey AbstractDoubleTimeSource AbstractIterator AbstractList AbstractLongTimeSource +syn keyword ktType AbstractMap AbstractMutableCollection AbstractMutableList AbstractMutableMap AbstractMutableSet AbstractSet AccessDeniedException Accessor Annotation +syn keyword ktType AnnotationRetention AnnotationTarget Any Appendable ArithmeticException Array ArrayDeque ArrayList AssertionError Boolean BooleanArray BooleanIterator +syn keyword ktType BuilderInference Byte ByteArray ByteIterator CName CallsInPlace CancellationException Char CharArray CharCategory CharDirectionality CharIterator CharProgression +syn keyword ktType CharRange CharSequence CharacterCodingException Charsets ClassCastException Cloneable ClosedFloatingPointRange ClosedRange Collection Comparable +syn keyword ktType ComparableTimeMark Comparator ConcurrentModificationException ConditionalEffect ContextFunctionTypeParams Continuation ContinuationInterceptor ContractBuilder +syn keyword ktType CopyActionContext CopyActionResult CoroutineContext DeepRecursiveFunction DeepRecursiveScope Delegates Deprecated DeprecatedSinceKotlin DeprecationLevel +syn keyword ktType Destructured Double DoubleArray DoubleIterator DslMarker Duration DurationUnit Effect Element EmptyCoroutineContext Entry Enum EnumEntries Error Exception +syn keyword ktType ExperimentalContracts ExperimentalJsExport ExperimentalMultiplatform ExperimentalObjCName ExperimentalObjCRefinement ExperimentalPathApi ExperimentalStdlibApi +syn keyword ktType ExperimentalSubclassOptIn ExperimentalTime ExperimentalTypeInference ExperimentalUnsignedTypes ExtensionFunctionType FileAlreadyExistsException +syn keyword ktType FileSystemException FileTreeWalk FileVisitorBuilder FileWalkDirection Float FloatArray FloatIterator FreezingIsDeprecated Function Function0 Function1 Function10 +syn keyword ktType Function11 Function12 Function13 Function14 Function15 Function16 Function17 Function18 Function19 Function2 Function20 Function21 Function22 Function3 Function4 +syn keyword ktType Function5 Function6 Function7 Function8 Function9 FunctionN Getter Grouping HashMap HashSet HiddenFromObjC HidesFromObjC Ignore IllegalArgumentException +syn keyword ktType IllegalStateException IndexOutOfBoundsException IndexedValue Int IntArray IntIterator IntProgression IntRange InvocationKind Iterable Iterator JsExport JsName +syn keyword ktType JvmDefault JvmDefaultWithCompatibility JvmDefaultWithoutCompatibility JvmField JvmInline JvmMultifileClass JvmName JvmOverloads JvmRecord JvmSerializableLambda +syn keyword ktType JvmStatic JvmSuppressWildcards JvmSynthetic JvmWildcard KAnnotatedElement KCallable KClass KClassifier KDeclarationContainer KFunction KMutableProperty +syn keyword ktType KMutableProperty0 KMutableProperty1 KMutableProperty2 KParameter KProperty KProperty0 KProperty1 KProperty2 KType KTypeParameter KTypeProjection KVariance +syn keyword ktType KVisibility Key Kind KotlinNullPointerException KotlinReflectionNotSupportedError KotlinVersion Lazy LazyThreadSafetyMode Level LinkedHashMap LinkedHashSet List +syn keyword ktType ListIterator Long LongArray LongIterator LongProgression LongRange Map MatchGroup MatchGroupCollection MatchNamedGroupCollection MatchResult Metadata Monotonic +syn keyword ktType MustBeDocumented MutableCollection MutableEntry MutableIterable MutableIterator MutableList MutableListIterator MutableMap MutableSet NoSuchElementException +syn keyword ktType NoSuchFileException NoWhenBranchMatchedException NotImplementedError Nothing NullPointerException Number NumberFormatException ObjCName ObservableProperty +syn keyword ktType OnErrorAction OnErrorResult OpenEndRange OptIn OptionalExpectation OverloadResolutionByLambdaReturnType Pair ParameterName PathWalkOption +syn keyword ktType PropertyDelegateProvider PublishedApi PurelyImplements Random RandomAccess ReadOnlyProperty ReadWriteProperty RefinesInSwift Regex RegexOption Repeatable +syn keyword ktType ReplaceWith RequiresOptIn RestrictsSuspension Result Retention Returns ReturnsNotNull RuntimeException Sequence SequenceScope Set Setter SharedImmutable Short +syn keyword ktType ShortArray ShortIterator ShouldRefineInSwift SimpleEffect SinceKotlin Strictfp String StringBuilder SubclassOptInRequired Suppress Synchronized Target +syn keyword ktType TestTimeSource ThreadLocal Throwable Throws TimeMark TimeSource TimedValue Transient Triple TypeCastException Typography UByte UByteArray UInt UIntArray +syn keyword ktType UIntProgression UIntRange ULong ULongArray ULongProgression ULongRange UShort UShortArray UninitializedPropertyAccessException Unit UnsafeVariance +syn keyword ktType UnsupportedOperationException ValueTimeMark Volatile WithComparableMarks +" }}} + +syn keyword ktModifier annotation companion enum inner abstract final open override sealed vararg dynamic expect actual suspend +syn keyword ktStructure class object interface typealias fun val var constructor init + +syn keyword ktReservedKeyword typeof + +syn keyword ktBoolean true false +syn keyword ktConstant null + +syn keyword ktModifier reified external inline noinline crossinline + +syn match ktModifier "\v<data>\ze\@=.*<(class|object)>" +syn match ktModifier "\v<value>\ze\@=.*<class>" +syn match ktModifier "\v<(tailrec|operator|infix)>\ze\@=.*<fun>" +syn match ktModifier "\v<const>\ze\@=.*<val>" +syn match ktModifier "\v<lateinit>\ze\@=.*<var>" +syn match ktModifier "\v<(internal|private|protected|public)>\ze\@=.*<(class|object|interface|typealias|fun|val|var|constructor|get|set)>" + +syn match ktOperator "\v\?:|::|\<\=? | \>\=?|[!=]\=\=?|<as>\??|[-*+/%]\=?|[!&|]" + +syn keyword ktTodo TODO FIXME XXX contained +syn match ktShebang "\v^#!.*$" +syn match ktLineComment "\v//.*$" contains=ktTodo,@Spell +syn region ktComment matchgroup=ktCommentMatchGroup start="/\*" end="\*/" contains=ktComment,ktTodo,@Spell + +syn region ktDocComment start="/\*\*" end="\*/" contains=ktDocTag,ktTodo,@Spell +syn match ktDocTag "\v\@(author|constructor|receiver|return|since|suppress)>" contained +syn match ktDocTag "\v\@(exception|param|property|throws|see|sample)>\s*\S+" contains=ktDocTagParam contained +syn match ktDocTagParam "\v(\s|\[)\S+" contained +syn match ktComment "/\*\*/" + +syn match ktSpecialCharError "\v\\." contained +syn match ktSpecialChar "\v\\([tbnr'"$\\]|u\x{4})" contained +syn region ktString start='"' skip='\\"' end='"' contains=ktSimpleInterpolation,ktComplexInterpolation,ktSpecialChar,ktSpecialCharError,@Spell +syn region ktString start='"""' end='""""*' contains=ktSimpleInterpolation,ktComplexInterpolation,@Spell +syn match ktCharacter "\v'[^']*'" contains=ktSpecialChar,ktSpecialCharError +syn match ktCharacter "\v'\\''" contains=ktSpecialChar +syn match ktCharacter "\v'[^\\]'" + +syn match ktAnnotation "\v(\w)@<!\@[[:alnum:]_.]*(:[[:alnum:]_.]*)?" +syn match ktLabel "\v\w+\@" +syn match ktLabel "\v(\w)@<=\@\w+" + +syn match ktSimpleInterpolation "\v\$\h\w*" contained +syn region ktComplexInterpolation matchgroup=ktComplexInterpolationBrace start="\v\$\{" end="\v\}" contains=ALLBUT,ktSimpleInterpolation,ktTodo,ktSpecialCharError,ktSpecialChar,ktDocTag,ktDocTagParam + +syn match ktNumber "\v<\d+[_[:digit:]]*(uL?|UL?|[LFf])?" +syn match ktNumber "\v<0[Xx]\x+[_[:xdigit:]]*(uL?|UL?|L)?" +syn match ktNumber "\v<0[Bb][01]+[_01]*(uL?|UL?|L)?" +syn match ktFloat "\v<\d*(\d[eE][-+]?\d+|\.\d+([eE][-+]?\d+)?)[Ff]?" + +syn match ktEscapedName "\v`.*`" + +syn match ktExclExcl "!!" +syn match ktArrow "->" + +syn region ktFold start="{" end="}" transparent fold + +exec "syntax sync ccomment ktComment minlines=10" + +hi def link ktStatement Statement +hi def link ktConditional Conditional +hi def link ktRepeat Repeat +hi def link ktOperator Operator +hi def link ktKeyword Keyword +hi def link ktException Exception +hi def link ktReservedKeyword Error + +hi def link ktInclude Include + +hi def link ktType Type +hi def link ktModifier StorageClass +hi def link ktStructure Structure +hi def link ktTypedef Typedef + +hi def link ktBoolean Boolean +hi def link ktConstant Constant + +hi def link ktTodo Todo +hi def link ktShebang Comment +hi def link ktLineComment Comment +hi def link ktComment Comment +hi def link ktCommentMatchGroup Comment +hi def link ktDocComment Comment +hi def link ktDocTag Special +hi def link ktDocTagParam Identifier + +hi def link ktSpecialChar SpecialChar +hi def link ktSpecialCharError Error +hi def link ktString String +hi def link ktCharacter Character + +hi def link ktAnnotation Identifier +hi def link ktLabel Identifier + +hi def link ktSimpleInterpolation Identifier +hi def link ktComplexInterpolationBrace Identifier + +hi def link ktNumber Number +hi def link ktFloat Float + +hi def link ktExclExcl Special +hi def link ktArrow Structure + +let b:current_syntax = 'kotlin' + +" vim:foldmethod=marker diff --git a/src/nvim/api/keysets.h b/src/nvim/api/keysets.h index 4e5e7af619..736ca9ce07 100644 --- a/src/nvim/api/keysets.h +++ b/src/nvim/api/keysets.h @@ -112,6 +112,7 @@ typedef struct { String footer_pos; String style; Boolean noautocmd; + Boolean fixed; } Dict(float_config); typedef struct { diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index 70c97be984..0ea2310042 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -833,8 +833,7 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int bool was_space = false; for (size_t i = 0; i < ncells; i++) { repeat++; - if (i == ncells - 1 || attrs[i] != attrs[i + 1] - || strcmp(chunk[i], chunk[i + 1]) != 0) { + if (i == ncells - 1 || attrs[i] != attrs[i + 1] || chunk[i] != chunk[i + 1]) { if (UI_BUF_SIZE - BUF_POS(data) < 2 * (1 + 2 + sizeof(schar_T) + 5 + 5) + 1) { // close to overflowing the redraw buffer. finish this event, // flush, and start a new "grid_line" event at the current position. @@ -859,7 +858,9 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int uint32_t csize = (repeat > 1) ? 3 : ((attrs[i] != last_hl) ? 2 : 1); nelem++; mpack_array(buf, csize); - mpack_str(buf, chunk[i]); + char sc_buf[MAX_SCHAR_SIZE]; + schar_get(sc_buf, chunk[i]); + mpack_str(buf, sc_buf); if (csize >= 2) { mpack_uint(buf, (uint32_t)attrs[i]); if (csize >= 3) { @@ -869,7 +870,7 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int data->ncells_pending += MIN(repeat, 2); last_hl = attrs[i]; repeat = 0; - was_space = strequal(chunk[i], " "); + was_space = chunk[i] == schar_from_ascii(' '); } } // If the last chunk was all spaces, add a clearing chunk even if there are @@ -893,8 +894,10 @@ void remote_ui_raw_line(UI *ui, Integer grid, Integer row, Integer startcol, Int for (int i = 0; i < endcol - startcol; i++) { remote_ui_cursor_goto(ui, row, startcol + i); remote_ui_highlight_set(ui, attrs[i]); - remote_ui_put(ui, chunk[i]); - if (utf_ambiguous_width(utf_ptr2char((char *)chunk[i]))) { + char sc_buf[MAX_SCHAR_SIZE]; + schar_get(sc_buf, chunk[i]); + remote_ui_put(ui, sc_buf); + if (utf_ambiguous_width(utf_ptr2char(sc_buf))) { data->client_col = -1; // force cursor update } } diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 916409b973..0a94b8aafc 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1947,7 +1947,9 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Arena *arena, E } ret = arena_array(arena, 3); size_t off = g->line_offset[(size_t)row] + (size_t)col; - ADD_C(ret, CSTR_AS_OBJ((char *)g->chars[off])); + char *sc_buf = arena_alloc(arena, MAX_SCHAR_SIZE, false); + schar_get(sc_buf, g->chars[off]); + ADD_C(ret, CSTR_AS_OBJ(sc_buf)); int attr = g->attrs[off]; ADD_C(ret, DICTIONARY_OBJ(hl_get_attr_by_id(attr, true, arena, err))); // will not work first time @@ -1963,6 +1965,11 @@ void nvim__screenshot(String path) ui_call_screenshot(path); } +void nvim__invalidate_glyph_cache(void) +{ + schar_cache_clear_force(); +} + Object nvim__unpack(String str, Error *err) FUNC_API_FAST { diff --git a/src/nvim/api/win_config.c b/src/nvim/api/win_config.c index 35b9e73d81..ebdbd896a5 100644 --- a/src/nvim/api/win_config.c +++ b/src/nvim/api/win_config.c @@ -16,7 +16,7 @@ #include "nvim/drawscreen.h" #include "nvim/extmark_defs.h" #include "nvim/globals.h" -#include "nvim/grid_defs.h" +#include "nvim/grid.h" #include "nvim/highlight_group.h" #include "nvim/macros.h" #include "nvim/mbyte.h" @@ -163,6 +163,8 @@ /// - noautocmd: If true then no buffer-related autocommand events such as /// |BufEnter|, |BufLeave| or |BufWinEnter| may fire from /// calling this function. +/// - fixed: If true when anchor is NW or SW, the float window +/// would be kept fixed even if the window would be truncated. /// /// @param[out] err Error details, if any /// @@ -351,7 +353,7 @@ Dictionary nvim_win_get_config(Window window, Error *err) for (size_t i = 0; i < 8; i++) { Array tuple = ARRAY_DICT_INIT; - String s = cstrn_to_string(config->border_chars[i], sizeof(schar_T)); + String s = cstrn_to_string(config->border_chars[i], MAX_SCHAR_SIZE); int hi_id = config->border_hl_ids[i]; char *hi_name = syn_id2name(hi_id); @@ -523,7 +525,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { struct { const char *name; - schar_T chars[8]; + char chars[8][MAX_SCHAR_SIZE]; bool shadow_color; } defaults[] = { { "double", { "╔", "═", "╗", "║", "╝", "═", "╚", "║" }, false }, @@ -534,7 +536,7 @@ static void parse_border_style(Object style, FloatConfig *fconfig, Error *err) { NULL, { { NUL } }, false }, }; - schar_T *chars = fconfig->border_chars; + char (*chars)[MAX_SCHAR_SIZE] = fconfig->border_chars; int *hl_ids = fconfig->border_hl_ids; fconfig->border = true; @@ -848,6 +850,10 @@ static bool parse_float_config(Dict(float_config) *config, FloatConfig *fconfig, fconfig->noautocmd = config->noautocmd; } + if (HAS_KEY_X(config, fixed)) { + fconfig->fixed = config->fixed; + } + return true; #undef HAS_KEY_X } diff --git a/src/nvim/buffer_defs.h b/src/nvim/buffer_defs.h index 3f55dbbc00..ff0fca1a56 100644 --- a/src/nvim/buffer_defs.h +++ b/src/nvim/buffer_defs.h @@ -954,7 +954,7 @@ typedef struct { WinStyle style; bool border; bool shadow; - schar_T border_chars[8]; + char border_chars[8][MAX_SCHAR_SIZE]; int border_hl_ids[8]; int border_attr[8]; bool title; @@ -966,6 +966,7 @@ typedef struct { VirtText footer_chunks; int footer_width; bool noautocmd; + bool fixed; } FloatConfig; #define FLOAT_CONFIG_INIT ((FloatConfig){ .height = 0, .width = 0, \ @@ -975,7 +976,8 @@ typedef struct { .focusable = true, \ .zindex = kZIndexFloatDefault, \ .style = kWinStyleUnused, \ - .noautocmd = false }) + .noautocmd = false, \ + .fixed = false }) // Structure to store last cursor position and topline. Used by check_lnums() // and reset_lnums(). @@ -1241,6 +1243,8 @@ struct window_S { // this window, w_allbuf_opt is for all buffers in this window. winopt_T w_onebuf_opt; winopt_T w_allbuf_opt; + // transform a pointer to a "onebuf" option into a "allbuf" option +#define GLOBAL_WO(p) ((char *)(p) + sizeof(winopt_T)) // A few options have local flags for P_INSECURE. uint32_t w_p_stl_flags; // flags for 'statusline' @@ -1256,9 +1260,6 @@ struct window_S { int w_briopt_list; // additional indent for lists int w_briopt_vcol; // indent for specific column - // transform a pointer to a "onebuf" option into a "allbuf" option -#define GLOBAL_WO(p) ((char *)(p) + sizeof(winopt_T)) - long w_scbind_pos; ScopeDictDictItem w_winvar; ///< Variable for "w:" dictionary. diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 811cfc1eb2..8b4786a98e 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -106,6 +106,8 @@ typedef struct { int c_extra; ///< extra chars, all the same int c_final; ///< final char, mandatory if set + int n_closing; ///< number of chars in fdc which will be closing + bool extra_for_extmark; ///< n_extra set for inline virtual text // saved "extra" items for when draw_state becomes WL_LINE (again) @@ -221,11 +223,11 @@ static int line_putchar(buf_T *buf, LineState *s, schar_T *dest, int maxcells, b if (*p == TAB) { cells = MIN(tabstop_padding(vcol, buf->b_p_ts, buf->b_p_vts_array), maxcells); for (int c = 0; c < cells; c++) { - schar_from_ascii(dest[c], ' '); + dest[c] = schar_from_ascii(' '); } goto done; } else if ((uint8_t)(*p) < 0x80 && u8cc[0] == 0) { - schar_from_ascii(dest[0], *p); + dest[0] = schar_from_ascii(*p); s->prev_c = u8c; } else { if (p_arshape && !p_tbidi && ARABIC_CHAR(u8c)) { @@ -252,10 +254,10 @@ static int line_putchar(buf_T *buf, LineState *s, schar_T *dest, int maxcells, b } else { s->prev_c = u8c; } - schar_from_cc(dest[0], u8c, u8cc); + dest[0] = schar_from_cc(u8c, u8cc); } if (cells > 1) { - dest[1][0] = 0; + dest[1] = 0; } done: s->p += c_len; @@ -348,17 +350,17 @@ static int draw_virt_text_item(buf_T *buf, int col, VirtText vt, HlMode hl_mode, max_col - col, false, vcol); // If we failed to emit a char, we still need to put a space and advance. if (cells < 1) { - schar_from_ascii(linebuf_char[col], ' '); + linebuf_char[col] = schar_from_ascii(' '); cells = 1; } for (int c = 0; c < cells; c++) { linebuf_attr[col++] = attr; } - if (col < max_col && linebuf_char[col][0] == 0) { + if (col < max_col && linebuf_char[col] == 0) { // If the left half of a double-width char is overwritten, // change the right half to a space so that grid redraws properly, // but don't advance the current column. - schar_from_ascii(linebuf_char[col], ' '); + linebuf_char[col] = schar_from_ascii(' '); } vcol += cells; } @@ -384,7 +386,8 @@ static void handle_foldcolumn(win_T *wp, winlinevars_T *wlv) // Allocate a buffer, "wlv->extra[]" may already be in use. xfree(wlv->p_extra_free); wlv->p_extra_free = xmalloc(MAX_MCO * (size_t)fdc + 1); - wlv->n_extra = (int)fill_foldcolumn(wlv->p_extra_free, wp, wlv->foldinfo, wlv->lnum); + wlv->n_extra = (int)fill_foldcolumn(wlv->p_extra_free, wp, wlv->foldinfo, wlv->lnum, + &wlv->n_closing); wlv->p_extra_free[wlv->n_extra] = NUL; wlv->p_extra = wlv->p_extra_free; wlv->c_extra = NUL; @@ -405,7 +408,7 @@ static void handle_foldcolumn(win_T *wp, winlinevars_T *wlv) /// /// Assume monocell characters /// @return number of chars added to \param p -size_t fill_foldcolumn(char *p, win_T *wp, foldinfo_T foldinfo, linenr_T lnum) +size_t fill_foldcolumn(char *p, win_T *wp, foldinfo_T foldinfo, linenr_T lnum, int *n_closing) { int i = 0; int level; @@ -447,16 +450,23 @@ size_t fill_foldcolumn(char *p, win_T *wp, foldinfo_T foldinfo, linenr_T lnum) } } + int n_closing_val = i; + if (closed) { if (symbol != 0) { // rollback previous write char_counter -= (size_t)len; memset(&p[char_counter], ' ', (size_t)len); + n_closing_val--; } len = utf_char2bytes(wp->w_p_fcs_chars.foldclosed, &p[char_counter]); char_counter += (size_t)len; } + if (n_closing) { + *n_closing = n_closing_val; + } + return MAX(char_counter + (size_t)(fdc - i), (size_t)fdc); } @@ -2737,7 +2747,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl wlv.col += n; } else { // Add a blank character to highlight. - schar_from_ascii(linebuf_char[wlv.off], ' '); + linebuf_char[wlv.off] = schar_from_ascii(' '); } if (area_attr == 0 && !has_fold) { // Use attributes from match with highest priority among @@ -2839,7 +2849,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl int col_stride = wp->w_p_rl ? -1 : 1; while (wp->w_p_rl ? wlv.col >= 0 : wlv.col < grid->cols) { - schar_from_ascii(linebuf_char[wlv.off], ' '); + linebuf_char[wlv.off] = schar_from_ascii(' '); linebuf_vcol[wlv.off] = MAXCOL; wlv.col += col_stride; if (draw_color_col) { @@ -2873,7 +2883,7 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl // logical line int n = wp->w_p_rl ? -1 : 1; while (wlv.col >= 0 && wlv.col < grid->cols) { - schar_from_ascii(linebuf_char[wlv.off], ' '); + linebuf_char[wlv.off] = schar_from_ascii(' '); linebuf_attr[wlv.off] = wlv.vcol >= TERM_ATTRS_MAX ? 0 : term_attrs[wlv.vcol]; linebuf_vcol[wlv.off] = wlv.vcol; wlv.off += n; @@ -2973,9 +2983,9 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl wlv.col--; } if (mb_utf8) { - schar_from_cc(linebuf_char[wlv.off], mb_c, u8cc); + linebuf_char[wlv.off] = schar_from_cc(mb_c, u8cc); } else { - schar_from_ascii(linebuf_char[wlv.off], (char)c); + linebuf_char[wlv.off] = schar_from_ascii((char)c); } if (multi_attr) { linebuf_attr[wlv.off] = multi_attr; @@ -2986,12 +2996,20 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl linebuf_vcol[wlv.off] = wlv.vcol; + if (wlv.draw_state == WL_FOLD) { + linebuf_vcol[wlv.off] = -2; + if (wlv.n_closing > 0) { + linebuf_vcol[wlv.off] = -3; + wlv.n_closing--; + } + } + if (utf_char2cells(mb_c) > 1) { // Need to fill two screen columns. wlv.off++; wlv.col++; // UTF-8: Put a 0 in the second screen char. - linebuf_char[wlv.off][0] = 0; + linebuf_char[wlv.off] = 0; linebuf_attr[wlv.off] = linebuf_attr[wlv.off - 1]; if (wlv.draw_state > WL_STC && wlv.filler_todo <= 0) { wlv.vcol++; diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index f71a47a596..e3e9fc8170 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -444,6 +444,15 @@ int update_screen(void) display_tick++; // let syntax code know we're in a next round of // display updating + // glyph cache full, very rare + if (schar_cache_clear_if_full()) { + // must use CLEAR, as the contents of screen buffers cannot be + // compared to their previous state here. + // TODO(bfredl): if start to cache schar_T values in places (like fcs/lcs) + // we need to revalidate these here as well! + type = MAX(type, UPD_CLEAR); + } + // Tricky: vim code can reset msg_scrolled behind our back, so need // separate bookkeeping for now. if (msg_did_scroll) { @@ -736,7 +745,10 @@ static void win_redr_border(win_T *wp) ScreenGrid *grid = &wp->w_grid_alloc; - schar_T *chars = wp->w_float_config.border_chars; + schar_T chars[8]; + for (int i = 0; i < 8; i++) { + chars[i] = schar_from_str(wp->w_float_config.border_chars[i]); + } int *attrs = wp->w_float_config.border_attr; int *adj = wp->w_border_adj; @@ -770,7 +782,7 @@ static void win_redr_border(win_T *wp) grid_puts_line_flush(false); } if (adj[1]) { - int ic = (i == 0 && !adj[0] && chars[2][0]) ? 2 : 3; + int ic = (i == 0 && !adj[0] && chars[2]) ? 2 : 3; grid_puts_line_start(grid, i + adj[0]); grid_put_schar(grid, i + adj[0], icol + adj[3], chars[ic], attrs[ic]); grid_puts_line_flush(false); @@ -784,7 +796,7 @@ static void win_redr_border(win_T *wp) } for (int i = 0; i < icol; i++) { - int ic = (i == 0 && !adj[3] && chars[6][0]) ? 6 : 5; + int ic = (i == 0 && !adj[3] && chars[6]) ? 6 : 5; grid_put_schar(grid, irow + adj[0], i + adj[3], chars[ic], attrs[ic]); } diff --git a/src/nvim/eval.c b/src/nvim/eval.c index a279b6d051..21c7cdee7d 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -1271,8 +1271,11 @@ void *call_func_retlist(const char *func, int argc, typval_T *argv) /// it in "*cp". Doesn't give error messages. int eval_foldexpr(win_T *wp, int *cp) { + const sctx_T saved_sctx = current_sctx; const bool use_sandbox = was_set_insecurely(wp, "foldexpr", OPT_LOCAL); + char *arg = wp->w_p_fde; + current_sctx = wp->w_p_script_ctx[WV_FDE].script_ctx; emsg_off++; if (use_sandbox) { @@ -1309,6 +1312,7 @@ int eval_foldexpr(win_T *wp, int *cp) } textlock--; clear_evalarg(&EVALARG_EVALUATE, NULL); + current_sctx = saved_sctx; return (int)retval; } diff --git a/src/nvim/grid.c b/src/nvim/grid.c index fa7f270172..cf6cd2f04e 100644 --- a/src/nvim/grid.c +++ b/src/nvim/grid.c @@ -17,6 +17,7 @@ #include "nvim/arabic.h" #include "nvim/buffer_defs.h" +#include "nvim/drawscreen.h" #include "nvim/globals.h" #include "nvim/grid.h" #include "nvim/highlight.h" @@ -36,6 +37,15 @@ // Per-cell attributes static size_t linebuf_size = 0; +// Used to cache glyphs which doesn't fit an a sizeof(schar_T) length UTF-8 string. +// Then it instead stores an index into glyph_cache.keys[] which is a flat char array. +// The hash part is used by schar_from_buf() to quickly lookup glyphs which already +// has been interned. schar_get() should used to convert a schar_T value +// back to a string buffer. +// +// The maximum byte size of a glyph is MAX_SCHAR_SIZE (including the final NUL). +static Set(glyph) glyph_cache = SET_INIT; + /// Determine if dedicated window grid should be used or the default_grid /// /// If UI did not request multigrid support, draw all windows on the @@ -56,25 +66,119 @@ void grid_adjust(ScreenGrid **grid, int *row_off, int *col_off) } /// Put a unicode char, and up to MAX_MCO composing chars, in a screen cell. -int schar_from_cc(char *p, int c, int u8cc[MAX_MCO]) +schar_T schar_from_cc(int c, int u8cc[MAX_MCO]) { - int len = utf_char2bytes(c, p); + char buf[MAX_SCHAR_SIZE]; + int len = utf_char2bytes(c, buf); for (int i = 0; i < MAX_MCO; i++) { if (u8cc[i] == 0) { break; } - len += utf_char2bytes(u8cc[i], p + len); + len += utf_char2bytes(u8cc[i], buf + len); + } + buf[len] = 0; + return schar_from_buf(buf, (size_t)len); +} + +schar_T schar_from_str(char *str) +{ + if (str == NULL) { + return 0; + } + return schar_from_buf(str, strlen(str)); +} + +/// @param buf need not be NUL terminated, but may not contain embedded NULs. +/// +/// caller must ensure len < MAX_SCHAR_SIZE (not =, as NUL needs a byte) +schar_T schar_from_buf(const char *buf, size_t len) +{ + assert(len < MAX_SCHAR_SIZE); + if (len <= 4) { + schar_T sc = 0; + memcpy((char *)&sc, buf, len); + return sc; + } else { + String str = { .data = (char *)buf, .size = len }; + + MHPutStatus status; + uint32_t idx = set_put_idx(glyph, &glyph_cache, str, &status); + assert(idx < 0xFFFFFF); +#ifdef ORDER_BIG_ENDIAN + return idx + ((uint32_t)0xFF << 24); +#else + return 0xFF + (idx << 8); +#endif + } +} + +/// Check if cache is full, and if it is, clear it. +/// +/// This should normally only be called in update_screen() +/// +/// @return true if cache was clered, and all your screen buffers now are hosed +/// and you need to use UPD_CLEAR +bool schar_cache_clear_if_full(void) +{ + // note: critical max is really (1<<24)-1. This gives us some marginal + // until next time update_screen() is called + if (glyph_cache.h.n_keys > (1<<21)) { + set_clear(glyph, &glyph_cache); + return true; + } + return false; +} + +/// For testing. The condition in schar_cache_clear_force is hard to +/// reach, so this function can be used to force a cache clear in a test. +void schar_cache_clear_force(void) +{ + set_clear(glyph, &glyph_cache); + must_redraw = UPD_CLEAR; +} + +bool schar_high(schar_T sc) +{ +#ifdef ORDER_BIG_ENDIAN + return ((sc & 0xFF000000) == 0xFF000000); +#else + return ((sc & 0xFF) == 0xFF); +#endif +} + +void schar_get(char *buf_out, schar_T sc) +{ + if (schar_high(sc)) { +#ifdef ORDER_BIG_ENDIAN + uint32_t idx = sc & (0x00FFFFFF); +#else + uint32_t idx = sc >> 8; +#endif + if (idx >= glyph_cache.h.n_keys) { + abort(); + } + xstrlcpy(buf_out, &glyph_cache.keys[idx], 32); + } else { + memcpy(buf_out, (char *)&sc, 4); + buf_out[4] = NUL; } - p[len] = 0; - return len; } +/// @return ascii char or NUL if not ascii +char schar_get_ascii(schar_T sc) +{ +#ifdef ORDER_BIG_ENDIAN + return (!(sc & 0x80FFFFFF)) ? *(char *)&sc : NUL; +#else + return (sc < 0x80) ? (char)sc : NUL; +#endif +} /// clear a line in the grid starting at "off" until "width" characters /// are cleared. void grid_clear_line(ScreenGrid *grid, size_t off, int width, bool valid) { for (int col = 0; col < width; col++) { - schar_from_ascii(grid->chars[off + (size_t)col], ' '); + grid->chars[off + (size_t)col] = schar_from_ascii(' '); } int fill = valid ? 0 : -1; (void)memset(grid->attrs + off, fill, (size_t)width * sizeof(sattr_T)); @@ -93,7 +197,7 @@ bool grid_invalid_row(ScreenGrid *grid, int row) static int line_off2cells(schar_T *line, size_t off, size_t max_off) { - return (off + 1 < max_off && line[off + 1][0] == 0) ? 2 : 1; + return (off + 1 < max_off && line[off + 1] == 0) ? 2 : 1; } /// Return number of display cells for char at grid->chars[off]. @@ -124,7 +228,7 @@ int grid_fix_col(ScreenGrid *grid, int col, int row) col += coloff; if (grid->chars != NULL && col > 0 - && grid->chars[grid->line_offset[row] + (size_t)col][0] == 0) { + && grid->chars[grid->line_offset[row] + (size_t)col] == 0) { return col - 1 - coloff; } return col - coloff; @@ -155,7 +259,7 @@ void grid_getbytes(ScreenGrid *grid, int row, int col, char *bytes, int *attrp) if (attrp != NULL) { *attrp = grid->attrs[off]; } - schar_copy(bytes, grid->chars[off]); + schar_get(bytes, grid->chars[off]); } /// put string '*text' on the window grid at position 'row' and 'col', with @@ -185,12 +289,12 @@ void grid_puts_line_start(ScreenGrid *grid, int row) put_dirty_grid = grid; } -void grid_put_schar(ScreenGrid *grid, int row, int col, char *schar, int attr) +void grid_put_schar(ScreenGrid *grid, int row, int col, schar_T schar, int attr) { assert(put_dirty_row == row); size_t off = grid->line_offset[row] + (size_t)col; - if (grid->attrs[off] != attr || schar_cmp(grid->chars[off], schar) || rdb_flags & RDB_NODELTA) { - schar_copy(grid->chars[off], schar); + if (grid->attrs[off] != attr || grid->chars[off] != schar || rdb_flags & RDB_NODELTA) { + grid->chars[off] = schar; grid->attrs[off] = attr; put_dirty_first = MIN(put_dirty_first, col); @@ -293,10 +397,12 @@ int grid_puts_len(ScreenGrid *grid, const char *text, int textlen, int row, int } schar_T buf; - schar_from_cc(buf, u8c, u8cc); + // TODO(bfredl): why not just keep the original byte sequence. arabshape is + // an edge case, treat it as such.. + buf = schar_from_cc(u8c, u8cc); - int need_redraw = schar_cmp(grid->chars[off], buf) - || (mbyte_cells == 2 && grid->chars[off + 1][0] != 0) + int need_redraw = grid->chars[off] != buf + || (mbyte_cells == 2 && grid->chars[off + 1] != 0) || grid->attrs[off] != attr || exmode_active || rdb_flags & RDB_NODELTA; @@ -320,15 +426,15 @@ int grid_puts_len(ScreenGrid *grid, const char *text, int textlen, int row, int // When at the start of the text and overwriting the right half of a // two-cell character in the same grid, truncate that into a '>'. - if (ptr == text && col > 0 && grid->chars[off][0] == 0) { - schar_from_ascii(grid->chars[off - 1], '>'); + if (ptr == text && col > 0 && grid->chars[off] == 0) { + grid->chars[off - 1] = schar_from_ascii('>'); } - schar_copy(grid->chars[off], buf); + grid->chars[off] = buf; grid->attrs[off] = attr; grid->vcols[off] = -1; if (mbyte_cells == 2) { - grid->chars[off + 1][0] = 0; + grid->chars[off + 1] = 0; grid->attrs[off + 1] = attr; grid->vcols[off + 1] = -1; } @@ -429,12 +535,12 @@ void grid_fill(ScreenGrid *grid, int start_row, int end_row, int start_col, int int dirty_last = 0; int col = start_col; - schar_from_char(sc, c1); + sc = schar_from_char(c1); size_t lineoff = grid->line_offset[row]; for (col = start_col; col < end_col; col++) { size_t off = lineoff + (size_t)col; - if (schar_cmp(grid->chars[off], sc) || grid->attrs[off] != attr || rdb_flags & RDB_NODELTA) { - schar_copy(grid->chars[off], sc); + if (grid->chars[off] != sc || grid->attrs[off] != attr || rdb_flags & RDB_NODELTA) { + grid->chars[off] = sc; grid->attrs[off] = attr; if (dirty_first == INT_MAX) { dirty_first = col; @@ -443,7 +549,7 @@ void grid_fill(ScreenGrid *grid, int start_row, int end_row, int start_col, int } grid->vcols[off] = -1; if (col == start_col) { - schar_from_char(sc, c2); + sc = schar_from_char(c2); } } if (dirty_last > dirty_first) { @@ -483,11 +589,10 @@ void grid_fill(ScreenGrid *grid, int start_row, int end_row, int start_col, int static int grid_char_needs_redraw(ScreenGrid *grid, size_t off_from, size_t off_to, int cols) { return (cols > 0 - && ((schar_cmp(linebuf_char[off_from], grid->chars[off_to]) + && ((linebuf_char[off_from] != grid->chars[off_to] || linebuf_attr[off_from] != grid->attrs[off_to] || (line_off2cells(linebuf_char, off_from, off_from + (size_t)cols) > 1 - && schar_cmp(linebuf_char[off_from + 1], - grid->chars[off_to + 1]))) + && linebuf_char[off_from + 1] != grid->chars[off_to + 1])) || rdb_flags & RDB_NODELTA)); } @@ -544,7 +649,7 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle if (wp->w_p_nu && wp->w_p_rnu) { // do not overwrite the line number, change "123 text" to // "123<<<xt". - while (skip < max_off_from && ascii_isdigit(*linebuf_char[off])) { + while (skip < max_off_from && ascii_isdigit(schar_get_ascii(linebuf_char[off]))) { off++; skip++; } @@ -554,9 +659,9 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle if (line_off2cells(linebuf_char, off, max_off_from) > 1) { // When the first half of a double-width character is // overwritten, change the second half to a space. - schar_from_ascii(linebuf_char[off + 1], ' '); + linebuf_char[off + 1] = schar_from_ascii(' '); } - schar_from_ascii(linebuf_char[off], '<'); + linebuf_char[off] = schar_from_ascii('<'); linebuf_attr[off] = HL_ATTR(HLF_AT); off++; } @@ -565,8 +670,7 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle if (rlflag) { // Clear rest first, because it's left of the text. if (clear_width > 0) { - while (col <= endcol && grid->chars[off_to][0] == ' ' - && grid->chars[off_to][1] == NUL + while (col <= endcol && grid->chars[off_to] == schar_from_ascii(' ') && grid->attrs[off_to] == bg_attr) { off_to++; col++; @@ -619,9 +723,9 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle clear_next = true; } - schar_copy(grid->chars[off_to], linebuf_char[off_from]); + grid->chars[off_to] = linebuf_char[off_from]; if (char_cells == 2) { - schar_copy(grid->chars[off_to + 1], linebuf_char[off_from + 1]); + grid->chars[off_to + 1] = linebuf_char[off_from + 1]; } grid->attrs[off_to] = linebuf_attr[off_from]; @@ -645,7 +749,7 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle if (clear_next) { // Clear the second half of a double-wide character of which the left // half was overwritten with a single-wide character. - schar_from_ascii(grid->chars[off_to], ' '); + grid->chars[off_to] = schar_from_ascii(' '); end_dirty++; } @@ -654,12 +758,10 @@ void grid_put_linebuf(ScreenGrid *grid, int row, int coloff, int endcol, int cle // blank out the rest of the line // TODO(bfredl): we could cache winline widths while (col < clear_width) { - if (grid->chars[off_to][0] != ' ' - || grid->chars[off_to][1] != NUL + if (grid->chars[off_to] != schar_from_ascii(' ') || grid->attrs[off_to] != bg_attr || rdb_flags & RDB_NODELTA) { - grid->chars[off_to][0] = ' '; - grid->chars[off_to][1] = NUL; + grid->chars[off_to] = schar_from_ascii(' '); grid->attrs[off_to] = bg_attr; if (start_dirty == -1) { start_dirty = col; diff --git a/src/nvim/grid.h b/src/nvim/grid.h index 0db97345d1..9cdc6c6677 100644 --- a/src/nvim/grid.h +++ b/src/nvim/grid.h @@ -33,30 +33,25 @@ EXTERN colnr_T *linebuf_vcol INIT(= NULL); // screen grid. /// Put a ASCII character in a screen cell. -static inline void schar_from_ascii(char *p, const char c) -{ - p[0] = c; - p[1] = 0; -} +/// +/// If `x` is a compile time constant, schar_from_ascii(x) will also be. +/// But the specific value varies per plattform. +#ifdef ORDER_BIG_ENDIAN +# define schar_from_ascii(x) ((schar_T)((x) << 24)) +#else +# define schar_from_ascii(x) ((schar_T)(x)) +#endif /// Put a unicode character in a screen cell. -static inline int schar_from_char(char *p, int c) -{ - int len = utf_char2bytes(c, p); - p[len] = NUL; - return len; -} - -/// compare the contents of two screen cells. -static inline int schar_cmp(char *sc1, char *sc2) -{ - return strncmp(sc1, sc2, sizeof(schar_T)); -} - -/// copy the contents of screen cell `sc2` into cell `sc1` -static inline void schar_copy(char *sc1, char *sc2) +static inline schar_T schar_from_char(int c) { - xstrlcpy(sc1, sc2, sizeof(schar_T)); + schar_T sc = 0; + if (c >= 0x200000) { + // TODO(bfredl): this must NEVER happen, even if the file contained overlong sequences + c = 0xFFFD; + } + utf_char2bytes(c, (char *)&sc); + return sc; } #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/grid_defs.h b/src/nvim/grid_defs.h index 4ad7d4cdb4..b0d1dafd29 100644 --- a/src/nvim/grid_defs.h +++ b/src/nvim/grid_defs.h @@ -9,9 +9,13 @@ #include "nvim/types.h" #define MAX_MCO 6 // fixed value for 'maxcombine' +// Includes final NUL. at least 4*(MAX_MCO+1)+1 +#define MAX_SCHAR_SIZE 32 -// The characters and attributes drawn on grids. -typedef char schar_T[(MAX_MCO + 1) * 4 + 1]; +// if data[0] is 0xFF, then data[1..4] is a 24-bit index (in machine endianess) +// otherwise it must be a UTF-8 string of length maximum 4 (no NUL when n=4) + +typedef uint32_t schar_T; typedef int sattr_T; enum { @@ -41,6 +45,9 @@ enum { /// /// vcols[] contains the virtual columns in the line. -1 means not available /// (below last line), MAXCOL means after the end of the line. +/// -2 or -3 means in fold column and a mouse click should: +/// -2: open a fold +/// -3: close a fold /// /// line_offset[n] is the offset from chars[], attrs[] and vcols[] for the start /// of line 'n'. These offsets are in general not linear, as full screen scrolling diff --git a/src/nvim/highlight.c b/src/nvim/highlight.c index df4bffdac3..3728db31d8 100644 --- a/src/nvim/highlight.c +++ b/src/nvim/highlight.c @@ -86,7 +86,7 @@ static int get_attr_entry(HlEntry entry) } retry: {} - MhPutStatus status; + MHPutStatus status; uint32_t k = set_put_idx(HlEntry, &attr_entries, entry, &status); if (status == kMHExisting) { return (int)k; diff --git a/src/nvim/map.c b/src/nvim/map.c index e1d0646083..54f1969df8 100644 --- a/src/nvim/map.c +++ b/src/nvim/map.c @@ -47,25 +47,6 @@ static inline uint32_t hash_cstr_t(const char *s) #define equal_cstr_t strequal -// when used as a key, String doesn't need to be NUL terminated, -// and can also contain embedded NUL:s as part of the data. -static inline uint32_t hash_String(String s) -{ - uint32_t h = 0; - for (size_t i = 0; i < s.size; i++) { - h = (h << 5) - h + (uint8_t)s.data[i]; - } - return h; -} - -static inline bool equal_String(String a, String b) -{ - if (a.size != b.size) { - return false; - } - return memcmp(a.data, b.data, a.size) == 0; -} - static inline uint32_t hash_HlEntry(HlEntry ae) { const uint8_t *data = (const uint8_t *)&ae; diff --git a/src/nvim/map.h b/src/nvim/map.h index 23a5ea36a3..2d5517c552 100644 --- a/src/nvim/map.h +++ b/src/nvim/map.h @@ -18,6 +18,25 @@ typedef const char *cstr_t; typedef void *ptr_t; +// when used as a key, String doesn't need to be NUL terminated, +// and can also contain embedded NUL:s as part of the data. +static inline uint32_t hash_String(String s) +{ + uint32_t h = 0; + for (size_t i = 0; i < s.size; i++) { + h = (h << 5) - h + (uint8_t)s.data[i]; + } + return h; +} + +static inline bool equal_String(String a, String b) +{ + if (a.size != b.size) { + return false; + } + return memcmp(a.data, b.data, a.size) == 0; +} + #define Set(type) Set_##type #define Map(T, U) Map_##T##U #define PMap(T) Map(T, ptr_t) @@ -57,7 +76,7 @@ typedef enum { kMHExisting = 0, kMHNewKeyDidFit, kMHNewKeyRealloc, -} MhPutStatus; +} MHPutStatus; void mh_clear(MapHash *h); void mh_realloc(MapHash *h, uint32_t n_min_buckets); @@ -65,20 +84,22 @@ void mh_realloc(MapHash *h, uint32_t n_min_buckets); // layer 1: key type specific defs // This is all need for sets. -#define KEY_DECLS(T) \ +#define MH_DECLS(T, K, K_query) \ typedef struct { \ MapHash h; \ - T *keys; \ + K *keys; \ } Set(T); \ \ - uint32_t mh_find_bucket_##T(Set(T) *set, T key, bool put); \ - uint32_t mh_get_##T(Set(T) *set, T key); \ + uint32_t mh_find_bucket_##T(Set(T) *set, K_query key, bool put); \ + uint32_t mh_get_##T(Set(T) *set, K_query key); \ void mh_rehash_##T(Set(T) *set); \ - uint32_t mh_put_##T(Set(T) *set, T key, MhPutStatus *new); \ + uint32_t mh_put_##T(Set(T) *set, K_query key, MHPutStatus *new); \ + +#define KEY_DECLS(T) \ + MH_DECLS(T, T, T) \ uint32_t mh_delete_##T(Set(T) *set, T *key); \ - \ static inline bool set_put_##T(Set(T) *set, T key, T **key_alloc) { \ - MhPutStatus status; \ + MHPutStatus status; \ uint32_t k = mh_put_##T(set, key, &status); \ if (key_alloc) { \ *key_alloc = &set->keys[k]; \ @@ -120,6 +141,7 @@ void mh_realloc(MapHash *h, uint32_t n_min_buckets); #define quasiquote(x, y) x##y +MH_DECLS(glyph, char, String) KEY_DECLS(int) KEY_DECLS(cstr_t) KEY_DECLS(ptr_t) diff --git a/src/nvim/map_glyph_cache.c b/src/nvim/map_glyph_cache.c new file mode 100644 index 0000000000..6dcbfe0532 --- /dev/null +++ b/src/nvim/map_glyph_cache.c @@ -0,0 +1,102 @@ +// 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 + +// Specialized version of Set() where interned strings is stored in a compact, +// NUL-separated char array. +// `String key` lookup keys don't need to be NULL terminated, but they +// must not contain embedded NUL:s. When reading a key from set->keys, they +// are always NUL terminated, though. Thus, it is enough to store an index into +// this array, and use strlen(), to retrive an interned key. + +#include "nvim/api/private/helpers.h" +#include "nvim/map.h" + +uint32_t mh_find_bucket_glyph(Set(glyph) *set, String key, bool put) +{ + MapHash *h = &set->h; + uint32_t step = 0; + uint32_t mask = h->n_buckets - 1; + uint32_t k = hash_String(key); + uint32_t i = k & mask; + uint32_t last = i; + uint32_t site = put ? last : MH_TOMBSTONE; + while (!mh_is_empty(h, i)) { + if (mh_is_del(h, i)) { + if (site == last) { + site = i; + } + } else if (equal_String(cstr_as_string(&set->keys[h->hash[i] - 1]), key)) { + return i; + } + i = (i + (++step)) & mask; + if (i == last) { + abort(); + } + } + if (site == last) { + site = i; + } + return site; +} + +/// @return index into set->keys if found, MH_TOMBSTONE otherwise +uint32_t mh_get_glyph(Set(glyph) *set, String key) +{ + if (set->h.n_buckets == 0) { + return MH_TOMBSTONE; + } + uint32_t idx = mh_find_bucket_glyph(set, key, false); + return (idx != MH_TOMBSTONE) ? set->h.hash[idx] - 1 : MH_TOMBSTONE; +} + +void mh_rehash_glyph(Set(glyph) *set) +{ + // assume the format of set->keys, i e NUL terminated strings + for (uint32_t k = 0; k < set->h.n_keys; k += (uint32_t)strlen(&set->keys[k]) + 1) { + uint32_t idx = mh_find_bucket_glyph(set, cstr_as_string(&set->keys[k]), true); + // there must be tombstones when we do a rehash + if (!mh_is_empty((&set->h), idx)) { + abort(); + } + set->h.hash[idx] = k + 1; + } + set->h.n_occupied = set->h.size = set->h.n_keys; +} + +uint32_t mh_put_glyph(Set(glyph) *set, String key, MHPutStatus *new) +{ + MapHash *h = &set->h; + // Might rehash ahead of time if "key" already existed. But it was + // going to happen soon anyway. + if (h->n_occupied >= h->upper_bound) { + mh_realloc(h, h->n_buckets + 1); + mh_rehash_glyph(set); + } + + uint32_t idx = mh_find_bucket_glyph(set, key, true); + + if (mh_is_either(h, idx)) { + h->size++; + h->n_occupied++; + + uint32_t size = (uint32_t)key.size + 1; // NUL takes space + uint32_t pos = h->n_keys; + h->n_keys += size; + if (h->n_keys > h->keys_capacity) { + h->keys_capacity = MAX(h->keys_capacity * 2, 64); + set->keys = xrealloc(set->keys, h->keys_capacity * sizeof(char)); + *new = kMHNewKeyRealloc; + } else { + *new = kMHNewKeyDidFit; + } + memcpy(&set->keys[pos], key.data, key.size); + set->keys[pos + key.size] = NUL; + h->hash[idx] = pos + 1; + return pos; + } else { + *new = kMHExisting; + uint32_t pos = h->hash[idx] - 1; + assert(equal_String(cstr_as_string(&set->keys[pos]), key)); + return pos; + } +} diff --git a/src/nvim/map_key_impl.c.h b/src/nvim/map_key_impl.c.h index 7e7b2f74fe..4d060f5fb8 100644 --- a/src/nvim/map_key_impl.c.h +++ b/src/nvim/map_key_impl.c.h @@ -80,7 +80,7 @@ void KEY_NAME(mh_rehash_)(SET_TYPE *set) /// if new item, indicates if keys[] was resized. /// /// @return keys index -uint32_t KEY_NAME(mh_put_)(SET_TYPE *set, KEY_TYPE key, MhPutStatus *new) +uint32_t KEY_NAME(mh_put_)(SET_TYPE *set, KEY_TYPE key, MHPutStatus *new) { MapHash *h = &set->h; // Might rehash ahead of time if "key" already existed. But it was diff --git a/src/nvim/map_value_impl.c.h b/src/nvim/map_value_impl.c.h index fffda280f0..8f07bd8fa7 100644 --- a/src/nvim/map_value_impl.c.h +++ b/src/nvim/map_value_impl.c.h @@ -28,7 +28,7 @@ VALUE_TYPE *MAP_NAME(map_ref_)(MAP_TYPE *map, KEY_TYPE key, KEY_TYPE **key_alloc VALUE_TYPE *MAP_NAME(map_put_ref_)(MAP_TYPE *map, KEY_TYPE key, KEY_TYPE **key_alloc, bool *new_item) { - MhPutStatus status; + MHPutStatus status; uint32_t k = KEY_NAME(mh_put_)(&map->set, key, &status); if (status != kMHExisting) { if (status == kMHNewKeyRealloc) { diff --git a/src/nvim/mouse.c b/src/nvim/mouse.c index b8c80cadf5..e35385dd43 100644 --- a/src/nvim/mouse.c +++ b/src/nvim/mouse.c @@ -1849,7 +1849,6 @@ static void mouse_check_grid(colnr_T *vcolp, int *flagsp) int click_grid = mouse_grid; int click_row = mouse_row; int click_col = mouse_col; - int mouse_char = ' '; int max_row = Rows; int max_col = Columns; bool multigrid = ui_has(kUIMultigrid); @@ -1864,7 +1863,6 @@ static void mouse_check_grid(colnr_T *vcolp, int *flagsp) if (wp && mouse_row >= 0 && mouse_row < max_row && mouse_col >= 0 && mouse_col < max_col) { ScreenGrid *gp = multigrid ? &wp->w_grid_alloc : &default_grid; - int fdc = win_fdccol_count(wp); int use_row = multigrid && mouse_grid == 0 ? click_row : mouse_row; int use_col = multigrid && mouse_grid == 0 ? click_col : mouse_col; @@ -1901,22 +1899,12 @@ static void mouse_check_grid(colnr_T *vcolp, int *flagsp) // concealed characters. *vcolp = col_from_screen; } - - // Remember the character under the mouse, might be one of foldclose or - // foldopen fillchars in the fold column. - mouse_char = utf_ptr2char((char *)gp->chars[off]); - } - - // Check for position outside of the fold column. - if (wp->w_p_rl ? click_col < wp->w_width_inner - fdc : - click_col >= fdc + (cmdwin_type == 0 ? 0 : 1)) { - mouse_char = ' '; } } - if (wp && mouse_char == wp->w_p_fcs_chars.foldclosed) { + if (col_from_screen == -2) { *flagsp |= MOUSE_FOLD_OPEN; - } else if (mouse_char != ' ') { + } else if (col_from_screen == -3) { *flagsp |= MOUSE_FOLD_CLOSE; } } diff --git a/src/nvim/msgpack_rpc/unpacker.c b/src/nvim/msgpack_rpc/unpacker.c index c3b1022db2..37e32729cc 100644 --- a/src/nvim/msgpack_rpc/unpacker.c +++ b/src/nvim/msgpack_rpc/unpacker.c @@ -9,6 +9,7 @@ #include "mpack/conv.h" #include "nvim/api/private/helpers.h" #include "nvim/ascii.h" +#include "nvim/grid.h" #include "nvim/macros.h" #include "nvim/memory.h" #include "nvim/msgpack_rpc/channel_defs.h" @@ -497,13 +498,13 @@ redo: if (g->icell == g->ncells - 1 && cellsize == 1 && cellbuf[0] == ' ' && repeat > 1) { g->clear_width = repeat; } else { + schar_T sc = schar_from_buf(cellbuf, cellsize); for (int r = 0; r < repeat; r++) { if (g->coloff >= (int)grid_line_buf_size) { p->state = -1; return false; } - memcpy(grid_line_buf_char[g->coloff], cellbuf, cellsize); - grid_line_buf_char[g->coloff][cellsize] = NUL; + grid_line_buf_char[g->coloff] = sc; grid_line_buf_attr[g->coloff++] = g->cur_attr; } } diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 4280d62a95..d730f247a9 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -4513,7 +4513,7 @@ static void nv_replace(cmdarg_T *cap) // Visual mode "r" if (VIsual_active) { if (got_int) { - reset_VIsual(); + got_int = false; } if (had_ctrl_v) { // Use a special (negative) number to make a difference between a diff --git a/src/nvim/option.c b/src/nvim/option.c index ae4e7c1fda..c34999ed32 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -1915,6 +1915,9 @@ bool parse_winhl_opt(win_T *wp) char *commap = xstrchrnul(hi, ','); size_t len = (size_t)(commap - hi); int hl_id = len ? syn_check_group(hi, len) : -1; + if (hl_id == 0) { + return false; + } int hl_id_link = nlen ? syn_check_group(p, nlen) : 0; HlAttrs attrs = HLATTRS_INIT; @@ -1967,6 +1970,10 @@ void set_option_sctx_idx(int opt_idx, int opt_flags, sctx_T script_ctx) curbuf->b_p_script_ctx[indir & PV_MASK] = last_set; } else if (indir & PV_WIN) { curwin->w_p_script_ctx[indir & PV_MASK] = last_set; + if (both) { + // also setting the "all buffers" value + curwin->w_allbuf_opt.wo_script_ctx[indir & PV_MASK] = last_set; + } } } } diff --git a/src/nvim/os/pty_process_win.c b/src/nvim/os/pty_process_win.c index abeb020645..763d30d4a2 100644 --- a/src/nvim/os/pty_process_win.c +++ b/src/nvim/os/pty_process_win.c @@ -256,9 +256,9 @@ static int build_cmd_line(char **argv, wchar_t **cmd_line, bool is_cmdexe) QUEUE_FOREACH(q, &args_q, { ArgNode *arg_node = QUEUE_DATA(q, ArgNode, node); xstrlcat(utf8_cmd_line, arg_node->arg, utf8_cmd_line_len); + QUEUE_REMOVE(q); xfree(arg_node->arg); xfree(arg_node); - QUEUE_REMOVE(q); if (!QUEUE_EMPTY(&args_q)) { xstrlcat(utf8_cmd_line, " ", utf8_cmd_line_len); } diff --git a/src/nvim/regexp.c b/src/nvim/regexp.c index 1d0a987780..14c7e56e97 100644 --- a/src/nvim/regexp.c +++ b/src/nvim/regexp.c @@ -59,11 +59,7 @@ #define un_Magic(x) ((x) + 256) #define is_Magic(x) ((x) < 0) -// We should define ftpr as a pointer to a function returning a pointer to -// a function returning a pointer to a function ... -// This is impossible, so we declare a pointer to a function returning a -// pointer to a function returning void. This should work for all compilers. -typedef void (*(*fptr_T)(int *, int))(void); +typedef void (*fptr_T)(int *, int); static int no_Magic(int x) { @@ -1494,34 +1490,14 @@ static inline char *cstrchr(const char *const s, const int c) // regsub stuff // //////////////////////////////////////////////////////////////// -// This stuff below really confuses cc on an SGI -- webb - -static fptr_T do_upper(int *d, int c) +static void do_upper(int *d, int c) { *d = mb_toupper(c); - - return (fptr_T)NULL; -} - -static fptr_T do_Upper(int *d, int c) -{ - *d = mb_toupper(c); - - return (fptr_T)do_Upper; } -static fptr_T do_lower(int *d, int c) +static void do_lower(int *d, int c) { *d = mb_tolower(c); - - return (fptr_T)NULL; -} - -static fptr_T do_Lower(int *d, int c) -{ - *d = mb_tolower(c); - - return (fptr_T)do_Lower; } /// regtilde(): Replace tildes in the pattern by the old pattern. @@ -1886,16 +1862,16 @@ static int vim_regsub_both(char *source, typval_T *expr, char *dest, int destlen } else if (vim_strchr("uUlLeE", (uint8_t)(*src))) { switch (*src++) { case 'u': - func_one = (fptr_T)do_upper; + func_one = do_upper; continue; case 'U': - func_all = (fptr_T)do_Upper; + func_all = do_upper; continue; case 'l': - func_one = (fptr_T)do_lower; + func_one = do_lower; continue; case 'L': - func_all = (fptr_T)do_Lower; + func_all = do_lower; continue; case 'e': case 'E': @@ -1954,11 +1930,13 @@ static int vim_regsub_both(char *source, typval_T *expr, char *dest, int destlen } else { c = utf_ptr2char(src - 1); } + // Write to buffer, if copy is set. if (func_one != NULL) { - func_one = (fptr_T)(func_one(&cc, c)); + func_one(&cc, c); + func_one = NULL; } else if (func_all != NULL) { - func_all = (fptr_T)(func_all(&cc, c)); + func_all(&cc, c); } else { // just copy cc = c; @@ -2061,11 +2039,10 @@ static int vim_regsub_both(char *source, typval_T *expr, char *dest, int destlen c = utf_ptr2char(s); if (func_one != (fptr_T)NULL) { - // Turbo C complains without the typecast - func_one = (fptr_T)(func_one(&cc, c)); + func_one(&cc, c); + func_one = NULL; } else if (func_all != (fptr_T)NULL) { - // Turbo C complains without the typecast - func_all = (fptr_T)(func_all(&cc, c)); + func_all(&cc, c); } else { // just copy cc = c; } diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 30c98a2f1e..4e000b8d29 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -1663,7 +1663,8 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n char *p = NULL; if (fold) { - size_t n = fill_foldcolumn(out_p, wp, stcp->foldinfo, (linenr_T)get_vim_var_nr(VV_LNUM)); + size_t n = fill_foldcolumn(out_p, wp, stcp->foldinfo, + (linenr_T)get_vim_var_nr(VV_LNUM), NULL); stl_items[curitem].minwid = -((stcp->use_cul ? HLF_CLF : HLF_FC) + 1); p = out_p; p[n] = NUL; diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 0983667695..9fea6442a9 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -24,6 +24,7 @@ #include "nvim/event/signal.h" #include "nvim/event/stream.h" #include "nvim/globals.h" +#include "nvim/grid.h" #include "nvim/grid_defs.h" #include "nvim/highlight_defs.h" #include "nvim/log.h" @@ -675,15 +676,15 @@ static void final_column_wrap(TUIData *tui) /// It is undocumented, but in the majority of terminals and terminal emulators /// printing at the right margin does not cause an automatic wrap until the /// next character is printed, holding the cursor in place until then. -static void print_cell(TUIData *tui, UCell *ptr) +static void print_cell(TUIData *tui, char *buf, sattr_T attr) { UGrid *grid = &tui->grid; if (!tui->immediate_wrap_after_last_column) { // Printing the next character finally advances the cursor. final_column_wrap(tui); } - update_attrs(tui, ptr->attr); - out(tui, ptr->data, strlen(ptr->data)); + update_attrs(tui, attr); + out(tui, buf, strlen(buf)); grid->col++; if (tui->immediate_wrap_after_last_column) { // Printing at the right margin immediately advances the cursor. @@ -703,8 +704,8 @@ static bool cheap_to_print(TUIData *tui, int row, int col, int next) return false; } } - if (strlen(cell->data) > 1) { - return false; + if (schar_get_ascii(cell->data) == 0) { + return false; // not ascii } cell++; } @@ -831,14 +832,16 @@ static void print_cell_at_pos(TUIData *tui, int row, int col, UCell *cell, bool { UGrid *grid = &tui->grid; - if (grid->row == -1 && cell->data[0] == NUL) { + if (grid->row == -1 && cell->data == NUL) { // If cursor needs to repositioned and there is nothing to print, don't move cursor. return; } cursor_goto(tui, row, col); - bool is_ambiwidth = utf_ambiguous_width(utf_ptr2char(cell->data)); + char buf[MAX_SCHAR_SIZE]; + schar_get(buf, cell->data); + bool is_ambiwidth = utf_ambiguous_width(utf_ptr2char(buf)); if (is_ambiwidth && is_doublewidth) { // Clear the two screen cells. // If the character is single-width in the host terminal it won't change the second cell. @@ -847,7 +850,7 @@ static void print_cell_at_pos(TUIData *tui, int row, int col, UCell *cell, bool cursor_goto(tui, row, col); } - print_cell(tui, cell); + print_cell(tui, buf, cell->attr); if (is_ambiwidth) { // Force repositioning cursor after printing an ambiguous-width character. @@ -976,6 +979,8 @@ void tui_grid_clear(TUIData *tui, Integer g) { UGrid *grid = &tui->grid; ugrid_clear(grid); + // safe to clear cache at this point + schar_cache_clear_if_full(); kv_size(tui->invalid_regions) = 0; clear_region(tui, 0, tui->height, 0, tui->width, 0); } @@ -1273,7 +1278,7 @@ void tui_flush(TUIData *tui) int clear_col; for (clear_col = r.right; clear_col > 0; clear_col--) { UCell *cell = &grid->cells[row][clear_col - 1]; - if (!(cell->data[0] == ' ' && cell->data[1] == NUL + if (!(cell->data == schar_from_ascii(' ') && cell->attr == clear_attr)) { break; } @@ -1281,7 +1286,7 @@ void tui_flush(TUIData *tui) UGRID_FOREACH_CELL(grid, row, r.left, clear_col, { print_cell_at_pos(tui, row, curcol, cell, - curcol < clear_col - 1 && (cell + 1)->data[0] == NUL); + curcol < clear_col - 1 && (cell + 1)->data == NUL); }); if (clear_col < r.right) { clear_region(tui, row, row + 1, clear_col, r.right, clear_attr); @@ -1399,7 +1404,10 @@ void tui_screenshot(TUIData *tui, String path) for (int i = 0; i < grid->height; i++) { cursor_goto(tui, i, 0); for (int j = 0; j < grid->width; j++) { - print_cell(tui, &grid->cells[i][j]); + UCell cell = grid->cells[i][j]; + char buf[MAX_SCHAR_SIZE]; + schar_get(buf, cell.data); + print_cell(tui, buf, cell.attr); } } flush_buf(tui); @@ -1446,13 +1454,13 @@ void tui_raw_line(TUIData *tui, Integer g, Integer linerow, Integer startcol, In { UGrid *grid = &tui->grid; for (Integer c = startcol; c < endcol; c++) { - memcpy(grid->cells[linerow][c].data, chunk[c - startcol], sizeof(schar_T)); + grid->cells[linerow][c].data = chunk[c - startcol]; assert((size_t)attrs[c - startcol] < kv_size(tui->attrs)); grid->cells[linerow][c].attr = attrs[c - startcol]; } UGRID_FOREACH_CELL(grid, (int)linerow, (int)startcol, (int)endcol, { print_cell_at_pos(tui, (int)linerow, curcol, cell, - curcol < endcol - 1 && (cell + 1)->data[0] == NUL); + curcol < endcol - 1 && (cell + 1)->data == NUL); }); if (clearcol > endcol) { @@ -1469,7 +1477,7 @@ void tui_raw_line(TUIData *tui, Integer g, Integer linerow, Integer startcol, In if (endcol != grid->width) { // Print the last char of the row, if we haven't already done so. - int size = grid->cells[linerow][grid->width - 1].data[0] == NUL ? 2 : 1; + int size = grid->cells[linerow][grid->width - 1].data == NUL ? 2 : 1; print_cell_at_pos(tui, (int)linerow, grid->width - size, &grid->cells[linerow][grid->width - size], size == 2); } diff --git a/src/nvim/types.h b/src/nvim/types.h index 3a05223023..afc1b3f5fe 100644 --- a/src/nvim/types.h +++ b/src/nvim/types.h @@ -46,4 +46,10 @@ typedef enum { typedef struct Decoration Decoration; +#ifndef ORDER_BIG_ENDIAN +# if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) +# define ORDER_BIG_ENDIAN +# endif +#endif + #endif // NVIM_TYPES_H diff --git a/src/nvim/ugrid.c b/src/nvim/ugrid.c index be1746983c..d3a1097aba 100644 --- a/src/nvim/ugrid.c +++ b/src/nvim/ugrid.c @@ -4,6 +4,7 @@ #include <assert.h> #include <string.h> +#include "nvim/grid.h" #include "nvim/memory.h" #include "nvim/ugrid.h" @@ -79,8 +80,7 @@ static void clear_region(UGrid *grid, int top, int bot, int left, int right, sat { for (int row = top; row <= bot; row++) { UGRID_FOREACH_CELL(grid, row, left, right + 1, { - cell->data[0] = ' '; - cell->data[1] = 0; + cell->data = schar_from_ascii(' '); cell->attr = attr; }); } diff --git a/src/nvim/ugrid.h b/src/nvim/ugrid.h index a85a6fb4a8..1c73c43867 100644 --- a/src/nvim/ugrid.h +++ b/src/nvim/ugrid.h @@ -11,10 +11,8 @@ struct ugrid; typedef struct ucell UCell; typedef struct ugrid UGrid; -#define CELLBYTES (sizeof(schar_T)) - struct ucell { - char data[CELLBYTES + 1]; + schar_T data; sattr_T attr; }; diff --git a/src/nvim/ui_compositor.c b/src/nvim/ui_compositor.c index e9b23d1298..fcb63801e5 100644 --- a/src/nvim/ui_compositor.c +++ b/src/nvim/ui_compositor.c @@ -55,7 +55,7 @@ static int msg_current_row = INT_MAX; static bool msg_was_scrolled = false; static int msg_sep_row = -1; -static schar_T msg_sep_char = { ' ', NUL }; +static schar_T msg_sep_char = schar_from_ascii(' '); static int dbghl_normal, dbghl_clear, dbghl_composed, dbghl_recompose; @@ -354,7 +354,7 @@ static void compose_line(Integer row, Integer startcol, Integer endcol, LineFlag grid = &msg_grid; sattr_T msg_sep_attr = (sattr_T)HL_ATTR(HLF_MSGSEP); for (int i = col; i < until; i++) { - memcpy(linebuf[i - startcol], msg_sep_char, sizeof(*linebuf)); + linebuf[i - startcol] = msg_sep_char; attrbuf[i - startcol] = msg_sep_attr; } } else { @@ -363,9 +363,8 @@ static void compose_line(Integer row, Integer startcol, Integer endcol, LineFlag memcpy(linebuf + (col - startcol), grid->chars + off, n * sizeof(*linebuf)); memcpy(attrbuf + (col - startcol), grid->attrs + off, n * sizeof(*attrbuf)); if (grid->comp_col + grid->cols > until - && grid->chars[off + n][0] == NUL) { - linebuf[until - 1 - startcol][0] = ' '; - linebuf[until - 1 - startcol][1] = '\0'; + && grid->chars[off + n] == NUL) { + linebuf[until - 1 - startcol] = schar_from_ascii(' '); if (col == startcol && n == 1) { skipstart = 0; } @@ -378,10 +377,10 @@ static void compose_line(Integer row, Integer startcol, Integer endcol, LineFlag for (int i = col - (int)startcol; i < until - startcol; i += width) { width = 1; // negative space - bool thru = strequal((char *)linebuf[i], " ") && bg_line[i][0] != NUL; - if (i + 1 < endcol - startcol && bg_line[i + 1][0] == NUL) { + bool thru = linebuf[i] == schar_from_ascii(' ') && bg_line[i] != NUL; + if (i + 1 < endcol - startcol && bg_line[i + 1] == NUL) { width = 2; - thru &= strequal((char *)linebuf[i + 1], " "); + thru &= linebuf[i + 1] == schar_from_ascii(' '); } attrbuf[i] = (sattr_T)hl_blend_attrs(bg_attrs[i], attrbuf[i], &thru); if (width == 2) { @@ -396,19 +395,18 @@ static void compose_line(Integer row, Integer startcol, Integer endcol, LineFlag // Tricky: if overlap caused a doublewidth char to get cut-off, must // replace the visible half with a space. - if (linebuf[col - startcol][0] == NUL) { - linebuf[col - startcol][0] = ' '; - linebuf[col - startcol][1] = NUL; + if (linebuf[col - startcol] == NUL) { + linebuf[col - startcol] = schar_from_ascii(' '); if (col == endcol - 1) { skipend = 0; } - } else if (col == startcol && n > 1 && linebuf[1][0] == NUL) { + } else if (col == startcol && n > 1 && linebuf[1] == NUL) { skipstart = 0; } col = until; } - if (linebuf[endcol - startcol - 1][0] == NUL) { + if (linebuf[endcol - startcol - 1] == NUL) { skipend = 0; } @@ -568,7 +566,7 @@ void ui_comp_msg_set_pos(Integer grid, Integer row, Boolean scrolled, String sep if (scrolled && row > 0) { msg_sep_row = (int)row - 1; if (sep_char.data) { - xstrlcpy(msg_sep_char, sep_char.data, sizeof(msg_sep_char)); + msg_sep_char = schar_from_buf(sep_char.data, sep_char.size); } } else { msg_sep_row = -1; diff --git a/src/nvim/window.c b/src/nvim/window.c index c0d399c4c4..1208494eaf 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -1009,7 +1009,9 @@ void ui_ext_win_position(win_T *wp, bool validate) comp_row += grid->comp_row; comp_col += grid->comp_col; comp_row = MAX(MIN(comp_row, Rows - wp->w_height_outer - (p_ch > 0 ? 1 : 0)), 0); - comp_col = MAX(MIN(comp_col, Columns - wp->w_width_outer), 0); + if (!c.fixed || east) { + comp_col = MAX(MIN(comp_col, Columns - wp->w_width_outer), 0); + } wp->w_winrow = comp_row; wp->w_wincol = comp_col; ui_comp_put_grid(&wp->w_grid_alloc, comp_row, comp_col, diff --git a/test/README.md b/test/README.md index 42d0ec0323..3aafe1273e 100644 --- a/test/README.md +++ b/test/README.md @@ -102,9 +102,25 @@ Debugging tests DBG 2022-06-15T18:37:45.229 T57.58016.0 read_cb:118: closing Stream (0x7fd5d700ea18): EOF (end of file) INF 2022-06-15T18:37:45.229 T57.58016.0 on_process_exit:400: exited: pid=58017 status=0 stoptime=0 ``` -- You can set `$GDB` to [run tests under gdbserver](https://github.com/neovim/neovim/pull/1527). - And if `$VALGRIND` is set it will pass `--vgdb=yes` to valgrind instead of +- You can set `$GDB` to [run functional tests under gdbserver](https://github.com/neovim/neovim/pull/1527): + + ```sh + GDB=1 TEST_FILE=test/functional/api/buffer_spec.lua TEST_FILTER='nvim_buf_set_text works$' make functionaltest + ``` + + Read more about [filtering tests](#filtering-tests). + + Then, in another terminal: + + ```sh + gdb -ex 'target remote localhost:7777' build/bin/nvim + ``` + + If `$VALGRIND` is also set it will pass `--vgdb=yes` to valgrind instead of starting gdbserver directly. + + See [test/functional/helpers.lua](https://github.com/neovim/neovim/blob/9cadbf1d36b63f53f0de48c8c5ff6c752ff05d70/test/functional/helpers.lua#L52-L69) for details. + - Hanging tests can happen due to unexpected "press-enter" prompts. The default screen width is 50 columns. Commands that try to print lines longer than 50 columns in the command-line, e.g. `:edit very...long...path`, will @@ -124,7 +140,7 @@ Filtering Tests ### Filter by name -Another filter method is by setting a pattern of test name to `TEST_FILTER` or `TEST_FILTER_OUT`. +Tests can be filtered by setting a pattern of test name to `TEST_FILTER` or `TEST_FILTER_OUT`. ``` lua it('foo api',function() diff --git a/test/functional/plugin/lsp/utils_spec.lua b/test/functional/plugin/lsp/utils_spec.lua index f544255d81..12763cfef5 100644 --- a/test/functional/plugin/lsp/utils_spec.lua +++ b/test/functional/plugin/lsp/utils_spec.lua @@ -88,6 +88,40 @@ describe('vim.lsp.util', function() end) end) + describe('normalize_markdown', function () + it('collapses consecutive blank lines', function () + local result = exec_lua [[ + local lines = { + 'foo', + '', + '', + '', + 'bar', + '', + 'baz' + } + return vim.lsp.util._normalize_markdown(lines) + ]] + local expected = {'foo', '', 'bar', '', 'baz'} + eq(expected, result) + end) + + it('removes preceding and trailing empty lines', function () + local result = exec_lua [[ + local lines = { + '', + 'foo', + 'bar', + '', + '' + } + return vim.lsp.util._normalize_markdown(lines) + ]] + local expected = {'foo', 'bar'} + eq(expected, result) + end) + end) + describe("make_floating_popup_options", function () local function assert_anchor(anchor_bias, expected_anchor) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index e0a8badb67..7e30af5058 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -3032,7 +3032,7 @@ describe('LSP', function() } return vim.lsp.util.convert_signature_help_to_markdown_lines(signature_help, 'cs', {','}) ]] - local expected = {'```cs', 'TestEntity.TestEntity()', '```', '<text>', 'some doc', '</text>'} + local expected = {'```cs', 'TestEntity.TestEntity()', '```', 'some doc'} eq(expected, result) end) end) diff --git a/test/functional/terminal/tui_spec.lua b/test/functional/terminal/tui_spec.lua index 57dcb14cf8..2d4613dda4 100644 --- a/test/functional/terminal/tui_spec.lua +++ b/test/functional/terminal/tui_spec.lua @@ -1663,21 +1663,18 @@ describe('TUI', function() it('draws correctly when cursor_address overflows #21643', function() helpers.skip(helpers.is_os('mac'), 'FIXME: crashes/errors on macOS') - screen:try_resize(77, 834) + screen:try_resize(77, 855) retry(nil, nil, function() - eq({true, 831}, {child_session:request('nvim_win_get_height', 0)}) + eq({true, 852}, {child_session:request('nvim_win_get_height', 0)}) end) -- Use full screen message so that redrawing afterwards is more deterministic. child_session:notify('nvim_command', 'intro') screen:expect({any = 'Nvim'}) -- Going to top-left corner needs 3 bytes. -- Setting underline attribute needs 9 bytes. - -- With screen width 77, 63857 characters need 829 full screen lines. - -- Drawing each full screen line needs 77 + 2 = 79 bytes (2 bytes for CR LF). - -- The incomplete screen line needs 24 + 3 = 27 bytes. - -- The whole line needs 3 + 9 + 79 * 829 + 27 = 65530 bytes. + -- The whole line needs 3 + 9 + 65515 + 3 = 65530 bytes. -- The cursor_address that comes after will overflow the 65535-byte buffer. - local line = ('a'):rep(63857) .. '℃' + local line = ('a'):rep(65515) .. '℃' child_session:notify('nvim_exec_lua', [[ vim.api.nvim_buf_set_lines(0, 0, -1, true, {...}) vim.o.cursorline = true @@ -1686,8 +1683,8 @@ describe('TUI', function() feed_data('\n') screen:expect( '{13:a}{12:' .. ('a'):rep(76) .. '}|\n' - .. ('{12:' .. ('a'):rep(77) .. '}|\n'):rep(828) - .. '{12:' .. ('a'):rep(24) .. '℃' .. (' '):rep(52) .. '}|\n' .. dedent([[ + .. ('{12:' .. ('a'):rep(77) .. '}|\n'):rep(849) + .. '{12:' .. ('a'):rep(65) .. '℃' .. (' '):rep(11) .. '}|\n' .. dedent([[ b | {5:[No Name] [+] }| | diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index 556859478e..f75fb52108 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -962,6 +962,71 @@ describe('float window', function() end end) + it('window position fixed', function() + command('rightbelow 20vsplit') + local buf = meths.create_buf(false,false) + local win = meths.open_win(buf, false, { + relative='win', width=15, height=2, row=2, col=10, anchor='NW', fixed=true}) + + if multigrid then + screen:expect{grid=[[ + ## grid 1 + [2:-------------------]{5:│}[4:--------------------]| + [2:-------------------]{5:│}[4:--------------------]| + [2:-------------------]{5:│}[4:--------------------]| + [2:-------------------]{5:│}[4:--------------------]| + [2:-------------------]{5:│}[4:--------------------]| + {5:[No Name] }{4:[No Name] }| + [3:----------------------------------------]| + ## grid 2 + | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 3 + | + ## grid 4 + ^ | + {0:~ }| + {0:~ }| + {0:~ }| + {0:~ }| + ## grid 5 + {1: }| + {2:~ }| + ]], float_pos={ + [5] = {{id = 1002}, "NW", 4, 2, 10, true, 50}; + }} + else + screen:expect([[ + {5:│}^ | + {0:~ }{5:│}{0:~ }| + {0:~ }{5:│}{0:~ }{1: }| + {0:~ }{5:│}{0:~ }{2:~ }| + {0:~ }{5:│}{0:~ }| + {5:[No Name] }{4:[No Name] }| + | + ]]) + end + + meths.win_set_config(win, {fixed=false}) + + if multigrid then + screen:expect_unchanged() + else + screen:expect([[ + {5:│}^ | + {0:~ }{5:│}{0:~ }| + {0:~ }{5:│}{0:~ }{1: }| + {0:~ }{5:│}{0:~ }{2:~ }| + {0:~ }{5:│}{0:~ }| + {5:[No Name] }{4:[No Name] }| + | + ]]) + end + end) + it('draws correctly with redrawdebug=compositor', function() -- NB: we do not test that it produces the "correct" debug info -- (as it is intermediate only, and is allowed to change by internal diff --git a/test/functional/ui/fold_spec.lua b/test/functional/ui/fold_spec.lua index 46de6b114e..c8ca5be282 100644 --- a/test/functional/ui/fold_spec.lua +++ b/test/functional/ui/fold_spec.lua @@ -423,6 +423,119 @@ describe("folded lines", function() :set norightleft | ]]) end + + if multigrid then + meths.input_mouse('left', 'press', '', 2, 0, 0) + screen:expect([[ + ## grid 1 + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [3:---------------------------------------------]| + ## grid 2 + {7:▸ }{5:^+-- 6 lines: aa···························}| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ## grid 3 + :set norightleft | + ]]) + else + meths.input_mouse('left', 'press', '', 0, 0, 0) + screen:expect([[ + {7:▸ }{5:^+-- 6 lines: aa···························}| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + :set norightleft | + ]]) + end + + -- Add a winbar to avoid double-clicks + command('setlocal winbar=!!!!!!') + if multigrid then + meths.input_mouse('left', 'press', '', 2, 1, 0) + screen:expect([[ + ## grid 1 + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [3:---------------------------------------------]| + ## grid 2 + {11:!!!!!! }| + {7:▾▸}{5:^+--- 5 lines: aa··························}| + {7:│ }ff | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + ## grid 3 + :set norightleft | + ]]) + else + meths.input_mouse('left', 'press', '', 0, 1, 0) + screen:expect([[ + {11:!!!!!! }| + {7:▾▸}{5:^+--- 5 lines: aa··························}| + {7:│ }ff | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + :set norightleft | + ]]) + end + + if multigrid then + meths.input_mouse('left', 'press', '', 2, 1, 1) + screen:expect([[ + ## grid 1 + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [2:---------------------------------------------]| + [3:---------------------------------------------]| + ## grid 2 + {11:!!!!!! }| + {7:▾▾}^aa | + {7:││}bb | + {7:││}cc | + {7:││}dd | + {7:││}ee | + {7:│ }ff | + ## grid 3 + :set norightleft | + ]]) + else + meths.input_mouse('left', 'press', '', 0, 1, 1) + screen:expect([[ + {11:!!!!!! }| + {7:▾▾}^aa | + {7:││}bb | + {7:││}cc | + {7:││}dd | + {7:││}ee | + {7:│ }ff | + :set norightleft | + ]]) + end end) it("works with split", function() @@ -2834,7 +2947,7 @@ describe("folded lines", function() command('5,6fold') command('2,6fold') if multigrid then - screen:expect{grid=[[ + screen:expect([[ ## grid 1 [2:------------------------------]| [2:------------------------------]| @@ -2852,7 +2965,7 @@ describe("folded lines", function() {1:~ }| ## grid 3 | - ]]} + ]]) else screen:expect([[ {7: }This is a | @@ -2868,7 +2981,7 @@ describe("folded lines", function() feed('zo') if multigrid then - screen:expect{grid=[[ + screen:expect([[ ## grid 1 [2:------------------------------]| [2:------------------------------]| @@ -2886,7 +2999,7 @@ describe("folded lines", function() {1:~ }| ## grid 3 | - ]]} + ]]) else screen:expect([[ {7: }This is a | @@ -2904,7 +3017,7 @@ describe("folded lines", function() command('hi! Visual guibg=Red') feed('V2k') if multigrid then - screen:expect{grid=[[ + screen:expect([[ ## grid 1 [2:------------------------------]| [2:------------------------------]| @@ -2922,7 +3035,7 @@ describe("folded lines", function() {1:~ }| ## grid 3 {11:-- VISUAL LINE --} | - ]]} + ]]) else screen:expect([[ {7: }This is a | diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index c68f4cf34c..931e1f9985 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -8,6 +8,7 @@ local feed_command, eq = helpers.feed_command, helpers.eq local curbufmeths = helpers.curbufmeths local funcs = helpers.funcs local meths = helpers.meths +local exec_lua = helpers.exec_lua describe('colorscheme compatibility', function() before_each(function() @@ -2641,4 +2642,17 @@ describe('highlight namespaces', function() | ]]} end) + + it('winhl does not accept invalid value #24586', function() + local res = exec_lua([[ + local curwin = vim.api.nvim_get_current_win() + vim.api.nvim_command("set winhl=Normal:Visual") + local _, msg = pcall(vim.api.nvim_command,"set winhl='Normal:Wrong'") + return { msg, vim.wo[curwin].winhl } + ]]) + eq({ + "Vim(set):E5248: Invalid character in group name", + "Normal:Visual", + },res) + end) end) diff --git a/test/functional/ui/multibyte_spec.lua b/test/functional/ui/multibyte_spec.lua index 5cecd423d7..417c7b797c 100644 --- a/test/functional/ui/multibyte_spec.lua +++ b/test/functional/ui/multibyte_spec.lua @@ -160,6 +160,22 @@ describe("multibyte rendering", function() {1:~ }| | ]]} + + -- nvim will reset the zalgo text^W^W glyph cache if it gets too full. + -- this should be exceedingly rare, but fake it to make sure it works + meths._invalidate_glyph_cache() + screen:expect{grid=[[ + ^L̓̉̑̒̌̚ơ̗̌̒̄̀ŕ̈̈̎̐̕è̇̅̄̄̐m̖̟̟̅̄̚ ̛̓̑̆̇̍i̗̟̞̜̅̐p̗̞̜̉̆̕s̟̜̘̍̑̏ū̟̞̎̃̉ḿ̘̙́́̐ ̖̍̌̇̉̚d̞̄̃̒̉̎ò́̌̌̂̐l̞̀̄̆̌̚ȯ̖̞̋̀̐r̓̇̌̃̃̚ ̗̘̀̏̍́s̜̀̎̎̑̕i̟̗̐̄̄̚t̝̎̆̓̐̒ ̘̇̔̓̊̚ȃ̛̟̗̏̅m̜̟̙̞̈̓é̘̞̟̔̆t̝̂̂̈̑̔,̜̜̖̅̄̍ ̛̗̊̓̆̚c̟̍̆̍̈̔ȯ̖̖̝̑̀n̜̟̎̊̃̚s̟̏̇̎̒̚e̙̐̈̓̌̚c̙̍̈̏̅̕ť̇̄̇̆̓e̛̓̌̈̓̈t̟̍̀̉̆̅u̝̞̎̂̄̚r̘̀̅̈̅̐ ̝̞̓́̇̉ã̏̀̆̅̕d̛̆̐̉̆̋ȉ̞̟̍̃̚p̛̜̊̍̂̓ȋ̏̅̃̋̚ṥ̛̏̃̕č̛̞̝̀̂í̗̘̌́̎n̔̎́̒̂̕ǧ̗̜̋̇̂ ̛̜̔̄̎̃ê̛̔̆̇̕l̘̝̏̐̊̏ĩ̛̍̏̏̄t̟̐́̀̐̎,̙̘̍̆̉̐ ̋̂̏̄̌̅s̙̓̌̈́̇e̛̗̋̒̎̏d̜̗̊̍̊̚ | + ď̘̋̌̌̕ǒ̝̗̔̇̕ ̙̍́̄̄̉è̛̛̞̌̌i̜̖̐̈̆̚ȕ̇̈̓̃̓ŝ̛̞̙̉̋m̜̐̂̄̋̂ȯ̈̎̎̅̕d̜̙̓̔̋̑ ̞̗̄̂̂̚t̝̊́̃́̄e̛̘̜̞̓̑m̊̅̏̉̌̕p̛̈̂̇̀̐ỏ̙̘̈̉̔r̘̞̋̍̃̚ ̝̄̀̇̅̇ỉ̛̖̍̓̈n̛̛̝̎̕̕c̛̛̊̅́̐ĭ̗̓̀̍̐d̞̜̋̐̅̚i̟̙̇̄̊̄d̞̊̂̀̇̚ủ̝̉̑̃̕n̜̏̇̄̐̋ť̗̜̞̋̉ ̝̒̓̌̓̚ȕ̖̙̀̚̕t̖̘̎̉̂̌ ̛̝̄̍̌̂l̛̟̝̃̑̋á̛̝̝̔̅b̝̙̜̗̅̒ơ̖̌̒̄̆r̒̇̓̎̈̄e̛̛̖̅̏̇ ̖̗̜̝̃́e̛̛̘̅̔̌ẗ̛̙̗̐̕ ̖̟̇̋̌̈d̞̙̀̉̑̕ŏ̝̂́̐̑l̞̟̗̓̓̀ơ̘̎̃̄̂r̗̗̖̔̆̍ẻ̖̝̞̋̅ ̜̌̇̍̈̊m̈̉̇̄̒̀a̜̞̘̔̅̆g̗̖̈̃̈̉n̙̖̄̈̉̄â̛̝̜̄̃ ̛́̎̕̕̚ā̊́́̆̌l̟̙̞̃̒́i̖̇̎̃̀̋q̟̇̒̆́̊ủ́̌̇̑̚ã̛̘̉̐̚.̛́̏̐̍̊ | + U̝̙̎̈̐̆t̜̍̌̀̔̏ ̞̉̍̇̈̃e̟̟̊̄̕̕n̝̜̒̓̆̕i̖̒̌̅̇̚m̞̊̃̔̊̂ ̛̜̊̎̄̂a̘̜̋̒̚̚d̟̊̎̇̂̍ ̜̖̏̑̉̕m̜̒̎̅̄̚i̝̖̓̂̍̕n̙̉̒̑̀̔ỉ̖̝̌̒́m̛̖̘̅̆̎ ̖̉̎̒̌̕v̖̞̀̔́̎e̖̙̗̒̎̉n̛̗̝̎̀̂ȉ̞̗̒̕̚ȧ̟̜̝̅̚m̆̉̐̐̇̈,̏̐̎́̍́ ̜̞̙̘̏̆q̙̖̙̅̓̂ủ̇́̀̔̚í̙̟̟̏̐s̖̝̍̏̂̇ ̛̘̋̈̕̕ń̛̞̜̜̎o̗̜̔̔̈̆s̞̘̘̄̒̋t̛̅̋́̔̈ȓ̓̒́̇̅ủ̜̄̃̒̍d̙̝̘̊̏̚ ̛̟̞̄́̔e̛̗̝̍̃̀x̞̖̃̄̂̅e̖̅̇̐̔̃r̗̞̖̔̎̚c̘̜̖̆̊̏ï̙̝̙̂̕t̖̏́̓̋̂ă̖̄̆̑̒t̜̟̍̉̑̏i̛̞̞̘̒̑ǒ̜̆̅̃̉ṅ̖̜̒̎̚ | + u̗̞̓̔̈̏ĺ̟̝́̎̚l̛̜̅̌̎̆a̒̑̆̔̇̃m̜̗̈̊̎̚ċ̘̋̇̂̚ơ̟̖̊́̕ ̖̟̍̉̏̚l̙̔̓̀̅̏ä̞̗̘̙̅ḃ̟̎̄̃̕o̞̎̓̓̓̚r̗̜̊̓̈̒ï̗̜̃̃̅s̀̒̌̂̎̂ ̖̗̗̋̎̐n̝̟̝̘̄̚i̜̒̀̒̐̕s̘̘̄̊̃̀ī̘̜̏̌̕ ̗̖̞̐̈̒ư̙̞̄́̌t̟̘̖̙̊̚ ̌̅̋̆̚̚ä̇̊̇̕̕l̝̞̘̋̔̅i̍̋́̆̑̈q̛̆̐̈̐̚ư̏̆̊́̚î̜̝̑́̊p̗̓̅̑̆̏ ̆́̓̔̋̋e̟̊̋̏̓̚x̗̍̑̊̎̈ ̟̞̆̄̂̍ë̄̎̄̃̅a̛̜̅́̃̈ ̔̋̀̎̐̀c̖̖̍̀̒̂ơ̛̙̖̄̒m̘̔̍̏̆̕ḿ̖̙̝̏̂ȍ̓̋̈̀̕d̆̂̊̅̓̚o̖̔̌̑̚̕ ̙̆́̔̊̒c̖̘̖̀̄̍o̓̄̑̐̓̒ñ̞̒̎̈̚s̞̜̘̈̄̄e̙̊̀̇̌̋q̐̒̓́̔̃ư̗̟̔̔̚å̖̙̞̄̏t̛̙̟̒̇̏.̙̗̓̃̓̎ | + D̜̖̆̏̌̌ư̑̃̌̍̕i̝̊̊̊̊̄s̛̙̒́̌̇ ̛̃̔̄̆̌ă̘̔̅̅̀ú̟̟̟̃̃t̟̂̄̈̈̃e̘̅̌̒̂̆ ̖̟̐̉̉̌î̟̟̙̜̇r̛̙̞̗̄̌ú̗̗̃̌̎r̛̙̘̉̊̕e̒̐̔̃̓̋ ̊̊̍̋̑̉d̛̝̙̉̀̓o̘̜̐̐̓̐l̞̋̌̆̍́o̊̊̐̃̃̚ṙ̛̖̘̃̕ ̞̊̀̍̒̕ȉ́̑̐̇̅ǹ̜̗̜̞̏ ̛̜̐̄̄̚r̜̖̈̇̅̋ĕ̗̉̃̔̚p̟̝̀̓̔̆r̜̈̆̇̃̃e̘̔̔̏̎̓h̗̒̉̑̆̚ė̛̘̘̈̐n̘̂̀̒̕̕d̗̅̂̋̅́ê̗̜̜̜̕r̟̋̄̐̅̂i̛̔̌̒̂̕t̛̗̓̎̀̎ ̙̗̀̉̂̚ȉ̟̗̐̓̚n̙̂̍̏̓̉ ̙̘̊̋̍̕v̜̖̀̎̆̐ő̜̆̉̃̎l̑̋̒̉̔̆ư̙̓̓́̚p̝̘̖̎̏̒t̛̘̝̞̂̓ȁ̘̆̔́̊t̖̝̉̒̐̎e̞̟̋̀̅̄ ̆̌̃̀̑̔v̝̘̝̍̀̇ȅ̝̊̄̓̕l̞̝̑̔̂̋ĭ̝̄̅̆̍t̝̜̉̂̈̇ | + ē̟̊̇̕̚s̖̘̘̒̄̑s̛̘̀̊̆̇e̛̝̘̒̏̚ ̉̅̑̂̐̎c̛̟̙̎̋̓i̜̇̒̏̆̆l̟̄́̆̊̌l̍̊̋̃̆̌ủ̗̙̒̔̚m̛̘̘̖̅̍ ̖̙̈̎̂̕d̞̟̏̋̈̔ơ̟̝̌̃̄l̗̙̝̂̉̒õ̒̃̄̄̚ŕ̗̏̏̊̍ê̞̝̞̋̈ ̜̔̒̎̃̚e̞̟̞̒̃̄ư̖̏̄̑̃ ̛̗̜̄̓̎f̛̖̞̅̓̃ü̞̏̆̋̕g̜̝̞̑̑̆i̛̘̐̐̅̚à̜̖̌̆̎t̙̙̎̉̂̍ ̋̔̈̎̎̉n̞̓́̔̊̕ư̘̅̋̔̚l̗̍̒̄̀̚l̞̗̘̙̓̍â̘̔̒̎̚ ̖̓̋̉̃̆p̛̛̘̋̌̀ä̙̔́̒̕r̟̟̖̋̐̋ì̗̙̎̓̓ȃ̔̋̑̚̕t̄́̎̓̂̋ư̏̈̂̑̃r̖̓̋̊̚̚.̒̆̑̆̊̎ ̘̜̍̐̂̚E̞̅̐̇́̂x̄́̈̌̉̕ć̘̃̉̃̕è̘̂̑̏̑p̝̘̑̂̌̆t̔̐̅̍̌̂ȇ̞̈̐̚̕ű̝̞̜́̚ŕ̗̝̉̆́ | + š̟́̔̏̀ȉ̝̟̝̏̅n̑̆̇̒̆̚t̝̒́̅̋̏ ̗̑̌̋̇̚ơ̙̗̟̆̅c̙̞̙̎̊̎c̘̟̍̔̊̊a̛̒̓̉́̐e̜̘̙̒̅̇ć̝̝̂̇̕ả̓̍̎̂̚t̗̗̗̟̒̃ ̘̒̓̐̇́c̟̞̉̐̓̄ȕ̙̗̅́̏p̛̍̋̈́̅i̖̓̒̍̈̄d̞̃̈̌̆̐a̛̗̝̎̋̉t̞̙̀̊̆̇a̛̙̒̆̉̚t̜̟̘̉̓̚ ̝̘̗̐̇̕n̛̘̑̏̂́ō̑̋̉̏́ň̞̊̆̄̃ ̙̙̙̜̄̏p̒̆̋̋̓̏r̖̖̅̉́̚ơ̜̆̑̈̚i̟̒̀̃̂̌d̛̏̃̍̋̚ë̖̞̙̗̓n̛̘̓̒̅̎t̟̗̙̊̆̚,̘̙̔̊̚̕ ̟̗̘̜̑̔s̜̝̍̀̓̌û̞̙̅̇́n̘̗̝̒̃̎t̗̅̀̅̊̈ ̗̖̅̅̀̄i̛̖̍̅̋̂n̙̝̓̓̎̚ ̞̋̅̋̃̚c̗̒̀̆̌̎ū̞̂̑̌̓ĺ̛̐̍̑́p̝̆̌̎̈̚a̖̙̒̅̈̌ ̝̝̜̂̈̀q̝̖̔̍̒̚ư̔̐̂̎̊ǐ̛̟̖̘̕ | + o̖̜̔̋̅̚f̛̊̀̉́̕f̏̉̀̔̃̃i̘̍̎̐̔̎c̙̅̑̂̐̅ȋ̛̜̀̒̚a̋̍̇̏̀̋ ̖̘̒̅̃̒d̗̘̓̈̇̋é̝́̎̒̄š̙̒̊̉̋e̖̓̐̀̍̕r̗̞̂̅̇̄ù̘̇̐̉̀n̐̑̀̄̍̐t̟̀̂̊̄̚ ̟̝̂̍̏́m̜̗̈̂̏̚ő̞̊̑̇̒l̘̑̏́̔̄l̛̛̇̃̋̊i̓̋̒̃̉̌t̛̗̜̏̀̋ ̙̟̒̂̌̐a̙̝̔̆̏̅n̝̙̙̗̆̅i̍̔́̊̃̕m̖̝̟̒̍̚ ̛̃̃̑̌́ǐ̘̉̔̅̚d̝̗̀̌̏̒ ̖̝̓̑̊̚ȇ̞̟̖̌̕š̙̙̈̔̀t̂̉̒̍̄̄ ̝̗̊̋̌̄l̛̞̜̙̘̔å̝̍̂̍̅b̜̆̇̈̉̌ǒ̜̙̎̃̆r̝̀̄̍́̕ư̋̊́̊̕m̜̗̒̐̕̚.̟̘̀̒̌̚ | + {1:~ }| + | + ]], reset=true} end) end) diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index b2b6ad80bb..e2b977c2b8 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -362,6 +362,7 @@ func s:GetFilenameChecks() abort \ 'lilo': ['lilo.conf', 'lilo.conf-file'], \ 'lilypond': ['file.ly', 'file.ily'], \ 'limits': ['/etc/limits', '/etc/anylimits.conf', '/etc/anylimits.d/file.conf', '/etc/limits.conf', '/etc/limits.d/file.conf', '/etc/some-limits.conf', '/etc/some-limits.d/file.conf', 'any/etc/limits', 'any/etc/limits.conf', 'any/etc/limits.d/file.conf', 'any/etc/some-limits.conf', 'any/etc/some-limits.d/file.conf'], + \ 'liquidsoap': ['file.liq'], \ 'liquid': ['file.liquid'], \ 'lisp': ['file.lsp', 'file.lisp', 'file.asd', 'file.el', 'file.cl', '.emacs', '.sawfishrc', 'sbclrc', '.sbclrc'], \ 'lite': ['file.lite', 'file.lt'], diff --git a/test/old/testdir/test_visual.vim b/test/old/testdir/test_visual.vim index a12ecefc73..5d70492451 100644 --- a/test/old/testdir/test_visual.vim +++ b/test/old/testdir/test_visual.vim @@ -1577,4 +1577,18 @@ func Test_visual_hl_with_showbreak() call StopVimInTerminal(buf) endfunc +func Test_Visual_r_CTRL_C() + new + " visual r_cmd + call setline(1, [' ']) + call feedkeys("\<c-v>$r\<c-c>", 'tx') + call assert_equal([''], getline(1, 1)) + + " visual gr_cmd + call setline(1, [' ']) + call feedkeys("\<c-v>$gr\<c-c>", 'tx') + call assert_equal([''], getline(1, 1)) + bw! +endfu + " vim: shiftwidth=2 sts=2 expandtab |