Integrating Neovim and Godot
In this note, I’ll share how I integrated neovim to work with godot, GDScript and GDShader
Extra Plugins
These are just some plugins I find very useful in general, and that I
think pair well with godot and gdscript.
Feel free to skip this section.
- neo-tree
- nvim-lspconfig
- Comment.nvim
- conform.nvim
- luasnip
- friendly-snippets
- lualine
- mason
- tokyonight.nvim theme
- virt-column.nvim
LSP
You don’t need a plugin for lsp anymore, but I find having nvim-lspconfig
installed to be handy for debugging as it gives you commands such as
:LspInfo :LspStart :LspStop
Installation
return {
"neovim/nvim-lspconfig",
event = { "BufReadPre", "BufNewFile" },
dependencies = {
"saghen/blink.cmp",
"williamboman/mason.nvim",
{
"williamboman/mason-lspconfig.nvim",
dependencies = {
"williamboman/mason.nvim",
},
},
},
opts = {
inlay_hints = { enabled = true },
},
config = function()
local lsp = require("lspconfig")
end,
}
Installing lsp servers
The easiest way to do this is with a plugin called Mason. to set it up, simply add it to your config:
return {
"williamboman/mason.nvim",
dependencies = {
"williamboman/mason-lspconfig.nvim",
"WhoIsSethDaniel/mason-tool-installer.nvim",
},
config = function()
-- NOTE: Import Mason
local mason = require("mason")
-- NOTE: Import mason-tool-installer
local mason_tool_installer = require("mason-tool-installer")
-- NOTE: Setup Mason Before mason_lspconfig as it is a dependency
mason.setup({
ui = {
icons = {
package_installed = " ",
package_pending = " ",
package_uninstalled = " ",
},
},
})
-- NOTE: Setup mason LSPCONFIG after mason
mason_lspconfig.setup({
automatic_enable = false,
ensure_installed = {
"lua_ls",
},
automatic_installation = true,
})
-- NOTE: mason-tool-installer setup
mason_tool_installer.setup({
automatic_enable = false,
ensure_installed = {
"gdtoolkit",
},
})
end,
}
With the above config, gdscript, gdshader,
should be installed automatically when you restart neovim.
If you prefer to install it manually, you can simply do:
:MasonInstall gdtoolkit
Configuration
first, I’ll create a file in nvim/lua/config/lsp.lua. In this file I’ll start the servers using the builtin lsp.
requirements
- blink.cmp
- To see how I do my lsp mappings see lsp_mappings.lua
---@diagnostic disable-next-line: undefined-global
local capabilities = require("blink.cmp").get_lsp_capabilities()
vim.api.nvim_create_autocmd("LspAttach", {
callback = function(ev)
local bufnr = ev.buf
local client = vim.lsp.get_client_by_id(ev.data.client_id)
if not client then return end
-- apply lsp_mappings
local keymaps = require("config.lsp_mappings")
keymaps(client, bufnr)
-- format with lsp
vim.api.nvim_buf_create_user_command(
bufnr,
"Format",
function() vim.lsp.buf.format() end,
{ desc = "LSP: Format current buffer with" }
)
end,
})
vim.lsp.inlay_hint.enable(true)
vim.lsp.config("*", { capabilities = capabilities })
-- a table of all the LSPs I want
local default_servers = {
"gdscript", -- GDScript
}
-- Iterate through the table and enable
for _, s in ipairs(default_servers) do
vim.lsp.enable({ s })
end
Treesitter (syntax highlighting)
return {
{
"nvim-treesitter/nvim-treesitter",
event = { "BufReadPre", "BufNewFile" },
build = ":TSUpdate",
branch = "master",
config = function()
local treesitter = require("nvim-treesitter.configs")
treesitter.setup({
auto_install = true,
ensure_installed = {
"gdscript",
"godot_resource",
"gdshader",
},
highlight = {
enable = true,
additional_vim_regex_highlighting = { "markdown" },
},
indent = {
enable = true,
},
})
end,
},
}
If you prefer to manually install treesitter parsers you can do:
:TsInstall gdscript godot_resource gdshader
Completion (blink.cmp)
to actually get autocompletion working, we’ll need a completion engine.
I really like Blink.cmp and have found it to work very well.
We also use it to get server capabilities for the lsp so it’s required.
If you prefer you can use nvim-lspconfig for the server capabilities and
another completion engine
return {
"saghen/blink.cmp",
version = "*",
--- @module 'blink.cmp'
--- @type blink.cmp.Config
opts = {
keymap = {
-- C-CR is not working as a select_and_accept input
["<C-CR>"] = { "select_and_accept", "fallback" },
["<C-y>"] = { "select_and_accept", "fallback" },
["<C-space>"] = { "show", "show_documentation", "hide_documentation", },
["<C-h>"] = { "hide", "fallback" },
["<A-j>"] = { "snippet_forward", "fallback", },
["<A-k>"] = { "snippet_backward", "fallback", },
["<Up>"] = { "select_prev", "fallback" },
["<Down>"] = { "select_next", "fallback" },
["<C-p>"] = { "select_prev", "fallback" },
["<C-n>"] = { "select_next", "fallback" },
["<C-b>"] = { "scroll_documentation_up", "fallback" },
["<C-f>"] = { "scroll_documentation_down", "fallback", },
["<C-s>"] = { "show_signature", "hide_signature", "fallback", },
},
signature = {
enabled = true,
},
sources = {
default = { "lsp", "buffer", "path",},
providers = {
lsp = {
name = "lsp",
enabled = true,
module = "blink.cmp.sources.lsp",
score_offset = 200,
},
},
},
-- this is completion setup for Neovim's cmdline.
-- You can freely skip this it is not required, but it
-- is convenient
cmdline = {
enabled = true,
completion = {
menu = {
auto_show = true,
},
},
keymap = {
-- C-CR is not working as a select_and_accept input
["<C-CR>"] = { "select_and_accept" },
["<C-h>"] = { "hide", "fallback" },
["<C-space>"] = { "show", "show_documentation", "hide_documentation", },
["<Tab>"] = { "snippet_forward", "select_and_accept", "fallback", },
["<S-Tab>"] = { "snippet_backward", "fallback" },
["<Up>"] = { "select_prev", "fallback" },
["<Down>"] = { "select_next", "fallback" },
["<C-Up>"] = { "scroll_documentation_up" },
["<C-Down>"] = { "scroll_documentation_up" },
["<C-s>"] = { "show_signature", "hide_signature", "fallback", },
},
},
-- this just customizes the appearance of the completion menu.
-- feel free to leave this out, and just use the defaults.
completion = {
menu = {
border = "rounded",
draw = {
columns = {
{
--[[ "source_name", ]]
"label",
"label_description",
gap = 1,
},
{ "kind_icon" },
},
gap = 1,
treesitter = { "lsp" },
},
},
documentation = {
auto_show = true,
auto_show_delay_ms = 350,
window = {
border = "rounded",
},
},
accept = {
auto_brackets = {
enabled = true,
},
},
ghost_text = {
enabled = true,
},
},
-- this is where you can customize icons for
-- the completion menu.
-- I've removed a ton here for the sake of brevity.
appearance = {
nerd_font_variant = "mono",
kind_icons = {
Text = " ",
Method = " ",
Function = " ",
Constructor = " ",
},
},
},
opts_extend = { "sources.default" },
}
you can see my full blink config here: Blink Config
It’s also worth noting that Blink has Default Mappings
DAP (Debug Adapter Protocol)
The debug adapter protocol will allow us to use keymaps in neovim to run and debug the project.
return {
{
"mfussenegger/nvim-dap",
dependencies = {
"williamboman/mason.nvim",
"nvim-neotest/nvim-nio",
},
config = function()
local dap = require("dap")
dap.adapters.godot = {
type = "server",
host = "127.0.0.1",
port = 6006,
}
dap.configurations.gdscript = {
{
type = "godot",
request = "launch",
name = "Launch scene",
project = "${workspaceFolder}/",
launch_scene = true,
},
}
local keymap = vim.keymap.set
-- Debugging keymaps
keymap("n", "<leader>db", dap.toggle_breakpoint, {
desc = "Debug: Toggle breakpoint",
})
keymap("n", "<leader>dc", dap.continue, {
desc = "Debug: Start/Continue",
})
keymap("n", "<leader>dj", dap.step_over, {
desc = "Debug: Step over",
})
keymap("n", "<leader>dl", dap.step_into, {
desc = "Debug: Step into",
})
keymap("n", "<leader>dk", dap.step_out, {
desc = "Debug: Step out",
})
keymap("n", "<leader>dr", function()
dap.run_last()
end, { desc = "Debug: Run Last" })
keymap("n", "<leader>dx", function()
dap.terminate()
end, { desc = "Debug: Terminate" })
end,
},
}
Auto starting The Neovim server
First, I decided to make a .gdproject file in the root of my project
(the parent of the actual godot project directory)
This is just a file that contains the name of the project directory on the first
line with this format:
project_name = your_projects_name
Note, that creating this file is not necessary when you open neovim
in a folder containing a project.godot file.
but I usually open my neovim in the parent directory because of cpp and GDEXtension.
so this will make it possible for us to work with either case
local autocmd = vim.api.nvim_create_autocmd
local server_started
local server_address
autocmd({ "VimEnter", "DirChanged" }, {
callback = function()
-- files to look for
local root_pattern = { ".gdproject", "project.godot", }
-- if files not found, return
local found = vim.fs.find(root_pattern, { upward = true, stop = vim.env.HOME })[1]
if not found then return end
local root_dir = vim.fs.dirname(found)
local addr = root_dir .. "/godothost"
-- if the file found is called .gdproject, then read it to find
-- the Project's Directory name
if found:match("%.gdproject$") then
local f = io.open(found, "r")
if f then
local line = f:read("*l")
f:close()
local project_folder = line:match("=%s*(%S+)") or line:match("(%S+)")
if project_folder then
addr = root_dir .. "/" .. project_folder .. "/godothost"
end
end
end
-- stop the server if it's already running
if server_address and server_address ~= addr then
pcall(vim.fn.serverstop, server_address)
end
-- start the server if it's not running already
server_address = addr
if not server_started then
local success = pcall(vim.fn.serverstart, server_address)
if success then
vim.notify("Godot Server started @: " .. server_address, vim.log.levels.INFO)
server_started = true
end
end
end,
})
-- close the server automagically when closing neovim
autocmd("VimLeave", {
callback = function()
if server_started then
vim.fn.serverstop(server_address)
end
end
})
Godot Settings and Configuration
You’ll want to ensure that you’re external text editor settings in godot looks like this:
Exec Path = nvim
Exec Flags = --server {project}/godothost --remote-send "<esc>:drop {file}<CR>:call cursor({line},{col})<CR>"
and make sure Use External Editor is Ticked