Skip to content

gitpushjoe/zuzu.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

55 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation


Logo

Fast, powerful, cross-platform build system for Neovim.


zuzu.nvim.showcase.mp4

🎁 Features

    • write multiple different build scripts in one profile
    • project-wide, file-specific, or even global profiles
    • restrict profiles to specific filetypes/depth
    • create generalized setup code to apply to all builds
    • if multiple profiles apply to one file, zuzu will intelligently choose the best one
    • allows you to create profiles that will work on any Python/Javascript/etc. file without setup
    • view runtime errors as diagnostic messages in your source code
    • jump between lines of an error traceback quickly
    • built-in core hooks for things like $file, $dir, $parent, etc.
    • create your own core hooks that will be always be initialized in every build
    • interactive interface for editing hooks
    • create hook choices to easily choose from a list of pre-defined options
    • create your own display strategy (command mode, split terminal right, split terminal below, etc.)
    • bind keymaps to different display strategies
    • you can even run builds in the background!
    • build scripts are also cached to avoid writing files several times on repeated runs
  • 🌐 cross-platform!

    • supports Windows, Linux, MacOS, and other UNIX-likes

Table of Contents

βš’ Installation

βœ… Requirements

  • neovim 0.10.0+ (but will likely work on older versions)

Important

If you are on Windows, you will need to configure Neovim to use Powershell as its shell. Add the following to your init.lua:

vim.o.shell = 'powershell.exe'
vim.o.shellxquote = ''
vim.o.shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command '
vim.o.shellquote = ''
vim.o.shellpipe = '| Out-File -Encoding UTF8 %s'
vim.o.shellredir = '| Out-File -Encoding UTF8 %s'

zuzu.nvim can be installed with the usual plugin managers:

lazy.nvim

{
	"gitpushjoe/zuzu.nvim",
	opts = {
		--- add options here
	}
}

packer.nvim

use {
	"gitpushjoe/zuzu.nvim",
	config = function ()
		require("zuzu").setup({
			--- add options here
		})
	end
}

βš™ Configuration

Default configuration:

require("zuzu").setup({
	build_count = 4,
	display_strategy_count = 4,
	keymaps = {
		build = {
			{ "zu", "ZU", "zU", "Zu" },
			{ "zv", "ZV", "zV", "Zv" },
			{ "zs", "ZS", "zS", "Zs" },
			{ "zb", "ZB", "zB", "Zb" },
		},
		reopen = {
			"z.",
			'z"',
			"z:",
		},
		new_profile = "z+",
		new_project_profile = "z/",
		edit_profile = "z=",
		edit_all_applicable_profiles = "z?",
		edit_all_profiles = "z*",
		edit_hooks = "zh",
		qflist_prev = "z[",
		qflist_next = "z]",
		stable_toggle_qflist = "z\\",
		toggle_qflist = "z|",
	},
	display_strategies = {
		require("zuzu.display_strategies").command,
		require("zuzu.display_strategies").split_right,
		require("zuzu.display_strategies").split_below,
		require("zuzu.display_strategies").background,
	},
	path = {
		root = require("zuzu.platform").join_path(
			vim.fn.stdpath("data"), 
			"zuzu"
		),
		atlas_filename = "atlas.json",
		last_stdout_filename = "stdout.txt",
		-- Note: last_stderr_filename is not used on Windows
		last_stderr_filename = "stderr.txt",
		compiler_filename = "compiler.txt",
	},
	core_hooks = {
		-- Note: these are actually "env:file", "env:dir", etc. on Windows
		{ "file", require("zuzu.hooks").file },
		{ "dir", require("zuzu.hooks").directory },
		{ "parent", require("zuzu.hooks").parent_directory },
		{ "base", require("zuzu.hooks").base },
		{ "filename", require("zuzu.hooks").filename },
	},
	zuzu_function_name = "zuzu_cmd",
	prompt_on_simple_edits = false,
	hook_choices_suffix = "__c",
	compilers = {
		-- https://vi.stackexchange.com/a/44620
		{ "python3", '%A %#File "%f"\\, line %l\\, in %o,%Z %#%m' },
		{ "lua", "%E%\\\\?lua:%f:%l:%m,%E%f:%l:%m" },
		-- https://github.com/felixge/vim-nodejs-errorformat/blob/master/ftplugin/javascript.vim
		-- Note: This will also work for bun.	
		{
			"node",
			[[%AError: %m,%AEvalError: %m,%ARangeError: %m,%AReferenceError: %m,%ASyntaxError: %m,%ATypeError: %m,%Z%*[\ ]at\ %f:%l:%c,%Z%*[\ ]%m (%f:%l:%c),%*[\ ]%m (%f:%l:%c),%*[\ ]at\ %f:%l:%c,%Z%p^,%A%f:%l,%C%m,%-G%.%#]],
		},
	},
	qflist_as_diagnostic = true,
	reverse_qflist_diagnostic_order = false,
	qflist_diagnostic_error_level = "WARN",
	write_on_run = true,
	fold_profiles_in_editor = true,
})
Key Explanation
build_count The number of different builds for each profile.
display_strategy_count The number of display strategies. The 4 strategies by default are "command-mode" :!source run.sh, split-right-terminal, and split-below-terminal, and background.
keymaps.build A 2D list of keymaps. The first row is mapped to the first display strategy, the second row to the second, and so on. The first keymap in each row is mapped to build #1, the second to build #2, and so on. So, for example, pressing "zV" will run the 3rd build in the current profile, with the 2nd build display style (split-right-terminal). Use "" to not bind any keymap.
keymaps.reopen Every time zuzu is run, its output is saved to the path.root directory at path.last_output_filename. Pressing keymap.reopen[i] will show the output from the last time zuzu was run, using display strategy #i.
keymaps.new_profile Creates a new profile. Sets the root to the current file and sets the depth to 0.
keymaps.new_project_profile Creates a new profile. Sets the root to the directory of the current file and sets the depth to -1 (any depth).
keymaps.edit_profile Shows the profile for the current file (the most applicable profile).
keymaps.edit_all_applicable_profiles Shows all applicable profiles for the current file, in order from least applicable to most.
keymaps.edit_all_profiles Shows all profiles.
keymaps.edit_hooks Opens an interactive menu for updating a hook.
keymaps.qflist_prev Opens the quickfix list if it's closed, and jumps to the previous error (see :cprevious).
keymaps.qflist_next Opens the quickfix list if it's closed, and jumps to the nextious error (see :cnext).
keymaps.stable_toggle_qflist Toggles the state of the quickfix list, keeping the cursor in the current window.
keymaps.toggle_qflist Toggles the state of the quickfix list, putting the cursor in the quickfix list. Also toggles whether quickfix diagnostics are shown/hidden.
display_strategies List of display strategies.
path.root The root directory zuzu will use to save any files its creates.
path.atlas_filename The filename for the atlas saved to path.root.
path.last_stdout_filename The filename to save the stdout to from the last time zuzu was run.
path.last_stderr_filename The filename to save the stderr to from the last time zuzu was run.
path.compiler_filename The filename to save the compiler name to from the last time zuzu was run.
core_hooks A list of tuples. The first item in each tuple is the name of the hook, and the second item is a callback to get the value of the hook. For example, by default, the hooks $file and $dir will be automatically initialized to the current file and directory, respectively, before every build.
zuzu_function_name To run a build, zuzu generates a shell file (.sh on UNIX-based, .ps1 on windows) and puts the build script in a function. This is the name of the function.
prompt_on_simple_edits If false, zuzu will skip the confirmation prompt on simple edits (no overwrites or deletes).
hook_choices_suffix See Hooks.
compilers A list of { compiler-name, errorformat } tuples. When running a build, zuzu will search this list first before running :compiler. See Quickfix.
qflist_as_diagnostic If true, the quickfix list locations will also be shown as diagnostics.
reverse_qflist_diagnostic_order If true, then the last quickfix diagnostic will be labeled as #1 instead of the first. The direction of qflist_{prev/next} will also be swapped.
qflist_diagnostic_error_level The severity to use for the quickfix list diagnostics.
write_on_run If true, the current file will be saved before running a build.
fold_profiles_in_editor If true, whenever multiple profiles are shown in the profile editor, they will all be folded, except for the most relevant profile.

⌨ Profiles

A build is a series of shell commands detailing how your code should be run. For example:

echo "Running!"
python3 ./main.py

A profile is a collection of independent, yet related builds. (By default every profile can have a maximum of four builds.) This is an example of a profile with four builds:

### {{ root: * }}
### {{ filetypes: py }}
### {{ depth: -1 }}
### {{ hooks }}
export input=input.txt

### {{ setup }}
cd $dir

### {{ zu }}
### {{ name: primary_build }}
python3 $file

### {{ ZU }}
### {{ name: type_checking }}
mypy $file

### {{ zU }}
### {{ name: benchmark }}
time -p python3 $file

### {{ Zu }}
### {{ name: tests }}
python3 -m unittest -b $file

πŸ“¦ Creating a New Profile

Note

On UNIX-based systems, use Bash syntax and commands in the profile editor. On Windows, use Powershell syntax and commands.

You can use z+ (by default) or require("zuzu").new_profile() to create a new build profile, using the current file as a template. For example, in a file such as /home/user/project/main.cpp (with default settings), you would see the following:

### {{ root: /home/user/project/main.cpp }}
### {{ filetypes: cpp }}
### {{ depth: 0 }}
### {{ hooks }}
### {{ setup }}


### {{ zu }}


### {{ ZU }}


### {{ zU }}


### {{ Zu }}

Because the depth is set to 0, this profile will only apply to the current file. If, for example, you wanted to create a profile that applied to all files in /home/user/project/, an easy way to do that is with z/ or require("zuzu").new_project_profile().

### {{ root: /home/user/project }}
### {{ filetypes: cpp }}
### {{ depth: -1 }}
### {{ hooks }}
### {{ setup }}


### {{ zu }}


### {{ ZU }}


### {{ zU }}


### {{ Zu }}

This sets the depth to -1, which will make your profile apply to all files under root. If you want your profile to apply to multiple filetypes, comma-separate them without spaces:

### {{ filetypes: cpp,cc,hpp,hh }}

You can alternatively use * to specify all filetypes:

### {{ filetypes: * }}

Hooks are covered in this section.

Everything inside the setup section will be executed before a build, regardless of which build is chosen. Here is an example for a C++ project:

### {{ root: /home/user/project }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
### {{ setup }}
cd $dir
rm -rf output
g++ -std=c++17 ./main.cpp -o ./main.o
echo "Compiled!"

### {{ zu }}


### {{ ZU }}


### {{ zU }}


### {{ Zu }}

Note

$dir is explained here.

Finally, zuzu, by default, allows four builds for each profile. These builds are labelled by the keymap used to trigger them. For example, if you create a profile such as this one:

### {{ root: /home/user/project }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
### {{ setup }}
cd $dir
rm -rf output
g++ -std=c++17 -g ./main.cpp -o ./main
echo "Compiled!"

### {{ zu }}
./main blur 5 ./apple.pgm ./output/apple.pgm 

### {{ ZU }}
./main blur 15 ./apple.pgm ./output/apple.pgm 

### {{ zU }}
./main blur 5 ./pear.pgm ./output/pear.pgm 

### {{ Zu }}
./main blur 15 ./pear.pgm ./output/pear.pgm 

and then save the profile with :w, you can press zu to run the first build command (blur the apple image). To cancel the creation of the profile, you can use :q and then select the "exit" option, or use :bd! or :bn or :bp.

Tip

When creating a new profile, you can copy and paste the template to create multiple profiles for different roots/depths/filetypes at once. This is also true when editing a profile.


✏ Editing Profiles

To edit a profile after it's been created, you can use z= to open it. This will open the most applicable profile for the currently-open file. z? will open all profiles that apply to the current file, in order from least applicable to most. z* will open all profiles. To apply your changes, use :w.


πŸ—‘ Deleting Profiles

To delete a profile, simply open the profile using one of the methods above. Deleting the text associated with the profile, then hitting :w, will delete the profile. So, for example, if you wanted to delete all profiles, first open all profiles with z*, delete everything in the buffer, then confirm with :w.


πŸ” Profile Resolution

As you may have noticed, it's very possible for one file to have multiple profiles that apply to it. In this case, zuzu will choose the most applicable profile to use based on the following criteria, in order of importance:

  1. Select the profiles with the closest root. So for a file like /home/user/project/main.cpp, profiles with root /home/user/project/main.cpp would be considered first. If there were no profiles with that root that matched, then profiles with root /home/user/project would be considered, then /home/user/, and so on.
  2. Then, select the profiles with the fewest number of filetypes. So for a .cpp file, a profile with filetypes cpp,h would be considered before a profile with filetypes cpp,c,txt.
  3. Finally, select the profiles with the lowest depth (treating -1 as infinity). Using the previous main.cpp example, if there are two profiles in /home/user/project, but one of them has depth 2 and the other has depth -1, then the profile with depth 2 would be selected.

πŸ’² Hooks

Hooks are environment variables.

If you define environment variables in the {{ hooks }} section, you can easily modify them without opening up the entire profile, by using zh. It will open up a window for you to select the hook you want to change, and then prompt you for the new value (see below). Hooks will be accessible in both {{ setup }} and the build commands.

Example

UNIX version

### {{ root: /home/user/project }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
export filter="blur"
export image="apple"
export level=15

### {{ setup }}
cd $dir
rm -rf output
g++ -std=c++17 -g ./main.cpp -o ./main
echo "Compiled!"

### {{ zu }}
echo "Applying filter $filter to $image with level $level"
./main $filter $level ${image}.pgm ./output/${image}.pgm 

### {{ ZU }}


### {{ zU }}


### {{ Zu }}

Windows version

### {{ root: /home/user/project }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
$filter = "blur"
$image = "apple"
$level = 15

### {{ setup }}
Set-Location -Path $dir
Remove-Item -Recurse -Force "output"
g++ -std=c++17 -g ./main.cpp -o ./main
Write-Output "Compiled!"

### {{ zu }}
Write-Output "Applying filter $filter to $image with level $level"
./main $filter $level "$image.pgm" "./output/$image.pgm"

### {{ ZU }}


### {{ zU }}


### {{ Zu }}


Core Hooks

Some hooks are available by default, and are automatically initialized every time a build command is run. These are called "core hooks" and can be changed, renamed, or added to in the setup() command. To create a new hook, simply pass a tuple to the core_hooks list of setup() with the first item being the name of the hook, and the second item being a function that returns the hook's value. The default core hooks are as follows:

Hook name Hook value
$file Always set to the absolute path of the current file.
$dir Always set to the directory of the current file.
$parent Always set to the parent directory of the current file.
$base Always set to the basename of the current file, without the extension.
$filename Always set to the basename of the current file, including the extension.

Hook Choices

If you declare a hook, but you know in advance that there are only a handful of values it will reasonably have, you can declare another hook that stores the choices for it in an array. This choices hook should have the same name as the original hook but end in "__c" (this can be changed). (Note: if on Windows, use the Powershell array syntax.) Then, when you go to edit the hook, it will display those options in a window for you to easily select.

Example

### {{ root: /home/user/project }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
export filter="blur"
export filter__c=("blur" "saturate" "posterize" "brighten")
export image="apple"
export image__c=(apple dog vacaction)
export level=15
export level__c=(0 5 15 100 150 300)
export optimization="-O3"
export optimization__c=(-O0 -O1 -O2 -O3)
export debug=
export debug__c=("-DDEBUG" "")

### {{ setup }}
cd $dir
rm -rf output
g++ -std=c++17 $debug -g ./main.cpp -o ./main
echo "Compiled!"

### {{ zu }}
echo "Applying filter $filter to $image with level $level"
./main $filter $level ${image}.pgm ./output/${image}.pgm 

### {{ ZU }}


### {{ zU }}


### {{ Zu }}


πŸ–ŒοΈ Customizing Builds

πŸ–‹ Naming Builds

You can also give builds a name (alphanumeric characters only, no spaces), like so:

### {{ zu }}
### {{ name: blur5 }}
./main blur 5 apple.pgm ./output/apple.pgm 

This will give the build a custom filename in the builds folder. Doing this has two primary benefits:

  • You can clearly see the name of the build being run, as opposed to something like /home/.../zuzu/1.sh, and it might improve readability in the profile editor.
  • zuzu implements caching based on filenames. If you frequently switch between the first build of two different profiles, then zuzu would have to write the build commands to that 1.sh file each time. However, if you give the two builds different names, then zuzu will only have to load each build in once.

βœ… Quickfix

Note

This feature is not supported on Windows.

Similarly to :make, zuzu.nvim allows you to assign the name of a compiler to a profile or build. (Neovim comes with quite a few compilers already configured. Type :compiler and then press [Tab] to see them.) Whenever a build is executed, the stderr output is always written to disk in the zuzu folder under the stderr filename specified in setup(). After executing a build, pressing z\ or z| (or also z] or z[) will parse the stderr file using the compiler assigned to the profile or build, and open the quickfix list. (The syntax for doing so is below the image.) Here is an example of how this will appear in Neovim:

Example

Tip

The numbers of the diagnostics count in the opposite direction of their appearances in the quickfix list. This can be preferable for compilers like Python, where the line closest to the source of the error is printed last rather than first. To reverse the order like this, use the reverse_qflist_diagnostic_order option in setup().


Assigning a Compiler

Compilers can be assigned either to all builds in a profile, or to a specific build. If the compiler assigned to a build is different from the compiler assigned to the profile, then the build-specific compiler will be chosen. To assign a compiler to a profile, insert ### {{ compiler: name-of-the-compiler }} under the root header, but above the filetypes header. To assign a compiler to a build, insert the header below the keymap header, and below the name header, if there is one.

### {{ root: * }}
### {{ compiler: python3 }}
### {{ filetypes: py }}
### {{ depth: -1 }}
### {{ hooks }}
### {{ setup }}
cd $dir

### {{ zu }}
# This build will use the "python3" compiler for stderr parsing.
python3 $file

### {{ ZU }}
### {{ compiler: pyunit }}
# This will override the "python3" compiler above, and use the "pyunit" compiler.
python3 -m unittest discover -s tests

Registering a New Compiler

If the compiler you use isn't available under :compiler (or you dislike its implementation), you can register it. Simply add the name of the compiler and its errorformat under the compilers parameter in setup(). errorformats for Python3, lua, node, and bash have been provided, but if you have found/written an errorformat for your language, feel free to submit a pull request.

πŸ–₯ Display Strategies

Display strategies control the way that build commands are run in Neovim. They are functions that take in the following arguments:

---@param shell_cmd string
---@param profile Profile
---@param build_idx integer
---@param last_stdout_path string
---@param last_stderr_path string
local function my_strategy(
	shell_cmd, 
	profile, 
	build_idx, 
	last_stdout_path, 
	last_stderr_path
)

By default, zuzu uses these four display strategies:

# require("zuzu.display_strategies")

local M = {}

M.command = function(cmd)
	vim.cmd("!" .. cmd)
end

M.split_right = function(cmd)
	vim.cmd("vertical rightbelow split | terminal " .. cmd)
end

M.split_below = function(cmd)
	vim.cmd("horizontal rightbelow split | terminal " .. cmd)
end

M.background = function(...)
	-- The implementation is too long to put here.
	-- See ./lua/zuzu/display_strategies.lua
	-- This display strategy runs the command in the "background" by opening 
	-- a new terminal buffer, and switching back to the current buffer. (No
        -- flash/change is noticeable). To access the buffer, use `:bnext`.
end

return M

To use your own custom display strategies, simply pass them to the display_strategies list in the setup() function.

πŸ”§ API

-- Runs build #`build_idx` on the current file.
---@param build_idx integer
---@param display_strategy_idx integer
require("zuzu").run(build_idx, display_strategy_idx)

-- Displays the output from the last time zuzu was run.
---@param build_idx integer
---@param display_strategy_idx integer
require("zuzu").reopen(display_strategy_idx)

-- Opens the profile editor, using the current file as a template for creating
-- a new profile.
require("zuzu").new_profile()

-- Opens the profile editor, using the directory the current file is in as a 
-- template for creating a new profile.
require("zuzu").new_project_profile()

-- Opens the profile editor on the most applicable profile for the current 
-- file.
require("zuzu").edit_profile()

-- Opens the profile editor on all profiles that apply to the current file.
require("zuzu").edit_all_applicable_profile()

-- Opens the profile editor on all profiles.
require("zuzu").edit_all_profiles()

-- Opens a prompt to enter a new name for a hook, or opens a window if the hook
-- has choices. If you want to directly set a hook with choices, skipping the
-- window, prepend "zuzu-direct-set: " to the beginning of `hook_name`.
---@param hook_name string
require("zuzu").edit_hook(hook_name)

-- Opens a window to edit all hooks for the current file.
require("zuzu").edit_hooks()

-- Assigns `hook_val` to the hook with the name `hook_name`.
-- @param hook_name string
-- @param hook_val string
require("zuzu").set_hook(hook_name, hook_val)

-- Opens the qflist if it's closed, and closes it if it's open. Also hides 
-- quickfix-related diagnostics, if they are enabled. If `is_stable` is `true`,
-- then the cursor will stay in the current buffer. If `is_stable` is `false`
-- or `nil`, the cursor will move to the quickfix list.
---@param is_stable boolean?
require("zuzu").toggle_qflist(is_stable)

-- Moves forward or backwards one item in the quickfix list, with wrap-around,
-- based on `is_next`.
---@param is_next boolean
require("zuzu").qflist_prev_or_next(is_next)

-- Prints the current zuzu verison.
require("zuzu").version()

⏰ Benchmarks

Compared to just using the typical command-mode in Neovim (:!), zuzu.nvim takes 0.1-0.5ms longer to run build commands. This includes the time taken for the initial write; note that if the same build is repeatedly run in the same file, zuzu.nvim will elide the redundant writes. After modifying the plugin to write on each build run, the overhead increases to about 0.4-0.6ms.

local zuzu_diffs = {}
local vim_cmd_diffs = {}
local last_output_path = require("zuzu.platform").join_path(
	vim.fn.stdpath("data"),
	"zuzu",
	"stdout.txt"
)
local count = 10

function ZuzuTest(use_zuzu)
    local diffs = use_zuzu and zuzu_diffs or vim_cmd_diffs
	for i = 1, count do
		if i ~= 1 then
			assert(io.popen("sleep 1")):close()
		end
		local handle = assert(io.popen("date +%s%6N"))
		local start_text = handle:read("*a")
		if use_zuzu then
		    require("zuzu").run(1, 1)
		else
		    vim.cmd("!date +\\%s\\%6N >" .. last_output_path)
		end
		local handle2 = assert(io.open(last_output_path, "r"))
		local end_text = handle2:read("*a")
		start_text = string.sub(start_text, 5)
		end_text = string.sub(end_text, 5)
		table.insert(diffs, tonumber(end_text) - tonumber(start_text))
		local sum = 0
		for _, diff in ipairs(diffs) do
			io.write(diff .. " ")
			sum = sum + diff
		end
		print("avg = " .. sum / #diffs .. "us")
		handle2:close()
		handle:close()
	end
end

vim.api.nvim_set_keymap(
	"n",
	"<leader>zt",
	":lua ZuzuTest(true)<CR>",
	{ noremap = true, silent = true }
)
vim.api.nvim_set_keymap(
	"n",
	"<leader>zT",
	":lua ZuzuTest(false)<CR>",
	{ noremap = true, silent = true }
)
### {{ root: * }}
### {{ filetypes: * }}
### {{ depth: -1 }}
### {{ hooks }}
### {{ setup }}

### {{ zu }}
date +%s%6N

πŸ– Highlight Groups

ZuzuCreate
ZuzuReplace
ZuzuOverwrite
ZuzuDelete
ZuzuHighlight
ZuzuBackgroundRun
ZuzuSuccess
ZuzuFailure

Releases

No releases published

Packages

No packages published

Languages