Skip to content

A HammerSpoon plugin to open a file in neovim (for use with Phoenix LiveView)

Notifications You must be signed in to change notification settings

JuneKelly/OpenInNeovim.spoon

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

35 Commits
 
 
 
 
 
 

Repository files navigation

HammerSpoon - Open in Neovim

Set up a HammerSpoon URL event to open a file in neovim. Can be used with phoenix-live-reload to jump to the definition (or caller) of a phoenix live-view component.

A demo of OpenInNeovim

Installation

First, install HammerSpoon.

Then, clone this repository to ~/.hammerspoon/Spoons/OpenInNeovim.spoon, like so...

git clone https://github.com/JuneKelly/OpenInNeovim.spoon ~/.hammerspoon/Spoons/OpenInNeovim.spoon

API Documentation

In hammerspoon code (either in the console, or in ~/.hammerspoon/init.lua), this spoon can be loaded like so:

openInNeovim = hs.loadSpoon("OpenInNeovim")

To bind a URL event handler, call openInNeovim.bind, with a table of configuration options.

Configuration Options

  • token: (required, minimum length 12 characters) the URL must include this token as a query parameter token. If the URL does not contain this parameter, or it does not match, then the error will be shown in a notification.

    • Why is this required? As much as we try to defend against security issues, in some sense this feature is fundamentally built around telling nvim to execute some code, so it is best to guard the entry-point so it can only be activated by URLs having this secret token.
  • nvimPath: (required) full path to the nvim executable

    • (this can be found easily by running command -v nvim in your shell)
  • nvimServerPipePath: (required) full path to the nvim server pipe file

    • (this is the file path you provided when starting nvim with the --listen flag)
  • eventName: (optional, default "openInNeovim") name of the hammerspoon event, which in practice means the part of the URL that comes after hammerspoon://.

  • foregroundApp: (optional, default nil) if present, bring this app to the foreground after the file has been opened. Must be the name of a MacOS app, like "iTerm2" or Ghostty

  • translateRootPath: (optional, default nil) a table with two fields: from, and to. If non-nil, the file path is altered to replace the segment matching from at the start, with to string to. Useful if your phoenix server runs in a docker environment where it's filesystem is different from the host where your nvim editor is running

  • skipValidateFileExists: (optional, default false) By default, we check that the target file path is a real path, and exists, before sending the command to nvim. This helps defend against remote code execution attacks. If this option is set to true, the validation is skipped. This could be useful if the nvim instance is operating on a different filesystem from hammerspoon.

Here's an example using all of the configuration options:

openInNeovim = hs.loadSpoon("OpenInNeovim")

openInNeovim.bind({
  nvimPath = "/opt/homebrew/bin/nvim",
  nvimServerPipePath = "/Users/somebody/.cache/nvim/server.pipe",
  token = "a_dreadful_secret",
  foregroundApp = "iTerm2",
  eventName = "aNiceCustomEventName",
  translateRootPath = {
    from = "/app/inside/docker/",
    to = "/Users/somebody/projects/cool-web-app/"
  }
})

URL Format

This event handler is triggered by opening a URL that looks like:

hammerspoon://openInNeovim?file=<File Path>&line=<Line Number>

The following query parameters are supported:

  • file: (required) path to the file to open (URL encoded)
  • line: (required) line number to open
  • token: (optional) secret token to check against config.token

Usage Examples

Prerequisites

As a prerequisite, we need to find the full path to the nvim executable:

command -v nvim

If you've installed neovim via homebrew, then the result is probably something like /opt/homebrew/bin/nvim.

Minimal Example: One Instance of Neovim

In this example, we have one instance of nvim, acting as a server. We then configure OpenInNeovim to open files in this single nvim server. If you tend to work on one project at a time, this should be sufficient.

1. Start nvim with --listen, and a path to a pipe file

To start nvim in server mode, we pass the --listen <path> parameter, where <path> is a path to a pipe file, which neovim will create:

nvim --listen ~/.cache/nvim/server.pipe

You can make this easier to do repeatedly by creating an alias. For example, in zsh:

# add this to .zshrc
alias nvim-server 'nvim --listen ~/.cache/nvim/server.pipe'

...or in fish:

# run this once in fish shell
alias --save nvim-server='nvim --listen ~/.cache/nvim/server.pipe'

2. Generate a secret token

We need a secret token, to secure this URL endpoint. An easy way to do this is by running uuidgen in the shell:

uuidgen
# => 07048977-9...

3. Configure OpenInNeovim, in the Hammerspoon config file

Add the following to ~/.hammerspoon/init.lua:

openInNeovim = hs.loadSpoon("OpenInNeovim")

openInNeovim.bind({
 nvimPath = "<full path to nvim executable>",
 nvimServerPipePath = "<full path to nvim server pipe>",
 token = "<random token string>",
})

Quit and re-open Hammerspoon. Look in the Hammerspoon console, and you should see log lines indicating that OpenInNeovim has been loaded, and a URL handler has been bound:

