Skip to content

Neovim plugin for ccls language server. Leverages off-spec extensions to LSP client with AST browser

License

Notifications You must be signed in to change notification settings

ranjithshegde/ccls.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ccls.nvim

A neovim plugin to configure ccls language server and use its extensions. ccls is a language server for c, cpp and variants that offers comparable on-spec features as clangd along with a many extensions.

This plugin offers a tree-browser structure to parse the AST provided by ccls extensions and to quickly navigate to them.

These AST features include:

  • member functions/variables of an object
  • base and derived hierarchy of a class
  • call hierarchy for a function
  • sturcts and variables of the same type in the project

There are some additional functionalities, follow the README for them.

ccls_demo.webm

Features include:

  • Most off-spec ccls features
  • Setup lsp either via lspconfig or built-in vim.lsp.start()
  • Use treesitter to highlight NodeTree window
  • Update tagstack on jump to new node
  • Setup codelens autocmds

ccls extensions

ccls LSP has many off-spec commands/calls. This plugin supports the following

Quickfix

The below functions return a quickfix list of items

$ccls/member

Called via require("ccls").member(kind). kind 4 = variables, 3 = functions, 2 = type

Individual member calls can also be made via

  • :CclsMember for Variables
  • :CclsMemberFunction for functions
  • :CclsMemberType for types

$ccls/call

Called via require("ccls").call(callee). true = outgoing calls, false = incoming calls Can also be called via

  • :CclsIncomingCalls
  • :CclsOutgoingCalls

$ccls/inheritance

Called via require("ccls").inheritance(derived) derived true for derived classes, false for base classes

Can also be called via

  • :CclsBase
  • :CclsDerived

$ccls/vars

Called via :CclsVars kind or require("ccls").vars(kind). This is similar to textDocument/references except it checks for the variable type. Kind values are 1 for all occurence of the variable type, 2 for defintion of current variable and 3 for references without definition.

Sidebar or float

The following functions are hierarchical and return either a sidebar or a floating window

Each lua callback has a view option. View is a table with example {type = "float"} to use floating window. For vim commands it can be passed via :CclsMemberHierarchy float When omitted it uses a sidebar.

Inside the window, use maps:

  • o to open a node under cursor.
  • c to close the node under cursor
  • O to toggle node under cursor
  • CR to jump to node under cursor
  • q To quit window

$ccls/member hierarchy

Called via require("ccls").memberHierarchy(kind, view). kind 4 = variables, 3 = functions, 2 = type

individual member calls can also be made via

  • :CclsMemberHierarchy for Variables
  • :CclsMemberFunction for functions
  • :CclsMemberTyoe for types

$ccls/call hierarchy

Called via require("ccls").callHierarchy(callee). true = outgoing calls, false = incoming calls Can also be called via

  • :CclsIncomingCallsHierarchy
  • :CclsOutgoingCallsHierarchy

$ccls/inheritance hierarchy

Called via require("ccls").inheritanceHierarchy(derived) derived true for derived classes, false for base classes

Can also be called via

  • :CclsBaseHierarchy
  • :CclsDerivedHierarchy

Configuration

Call require("ccls").setup(config) somewhere in your config

The default values are:

Code
defaults = {
    win_config = {
        -- Sidebar configuration
        sidebar = {
            size = 50,
            position = "topleft",
            split = "vnew",
            width = 50,
            height = 20,
        },
        -- floating window configuration. check :help nvim_open_win for options
        float = {
            style = "minimal",
            relative = "cursor",
            width = 50,
            height = 20,
            row = 0,
            col = 0,
            border = "rounded",
        },
    },
    filetypes = {"c", "cpp", "objc", "objcpp"},

    -- Lsp is not setup by default to avoid overriding user's personal configurations.
    -- Look ahead for instructions on using this plugin for ccls setup
    lsp = {
        codelens = {
            enabled = false,
            events = {"BufEnter", "BufWritePost"}
        }
    }
}

Any of the configuration options can be omitted.

Window configuration

win_config table accepts two keys:

-sidebar: split options

-float: same options supplied to nvim_open_win or other default floating windows

Filetypes

By default, this plugin works on all filetypes accepted by ccls language server. You can customize this by adding filetypes table to the config

require("ccls").setup({filetypes = {"c", "cpp", "opencl"}})

Lsp

You can optionally setup LSP through the plugin. By default no setup calls are initiated.

There are two methods.

Using lspcofing

This requires that you have nvim-lspconfig plugin installed (and already loaded if lazy-loading). Pass the appropriate configurations like this.

Code
    local util = require "lspconfig.util"
    local server_config = {
        filetypes = { "c", "cpp", "objc", "objcpp", "opencl" },
        root_dir = function(fname)
            return util.root_pattern("compile_commands.json", "compile_flags.txt", ".git")(fname)
                or util.find_git_ancestor(fname)
        end,
        init_options = { cache = {
            directory = vim.env.XDG_CACHE_HOME .. "/ccls/",
            -- or vim.fs.normalize "~/.cache/ccls" -- if on nvim 0.8 or higher
        } },
        --on_attach = require("my.attach").func,
        --capabilities = my_caps_table_or_func
    }
    require("ccls").setup { lsp = { lspconfig = server_config } }

