Learn Neovim - Jump Navigation & Session Management (flash.nvim / harpoon)
Series/Learn Neovim/Episode 13
Episode 13 of 28

Learn Neovim - Jump Navigation & Session Management (flash.nvim / harpoon)

This phase closes with extreme acceleration: jumping to a target word with just two key presses via flash.nvim, saving favorite files with harpoon, restoring session layouts with persistence.nvim, and browsing the entire change history with undotree.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After building the visual file explorer with neo-tree.nvim and the buffer-based approach with oil.nvim in episode 12, you can now open, create, and move any file without leaving the keyboard. But there is a deeper question: once you are inside a file, how do you move within it as fast as possible?

Notice your current habits. Want to jump to the word return on line 120? You press /return, Enter, then n several times. Want to return to the same file you were just editing? You open telescope again and retype its name. Working with three or four favorite files simultaneously? You go back and forth with :bnext until you find the right one. All of this works, but it drains precious seconds and breaks your flow of thought.

In episode 13, the finale of Phase 3, we will close all those gaps with four weapons: flash.nvim for jumping to a target word with just two key presses, harpoon for marking favorite files and switching instantly, persistence.nvim for saving and restoring work session layouts, and undotree for browsing the entire change history and returning to any point. Let's begin.

Main Discussion

Jumping to a Target Word with flash.nvim

The traditional way to move fast within a file is search (/pattern then n). The problem: you have to type the pattern, press Enter, then press n repeatedly if there are many matches. flash.nvim completely reworks this concept by leveraging Treesitter (the syntax parser we will discuss in depth in episode 14) to mark targets visually.

lua/plugins/flash.lua
return {
  {
    "folke/flash.nvim",
    event = "VeryLazy",
    opts = {},
    keys = {
      { "s", mode = { "n", "x", "o" }, function()
          require("flash").jump()
        end, desc = "Flash jump" },
      { "S", mode = { "n", "x", "o" }, function()
          require("flash").treesitter()
        end, desc = "Flash treesitter" },
      { "r", mode = "o", function()
          require("flash").remote()
        end, desc = "Flash remote" },
      { "R", mode = { "o", "x" }, function()
          require("flash").treesitter_search()
        end, desc = "Flash treesitter search" },
    },
  },
}

How s (jump) works — with a simple example:

  1. Press s, then type a few characters of the target word, e.g. ret.
  2. Flash immediately marks every visible occurrence of ret on screen with lowercase letter labels.
  3. Press the target label, e.g. k. Done — the cursor jumps immediately.

The key point here: flash only marks targets that are visible on screen. It does not scan the whole file, so movement is very fast and never surprising — the cursor only jumps to what you can see. To jump between empty lines or to positions that are not words, flash also has a treesitter mode: S marks syntax nodes (e.g. function arguments, strings, comments) so jumping is more contextual than mere words.

Warning

s and S in standard Vim are substitute commands (replacing characters/lines). By remapping both to flash, you lose those standard substitutes. The good news: "single character" substitution (s, S) is rarely used compared to pattern-based substitution (:%s/old/new/g from episode 4), so most users gladly trade it for flash's speed. If you still want the substitute function, you can use cl (change char) instead.

The Four Flash Modes to Master

Flash is not just a single mode. The four mappings in our config each have a different role, and understanding the differences is the key to using it to the fullest:

ShortcutModeFunctionWhen to use
sjumpJump to a target character/word on screenFast movement between far-apart lines
StreesitterJump between syntax nodes (arguments, strings, comments)Structural refactoring/editing within one function
rremote (operator-pending)Becomes operator + flash, e.g. dr deletes up to targetCombined with d/c/y operators
Rtreesitter_searchSearch nodes with highlight previewNavigating large parts of a file

The mode with the most "different feel" is r (remote). It is invoked in operator-pending mode — meaning pressing dr then a target label will delete from the cursor position to the target, without needing to set a manual visual selection. Example: cursor at the start of a line, press dr then e → the line is deleted up to the word marked e. This is exactly the operator + motion mental model from episode 3, except the motion now jumps visually. Mode R can even scan the entire file, not just the screen — useful when searching for a node located far below.

Harpoon: Marking Favorite Files for Instant Switching

