diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/_meta/api.lua | 5 | ||||
-rw-r--r-- | runtime/lua/vim/_meta/api_keysets.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/filetype.lua | 1 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/handlers.lua | 15 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 178 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/_meta.lua | 3 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/languagetree.lua | 57 |
7 files changed, 171 insertions, 89 deletions
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. --- |