"Life is all about sharing. If we are good at something, pass it on." - Mary Berry

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:

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:

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

Edit on GitHub