Skip to content

Commit

Permalink
fix(ghost_text): ensure multiline indent is correct
Browse files Browse the repository at this point in the history
  • Loading branch information
xzbdmw committed Jan 7, 2025
1 parent 47efef8 commit 669c34e
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 1 deletion.
3 changes: 2 additions & 1 deletion lua/blink/cmp/completion/windows/ghost_text.lua
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ function ghost_text.draw_preview(bufnr)

if ghost_text.selected_item.insertTextFormat == vim.lsp.protocol.InsertTextFormat.Snippet then
local expanded_snippet = snippets_utils.safe_parse(text_edit.newText)
text_edit.newText = expanded_snippet and tostring(expanded_snippet) or text_edit.newText
text_edit.newText = expanded_snippet and table.concat(snippets_utils.to_static_text(expanded_snippet), '\n')
or text_edit.newText
end

local display_lines = vim.split(get_still_untyped_text(text_edit), '\n', { plain = true }) or {}
Expand Down
131 changes: 131 additions & 0 deletions lua/blink/cmp/sources/snippets/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,137 @@ function utils.read_snippet(snippet, fallback)
return snippets
end

--@see https://github.com/neovim/neovim/blob/9afa1fd35510c5fe485f4a1dfdabf94e5f051a1c/runtime/lua/vim/snippet.lua#L59

--- Transforms the given text into an array of lines (so no line contains `\n`).
---
--- @param text string|string[]
--- @return string[]
local function text_to_lines(text)
text = type(text) == 'string' and { text } or text
--- @cast text string[]
return vim.split(table.concat(text), '\n', { plain = true })
end

--@see https://github.com/neovim/neovim/blob/b67fcd0488746b079a3b721ae4800af94cd126e1/runtime/lua/vim/snippet.lua#L26
--- Resolves variables (like `$name` or `${name:default}`) as follows:
--- - When a variable is unknown (i.e.: its name is not recognized in any of the cases below), return `nil`.
--- - When a variable isn't set, return its default (if any) or an empty string.
---
--- Note that in some cases, the default is ignored since it's not clear how to distinguish an empty
--- value from an unset value (e.g.: `TM_CURRENT_LINE`).
---
--- @param var string
--- @param default string
--- @return string?
local function resolve_variable(var, default)
--- @param str string
--- @return string
local function expand_or_default(str)
local expansion = vim.fn.expand(str) --[[@as string]]
return expansion == '' and default or expansion
end

if var == 'TM_SELECTED_TEXT' then
-- Snippets are expanded in insert mode only, so there's no selection.
return default
elseif var == 'TM_CURRENT_LINE' then
return vim.api.nvim_get_current_line()
elseif var == 'TM_CURRENT_WORD' then
return expand_or_default('<cword>')
elseif var == 'TM_LINE_INDEX' then
return tostring(vim.fn.line('.') - 1)
elseif var == 'TM_LINE_NUMBER' then
return tostring(vim.fn.line('.'))
elseif var == 'TM_FILENAME' then
return expand_or_default('%:t')
elseif var == 'TM_FILENAME_BASE' then
return expand_or_default('%:t:r')
elseif var == 'TM_DIRECTORY' then
return expand_or_default('%:p:h:t')
elseif var == 'TM_FILEPATH' then
return expand_or_default('%:p')
end

-- Unknown variable.
return nil
end

--@see https://github.com/neovim/neovim/blob/b67fcd0488746b079a3b721ae4800af94cd126e1/runtime/lua/vim/snippet.lua#L477
function utils.to_static_text(node)
local snippet = node
local snippet_text = {}
local base_indent = vim.api.nvim_get_current_line():match('^%s*') or ''

local grammar = require('vim.lsp._snippet_grammar')
-- Get the placeholders we should use for each tabstop index.
--- @type table<integer, string>
local placeholders = {}
for _, child in ipairs(snippet.data.children) do
local type, data = child.type, child.data
if type == grammar.NodeType.Placeholder then
--- @cast data vim.snippet.PlaceholderData
local tabstop, value = data.tabstop, tostring(data.value)
if placeholders[tabstop] and placeholders[tabstop] ~= value then
error('Snippet has multiple placeholders for tabstop $' .. tabstop)
end
placeholders[tabstop] = value
end
end

--- Appends the given text to the snippet, taking care of indentation.
---
--- @param text string|string[]
local function append_to_snippet(text)
local snippet_lines = text_to_lines(snippet_text)
-- Get the base indentation based on the current line and the last line of the snippet.
if #snippet_lines > 0 then
base_indent = base_indent .. (snippet_lines[#snippet_lines]:match('(^%s+)%S') or '') --- @type string
end

local shiftwidth = vim.fn.shiftwidth()
local curbuf = vim.api.nvim_get_current_buf()
local expandtab = vim.bo[curbuf].expandtab

local lines = {} --- @type string[]
for i, line in ipairs(text_to_lines(text)) do
-- Replace tabs by spaces.
if expandtab then
line = line:gsub('\t', (' '):rep(shiftwidth)) --- @type string
end
-- Add the base indentation.
if i > 1 then line = base_indent .. line end
lines[#lines + 1] = line
end

table.insert(snippet_text, table.concat(lines, '\n'))
end

for _, child in ipairs(snippet.data.children) do
local type, data = child.type, child.data
if type == grammar.NodeType.Tabstop then
--- @cast data vim.snippet.TabstopData
local placeholder = placeholders[data.tabstop]
if placeholder then append_to_snippet(placeholder) end
elseif type == grammar.NodeType.Placeholder then
--- @cast data vim.snippet.PlaceholderData
local value = placeholders[data.tabstop]
append_to_snippet(value)
elseif type == grammar.NodeType.Variable then
--- @cast data vim.snippet.VariableData
-- Try to get the variable's value.
local value = resolve_variable(data.name, data.default and tostring(data.default) or '')
if value ~= nil then append_to_snippet(value) end
elseif type == grammar.NodeType.Text then
--- @cast data vim.snippet.TextData
append_to_snippet(data.text)
end
end

snippet_text = text_to_lines(snippet_text)
return snippet_text
end

function utils.get_tab_stops(snippet)
local expanded_snippet = require('blink.cmp.sources.snippets.utils').safe_parse(snippet)
if not expanded_snippet then return end
Expand Down

0 comments on commit 669c34e

Please sign in to comment.