Neovim Go-to-file Debug Workflow

I've recently gone back to using Neovim as my primary editor. I appreciate the snappiness of it compared to VSCode. However, it has a steeper skill curve than VS Code.

One of the features I miss from VS Code was the ability to click on relative file paths in the integrated terminal to go there. It was a common part of my editing workflow. If I make a type change that's going to require edits in many files, I'd do something like npm run type-check, then start working through the errors by clicking and editing. Luckily, I found two replacement features that get you 90% of the way there.

Update

Since I wrote this, I've discovered a better way - Neovim's quickfix list.

Strdr4605 has a nice article on how to setup and use this feature. The quickfix list makes debugging and refactoring much faster.

The one thing I haven't been able to solve yet is using the quickfix list from a non-root part of the project. I'll often use lf to navigate and open the file I want to edit. This sets my working directory to the directory the file is in, which is rarely the root of the project. However, the :make command with this setup produces paths from the project root, which leaves Neovim unable to find the paths.

One option would be to traverse upwards and set my working directory to the nearest `package.json`. I'm going to try it out for a bit and see if it sticks

Neovim's integrated Terminal and 'gf'

One way to replicate the feature is to use Neovim's integrated terminal through the :terminal command. The i key will attach your cursor to the terminal. You can then run commands as you would in a normal terminal. The "escape" key will detach your cursor. Then you can use Neovim's movement keys to move through the terminal as a normal buffer. Finally, when you hover over a file path, you can use the key combination gf (go file) to go to that file, make your fixes, and control+o to go back to the terminal.

Let's look at an example:
main.go

package main

import (
  "fmt"
)

func main() {
  hello := "hello"
  world := "world!"
  fmt.Printf("%s\n", hello)
}
Now using our the :terminal command, we can build our project.
$ go build
# github.com/zemberdotnet/example
main.go:9:3: world declared but not used
Now, when you press escape and put the cursor over "main.go" and type "gf", Neovim will open "main.go" in a new buffer.

Custom Shell Function

Often times, I'm just in a plain shell running commands. This is a simple function that will take the output of a shell command and open it as a read-only file in Neovim. You can then use the same gf key combination on the results.

bash
function dbn() {
  eval "$@" 2>&1 | tee $(tty) | nvim -R -
}
zsh
dbn() {
  eval "$@" 2>&1 | tee $(tty) | nvim -R -
}
fish
function dbn
  eval "$arv 2>&1" | tee $(tty) | nvim -R -
end

In these commands, $@ (or $argv in Fish) represents all the parameters the user passes to the function. For example, if you enter dbn go build, then $@ (or $argv) becomes "go build".

The 2>&1 redirects stderr to stdout, with '2' and '1' being the file descriptors for stderr and stdout, respectively. The output is then piped into the tee command, which writes to both a file and stdout. The result is that we write to stdout twice: once via $(tty) for preserving normal command history and again for Neovim, which allows you to navigate through the output.

More thoughts

This flow isn't perfect. Part of the benefit of being able to click with VSCode is it lets you decide to go to the file after you've run a command. This means you can to navigate even when you weren't expecting the command to return something to navigate to. I'm still looking at improving this flow through the use of one of the features of the Kitty terminal.

Edit: warp.dev has a command output highlighting feature that may be related to the ability to inject code or text allowing post-hoc navigation or commands.