Pueden leer la versión en español aquí.
Maybe I should have called this "How to enable IDE-like features without third party plugins." Sounds interesting, right? That's basically what I want to show you here.
We don't need any third party plugins, but we do need this:
- Neovim v0.8 or greater.
- A language server.
- Patience/Energy to write some lua code for each language server.
If you want to implement any of this stuff in your own configuration, consider dedicating a little bit of time to learn the basics of lua. Here are a couple of links to help you with that:
- Lua crash course (11 min video)
- Learn X in Y minutes: Where X = lua
- And... there is also Neovim's official lua guide
Let's start with the language server
A language server is an external program that follows the Language Server Protocol. The LSP specification defines what type of messages a language server can receive, and also how it should respond. The idea here is that any tool that follows the LSP specification can communicate with a language server.
And so the language server is the thing that analyzes our source code and it can tell the editor what to do.
Where can we find these language servers?
The website for the LSP specification has a list.
In this particular case...
I'm going to use intelephense to show the minimal configuration needed to setup a language server in Neovim.
If you want to test intelephense you need to install NodeJS. And then you can install the server running this command in the terminal.
npm install -g intelephense
Once you have a language server installed it's a good idea to check if Neovim "knows" where it is. You can execute this command inside Neovim.
:echo exepath('intelephense')
This should show you the path to the language server executable. If it doesn't, it means something went wrong during the installation.
In case you didn't click on the link to intelephense, you should know that is a language server for php. If you just want to test the code I show here, you don't need the php interpreter installed, just the source code of a php project. You can use this repository: minicli, is a decent size codebase and doesn't depend on any other php libraries.
Basic Usage
Before we write any code we should learn how to use the language server. The first piece of information we need is the command that starts the server. This should be in the official documentation of said server.
If we can't find the basic usage in the documentation we can go to nvim-lspconfig's github repository. In there we look for a folder called configs, this contains configuration files for a bunch of language servers.
Right now we are interested in intelephense, so we should inspect the contents of intelephense.lua. The thing we are looking for is in a property called default_config
. This piece of code right here:
default_config = {
cmd = { 'intelephense', '--stdio' },
filetypes = { 'php' },
root_dir = function(pattern)
local cwd = vim.uv.cwd()
local root = util.root_pattern('composer.json', '.git')(pattern)
-- prefer cwd if root is a descendant
return util.path.is_descendant(cwd, root) and cwd or root
end,
},
The cmd
property has the command we need to start the language server. filetypes
is the list of languages the server can handle. And I'm going to talk about the root_dir
in a little while.
Execute on filetype
Since we only need intelephense in php files we can use something called a filetype plugin. That's a script that gets executed after Neovim assigns a filetype to a buffer.
We create a filetype plugin in our Neovim configuration simply by adding a script in the folder ftplugin
. Note that the name of the script needs to be the same as a valid filetype.
We can navigate to Neovim's configuration folder, open Neovim and then create the ftplugin
folder.
:call mkdir('./ftplugin', 'p')
We want a filetype plugin for php, so we create a new file called php.lua
.
:edit ftplugin/php.lua | write
Inside this new file we are going to execute the function that enables intelephense.
Root directory
The last piece of information we need is the root directory. We just have to tell the language server where is our project folder.
In our filetype plugin we are going to use a function called vim.fs.find(). We will give it a list of files and it will return the path of the first match it finds.
What do we look for? We search for common configuration files that projects have in their root folder. So, in php is very common to have a composer.json
file. Javascript projects usually have a package.json
. Rust projects have a cargo.toml
. We feed this information to vim.fs.find()
and it should give us a path we can use.
We can make a test already by adding this piece of code in the newly created php.lua
.
-- ftplugin/php.lua
local root_files = {'composer.json'}
local paths = vim.fs.find(root_files, {stop = vim.env.HOME})
print(vim.fs.dirname(paths[1]))
By default vim.fs.find()
will look in the current folder and then the parent folders. The stop
argument tells the function it should stop the search if it hits the home
folder (you don't want your language server to analyze your home folder by accident).
Since vim.fs.find()
returns a list we just pick the first item. And to make sure we get the path to a folder we use vim.fs.dirname()
.
We can navigate to a php project with a composer.json file and check that the project path is detected correctly.
Start the client
We are ready to enable the language server. Now we call the function vim.lsp.start(). The first time this is executed it will launch the language server as an external process. When called again with the same root directory it will only send information to the existing process.
Our php filetype plugin should look like this.
-- ftplugin/php.lua
local root_files = {'composer.json'}
local paths = vim.fs.find(root_files, {stop = vim.env.HOME})
local root_dir = vim.fs.dirname(paths[1])
if root_dir then
vim.lsp.start({
cmd = {'intelephense', '--stdio'},
root_dir = root_dir,
})
end
With this setup we should get "diagnostics" out the box. If there is an error in a php file Neovim will show what line has the error, and also the message. Something like this.
The E
in line 8 is a diagnostic sign, it indicates there is an error. And the thing after the ■
symbol is "virtual text" showing the error message.
Server settings
Now, some server specific configuration should be placed in a property called settings
in the vim.lsp.start()
function. But here's the thing, you may find the documentation of some language servers shows them in this format:
intelephense.files.maxSize: 1000000
We would need to adapt this so it works with Neovim's LSP client. Let me show you how it should be.
vim.lsp.start({
cmd = {'intelephense', '--stdio'},
root_dir = root_dir,
settings = {
intelephense = {
files = {
maxSize = 1000000,
},
}
}
})
Basically, each dot is a nested "lua table" we need to add.
If there was another setting with the same namespace intelephense.files
, we just add it to the existing table.
vim.lsp.start({
cmd = {'intelephense', '--stdio'},
root_dir = root_dir,
settings = {
intelephense = {
files = {
maxSize = 1000000,
anotherOptionExample = false,
},
}
}
})
Where to look if something goes wrong?
If Neovim wasn't able to start the language server, you can take a look at the log file, execute this command inside Neovim:
:lua vim.cmd.edit(vim.lsp.get_log_path())
Look for the lines that start with [ERROR]
. Maybe there is an error message with some useful information.
If you want the logs to have more details, increase the log level. Add this in your init.lua
file.
vim.lsp.set_log_level('debug')
About the diagnostics
So the diagnostics signs, the thing Neovim uses to tell us there is an error in our source code... by default the space needed to render that sign is hidden and when there is a sign the whole screen shifts to the right. That behavior can be configured in the init.lua
file.
If you set the option signcolumn
to the string yes
, Neovim will reserve the space for the sign. You will have a whitespace reserved for any type of signs in the gutter.
vim.opt.signcolumn = 'yes'
If you set signcolumn
to the string no
, Neovim will hide the column altogether. Don't do that unless you are fully aware of the consequences. There is a better way to hide the diagnostic signs.
vim.diagnostic
There is a lua module dedicated specifically to diagnostics: vim.diagnostic. This has a .config() function we can use to configure the interface of the diagnostics.
We can add the following to our init.lua
.
- Hide diagnostic signs
This is the "safe" way to disable the diagnostics sign.
vim.diagnostic.config({
signs = false,
})
- Disable virtual text
Yes, there's also an option to hide the virtual text that contains the error message.
vim.diagnostic.config({
virtual_text = false,
})
If you have Neovim v0.10 or greater you can read the diagnostic message under the cursor with the keybinding <C-w>d
(control + w then d). This will trigger the function vim.diagnostic.open_float().
When using Neovim v0.9.5 or lower you'll have to create that keybinding yourself.
vim.keymap.set('n', '<C-w>d', '<cmd>lua vim.diagnostic.open_float()<cr>')
Now, I would love to explain all the options vim.diagnostic.config()
supports but we don't have time for that. If you want to know more you can read the documentation.
What else do we get for free?
These are things Neovim does when a language server active in the buffer.
Since Neovim v0.8
There is an LSP powered
tagfunc
. That means we can jump to the definition of a function or class using the keybinding<C-]>
(control + ]). And can jump back to where we were using<C-t>
.The
formatexpr
option is set to a function that uses the language server. This means thegq
operator can format a piece of code. We can enter visual mode, select a piece of text, pressgq
and Neovim will request the language server to format the code.The
omnifunc
option is also set. This one enables smart code completions. In insert mode the keybinding<C-x><C-o>
(control + x then control + o) will trigger the completion menu with suggestions that the language server think are relevant.
Since Neovim v0.9
- Semantic highlight is supported. Some language servers can provide information about the tokens in the source code, this allows for a more accurate syntax highlight.
Since Neovim v0.10
In normal mode, if we don't have a custom keybinding for
K
then it will display the available documentation for the symbol under the cursor.In normal mode,
<C-w>d
opens a floating window showing the diagnostics in the line under the cursor.In normal mode,
[d
and]d
can be used to move the cursor to the previous and next diagnostic of the current file.
Since Neovim v0.11
v0.11 is still under development. Everything in this section is subject to change.
In normal mode,
grn
renames all references of the symbol under the cursor.In normal mode,
gra
shows a list of code actions available in the line under the cursor.In normal mode,
grr
lists all the references of the symbol under the cursor.In insert mode,
<Ctrl-s>
displays the function signature of the symbol under the cursor.
Let's get some easy wins
Here's a list of features we can use without too much effort, pretty much the only thing we have to do is call a lua function.
- Jump to declaration
- Lists implementations
- Jump to type definition
- List all references
- Display a function's signature information
- Rename all references to the symbol under the cursor
- Format current file (or selected range)
- List and execute a code action
All of these can be used in custom keybindings.
Custom keybindings
Is very common for people to follow the same patterns Neovim's defaults have, that is only enable features when the language server is active. So let me introduce you to the LspAttach
event, this is triggered everytime Neovim enables a language server in a buffer. And with the function nvim_create_autocmd
we can tell Neovim we want to execute a callback function everytime this event happens.
You can find more details about autocommands here: lua-guide-autocommands.
So this is one way to setup custom keybindings.
-- you can add this in your init.lua
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'LSP actions',
callback = function(event)
local bufmap = function(mode, lhs, rhs)
local opts = {buffer = event.buf}
vim.keymap.set(mode, lhs, rhs, opts)
end
-- You can find details of these function in the help page
-- see for example, :help vim.lsp.buf.hover()
-- Trigger code completion
bufmap('i', '<C-Space>', '<C-x><C-o>')
-- Display documentation of the symbol under the cursor
bufmap('n', 'K', '<cmd>lua vim.lsp.buf.hover()<cr>')
-- Jump to the definition
bufmap('n', 'gd', '<cmd>lua vim.lsp.buf.definition()<cr>')
-- Jump to declaration
bufmap('n', 'gD', '<cmd>lua vim.lsp.buf.declaration()<cr>')
-- Lists all the implementations for the symbol under the cursor
bufmap('n', 'gi', '<cmd>lua vim.lsp.buf.implementation()<cr>')
-- Jumps to the definition of the type symbol
bufmap('n', 'go', '<cmd>lua vim.lsp.buf.type_definition()<cr>')
-- Lists all the references
bufmap('n', 'gr', '<cmd>lua vim.lsp.buf.references()<cr>')
-- Displays a function's signature information
bufmap('n', '<C-k>', '<cmd>lua vim.lsp.buf.signature_help()<cr>')
-- Renames all references to the symbol under the cursor
bufmap('n', '<F2>', '<cmd>lua vim.lsp.buf.rename()<cr>')
-- Format current file
bufmap('n', '<F3>', '<cmd>lua vim.lsp.buf.format()<cr>')
-- Selects a code action available at the current cursor position
bufmap('n', '<F4>', '<cmd>lua vim.lsp.buf.code_action()<cr>')
end
})
Fair warning
Not every language server implements the entire LSP specification. The features of LSP servers may not be consistent between servers.
For example, intelephense can show diagnostics in real time, there is no need to save the file to get new diagnostics. But rust-analyzer, the language server for rust, can only update diagnostics after saving the file.
Here's another example: ruff-lsp, a language server for python. It describes itself as a linter and code formatter. As far as I can tell ruff-lsp
does not provide code completions or semantic highlights.
What I want say is this: read the documentation of the language server so you know what it can do.
Bonus content
At this point I'd say you have all the essential knowledge needed to be productive. What follows are tips, configurations, and features you can implement by adding some boilerplate code in your Neovim configuration.
Configure a language server for multiple filetypes
Sometimes a language server can support multiple filetypes. An example of this is typescript-language-server, the language server for javascript and typescript. In this case a filetype plugin can still work but there is an easier way to go about it.
One option to consider is a "global plugin." In there we can configure the language server in the callback function of a FileType
autocommand.
If you want follow along, install typescript-language-server
using this command in the terminal.
npm install -g typescript typescript-language-server
For this we need to create a plugin
folder inside Neovim's configuration folder. So, we navigate to Neovim's config folder, open Neovim, then execute this command to create the plugin folder.
:call mkdir('./plugin', 'p')
Next we create a lua script, it can have any name we want. We can call it tsserver.lua
.
:edit plugin/tsserver.lua | write
In ts_ls.lua
we are going to adapt the configuration in nvim-lspconfig's source code.
-- plugin/tsserver.lua
local function start_tsserver()
local root_files = {'package.json', 'tsconfig.json', 'jsconfig.json'}
local paths = vim.fs.find(root_files, {stop = vim.env.HOME})
local root_dir = vim.fs.dirname(paths[1])
if root_dir == nil then
-- root directory was not found
return
end
vim.lsp.start({
name = 'tsserver',
cmd = {'typescript-language-server', '--stdio'},
root_dir = root_dir,
init_options = {hostInfo = 'neovim'},
})
end
vim.api.nvim_create_autocmd('FileType', {
pattern = {'javascript', 'javascriptreact', 'javascript.jsx', 'typescript', 'typescriptreact', 'typescript.tsx'},
desc = 'Start typescript LSP',
callback = start_tsserver,
})
This will work exactly like a filetype plugin, except here we are executing one lua function and not an entire file. The advantage of the autocommand is we can define multipe filetypes in the pattern
property.
By the way, this doesn't have to be a global plugin, we can setup the autocommand in the init.lua
file.
Add borders to floating windows
Sadly, there is no way to add borders to all floating windows, this means we have to enable it for each feature.
We can add the following to our init.lua
file.
- Diagnostic details
vim.diagnostic.config({
float = {
border = 'rounded',
},
})
- Documentation window
The one used by the function vim.lsp.buf.hover().
vim.lsp.handlers['textDocument/hover'] = vim.lsp.with(
vim.lsp.handlers.hover,
{border = 'rounded'}
)
- Signature help
The one used by the function vim.lsp.buf.signature_help().
vim.lsp.handlers['textDocument/signatureHelp'] = vim.lsp.with(
vim.lsp.handlers.signature_help,
{border = 'rounded'}
)
Format on save
The only thing we will do here is trigger the function vim.lsp.buf.format() before Neovim saves a file. And of course, we only do it when there is an active language server.
Important note: most language servers with formatting capabilities have their own style settings. For example, we can have 2 space indent in our Neovim config but maybe the language server formats the code with 4 space indent. So it's a good idea to check the documentation of the language server to see how to configure that.
-- You can add this in your init.lua
-- or a global plugin
local fmt_group = vim.api.nvim_create_augroup('autoformat_cmds', {clear = true})
local function setup_autoformat(event)
local id = vim.tbl_get(event, 'data', 'client_id')
local client = id and vim.lsp.get_client_by_id(id)
if client == nil then
return
end
vim.api.nvim_clear_autocmds({group = fmt_group, buffer = event.buf})
local buf_format = function(e)
vim.lsp.buf.format({
bufnr = e.buf,
async = false,
timeout_ms = 10000,
})
end
vim.api.nvim_create_autocmd('BufWritePre', {
buffer = event.buf,
group = fmt_group,
desc = 'Format current buffer',
callback = buf_format,
})
end
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'Setup format on save',
callback = setup_autoformat,
})
Change diagnostics sign text
When using Neovim v0.9.5 or lower we need to call the function vim.fn.sign_define().
-- You can add this in your init.lua
-- or a global plugin
local function sign_define(args)
vim.fn.sign_define(args.name, {
texthl = args.name,
text = args.text,
numhl = ''
})
end
sign_define({name = 'DiagnosticSignError', text = '✘'})
sign_define({name = 'DiagnosticSignWarn', text = '▲'})
sign_define({name = 'DiagnosticSignHint', text = '⚑'})
sign_define({name = 'DiagnosticSignInfo', text = '»'})
When using Neovim v0.10 or greater we should do this with vim.diagnostic.config()
.
-- You can add this in your init.lua
-- or a global plugin
vim.diagnostic.config({
signs = {
text = {
[vim.diagnostic.severity.ERROR] = '✘',
[vim.diagnostic.severity.WARN] = '▲',
[vim.diagnostic.severity.HINT] = '⚑',
[vim.diagnostic.severity.INFO] = '»',
},
},
})
Disable diagnostics in insert mode
This is already the default behavior but there is a problem... not really a problem, just a minor detail: the diagnostics only disappear after we start typing something.
This is the code I use to disable diagnostics right after going into insert mode (or select mode).
-- You can add this in your init.lua
-- or a global plugin
vim.api.nvim_create_autocmd('ModeChanged', {
pattern = {'n:i', 'v:s'},
desc = 'Disable diagnostics in insert and select mode',
callback = function(e) vim.diagnostic.disable(e.buf) end
})
vim.api.nvim_create_autocmd('ModeChanged', {
pattern = 'i:n',
desc = 'Enable diagnostics when leaving insert mode',
callback = function(e) vim.diagnostic.enable(e.buf) end
})
Disable semantic highlights
Neovim's documentation suggest that we "clear" the highlight groups of the @lsp
namespace. So, we can do this.
-- You can add this in your init.lua
-- this should be executed before setting the colorscheme
vim.api.nvim_create_autocmd('ColorScheme', {
desc = 'Clear LSP highlight groups',
callback = function()
for _, group in ipairs(vim.fn.getcompletion('@lsp', 'highlight')) do
vim.api.nvim_set_hl(0, group, {})
end
end,
})
The ColorScheme
autocommand should be created before setting up the colorscheme. This way Neovim can clear the highlights even if we change the colorscheme in the middle of a coding session.
Highlight symbol under cursor
What we want to do here is call the function vim.lsp.buf.document_highlight() when the cursor spends some amount of time on top of a symbol. And then clear the highlight when the cursor moves.
Note, for this to work properly the colorscheme needs to support the following highlight groups:
- LspReferenceRead
- LspReferenceText
- LspReferenceWrite
If the colorscheme does not support these highlight groups, we can "link" them to an existing group. Here's an example using the Search
highlight group.
vim.api.nvim_set_hl(0, 'LspReferenceRead', {link = 'Search'})
vim.api.nvim_set_hl(0, 'LspReferenceText', {link = 'Search'})
vim.api.nvim_set_hl(0, 'LspReferenceWrite', {link = 'Search'})
-- You can add this in your init.lua
-- or a global plugin
-- time it takes to trigger the `CursorHold` event
vim.opt.updatetime = 400
local function highlight_symbol(event)
local id = vim.tbl_get(event, 'data', 'client_id')
local client = id and vim.lsp.get_client_by_id(id)
if client == nil or not client.supports_method('textDocument/documentHighlight') then
return
end
local group = vim.api.nvim_create_augroup('highlight_symbol', {clear = false})
vim.api.nvim_clear_autocmds({buffer = event.buf, group = group})
vim.api.nvim_create_autocmd({'CursorHold', 'CursorHoldI'}, {
group = group,
buffer = event.buf,
callback = vim.lsp.buf.document_highlight,
})
vim.api.nvim_create_autocmd({'CursorMoved', 'CursorMovedI'}, {
group = group,
buffer = event.buf,
callback = vim.lsp.buf.clear_references,
})
end
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'Setup highlight symbol',
callback = highlight_symbol,
})
Simple tab complete
In this one we will use the Tab
(and shift tab) key to navigate between the items in the completion menu. When the completion menu is not visible and the cursor is in a whitespace character, it will insert a tab character. Else, it will trigger the completion menu.
When the language server can provide code completion it'll use that. Otherwise, it will try to suggest words found in the current buffer.
Note that you can use the Enter key or <C-y>
to confirm the current item in the completion menu
-- You can add this in your init.lua
-- or a global plugin
vim.opt.completeopt = {'menu', 'menuone', 'noselect', 'noinsert'}
vim.opt.shortmess:append('c')
local function tab_complete()
if vim.fn.pumvisible() == 1 then
-- navigate to next item in completion menu
return '<Down>'
end
local c = vim.fn.col('.') - 1
local is_whitespace = c == 0 or vim.fn.getline('.'):sub(c, c):match('%s')
if is_whitespace then
-- insert tab
return '<Tab>'
end
local lsp_completion = vim.bo.omnifunc == 'v:lua.vim.lsp.omnifunc'
if lsp_completion then
-- trigger lsp code completion
return '<C-x><C-o>'
end
-- suggest words in current buffer
return '<C-x><C-n>'
end
local function tab_prev()
if vim.fn.pumvisible() == 1 then
-- navigate to previous item in completion menu
return '<Up>'
end
-- insert tab
return '<Tab>'
end
vim.keymap.set('i', '<Tab>', tab_complete, {expr = true})
vim.keymap.set('i', '<S-Tab>', tab_prev, {expr = true})
Expand snippets
Neovim v0.11 introduced a new module called vim.lsp.completion, this will extend the behavior of the builtin completion so it can support "additional text edits" a language server can provide. Additional edits can be things like adding missing import statements or expanding code snippets.
Right now you have to opt-in to the features vim.lsp.completion
provides. So, when a language server is active you have to call the vim.lsp.completion.enable() function.
-- You can add this in your init.lua
-- or a global plugin
vim.opt.completeopt = {'menu', 'menuone', 'noinsert', 'noselect'}
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'Enable vim.lsp.completion',
callback = function(event)
local client_id = vim.tbl_get(event, 'data', 'client_id')
if client_id == nil then
return
end
-- warning: this api is unstable
vim.lsp.completion.enable(true, client_id, event.buf, {autotrigger = false})
-- warning: this api is unstable
-- Trigger lsp completion manually using Ctrl + Space
vim.keymap.set('i', '<C-Space>', '<cmd>lua vim.lsp.completion.trigger()<cr>')
end
})
Notice in the last argument to .enable()
there is a property called autotrigger
. false
is the default value so I just leave it like that. If you set it to true
Neovim will trigger the completion menu when it finds a trigger character. Trigger characters change depending on the language server. In lua for example, the completion will be triggered automatically after a .
or :
character.
If you are using Neovim v0.10 you don't have access to vim.lsp.completion
but you do have access to the module vim.snippet
. You can use that to implement your own snippet expand autocommand.
-- You can add this in your init.lua
-- or a global plugin
-- note: this doesn't support "additional text edits" like
-- adding missing import statements.
local function expand_snippet(event)
local comp = vim.v.completed_item
local kind = vim.lsp.protocol.CompletionItemKind
local item = vim.tbl_get(comp, 'user_data', 'nvim', 'lsp', 'completion_item')
-- Check that we were given a snippet
if (
not item
or not item.insertTextFormat
or item.insertTextFormat == 1
or not (
item.kind == kind.Snippet
or item.kind == kind.Keyword
)
) then
return
end
-- Remove the inserted text
local cursor = vim.api.nvim_win_get_cursor(0)
local line = vim.api.nvim_get_current_line()
local lnum = cursor[1] - 1
local start_col = cursor[2] - #comp.word
if start_col < 0 then
return
end
local set_text = vim.api.nvim_buf_set_text
local ok = pcall(set_text, bufnr, lnum, start_col, lnum, #line, {''})
if not ok then
return
end
-- Insert snippet
local snip_text = vim.tbl_get(item, 'textEdit', 'newText') or item.insertText
assert(snip_text, "Language server indicated it had a snippet, but no snippet text could be found!")
vim.snippet.expand(snip_text)
end
vim.api.nvim_create_autocmd('CompleteDone', {
desc = 'Expand LSP snippet',
callback = expand_snippet
})
vim.snippet
also supports snippet placeholders. This means we can jump to different places in the current active snippet. In Neovim v0.11 the default keybindings to navigate between snippets are Tab
and Shift + Tab
.
If you are using Neovim v0.10, here are some keybindings that you can use.
-- You can add this in your init.lua
-- Control + f: Jump to next snippet placeholder
vim.keymap.set({'i', 's'}, '<C-f>', function()
if vim.snippet.active({direction = 1}) then
return '<cmd>lua vim.snippet.jump(1)<cr>'
else
return '<C-f>'
end
end, {expr = true})
-- Control + b: Jump to previous snippet placeholder
vim.keymap.set({'i', 's'}, '<C-b>', function()
if vim.snippet.active({direction = -1}) then
return '<cmd>lua vim.snippet.jump(-1)<cr>'
else
return '<C-b>'
end
end, {expr = true})
-- Control + l: Exit current snippet
vim.keymap.set({'i', 's'}, '<C-l>', function()
if vim.snippet.active() then
return '<cmd>lua vim.snippet.exit()<cr>'
else
return '<C-l>'
end
end, {expr = true})
Enable inlay hints
Neovim v0.10 or greater is required.
For this we can use the function vim.lsp.inlay_hint.enable().
Note that some language servers may have inlay hints disabled by default. The settings needed to enable hints should be in the documentation of the language server.
-- You can add this in your init.lua
-- or a global plugin
vim.api.nvim_create_autocmd('LspAttach', {
desc = 'Enable inlay hints',
callback = function(event)
local id = vim.tbl_get(event, 'data', 'client_id')
local client = id and vim.lsp.get_client_by_id(id)
if client == nil or not client.supports_method('textDocument/inlayHint') then
return
end
vim.lsp.inlay_hint.enable(true, {bufnr = event.buf})
end,
})
Conclusion
Hopefully I showed is not difficult to "connect" a language server with Neovim. Think about it, 1 shell commmand and 9 lines of lua code is all it takes to get intelephense working in Neovim.
The hard part is gathering all the context inside your head. What does LSP even mean? What's a language server? Filetype plugin? hardly know her. But once you know about the moving pieces and where to find the information you need, it gets easier.
One last thing, don't ignore the basics. Take your time and learn lua, read Neovim's lua guide and learn how to navigate Neovim's documentation with the :help
command.
Thank you for your time. If you find this article useful and want to support my efforts, consider leaving a tip in ko-fi.com/vonheikemen.
Top comments (0)