harpoon from ThePrimeagen answers the "going back and forth to the same file" problem. Think of it like pins/bookmarks in a browser, but for files. Instead of opening telescope and retyping the name, you mark a file once then switch to it with a single key combination. The most fitting scenario: working back and forth with 4-5 core files (e.g. main.go, handler.go, config.go, test/main_test.go).

lua/plugins/harpoon.lua
return {
  {
    "ThePrimeagen/harpoon",
    branch = "harpoon2",
    dependencies = { "nvim-lua/plenary.nvim" },
    keys = {
      { "<leader>a", function()
          require("harpoon"):list():add()
        end, desc = "Harpoon: tandai file aktif" },
      { "<C-e>", function()
          require("harpoon").ui:toggle_quick_menu(require("harpoon"):list())
        end, desc = "Harpoon: buka menu file" },
      { "<C-h>", function()
          require("harpoon"):list():select(1)
        end, desc = "Harpoon: file 1" },
      { "<C-j>", function()
          require("harpoon"):list():select(2)
        end, desc = "Harpoon: file 2" },
      { "<C-k>", function()
          require("harpoon"):list():select(3)
        end, desc = "Harpoon: file 3" },
      { "<C-l>", function()
          require("harpoon"):list():select(4)
        end, desc = "Harpoon: file 4" },
    },
  },
}

The harpoon workflow:

  1. Open the file you want to mark, press <leader>a — the file is added to the harpoon list.
  2. To switch quickly, press <C-h>, <C-j>, <C-k>, or <C-l> according to slots 1-4.
  3. Press <C-e> to open the menu showing all marked files — from here you can remove marks or navigate to another file.

Important

An important difference: harpoon is global by default — the file list is stored across all work sessions, not per-project. That means files marked in project A will still be there when you open project B. For per-project behavior, harpoon v2 provides scopes (local list via require("harpoon"):list({ scope = "dir" })). Decide from the start which one fits your workflow.

Harpoon Commands Worth Knowing

Besides the keymaps, harpoon exposes several :Harpoon... commands useful for list management:

Perintah Harpoon
:Harpoon list             " tampilkan daftar file yang ditandai
:Harpoon add              " tandai file aktif (sama dengan <leader>a)
:Harpoon select 1         " langsung pindah ke slot 1
:Harpoon switch-to 1      " alias select

If you work with many slots (more than 4), the Ctrl+h/j/k/l keymaps are no longer sufficient. The most elegant solution is mapping keys based on left-middle-right position like vi-mode's style, or using the <C-e> menu and pressing numbers. Many users also combine harpoon with which-key.nvim (episode 20) so pressing <leader>h shows the list of slots along with their descriptions.

Session Management: Saving & Restoring Layouts

One of the least-known but most impactful features for productivity is session management. A session in Neovim is a snapshot of the entire state: open buffers, window/split layout, cursor positions, and working directory. With persistence.nvim, you can lock in your work session in the afternoon and reopen it exactly as it was the next morning — without reopening files one by one.

lua/plugins/persistence.lua
return {
  {
    "folke/persistence.nvim",
    event = "BufReadPre",
    opts = {
      options = { "buffers", "curdir", "tabpages", "winsize", "winpos" },
      save_dir = vim.fn.stdpath("state") .. "/sessions/",
    },
  },
}

How it works: persistence.nvim automatically saves the session when you exit Neovim (the VimLeavePre event). When opening Neovim again in the same directory, it detects the stored session and restores buffers, windows, and cursor positions. Three manual commands to remember:

CommandFunction
:PersistenceSaveSave the session manually
:PersistenceLoadLoad the last session for this directory
:PersistenceStopDo not load the session on startup (for this directory)

Note

A lighter alternative is mini.sessions from the mini.nvim ecosystem — a modular one-file library. The difference: mini.sessions focuses on manual control (not automatically saved on exit), so it suits those who want full control: save a session with :SessionsSave, load with :SessionsLoad. The choice between persistence.nvim (automatic) and mini.sessions (manual) depends on your working style.

Controlling What Is Saved: sessionoptions

Behind the scenes, a session is saved as a file containing a series of Neovim commands (like badd for the buffer list, set for options, and normal! for cursor positions). What gets recorded is controlled by the sessionoptions option. Understanding this is important because the default saves some things you may not want — and does not save some things you need:

