Playdate Development With Neovim
I have been using Vim/Neovim as my editor for two years now, but I haven’t made an effort to dive into the topic that is LSP—Language Server Protocol. Since I also want to get into game development for the Playdate, I figured I’d marry those two topics and see how far I can get making Neovim my Playdate IDE (integrated development environment).
Others have tried that before and have shared their approach in the Neovim central thread on the Playdate forum.
Add LSP Features to Neovim
Neovim supports LSP by acting as an LSP client. I decided not to go with a one-stop shop solution like LSP Zero to learn a bit more about the underlying mechanics. There is abundant material on (Neo)Vim and LSP online, but everyone does it a tiny bit different which makes it tricky to piece everything together. My advice: Take the time to learn Lua and learn from other people’s configuration files.
Some resources that have helped me shape my understanding:
- Setup nvim-lspconfig + nvim-cmp
- Ultimate Neovim Config | 2024
- Neovim Config - Part 3 -- LSP
- Config by ThePrimeagen
- Config by Treeman
After doing my research and understanding the vocabulary of the LSP landscape, I decided to go with these well-established plugins:
- nvim-lspconfig for LSP server configurations
- mason to install LSP servers
- For now, I have only installed the lua-language-server (or lua-lsinlspconfigspeak); I will need to improve my configuration once I want to add more language servers
 
