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 LSPLanguage 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:

After doing my research and understanding the vocabulary of the LSP landscape, I decided to go with these well-established plugins:

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:

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.

2025-02-21: 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!

On Vim compiler plugins, the quickfix list, and vim-dispatch:

2025-02-27: 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!

Code, compilation, simulator, debug output—everything happens or is triggered inside Neovim.

2025-05-07: 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: 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 :make command executes the command given with the makeprg option. This is done by passing the command to the shell given with the shell option. This works almost like typing

:!{makeprg} [arguments] {shellpipe} {errorfile}.

So after collecting the facts that

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:

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).