2024-12-26 14:26:31: -- Loading Spoon: OpenInNeovim
2024-12-26 14:26:31: [OpenInNeovim] Bind {
  ...
}
2024-12-26 14:26:31: [OpenInNeovim] Binding to URL 'openInNeovim'

4. Configure phoenix_live_reload to trigger this URL event

See the "Jumping to HEEX Function Definitions" section of the phoenix_live_reload README file.

PLUG_EDITOR = 'hammerspoon://openInNeovim?token=<TOKEN>&file=__FILE__&line=__LINE__'

Now, when you hold d and click a phoenix live-view component in the browser, it should open the component definition in neovim, and show a notification to that effect. If not, check the hammerspoon logs.

Realistic Example: Multiple Instances of Neovim

The previous example doesn't work so well if we tend to keep multiple instances of neovim open at a given time, like if we work on several projects at once. In this case, we want to start multiple neovim servers, one for each project, and configure multiple instances of OpenInNeovim, each pointing to the relevant neovim server.

Let's imagine that we regularly work on two phoenix projects: statler and waldorf. Both of these projects live in our ~/code directory:

  • /Users/somebody/code/statler
  • /Users/somebody/code/waldorf

1. Start nvim with --listen, and a pipe file derived from the PWD

First, we need a way to start an nvim server with a pipe file at a predictable location on the file-system. We can solve this problem by taking the following steps:

  1. Take the current working directory from pwd, as a string
  2. Change all / characters to _, to form a "slug" representation of the working directory
  3. Use this slug to name a pipe file in the user's temp directory ($TMPDIR)

For example, in zsh:

echo "${TMPDIR}nvim-server$(pwd | sed 's/\//_/g').pipe"

This will produce a string like:

/var/folders/vr/c_awj73s1264r2bz1f72y99m1241zl/T/nvim-server_Users_somebody_code_waldorf.pipe

If we create our pipe file in this way, we can identify separate neovim servers in separate project directories:

# in zsh
nvim --listen "${TMPDIR}nvim-server$(pwd | sed 's/\//_/g').pipe"

# in fish
nvim --listen (string join '' $TMPDIR "nvim-server" (pwd | sed 's/\\//_/g') ".pipe")

To make this invocation easier, we can create a shell function.

If you're using zsh (the default shell on recent versions of macOS), add the following to ~/.zshrc:

function nvim-server() {
  nvim --listen "${TMPDIR}nvim-server$(pwd | sed 's/\//_/g').pipe" "$@"
}

Or, if you use fish, add the following to ~/.config/fish/functions/nvim-server.fish:

function nvim-server --wraps='nvim' --description 'Start nvim server with pipe file based on PWD'
    nvim --listen (string join '' $TMPDIR "nvim-server" (pwd | sed 's/\\//_/g') ".pipe") $argv
end

Open a new shell, run nvim-server, and look in $TMPDIR (by running ls -la $TMPDIR). You should see a file ending in .pipe, with a name based on the directory in which you ran nvim-server.

For example, if we were to navigate to each of our demo projects (statler and waldorf), and run nvim-server in each of them, when we run ls -la $TMPDIR we should see two files like:

... nvim-server_Users_somebody_code_statler.pipe
... nvim-server_Users_somebody_code_waldorf.pipe

2. Generate a secret token

We need one secret token for each project, to secure each URL endpoint. An easy way to do this is by running uuidgen in the shell multiple times:

uuidgen
# => 07048977-9...

uuidgen
# => AC7EA84C-6...

3. Configure OpenInNeovim for each project

Let's configure OpenInNeovim for each of our projects, statler and waldorf.

Add the following to ~/.hammerspoon/init.lua, replacing the placeholder strings with your own values:

openInNeovim = hs.loadSpoon("OpenInNeovim")

local function pipePath(path)
 return os.getenv("TMPDIR") .. "nvim-server" .. path:gsub("/", "_") .. ".pipe"
end

local nvimPath = "<full path to nvim executable>"

openInNeovim.bind({
 eventName = "openInNeovim_statler",
 nvimPath = nvimPath,
 nvimServerPipePath = pipePath("/Users/somebody/code/statler"),
 token = "<a random string>",
 foregroundApp = "iTerm2",
})

openInNeovim.bind({
 eventName = "openInNeovim_waldorf",
 nvimPath = nvimPath,
 nvimServerPipePath = pipePath("/Users/somebody/code/waldorf"),
 token = "<a different random string>",
 foregroundApp = "iTerm2",
})

4. Configure phoenix_live_reload in each project

Finally, in each of our projects, we can set PLUG_EDITOR:

In the statler project:

PLUG_EDITOR = 'hammerspoon://openInNeovim_statler?token=<TOKEN>&file=__FILE__&line=__LINE__'

And in the waldorf project:

PLUG_EDITOR = 'hammerspoon://openInNeovim_waldorf?token=<TOKEN>&file=__FILE__&line=__LINE__'

(See the "Jumping to HEEX Function Definitions" section of the phoenix_live_reload README file.)

License

This software is published under the MIT license:

Copyright 2025 June Kelly

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

About

A HammerSpoon plugin to open a file in neovim (for use with Phoenix LiveView)

Topics

Resources

Stars

Watchers

Forks

Languages