hrsh7th / nvim-cmp

A completion plugin for neovim coded in Lua.
MIT License
7.9k stars 394 forks source link

Astro LSP completions sometimes write over incorrect ranges #1705

Closed Trildar closed 6 months ago

Trildar commented 1 year ago

FAQ

Announcement

Minimal reproducible full config

local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  -- bootstrap lazy.nvim
  -- stylua: ignore
  vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", "--branch=stable", lazypath })
end
vim.opt.rtp:prepend(vim.env.LAZY or lazypath)

require("lazy").setup({
    spec = {
        {
            "williamboman/mason.nvim",
            cmd = "Mason",
            keys = { { "<leader>cm", "<cmd>Mason<cr>", desc = "Mason" } },
            build = ":MasonUpdate",
            opts = {
                ensure_installed = {
                    "astro-language-server",
                },
            },
            ---@param opts MasonSettings | {ensure_installed: string[]}
            config = function(_, opts)
                require("mason").setup(opts)
                local mr = require("mason-registry")
                local function ensure_installed()
                    for _, tool in ipairs(opts.ensure_installed) do
                        local p = mr.get_package(tool)
                        if not p:is_installed() then
                            p:install()
                        end
                    end
                end
                if mr.refresh then
                    mr.refresh(ensure_installed)
                else
                    ensure_installed()
                end
            end,
        },
        "hrsh7th/cmp-nvim-lsp",
        {
            "hrsh7th/nvim-cmp",
            version = false, -- last release is way too old
            event = "InsertEnter",
            dependencies = {
                "hrsh7th/cmp-nvim-lsp",
            },
            opts = function()
                vim.api.nvim_set_hl(0, "CmpGhostText", { link = "Comment", default = true })
                local cmp = require("cmp")
                local defaults = require("cmp.config.default")()
                return {
                    completion = {
                        completeopt = "menu,menuone,noinsert",
                    },
                    mapping = cmp.mapping.preset.insert({
                        ["<C-n>"] = cmp.mapping.select_next_item({ behavior = cmp.SelectBehavior.Insert }),
                        ["<C-p>"] = cmp.mapping.select_prev_item({ behavior = cmp.SelectBehavior.Insert }),
                        ["<C-b>"] = cmp.mapping.scroll_docs(-4),
                        ["<C-f>"] = cmp.mapping.scroll_docs(4),
                        ["<C-l>"] = cmp.mapping.complete(),
                        ["<C-e>"] = cmp.mapping.abort(),
                        ["<CR>"] = cmp.mapping.confirm({ select = true }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
                        ["<S-CR>"] = cmp.mapping.confirm({
                            behavior = cmp.ConfirmBehavior.Replace,
                            select = true,
                        }), -- Accept currently selected item. Set `select` to `false` to only confirm explicitly selected items.
                    }),
                    sources = cmp.config.sources({
                        { name = "nvim_lsp" },
                    }),
                    experimental = {
                        ghost_text = {
                            hl_group = "CmpGhostText",
                        },
                    },
                    sorting = defaults.sorting,
                }
            end,
        },
        "williamboman/mason-lspconfig.nvim",
        {
            "neovim/nvim-lspconfig",
            event = { "BufReadPre", "BufNewFile" },
            dependencies = {
                "mason.nvim",
                "williamboman/mason-lspconfig.nvim",
                "hrsh7th/cmp-nvim-lsp",
            },
            ---@class PluginLspOpts
            opts = {
                -- LSP Server Settings
                ---@type lspconfig.options
                servers = {
                    astro = {},
                },
                setup = {},
            },
            ---@param opts PluginLspOpts
            config = function(_, opts)
                local servers = opts.servers
                local has_cmp, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp")
                local capabilities = vim.tbl_deep_extend(
                    "force",
                    {},
                    vim.lsp.protocol.make_client_capabilities(),
                    has_cmp and cmp_nvim_lsp.default_capabilities() or {}
                )

                local function setup(server)
                    local server_opts = vim.tbl_deep_extend("force", {
                        capabilities = vim.deepcopy(capabilities),
                    }, servers[server] or {})

                    if opts.setup[server] then
                        if opts.setup[server](server, server_opts) then
                            return
                        end
                    elseif opts.setup["*"] then
                        if opts.setup["*"](server, server_opts) then
                            return
                        end
                    end
                    require("lspconfig")[server].setup(server_opts)
                end

                -- get all the servers that are available through mason-lspconfig
                local have_mason, mlsp = pcall(require, "mason-lspconfig")
                local all_mslp_servers = {}
                if have_mason then
                    all_mslp_servers = vim.tbl_keys(require("mason-lspconfig.mappings.server").lspconfig_to_package)
                end

                local ensure_installed = {} ---@type string[]
                for server, server_opts in pairs(servers) do
                    if server_opts then
                        server_opts = server_opts == true and {} or server_opts
                        -- run manual setup if mason=false or if this is a server that cannot be installed with mason-lspconfig
                        if server_opts.mason == false or not vim.tbl_contains(all_mslp_servers, server) then
                            setup(server)
                        else
                            ensure_installed[#ensure_installed + 1] = server
                        end
                    end
                end

                if have_mason then
                    mlsp.setup({ ensure_installed = ensure_installed, handlers = { setup } })
                end
            end,
        },
    },
    defaults = {
        -- By default, only LazyVim plugins will be lazy-loaded. Your custom plugins will load during startup.
        -- If you know what you're doing, you can set this to `true` to have all your custom plugins lazy-loaded by default.
        lazy = false,
        -- It's recommended to leave version=false for now, since a lot the plugin that support versioning,
        -- have outdated releases, which may break your Neovim install.
        version = false, -- always use the latest git commit
        -- version = "*", -- try installing the latest stable version for plugins that support semver
    },
    performance = {
        rtp = {
            -- disable some rtp plugins
            disabled_plugins = {
                "gzip",
                -- "matchit",
                -- "matchparen",
                -- "netrwPlugin",
                "tarPlugin",
                "tohtml",
                "tutor",
                "zipPlugin",
            },
        },
    },
})

Description

Under some circumstances, inserting completions from the Astro LSP inside interpolations in the HTML template result in what looks like the completions being written over incorrect ranges in the buffer.

Steps to reproduce

Initialize an Astro project with npm create astro@latest or equivalent. Then try to ciw the test inside the {test} interpolations in a file like the below, type t, and insert a completion for the test variable.

---
const test = "TEST";
---
<!-- Case 1: Multiline tag -->
<div
    id={test}
>
</div>

<!-- Case 2: Nested interpolation -->
{<div id={test}></div>}

<!-- Case 3: Space before = -->
<div id ={test}></div>

Expected behavior

The completion gets inserted and the file is as given in the reproduction example.

Actual behavior

Individually, the cases will become as below. Note that doing the replacement and completions sequentially within the same file may not produce this due to the missing brackets changing the syntax.

---
const test = "TEST";
---
<!-- Case 1: Multiline tag -->
<div
    id={ttest
>
</div>

<!-- Case 2: Nested interpolation -->
{<div id={ttest

<!-- Case 3: Space before = -->
<div id =test}></div>

Additional context

Another user in the discussion linked below found no issue when using Neovim omnifunc to insert completions in similar circumstances.

Astro framework: https://astro.build/ Some prior discussion of this issue can be found here: https://github.com/LazyVim/LazyVim/discussions/1455

yaegassy commented 12 months ago

Hi, @Trildar

I think this problem is a bug on the language server side. it does not cause problems with VSCode, but seems to cause problems with LSP clients of other editors. The start and end ranges for the selected completion items returned by the language server are probably incorrect. (completionItem/resolve)

You might want to report the issue to the language server side.

Trildar commented 12 months ago

Hi, @Trildar

I think this problem is a bug on the language server side. it does not cause problems with VSCode, but seems to cause problems with LSP clients of other editors. The start and end ranges for the selected completion items returned by the language server are probably incorrect. (completionItem/resolve)

You might want to report the issue to the language server side.

Yeah, looking at the language server responses, there does seem to be an issue with the ranges returned for completionItem/resolve, so I filed an issue here: https://github.com/withastro/language-tools/issues/664

But seeing as how Neovim omnifunc apparently also works fine (mentioned in the discussion on LazyVim repo), I have to wonder if it's just a matter of that and VS Code not using ranges from completionItem/resolve or some bigger issue with how nvim-cmp interacts with the language server.

hrsh7th commented 6 months ago

This issue already solved in latest @astrojs/language-server