Afterword
By and large, the only topic left open was an alternative to Conqueror of Completion for Neovim in Lua. The reason that alternative went untouched was simple: I didn't need one. Or rather, maybe there was a reason — first, to satisfy curiosity; second, CoC really does seem bloated and fairly complex inside. A Lua variant would have come easier to me if I ever wanted to tinker with it. Probably.
On the other hand, functionally the CoC + PhpActor pair covers just about every modern development scenario in projects that aren't overly complicated. CoC cleanly tells apart HTML, CSS, JavaScript and SQL snippets embedded right in a single PHP file; PhpActor handles autoload paths deftly, catches tricky errors, gives signature hints at a decent clip, finds references, and lets you jump freely between declarations.
Still, the project I've been busy with lately clearly isn't simple or even mid-sized. It's a fairly serious, sprawling codebase with plenty of dependencies and, frankly, a nontrivial structure. And there, some features started dropping out and lagging.
Diagnostics went first. Not only did the language server start eating memory, it sometimes just crashed for no visible reason.
Second, when moving quickly through the code, the editor started inserting strange characters into the source. That is plainly incompatible with professional work.
It should be said right away that the Neovim (Lua) route didn't solve everything — new problems appeared in its place. As it turned out later, the culprit was less Vim or CoC and more the PhpActor LSP itself. I haven't managed to pin down the causes of that degradation in detail, and swapping the LSP for Intelephense or Psalm didn't resolve the situation either.
Psalm, as it turns out, is only good as an extra diagnostic utility — for its tasks, PHP Stan is more than enough for me personally. And Intelephense requires a paid license to activate many of the features I need, and buying one from my country is simply impossible. And really there was no point even thinking about it, given that.
What did occur to me was that the missing features could be filled in with separate plugins and gadgets. So here they are in more detail.
Language server
Intelephense, at least in its cut-down form, so far performs better than PhpActor on speed and resource usage. With npm in place, the server installs trivially:
npm i intelephense -g
And it's added to Neovim via the special nvim-lspconfig plugin, plus the helper cmp-nvim-lsp, which mixes completion results into the native omnifunc output. In my case, language server settings live in a separate file in the repository — ~/.config/nvim/lua/plugins/nvim-lspconfig.lua — and its contents probably aren't worth going over in detail. Here's only the part that covers Intelephense directly:
-- import lspconfig plugin safely
local lspconfig_status, lspconfig = pcall(require, "lspconfig")
if not lspconfig_status then
return
end
-- import cmp-nvim-lsp plugin safely
local cmp_nvim_lsp_status, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp")
if not cmp_nvim_lsp_status then
return
end
-- enable keybinds only for when lsp server available
local on_attach = require("plugins.handlers").on_attach
-- used to enable autocompletion (assign to every lsp server config)
local capabilities = cmp_nvim_lsp.default_capabilities()
...
lspconfig.intelephense.setup({
capabilities = capabilities,
filetypes = { "php" },
on_attach = on_attach,
settings = {
intelephense = {
files = {
maxSize = 5000000,
},
},
},
on_init = function(client)
client.server_capabilities.documentFormattingProvider = false
end,
})
...
First it pulls in two plugins in sequence: lspconfig to activate the server, cmp_nvim_lsp to integrate autocompletion. Then comes the instruction for adding extra keybindings, but only when we're dealing with a file that needs a language server. Those combinations live in a separate file, .config/nvim/lua/plugins/handlers.lua. (Don't ask why — I picked that up somewhere too.) Two local variables, on_attach and capabilities, are declared for convenience. Finally, the LSP server itself is wired up when the file type matches the specified one.
The extra settings and the disabled formatting are my current choices. For formatting, for example, I use PHP Code Style Fixer, covered below.
Refactoring
The main features missing for real work with code in the cut-down Intelephense are rename support and navigation to declarations. The latter is fully covered by alternative code navigation tools, but with renaming variables and extracting blocks of code into functions and literals — and inlining them back into the code — I ran into certain problems.
Second, the only refactoring plugin I could find is seriously outdated, and it probably doesn't fully suit modern code. Once you add it — with Packer, say — you get a few functions that partly cover my needs.
...
use("adoy/vim-php-refactoring-toolbox")
...
Overall, in simple situations I haven't noticed any trouble from the staleness so far, and the most common uses work. Still, it's unpleasant to know the project is dead and that the next version of the language may make the plugin completely useless.
You can accept the plugin's default keymap or not. I didn't, because I'm trying to settle on my own semantic approach to key combinations in general. To disable the native scheme, set vim.g.vim_php_refactoring_use_default_mapping to 0 somewhere at startup. Mine lives in ~/.config/nvim/lua/plugins/php-refactoring-toolbox.lua and is called from ~/config/nvim/ftplugin/php.lua.
navigator.lua
Along the way I also had to replace the GUI navigation and completion plugin, since lspsaga also stopped being maintained and generally caused friction with its constantly changing API. Unlike refactoring functions, language specifics don't matter much here, and there are a few more plugins to choose from. I settled on ray-x/navigator.lua, which, besides tag navigation itself, integrates with nvim-lspconfig. Add to the Packer call:
...
use("ray-x/guihua.lua")
use({
"ray-x/navigator.lua",
requires = {
{ "ray-x/guihua.lua", run = "cd lua/fzy && make" },
{ "neovim/nvim-lspconfig" },
},
})
...
Then it's important to disable the configuration of the LSP server previously set up through nvim-lspconfig, because the plugin tries to configure access to the server on its own. In my config that lives in ~/.config/nvim/lua/plugins/navigator.lua:
-- import x-ray/navigator safely
local nav_status, nav = pcall(require, "navigator")
if not nav_status then
return
end
nav.setup({
lsp = {
disable_lsp = {
"lua_ls",
"intelephense",
},
},
})
I could go on with various extra bits and decorations, but they're common to all language servers and somewhat off topic here. So the repository itself is at the reader's service, where it's all set up and working.
Syntax highlighting
As for highlighting, the necessary and sufficient functionality is provided globally by the tree-sitter plugin and needs no extra configuration. For that, this instruction is added to ~/.config/nvim/lua/plugins.lua:
...
-- tree sitter
use({
"nvim-treesitter/nvim-treesitter",
run = function()
require("nvim-treesitter.install").update({ with_sync = true })
end,
})
...
Formatting
The last thing to mention is the formatting function, which I disabled in Intelephense. First, for some reason it can't be configured at all — or maybe I just didn't find how. Second, my current projects use PHP Code Style Fixer, which runs separately and doesn't really have to be embedded in the editor at all costs. But if you're in a similar situation, the next plugin may come in handy too.
This is also an abandoned project, though I haven't found an alternative so far. Or rather, I haven't really looked for one yet. It's called null-ls and is actually a kind of counterpart to Lspconfig, but it works with individual features such as diagnostics, formatting and code actions. You can bolt all sorts of other useful things onto it that aren't part of the main language server — formatting in particular. Add it to the plugins:
...
-- formatting
use({
"jose-elias-alvarez/null-ls.nvim",
config = function()
require("null-ls").setup()
end,
requires = { "nvim-lua/plenary.nvim" },
})
...
And call the settings file ~/.config/nvim/lua/plugins/null-ls.lua from ~/.config/nvim/init.lua with the following contents:
-- import null-ls plugin safely
local setup, null_ls = pcall(require, "null-ls")
if not setup then
return
end
-- for conciseness
local formatting = null_ls.builtins.formatting -- to setup formatters
local diagnostics = null_ls.builtins.diagnostics -- to setup linters
null_ls.setup({
sources = {
formatting.stylua,
formatting.google_java_format,
diagnostics.eslint,
formatting.phpcsfixer.with({
command = "./tools/php-cs-fixer/vendor/bin/php-cs-fixer"
}),
},
timeout = 100000,
timeout_ms = 100000,
debug = false,
})