- For now, I have only installed the lua-language-server (or 
- nvim-cmp for autocompletion
- LuaSnip for snippets
 
My Neovim configuration is on Sourcehut. The other listed dependencies are necessary for plugins to play nice with each other and to extend the autocompletion capabilities.
Playdate-Specific Configurations
After working through the Playdate documentation for Lua and installing the SDK (software development kit), I wanted to add Playdate-specific settings to my LSP configuration. All possible settings can be found in the wiki of the Lua language server.
I changed my settings to achieve the following:
- Add the SDK files as a library so that types are picked up by autocompletion
- Declare importof the Playdate runtime as an alternative torequire- Change completion.requireSeparatorto use slashes instead of dots
 
- Change 
- Make the additional assignment operators known
It was not enough to add $PLAYDATE_SDK_PATH/CoreLibs as a library; I would get some autocompletion but not everything. For example, the global playdate did not get picked up along with all fields as it seems. playdate-luacats is doing a much better job! In the end, I decided to add both: $PLAYDATE_SDK_PATH (without the /CoreLibs) to complete CoreLibs imports, and playdate-luacats for everything else.
Compiler Plugin
The Playdate SDK comes with its own compiler pdc. Since leaving Vim to build my code and going back into Vim to make necessary changes is a cumbersome workflow, I started to look into ways of compiling my code inside the editor. Vim offers a couple of solutions for that.
The naïve approach is running executables via Vim’s command line, for example :!pdc % build/out.pdx where % is a shorthand for the filepath of the active buffer. I am calling it naïve because there are two flaws: (1) the compiler output is gone as soon as I hit a key, and (2) the compiling happens synchronously, i.e. I can’t operate Vim while waiting for the compilation to finish.
The first point can be addressed by creating a compiler plugin. Vim ships with many compiler plugins out of the box—use :compiler <tab> to cycle through them—but not for pdc. Luckily, creating one myself comes down to only setting two options: makeprg for the executable I want to run my code against, and errorformat to tell Vim how to parse compiler output into the quickfix list.
if vim.g.current_compiler ~= nil then
  return
end
vim.g.current_compiler = "pdc"
-- Create build folder
local cwd = vim.fn.getcwd()
local project = vim.fn.fnamemodify(cwd, ":t")
vim.fn.mkdir(cwd .. "/build", "p") -- If exists, exits silently
local errorformat = {
  "%t%*[^:]: %f:%l:%m"
}
vim.o.makeprg = "pdc " .. cwd .. "/source " .. cwd .. "/build/" .. project .. ".pdx"
vim.o.errorformat = table.concat(errorformat, ",")
This code also created a dedicated build folder to keep things clean in my project directory. Now I can tell Vim to use my compiler plugin for Playdate Lua files with :compiler pdc and run :make to compile my code. If there are any issues, Vim will populate the quickfix list with entries that take me to the respective lines.
To solve the second nuisance—synchronous compilation that blocks me—I installed the plugin vim-dispatch. This extension adds the command :Make which compiles my code asynchronously and automatically opens the quickfix list should there be any issues. Nice!
Recommended Resources
On Vim compiler plugins, the quickfix list, and vim-dispatch:
- Chapter Compile Code and Navigate Errors with the Quickfix List in Practical Vim, Second Edition by Drew Neil
- Part 4 Working with the Quickfix List in Modern Vim by Drew Neil
More Convenience
Spoiler: I am madly in love with my current Playdate development setup in Neovim!
What did I do? First, I created an ftplugin for Lua that sets pdc as the compiler and adds a keyboard shortcut for :Make if there is a source/main.lua file in the working directory. I use that—admittedly somewhat flimsy—check (derived from Playdate’s suggested project structure) to detect whether a Lua project is also a Playdate project. I might change that to checking for a pdxinfo file at the project root.
Second, I extended my makeprg command to include a call to the Playdate simulator after compiling the code. Thanks to the added support for Neovim’s terminal emulator via the dispatch-neovim plugin, I get all the console output of the simulator straight into my editor. Build and run at the press of a button!

Debugging My Setup and Altering the Quickfix List
I might have jumped the gun on declaring mad love for my Playdate dev setup in Neovim a bit. And by that I mean I definitely have as those two breaking bugs clearly laugh in my face:
- Bug 1: A compilation error reported by pdcled to errorE40: Can’t open errorfile. But only when calling:make,:Makeworked fine. Although…
- Bug 2: The quickfix items generated by the console output of the PlaydateSimulatorwhen calling:Makehad weird line breaks and empty lines.
Bug 1: E40: Can’t open errorfile and Bash Scripting
Let’s begin with understanding what Neovim is actually doing when :make is run by turning to the docs.
The
:makecommand executes the command given with themakeprgoption. This is done by passing the command to the shell given with theshelloption. This works almost like typing
:!{makeprg} [arguments] {shellpipe} {errorfile}.
So after collecting the facts that
- shellis set to Bash
- shellpipeis set to- 2>&1| teefor Bash
- errorfileis probably a mistake because- makeefis the error file used for the- :makecommand, which is empty and thus defaults to an internally generated temp file (- /tmp/nvim.<user>/<gibberish>/0in my case)
- no argumentsare used
I get the following command run in a Bash shell (simplified for better readability):
pdc source/ build/test.pdx && PlaydateSimulator build/test.pdx 2>&1 | tee test.err
And sure enough, if the pdc compiler reports an error, I get no error file test.err. I simplified this command further to verify my hypothesis that the pipe | control operator has precedence over the logical operator &&.
false && echo "Hello" 2>&1 | tee test.err
This command demonstrates the same behavior: false exits with a status code emulating failed compilation and no test.err file. I was onto something, but getting the hypothesis of “operator precedence” verified by some kind of trusted source online was harder than expected. I settled with this StackExchange answer on precedence of pipe and logical and in Bash and put my makeprg inside parenthesis to run it in a subshell.
(pdc source/ build/test.pdx && PlaydateSimulator build/test.pdx) 2>&1 | tee test.err
Success! Running this on the command line gives me the compiler error on both standard output and inside the test.err file. That error file can then by parsed with errorformat for the Quickfix list. I suspect vim-dispatch to do something similar, as I didn’t get the same behavior when using :Make instead of :make; but I have to admit that I didn’t take the time to read through the Vimscript code of the plugin just like I skipped drilling deeper into Bash technicalities—for example, how the pipe is not really an operator, but rather a control sequence?! I’m sure I will go down that rabbit hole one day.
Bug 2: Beware of Age-Old Plugins
The second bug was way more irritating, as I actually want to use vim-dispatch’s async :Make over the built-in :make. But :Make gave me broken and empty lines in the Quickfix list that apparently did not get properly parsed by errorformat. What first confused me even more ultimately led to the solution: The faulty behavior only happened with errors reported by PlaydateSimulator, and errors were correctly parsed depending on the window width of my terminal emulator that ran Neovim. That’s when I started to suspect the plugin vim-dispatch-neovim to screw things up by piping the console output of PlaydateSimulator into a Neovim terminal.
Again, I’m sorry to report that I did not dig too deep into the plugin’s code, as its last commit being nine years old was reason enough for me to uninstall it. Having the output of external programs inside Neovim’s terminal was nice but not a necessity. Removing the plugin restored my :Make shortcut. Wicked! I guess the moral of this story is: know and check your plugins, use the platform as much as possible.
Altering the Quickfix List
Funnily enough, both bugs were a mere distraction from the actual issue I set out to solve: Jumping to a quickfix item created from the PlaydateSimulator console ouput created a new empty buffer. What I expected was Neovim navigating me to the existing file. To understand the root cause—rather a nuisance on Playdate’s side—I had to turn to my project’s folder structure:
project/
├── build/
│   └── …
└── source/
    ├── main.lua
    └── …
My current working directory for Neovim is project/, but the working directory for both pdc and PlaydateSimulator is source/. That means when there is a runtime error in file main.lua, PlaydateSimulator reports an error relative to the source/ folder. Neovim combines that path with my working directory leading to file project/main.lua which does not exist. Navigating to that file via Quickfix list hence opens a new empty buffer.
To fix that behavior I decided to write an autocommand that alters the file name of Quickfix list items. The code is a bit too declarative for my taste and I am sure there is room for improvement, but for now it gets the job done.
What I learned:
- The answer to a StackOverflow post on altering errorformat-parsed file names taught me that there is aQuickFixCmdPostevent and that Quickfix items can be retrieved and changed withgetqflist()andsetqflist(), respectively.
- Another forum answer on how to change Quickfix file names helped me understand that Quickfix items don’t necessarily come with a filenameattribute but rather abufnr.vim.api.nvim_buf_get_name(<bufnr>)gets me the corresponding file name.
- The bufnraddresses a so-called unlisted buffer.:lswon’t show them, but:ls!does. I can delete an unlisted buffer with:bwipeout <bufnr>.
What Else?
After my joyous experience of setting up LSP for PHP in Neovim 0.11, I decided to also move my Lua language server config to ~/.config/nvim/after/lsp/lua-ls.lua. Project-specific settings now live in local .luarc.json files. I also switched from nvim-cmp to blink.cmp for completion. Mainly because the setup was a lot simpler and LSP capabilities are set automatically since Neovim 0.11 (check with :checkhealth lsp).