Neovim

The Neovim client lives in clients/neovim/ in the repo. It uses Neovim 0.11's built-in LSP API (vim.lsp.enable) with automatic workspace detection (root_markers = { '.jj' }).

Requirements

Installation

Pick the recipe that matches your plugin manager. Replace /absolute/path/to/bad-juju with the path to your local checkout.

{
  dir = '/absolute/path/to/bad-juju/clients/neovim',
  name = 'bad-juju',
  ft = 'jujutsu',
  opts = {},
}

packer.nvim

use {
  '/absolute/path/to/bad-juju/clients/neovim',
  config = function() require('badjuju').setup({}) end,
}

vim-plug

Plug 'jennings/bad-juju', { 'rtp': 'clients/neovim' }
require('badjuju').setup({})

pathogen

ln -s /absolute/path/to/bad-juju/clients/neovim \
  ~/.vim/bundle/bad-juju

Vundle

Plugin 'jennings/bad-juju'
set rtp+=~/.vim/bundle/bad-juju/clients/neovim

Neovim built-in packages

ln -s /absolute/path/to/bad-juju/clients/neovim \
  ~/.local/share/nvim/site/pack/badjuju/start/bad-juju

Manual / no plugin manager

vim.opt.rtp:prepend('/absolute/path/to/bad-juju/clients/neovim')
require('badjuju').setup({})

Configuration

setup() is optional. Only call it if you want to override defaults:

require('badjuju').setup({
  -- Path to the jj binary; forwarded to the server.
  binaryPath = nil,
  -- Default revset for :JJLog when called with no argument.
  defaultLogRevset = nil,
  -- Hotkey profile: "magit" (default), "vim", or "none".
  keymapProfile = nil,
})

Commands

CommandDescription
:JJStatusOpen .jj/badjuju/status.jujutsu
:JJLog [revset]Open the log; defaults to @
:JJLogFile [revset]Open the per-file log for the current buffer's file; defaults to ..@
:JJDescribe [revision]Edit a commit message (defaults to @)
:JJDiff [revision]Open a diff (defaults to @)
:JJNewCreate a new change
:JJRefreshRefresh the badjuju buffer at the cursor
:JJSquash [file] [revision]Squash a file into its parent
:JJUnsquash [file] [revision]Unsquash a file from parent into child
:JJUndoRun jj undo and refresh
:JJAbandon [revision]Abandon a revision (defaults to @)

Commands auto-start the LSP for the current workspace if it isn't already running.

Keymaps

Two built-in profiles, plus "none" to disable defaults. Set keymapProfile in setup() to switch.

magit profile (default)

status.jujutsu

KeyAction
RRefresh
nNew change
LOpen log
l fOpen the per-file log for the file at cursor
fGit fetch
pGit push
PGit push --force-with-lease
eEdit commit at cursor (move @)
b c/m/d/t/fBookmark: create / move / delete / track / forget
r sMark rebase source (jj rebase -s)
r rMark rebase source (jj rebase -r)
r bMark rebase source (jj rebase -b)
r oRebase onto commit at cursor (-d)
r AInsert after commit at cursor (--insert-after)
r BInsert before commit at cursor (--insert-before)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
sSelect squash source or destination (two-step)
SSquash file at cursor into parent
uUnsquash file at cursor
aAbandon commit at cursor
UUndo
xCancel pending operation (squash or rebase)
=Diff (alias for d)
qClose window
?Show key binding help

log.jujutsu

KeyAction
RRefresh
eEdit commit at cursor (move @)
b c/m/d/t/fBookmark: create / move / delete / track / forget
r sMark rebase source (jj rebase -s)
r rMark rebase source (jj rebase -r)
r bMark rebase source (jj rebase -b)
r oRebase onto commit at cursor (-d)
r AInsert after commit at cursor (--insert-after)
r BInsert before commit at cursor (--insert-before)
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
c wCommit transient → reword (describe) commit at cursor
c nCommit transient → new child commit
sSelect squash source or destination (two-step)
aAbandon commit at cursor
UUndo
xCancel pending operation (squash or rebase)
=Diff (alias for d)
<CR>Apply revset shortcut on cursor line
qClose window
?Show key binding help

diff.jujutsu

KeyAction
RRefresh (re-runs jj diff)
qClose window
?Show key binding help

describe.jujutsu

Key (mode)Action
<C-c><C-c> (normal)Finalize commit (save and close)
<C-c><C-k> (normal)Abort (close without saving)
<C-c> (insert)Finalize commit (save and close)
? (normal)Show key binding help

vim profile

Two-letter verb chords inspired by Fugitive. Most actions use doubled letters (nn, dd, etc.) to keep single keys free for text navigation:

KeyAction
nnNew change
llOpen log
eeEdit commit at cursor (move @)
ddDescribe commit at cursor
dDiff change at cursor (updates on amend)
DDiff commit at cursor (pinned, immutable)
=Diff (alias for d)
bb c/m/d/t/fBookmark: create / move / delete / track / forget
rr sMark rebase source (jj rebase -s)
rr rMark rebase source (jj rebase -r)
rr bMark rebase source (jj rebase -b)
rr oRebase onto commit at cursor (-d)
rr AInsert after commit at cursor (--insert-after)
rr BInsert before commit at cursor (--insert-before)
ssSelect squash source or destination (two-step)
SSSquash file at cursor into parent
uuUnsquash file at cursor
UUUndo
aaAbandon commit at cursor
xCancel pending operation (squash or rebase)
ffGit fetch
ppGit push
PPGit push --force-with-lease

Enable with:

require('badjuju').setup({ keymapProfile = 'vim' })

none profile

Disables all built-in keymaps. Define your own using the :JJ* commands above.

Auto-refresh

Open status, log, and diff buffers auto-refresh whenever a jj operation runs — even one you ran in a terminal. No manual reload required.

Syntax highlighting

Highlights come from the LSP via semantic tokens. Your colorscheme's standard token groups (comments, keywords, strings, types, enum members, numbers, operators) are picked up automatically; no extra configuration needed.