My Golang IDE: WezTerm, Helix, and CLI Tools
2024-10-10
It’s has been one year from my last post. Today I would like to show how I use Helix as my Golang IDE, powered by WezTerm and collection of CLI tools.
Open Project
I created a simple fish function to pipe all directories in a specific folder into fzf:
function fo --description 'Fuzzy open directory in Helix'
if set -q argv[1]
set searchdir $argv[1]
else
set searchdir $HOME
end
set -l dir (fd --type d . $searchdir | fzf --height=50% --preview 'eza --tree --level=3 --color=always --icons=always {}')
if test -z "$dir"
commandline -f repaint
return 1
end
cd "$dir"
$EDITOR .
end
then I bind it to a key in WezTerm:
local my_keys = {
{
key = "o",
mods = "CMD",
action = act.SpawnCommandInNewTab({
args = {
"fish",
"-c",
"fo ~/Code",
},
}),
},
This allows me to jump into any project directory and open it in Helix almost instantly.
File explorer
Updated: Although file explorer has already been merged, I still prefer using Yazi due to its Vim-style keybindings and full file operation (create, rename, move, delete, etc.):
[keys.normal.";"]
e = [
':sh rm -f /tmp/unique-file',
':insert-output yazi %{buffer_name} --chooser-file=/tmp/unique-file',
':insert-output echo "\x1b[?1049h\x1b[?2004h" > /dev/tty',
':open %sh{cat /tmp/unique-file}',
':redraw',
]
E = [
':sh rm -f /tmp/unique-file',
':insert-output yazi --chooser-file=/tmp/unique-file',
':insert-output echo "\x1b[?1049h\x1b[?2004h" > /dev/tty',
':open %sh{cat /tmp/unique-file}',
':redraw',
]
In my previous post, I showed how to use nnn or broot to open a file tree in the sidebar. However, in practice, I now rely on an earlier PR that led me keep:
- the main editor pan
- a bottom terminal
One I familiar with the codebase, I found myself using the file explorer far less. Still, having it available is useful, and as a bonus, it centers the editor instead of pushing code to the left.
To be hornest, once I familiar myself with the codebase, I don’t use the file explorer too much. However, as a side effect, the file explorer in the sidebar move my code to the center view instead of the left side.
Floating File Explorer
I really like the floating pane feature from Zellij, and initially tried to implement a file explorer using that approach. However, the extra nesting and keybinding complexity eventually pushed me toward a WezTerm-based solution.
With a recent PR adding floating pane, I decided to give it another try:
actions:
explorer:
position: floating
command: HX_PANE_ID=$WEZTERM_PANE YAZI_CONFIG_HOME=~/.config/yazi/filetree yazi
With version 2 of my helix-wezterm.sh script, you can now define:
- an action name
- pane position (left, bottom, floating, etc.)
- the command to execute
The idea of using Yai as a file tree was inspired by a Reddit post.
Helix keybinding:
[keys.normal.";"]
e = ":sh helix-wezterm.sh explorer"
Yazi keymap:
[[manager.prepend_keymap]]
on = ["l"]
run = 'plugin --sync smart-enter'
desc = 'Enter the child directory, or open the file'
Smart-enter plugin ~/.config/yazi/filetree/plugins/smart-enter.yazi/init.lua:
return {
entry = function()
local h = cx.active.current.hovered
if h.cha.is_dir then
ya.manager_emit('enter' or 'open', { hovered = true })
else
local file_path = tostring(h.url)
local hx_pane_id = os.getenv("HX_PANE_ID")
-- Send ":" to start command input in Helix
os.execute('wezterm cli send-text --pane-id ' .. hx_pane_id .. ' --no-paste ":"')
-- Send the "open" command with file path(s) to the pane
os.execute('wezterm cli send-text --pane-id ' .. hx_pane_id .. ' "open ' .. file_path .. '"')
-- Simulate 'Enter' key to execute the command
os.execute('printf "\r" | wezterm cli send-text --pane-id ' .. hx_pane_id .. ' --no-paste')
os.execute('wezterm cli activate-pane --pane-id ' .. hx_pane_id)
end
end,
}
Pressing ; e opens Yazi in a floating pane. Navigating to a file and pressing l opens it directly in Helix.
Converting JSON to struct
Using quicktype:
[keys.select.";"]
q = ["yank_to_clipboard", "collapse_selection", ":insert-output pbpaste | quicktype -l go"]
Database
Linting
actions:
lint:
extensions:
go: golangci-lint run -v $buffer_name
Mocking
Extract the interface name in ~/.local/bin/helix-wezterm.sh:
case "$action" in
"mock")
case "$extension" in
"go")
current_line=$(head -$cursor_line $buffer_name | tail -1)
export interface_name=$(echo $current_line | sed -n 's/^type \([A-Za-z0-9_]*\) interface {$/\1/p')
;;
esac
;;
Then define the mock generator:
actions:
mock:
description: Generate mocks
command: mockery --with-expecter --dir $basedir --name $interface_name
Generating tests
You can generate Go tests using gotests:
actions:
generate_tests:
description: Generate Go tests for the current file
command: gotests -w -all $buffer_name
Testing
Based on cursor position, extract the function name ~/.local/bin/helix-wezterm.sh:
case "$action" in
"test")
case "$extension" in
"go")
export test_name=$(head -$cursor_line $buffer_name | tail -1 | sed -n 's/func \([^(]*\).*/\1/p')
;;
Run the test in ~/.helix-wezterm.yaml:
actions:
test:
extensions:
go: go test -run=$test_name -v ./$basedir/...
[keys.normal.";"]
t = ":sh helix-wezterm.sh test %{buffer_name} %{cursor_line}"
This lets me run either all tests or a specific test function depending on the cursor position.
Integration testing with Hurl
case "$action" in
"test")
case "$extension" in
"hurl")
current_line=$(head -$cursor_line $buffer_name | tail -1)
export entry=$(awk -v cur_line=$cursor_line '
/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/ { entry_line = NR; entry_num++ }
NR == cur_line { print entry_num }
' "$buffer_name")
;;
~/.helix-wezterm.yaml:
test:
description: Test the current file
extensions:
hurl: >
hurl --test --very-verbose --color --to-entry $entry $buffer_name
Running code
actions:
run:
extensions:
go: go run $basedir/*.go
[keys.normal.";"]
r = ":sh helix-wezterm.sh run %{buffer_name} %{cursor_line}"
Debugging
https://overlandandseas.dev/blog/debugging-go-delve-helix/
Opening current file in GitHub / GitLab
case "$action" in
"open")
remote_url=$(git config remote.origin.url)
current_branch=$(git rev-parse --abbrev-ref HEAD)
tracking_branch=$(git for-each-ref --format='%(upstream:short)' refs/heads/$current_branch)
if [[ $remote_url == *"github.com"* ]]; then
tracking_remote=$(cut -d'/' -f1 <<< "$tracking_branch")
tracking_branch_name=$(cut -d'/' -f2- <<< "$tracking_branch")
gh browse "$buffer_name:$cursor_line" --repo "$(git config remote.$tracking_remote.url)" --branch "$tracking_branch_name"
else
if [[ $remote_url == "git@"* ]]; then
open $(echo $remote_url | sed -e 's|:|/|' -e 's|\.git||' -e 's|git@|https://|')/-/blob/${current_branch}/${buffer_name}#L${cursor_line}
else
open $(echo $remote_url | sed -e 's|\.git||')/-/blob/${current_branch}/${buffer_name}#L${cursor_line}
fi
fi
;;
Categories: Development Environment
Tags: helix lazygit tig wezterm yazi
Quan Tong