Any option omitted will use lspconfig defaults.

It is also possible to entirely use lspconfig defaults like this:

require("ccls").setup({lsp = {use_defaults = true}})

Using direct call

If using nvim 0.8, you can use vim.lsp.start() call instead which has the benefit of reusing the same client on files within the same workspace.

To use that, pass this in your config, without supplying the keys use_defaults or lspconfig.

Warning: Requires nvim 0.8

Code
require("ccls").setup {
    lsp = {
        -- check :help vim.lsp.start for config options
        server = {
            name = "ccls", --String name
            cmd = {"/usr/bin/ccls"}, -- point to your binary, has to be a table
            args = {--[[Any args table]] },
            offset_encoding = "utf-32", -- default value set by plugin
            root_dir = vim.fs.dirname(vim.fs.find({ "compile_commands.json", ".git" }, { upward = true })[1]), -- or some other function that returns a string
            --on_attach = your_func,
            --capabilites = your_table/func
        },
    },
}

If neither use_defaults, lspconfig nor server are set, then the plugin assumes you have setup ccls LSP elsewhere in your config. This is the default behaviour.

Codelens

ccls has minimal codelens capabilites. If you are not familiar with codenels, see Lsp spec documentation. According to ccls server capabilities tree, ccls supports resolveProvider option of codelens.

To enable codelens, set lsp = { codelens = {enable = true}} in the config. It is necessary to setup autocmds to refresh codelens. The default events are BufEnter and BufWritePost. You can customize it this way:

require('ccls').setup({
    lsp = {
        codelens = {
            enable = true,
            events = {"BufWritePost", "InsertLeave"}
        }
    }
})

Note: Setting up codelens using this plugin requires neovim >= 0.8 as LspAttach autocmd is only avaialble from version 0.8

Coexistence with clangd

If you wish to use clangd alongside ccls and want to avoid conflicting parallel requests, you can use the following table to disable specific capabilities.

Warning: Upstream (neovim) maintainers label the process of disabling capabilities as hacky. Until there is a mechanism in-place upstream that uses predicates to select clients for calls, this is the best solution.

This method uses both disabling certain capabilities and passing nil handlers to others. This makes running two language servers more resource efficient.

Use only the following options. If you do not wish to disable said option, either set it to false or simply leave out that option.

Code
require("ccls").setup {
    lsp = {
        disable_capabilities = {
            completionProvider = true,
            documentFormattingProvider = true,
            documentRangeFormattingProvider = true,
            documentHighlightProvider = true,
            documentSymbolProvider = true,
            workspaceSymbolProvider = true,
            renameProvider = true,
            hoverProvider = true,
            codeActionProvider = true,
        },
        disable_diagnostics = true,
        disable_signature = true,
    },
}

Note: For these disabling mechanisms to be attached to the initiated/running ccls instance, you will have to configure the server through the plugin either using lsp = {lspconfig = {my_config_table}} or lsp={server={my_0.8.config}} as descried earlier.

Here is a complete setup example from my config (using nvim 0.8 features)
    local filetypes = { "c", "cpp", "objc", "objcpp", "opencl" }
    local server_config = {
        filetypes = filetypes,
        init_options = { cache = {
            directory = vim.fs.normalize "~/.cache/ccls/",
        } },
        name = "ccls",
        cmd = { "ccls" },
        offset_encoding = "utf-32",
        root_dir = vim.fs.dirname(
            vim.fs.find({ "compile_commands.json", "compile_flags.txt", ".git" }, { upward = true })[1]
        ),
    }
    require("ccls").setup {
        filetypes = filetypes,
        lsp = {
            server = server_config,
            disable_capabilities = {
                completionProvider = true,
                documentFormattingProvider = true,
                documentRangeFormattingProvider = true,
                documentHighlightProvider = true,
                documentSymbolProvider = true,
                workspaceSymbolProvider = true,
                renameProvider = true,
                hoverProvider = true,
                codeActionProvider = true,
            },
            disable_diagnostics = true,
            disable_signature = true,
            codelens = { enable = true }
        },
    }
Notes

NodeTree

As of now, the NodeTree filetype which renders a tree structure is a direct lua rewrite of Martin Pilia's vim-yggdrasil. At some point in the future I will rewrite the logic to utilize more lua-ecosystem features and make it a general purpose Tree browser.

For now, it works exactly as intended but is not easy read. The code structure is as follows.

  • ccls/provider.lua contains functions to make LSP results compatible with NodeTree.
  • ccls/tree Folder has the luafied yggdrasil tree code
    • ccls/tree/tree.lua has the Tree class.
    • ccls/tree/node.lua has the node class reduced to a single node generator call to avoid caching problems. Will be modularized when I rewrite the logic.
    • ccls/tree/utils.lua has other function calls not part of tree or node class but necessary

TODO

Preview

Open a floating preview window for node under the cursor from Sidebar

Tests

This will take some time. Need to figure out how to run a language server for testing. I will look through other plugins to see how they handle it. No promise on time.

Credits

  • MaskRay Thank you for creating the LSP!
  • vim-ccls for inspiration and speicifc ideas on translating LSP data into tree-like structure.
  • vim-yggdrasil The entire tree-browser part of the code is a lua rewrite of this plugin.