lua/config/options.lua (tambahan)
vim.opt.sessionoptions = "blank,buffers,curdir,folds,help,tabpages,winsize,terminal"

The two most debated:

  • terminal — saves the contents of terminal buffers in the session. Useful if you often use the embedded terminal (covered in episode 19), but makes session files bigger.
  • folds — saves fold state (which we discussed with neo-tree and will appear again with treesitter). If you do not use folds intensively, removing it from the list makes sessions lighter and restore faster.

Caution

If a restored session does not show the expected windows/splits, first check vim.o.sessionoptions. The classic symptom: buffers are restored but the split layout is lost — that means winsize (or winpos) is not included in sessionoptions.

An example diff if you decide to also save terminal buffers in sessions (relevant after we install the embedded terminal in episode 19):

lua/config/options.lua
# [!code --:1]
vim.opt.sessionoptions = "blank,buffers,curdir,folds,help,tabpages,winsize"
# [!code ++:1]
vim.opt.sessionoptions = "blank,buffers,curdir,folds,help,tabpages,winsize,terminal"

Undo Tree: Browsing the Entire Change History

Vim's undo feature is already good: u for undo, Ctrl+r for redo. But there is a painful limitation: undo is linear. You make changes A → B → C, then undo back to B, then type D. History C is lost forever — even though C might have been exactly what you needed.

The solution is undotree. Neovim actually already stores the entire undo tree in memory, it is just that the default interface does not show it. undotree.nvim makes it visible: every branch of changes is drawn as a tree, and you can jump to any undo point, including branches that were "severed" by undo followed by retyping.

lua/plugins/undotree.lua
return {
  {
    "mbbill/undotree",
    cmd = "UndotreeToggle",
    keys = {
      { "<leader>u", "<cmd>UndotreeToggle<CR>", desc = "Toggle undo tree" },
    },
  },
}

How to read the undotree:

  • Every node in the tree is one change (not per line, but per editing action).
  • The cursor in the left panel shows the current history position; right/left arrows move to other changes.
  • Nodes with a "timestamp" (e.g. 2 hours ago) mark old changes that can be jumped to.

Tip

The undo tree works best if you enable persistent undo (vim.opt.undofile = true from episode 7). With undofile, the undo history is stored in a hidden file (~/.local/state/nvim/undo/) so it survives restarts — you can open the same file tomorrow and still jump to an undo point from last week. Undotree + undofile = a complete safety net for code experiments.

Common Jump Navigation & Session Management Pitfalls

MistakeSymptomSolution
Flash hijacks / searchHave to always use flash even when wanting plain searchFlash does not override /; but if s/S bothers you, remap to gs/gS
Harpoon marks are globalFiles from other projects appear in this project's listUse per-directory scope: require("harpoon"):list({ scope = "dir" })
Session restores the wrong directoryOpening nvim in a new folder but the old layout appearsDelete the old session (:PersistenceStop), or set save_dir per-project
Forgot undofile = trueUndo tree empty after restartEnable vim.opt.undofile = true in lua/config/options.lua
Flash marks no labelsNo labels appear when pressing sMake sure event = "VeryLazy" is set and the language's Treesitter parser is installed (:TSInstall)
Ctrl+h/j/k/l harpoon conflicts with window navigationSwitching windows instead switches filesDo not map harpoon to Ctrl+h/j/k/l if you use split windows; change to Alt+1..4

Closing

In episode 13 we closed Phase 3 with four big accelerators: flash.nvim for jumping to a target word with just two key presses based on Treesitter, harpoon for marking favorite files and switching instantly with a single key combination, persistence.nvim (or mini.sessions) for saving and restoring entire work session layouts, and undotree for browsing the change history tree and returning to any undo point. With this, your journey from "staring at a blank screen" to "navigating a large project as fast as thinking" is complete.

However, everything we built during Phases 1-3 — modal editing, Lua config, plugin manager, visuals, search, navigation — is only the foundation. Movement speed is now maximized, but the editor does not yet truly "understand" the programming language you write. In Phase 4, we will enter the heart of modern Neovim: in episode 14 we will dissect Treesitter and how Neovim does real-time syntax parsing — before building LSP, autocompletion, and formatting in the following episodes. Stay motivated!

Learn Neovim - Jump Navigation & Session Management (flash.nvim / harpoon) | Learn Neovim