A simple project switcher for Kakoune

Posted on in

I always appreciate a project-switching interface in my editor.

I didn't expect Kakoune, a barebones terminal editor, to have a built-in project switcher. But, after executing cd $project_dir; kak for the nth time, I started looking for a solution.

How other editors implement a project switcher

VS Code provides a project switcher through Open Recent action, bound to Ctrl+R on Linux. In my experience, any file or folder opened with VS Code's Open File or Open Folder dialogs show up here. So its a bit of a stretch to call it a project switcher if you frequently open files directly.

The following video snippet demonstrates VS Code's switcher:

JetBrains IDEs offer an unbounded Open Recent Project action, which opens the switcher at the tip of the cursor. though I use a FrameSwitcher plugin which performs the job better.

The following video snippet demonstrates WebStorm's switcher:

Elements of a project switcher

I identify two essential elements of a project switcher interface:

  1. Display a list of projects already registered with the interface, through which one can fuzzy find and switch to a project. Once switched, the selected project's directory becomes the current directory within the editor's context, so that editor actions happen in the selected project directory's context. So, for instance, a file finder would now look for a file within the selected project's directory. And so on.
  2. Add a new project to the interface. Usually this is automatic. When user opens a new project in JetBrains IDEs or VS Code, the editor automatically register the project to the switcher interface.

How I manifested the elements in Kakoune

Kakoune's scripting primitives, combined with its philosophy to use shell scripting to extend itself, were adequate tools to build a simple project switcher.

I needed a place to persistently register opened project directories in Kakoune. I chose a plaintext projects file that sits besides kakoune's config file ~/.config/kak/kakrc. Each line in projects store a path to a project dir.

Registering a new project with the interface

I wrote a Kakoune command :project-add that:

  1. lets user choose a directory from their filesystem, using Kakoune's path completion interface
  2. appends the path of the chosen directory in projects file
  3. uses Kakoune's :change-directory primitive to set the chosen directory as Kakoune's working directory

Here is the implementation:

define-command project-add \
  -docstring "Add a project" \
  -params 1 %{
    nop %sh{ printf "%s\n" "$1" >> ~/.config/kak/projects }
    change-directory %arg{1}
    exec ":file-picker "
  }

complete-command project-add file

Switching to a registered project

I implemented a Kakoune command :project-pick that:

  1. lists the paths stored in projects file using Kakoune's completion interface
  2. sets the working directory of Kakoune to the selected path using its :change-directory primitive

Here is the implementation:

define-command project-pick \
  -docstring "Pick a project" \
  -params 1 %{
    change-directory %arg{1}
    exec ":file-picker "
  }

complete-command project-pick \
  shell-script-candidates \
  %{ cat ~/.config/kak/projects }

Bonus - file picker

Both :project-add and :project-pick commands lands the user on a file picker interface, which again builds on top of Kakoune's completion interface, so that they can start working right away on their project.

define-command file-picker \
  -docstring "Pick a file from current directory" \
  -params 1 %{ edit %arg{1} }

complete-command file-picker \
  shell-script-candidates \
  %{ git ls-files -c -o --exclude-standard }

Bonus - keybindings

Both the commands received their own keybindings for quick execution. Kakoune offers custom user modes which can act as namespaces to neatly tuck away related keybindings. I created a project user mode to do just that for my project switcher.

declare-user-mode project

map global user p ":enter-user-mode project
     
      " \
  -docstring "Project commands"

map global project p ":project-pick " \
  -docstring "Pick a project"

map global project a ":project-add " \
  -docstring "Add a project"
     
    
   

In closing

I could implement guardrails like checking for existence of the projects file, or add a :project-delete command, but I'm satisfied with the current state. Any comments are welcome.

Post author's photo Written by Jayesh Bhoot