Full Code of ThePrimeagen/harpoon for AI

master 1bc17e3e42ea cached
26 files
67.5 KB
17.8k tokens
1 requests
Download .txt
Repository: ThePrimeagen/harpoon
Branch: master
Commit: 1bc17e3e42ea
Files: 26
Total size: 67.5 KB

Directory structure:
gitextract_l5y7hm4o/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── format.yml
│       └── lint.yml
├── .luacheckrc
├── .stylua.toml
├── LICENSE
├── Makefile
├── README.md
├── TODO.md
├── lua/
│   ├── harpoon/
│   │   ├── cmd-ui.lua
│   │   ├── dev.lua
│   │   ├── init.lua
│   │   ├── mark.lua
│   │   ├── tabline.lua
│   │   ├── term.lua
│   │   ├── test/
│   │   │   ├── manage-a-mark.lua
│   │   │   └── manage_cmd_spec.lua
│   │   ├── tmux.lua
│   │   ├── ui.lua
│   │   └── utils.lua
│   └── telescope/
│       └── _extensions/
│           ├── harpoon.lua
│           └── marks.lua
└── scripts/
    └── tmux/
        └── switch-back-to-nvim

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# top-most EditorConfig file
root = true

# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
indent_style = space
indent_size = 4
insert_final_newline = true


================================================
FILE: .github/FUNDING.yml
================================================
github: theprimeagen


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Found something wrong with Harpoon2?
title: ''
labels: ''
assignees: ''

---

**WARNING**
If this is about Harpoon1, the issue will be closed.  All support and everything of harpoon1 will be frozen on `master` until 4/20 or 6/9 and then harpoon2 will become master

Please use `harpoon2` for branch
---------------


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''

---

**What issue are you having that you need harpoon to solve?**

**Why doesn't the current config help?**

**What proposed api changes are you suggesting?**


================================================
FILE: .github/workflows/format.yml
================================================
name: Format

on: [push, pull_request]

jobs:
    format:
        name: Stylua
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - run: date +%W > weekly

            - name: Restore cache
              id: cache
              uses: actions/cache@v2
              with:
                path: |
                  ~/.cargo/bin
                key: ${{ runner.os }}-cargo-${{ hashFiles('weekly') }}

            - name: Install
              if: steps.cache.outputs.cache-hit != 'true'
              run: cargo install stylua

            - name: Format
              run: stylua --check lua/ --config-path=.stylua.toml


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on: [push, pull_request]

jobs:
    lint:
        name: Luacheck
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v2
            - name: Setup
              run: |
                  sudo apt-get update
                  sudo apt-get install luarocks
                  sudo luarocks install luacheck

            - name: Lint
              run: luacheck lua/ --globals vim


================================================
FILE: .luacheckrc
================================================
std = luajit
cache = true
codes = true

globals = {
    "HarpoonConfig",
    "Harpoon_bufh",
    "Harpoon_win_id",
    "Harpoon_cmd_win_id",
    "Harpoon_cmd_bufh",
}
read_globals = { "vim" }


================================================
FILE: .stylua.toml
================================================
column_width = 80
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 4
quote_style = "AutoPreferDouble"


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 ThePrimeagen

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: Makefile
================================================
fmt:
	echo "===> Formatting"
	stylua lua/ --config-path=.stylua.toml

lint:
	echo "===> Linting"
	luacheck lua/ --globals vim

pr-ready: fmt lint


================================================
FILE: README.md
================================================
<div align="center">

## ⇁  HARPOON 2
This is a deprecated and all future changes will be to the branch `harpoon2`.

[Harpoon 2](https://github.com/ThePrimeagen/harpoon/tree/harpoon2)

**STATUS**: Merging into mainline April 20th or June 9th (nice)

-------------------------------
# Legacy Harpoon README

# Harpoon
##### Getting you where you want with the fewest keystrokes.

[![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org)
[![Neovim](https://img.shields.io/badge/Neovim%200.5+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io)
</div>

![Harpoon](harpoon.png)
-- image provided by **Bob Rust**

## ⇁  WIP
This is not fully baked, though used by several people. If you experience any
issues, see some improvement you think would be amazing, or just have some
feedback for harpoon (or me), make an issue!


## ⇁ The Problems:
1. You're working on a codebase. medium, large, tiny, whatever. You find
yourself frequenting a small set of files and you are tired of using a fuzzy finder,
`:bnext` & `:bprev` are getting too repetitive, alternate file doesn't quite cut it, etc etc.
1. You want to execute some project specific commands or have any number of
persistent terminals that can be easily navigated to.


## ⇁  The Solutions:
1. The ability to specify, or on the fly, mark and create persisting key strokes
to go to the files you want.
1. Unlimited terminals and navigation.


## ⇁ Installation
* neovim 0.5.0+ required
* install using your favorite plugin manager (`vim-plug` in this example)
```vim
Plug 'nvim-lua/plenary.nvim' " don't forget to add this one if you don't have it yet!
Plug 'ThePrimeagen/harpoon'
```

## ⇁ Harpooning
here we'll explain how to wield the power of the harpoon:


### Marks
you mark files you want to revisit later on
```lua
:lua require("harpoon.mark").add_file()
```

### File Navigation
view all project marks with:
```lua
:lua require("harpoon.ui").toggle_quick_menu()
```
you can go up and down the list, enter, delete or reorder. `q` and `<ESC>` exit and save the menu

you also can switch to any mark without bringing up the menu, use the below with the desired mark index
```lua
:lua require("harpoon.ui").nav_file(3)                  -- navigates to file 3
```
you can also cycle the list in both directions
```lua
:lua require("harpoon.ui").nav_next()                   -- navigates to next mark
:lua require("harpoon.ui").nav_prev()                   -- navigates to previous mark
```
from the quickmenu, open a file in:
a vertical split with control+v,
a horizontal split with control+x,
a new tab with control+t

### Terminal Navigation
this works like file navigation except that if there is no terminal at the specified index
a new terminal is created.
```lua
lua require("harpoon.term").gotoTerminal(1)             -- navigates to term 1
```

### Commands to Terminals
commands can be sent to any terminal
```lua
lua require("harpoon.term").sendCommand(1, "ls -La")    -- sends ls -La to tmux window 1
```
further more commands can be stored for later quick
```lua
lua require('harpoon.cmd-ui').toggle_quick_menu()       -- shows the commands menu
lua require("harpoon.term").sendCommand(1, 1)           -- sends command 1 to term 1
```

### Tmux Support
tmux is supported out of the box and can be used as a drop-in replacement to normal terminals
by simply switching `'term' with 'tmux'` like so

```lua
lua require("harpoon.tmux").gotoTerminal(1)             -- goes to the first tmux window
lua require("harpoon.tmux").sendCommand(1, "ls -La")    -- sends ls -La to tmux window 1
lua require("harpoon.tmux").sendCommand(1, 1)           -- sends command 1 to tmux window 1
```

`sendCommand` and `goToTerminal` also accept any valid [tmux pane identifier](https://man7.org/linux/man-pages/man1/tmux.1.html#COMMANDS).
```lua
lua require("harpoon.tmux").gotoTerminal("{down-of}")   -- focus the pane directly below
lua require("harpoon.tmux").sendCommand("%3", "ls")     -- send a command to the pane with id '%3'
```

Once you switch to a tmux window you can always switch back to neovim, this is a
little bash script that will switch to the window which is running neovim.

In your `tmux.conf` (or anywhere you have keybinds), add this
```bash
bind-key -r G run-shell "path-to-harpoon/harpoon/scripts/tmux/switch-back-to-nvim"
```

### Telescope Support
1st register harpoon as a telescope extension
```lua
require("telescope").load_extension('harpoon')
```
currently only marks are supported in telescope
```
:Telescope harpoon marks
```

## ⇁ Configuration
if configuring harpoon is desired it must be done through harpoons setup function
```lua
require("harpoon").setup({ ... })
```

### Global Settings
here are all the available global settings with their default values
```lua
global_settings = {
    -- sets the marks upon calling `toggle` on the ui, instead of require `:w`.
    save_on_toggle = false,

    -- saves the harpoon file upon every change. disabling is unrecommended.
    save_on_change = true,

    -- sets harpoon to run the command immediately as it's passed to the terminal when calling `sendCommand`.
    enter_on_sendcmd = false,

    -- closes any tmux windows harpoon that harpoon creates when you close Neovim.
    tmux_autoclose_windows = false,

    -- filetypes that you want to prevent from adding to the harpoon list menu.
    excluded_filetypes = { "harpoon" },

    -- set marks specific to each git branch inside git repository
    mark_branch = false,

    -- enable tabline with harpoon marks
    tabline = false,
    tabline_prefix = "   ",
    tabline_suffix = "   ",
}
```


### Preconfigured Terminal Commands
to preconfigure terminal commands for later use
```lua
projects = {
    -- Yes $HOME works
    ["$HOME/personal/vim-with-me/server"] = {
        term = {
            cmds = {
                "./env && npx ts-node src/index.ts"
            }
        }
    }
}
```

## ⇁ Logging
- logs are written to `harpoon.log` within the nvim cache path (`:echo stdpath("cache")`)
- available log levels are `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. `warn` is default
- log level can be set with `vim.g.harpoon_log_level` (must be **before** `setup()`)
- launching nvim with `HARPOON_LOG=debug nvim` takes precedence over `vim.g.harpoon_log_level`.
- invalid values default back to `warn`.

## ⇁ Others
#### How do Harpoon marks differ from vim global marks
they serve a similar purpose however harpoon marks differ in a few key ways:
1. They auto update their position within the file
1. They are saved _per project_.
1. They can be hand edited vs replaced (swapping is easier)

#### The Motivation behind Harpoon terminals
1. I want to use the terminal since I can gF and <c-w>gF to any errors arising
from execution that are within the terminal that are not appropriate for
something like dispatch. (not just running tests but perhaps a server that runs
for X amount of time before crashing).
1. I want the terminal to be persistent and I can return to one of many terminals
with some finger wizardry and reparse any of the execution information that was
not necessarily error related.
1. I would like to have commands that can be tied to terminals and sent them
without much thinking. Some sort of middle ground between vim-test and just
typing them into a terminal (configuring netflix's television project isn't
quite building and there are tons of ways to configure).

#### Use a dynamic width for the Harpoon popup menu
Sometimes the default width of `60` is not wide enough.
The following example demonstrates how to configure a custom width by setting
the menu's width relative to the current window's width.

```lua
require("harpoon").setup({
    menu = {
        width = vim.api.nvim_win_get_width(0) - 4,
    }
})
```


#### Tabline

By default, the tabline will use the default theme of your theme.  You can customize by editing the following highlights:

* HarpoonInactive
* HarpoonActive
* HarpoonNumberActive
* HarpoonNumberInactive

Example to make it cleaner:

```lua
vim.cmd('highlight! HarpoonInactive guibg=NONE guifg=#63698c')
vim.cmd('highlight! HarpoonActive guibg=NONE guifg=white')
vim.cmd('highlight! HarpoonNumberActive guibg=NONE guifg=#7aa2f7')
vim.cmd('highlight! HarpoonNumberInactive guibg=NONE guifg=#7aa2f7')
vim.cmd('highlight! TabLineFill guibg=NONE guifg=white')
```

Result:
![tabline](https://i.imgur.com/8i8mKJD.png)

## ⇁ Social
For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server.
* [Discord](https://discord.gg/theprimeagen)
* [Twitch](https://www.twitch.tv/theprimeagen)
* [Twitter](https://twitter.com/ThePrimeagen)


================================================
FILE: TODO.md
================================================
### Manage A Mark 1.0
* Logo
* floating term / split term
* TODO: Fill me in, that one really important thing....
* README.md

### Harpoon (upon requests)
* Add hooks for vim so that someone can make it for me
* ackshual tests.
* interactive menu
* cycle
* make setup() callable more than once and just layer in the commands


================================================
FILE: lua/harpoon/cmd-ui.lua
================================================
local harpoon = require("harpoon")
local popup = require("plenary.popup")
local utils = require("harpoon.utils")
local log = require("harpoon.dev").log
local term = require("harpoon.term")

local M = {}

Harpoon_cmd_win_id = nil
Harpoon_cmd_bufh = nil

local function close_menu(force_save)
    force_save = force_save or false
    local global_config = harpoon.get_global_settings()

    if global_config.save_on_toggle or force_save then
        require("harpoon.cmd-ui").on_menu_save()
    end

    vim.api.nvim_win_close(Harpoon_cmd_win_id, true)

    Harpoon_cmd_win_id = nil
    Harpoon_cmd_bufh = nil
end

local function create_window()
    log.trace("_create_window()")
    local config = harpoon.get_menu_config()
    local width = config.width or 60
    local height = config.height or 10
    local borderchars = config.borderchars
        or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }
    local bufnr = vim.api.nvim_create_buf(false, false)

    local Harpoon_cmd_win_id, win = popup.create(bufnr, {
        title = "Harpoon Commands",
        highlight = "HarpoonWindow",
        line = math.floor(((vim.o.lines - height) / 2) - 1),
        col = math.floor((vim.o.columns - width) / 2),
        minwidth = width,
        minheight = height,
        borderchars = borderchars,
    })

    vim.api.nvim_win_set_option(
        win.border.win_id,
        "winhl",
        "Normal:HarpoonBorder"
    )

    return {
        bufnr = bufnr,
        win_id = Harpoon_cmd_win_id,
    }
end

local function get_menu_items()
    log.trace("_get_menu_items()")
    local lines = vim.api.nvim_buf_get_lines(Harpoon_cmd_bufh, 0, -1, true)
    local indices = {}

    for _, line in pairs(lines) do
        if not utils.is_white_space(line) then
            table.insert(indices, line)
        end
    end

    return indices
end

function M.toggle_quick_menu()
    log.trace("cmd-ui#toggle_quick_menu()")
    if
        Harpoon_cmd_win_id ~= nil
        and vim.api.nvim_win_is_valid(Harpoon_cmd_win_id)
    then
        close_menu()
        return
    end

    local win_info = create_window()
    local contents = {}
    local global_config = harpoon.get_global_settings()

    Harpoon_cmd_win_id = win_info.win_id
    Harpoon_cmd_bufh = win_info.bufnr

    for idx, cmd in pairs(harpoon.get_term_config().cmds) do
        contents[idx] = cmd
    end

    vim.api.nvim_win_set_option(Harpoon_cmd_win_id, "number", true)
    vim.api.nvim_buf_set_name(Harpoon_cmd_bufh, "harpoon-cmd-menu")
    vim.api.nvim_buf_set_lines(Harpoon_cmd_bufh, 0, #contents, false, contents)
    vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "filetype", "harpoon")
    vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "buftype", "acwrite")
    vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "bufhidden", "delete")
    vim.api.nvim_buf_set_keymap(
        Harpoon_cmd_bufh,
        "n",
        "q",
        "<Cmd>lua require('harpoon.cmd-ui').toggle_quick_menu()<CR>",
        { silent = true }
    )
    vim.api.nvim_buf_set_keymap(
        Harpoon_cmd_bufh,
        "n",
        "<ESC>",
        "<Cmd>lua require('harpoon.cmd-ui').toggle_quick_menu()<CR>",
        { silent = true }
    )
    vim.api.nvim_buf_set_keymap(
        Harpoon_cmd_bufh,
        "n",
        "<CR>",
        "<Cmd>lua require('harpoon.cmd-ui').select_menu_item()<CR>",
        {}
    )
    vim.cmd(
        string.format(
            "autocmd BufWriteCmd <buffer=%s> lua require('harpoon.cmd-ui').on_menu_save()",
            Harpoon_cmd_bufh
        )
    )
    if global_config.save_on_change then
        vim.cmd(
            string.format(
                "autocmd TextChanged,TextChangedI <buffer=%s> lua require('harpoon.cmd-ui').on_menu_save()",
                Harpoon_cmd_bufh
            )
        )
    end
    vim.cmd(
        string.format(
            "autocmd BufModifiedSet <buffer=%s> set nomodified",
            Harpoon_cmd_bufh
        )
    )
end

function M.select_menu_item()
    log.trace("cmd-ui#select_menu_item()")
    local cmd = vim.fn.line(".")
    close_menu(true)
    local answer = vim.fn.input("Terminal index (default to 1): ")
    if answer == "" then
        answer = "1"
    end
    local idx = tonumber(answer)
    if idx then
        term.sendCommand(idx, cmd)
    end
end

function M.on_menu_save()
    log.trace("cmd-ui#on_menu_save()")
    term.set_cmd_list(get_menu_items())
end

return M


================================================
FILE: lua/harpoon/dev.lua
================================================
-- Don't include this file, we should manually include it via
-- require("harpoon.dev").reload();
--
-- A quick mapping can be setup using something like:
-- :nmap <leader>rr :lua require("harpoon.dev").reload()<CR>
local M = {}

function M.reload()
    require("plenary.reload").reload_module("harpoon")
end

local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" }
local function set_log_level()
    local log_level = vim.env.HARPOON_LOG or vim.g.harpoon_log_level

    for _, level in pairs(log_levels) do
        if level == log_level then
            return log_level
        end
    end

    return "warn" -- default, if user hasn't set to one from log_levels
end

local log_level = set_log_level()
M.log = require("plenary.log").new({
    plugin = "harpoon",
    level = log_level,
})

local log_key = os.time()

local function override(key)
    local fn = M.log[key]
    M.log[key] = function(...)
        fn(log_key, ...)
    end
end

for _, v in pairs(log_levels) do
    override(v)
end

function M.get_log_key()
    return log_key
end

return M


================================================
FILE: lua/harpoon/init.lua
================================================
local Path = require("plenary.path")
local utils = require("harpoon.utils")
local Dev = require("harpoon.dev")
local log = Dev.log

local config_path = vim.fn.stdpath("config")
local data_path = vim.fn.stdpath("data")
local user_config = string.format("%s/harpoon.json", config_path)
local cache_config = string.format("%s/harpoon.json", data_path)

local M = {}

local the_primeagen_harpoon = vim.api.nvim_create_augroup(
    "THE_PRIMEAGEN_HARPOON",
    { clear = true }
)

vim.api.nvim_create_autocmd({ "BufLeave", "VimLeave" }, {
    callback = function()
        require("harpoon.mark").store_offset()
    end,
    group = the_primeagen_harpoon,
})

vim.api.nvim_create_autocmd("FileType", {
    pattern = "harpoon",
    group = the_primeagen_harpoon,

    callback = function()
        -- Open harpoon file choice in useful ways
        --
        -- vertical split (control+v)
        vim.keymap.set("n", "<C-V>", function()
            local curline = vim.api.nvim_get_current_line()
            local working_directory = vim.fn.getcwd() .. "/"
            vim.cmd("vs")
            vim.cmd("e " .. working_directory .. curline)
        end, { buffer = true, noremap = true, silent = true })

        -- horizontal split (control+x)
        vim.keymap.set("n", "<C-x>", function()
            local curline = vim.api.nvim_get_current_line()
            local working_directory = vim.fn.getcwd() .. "/"
            vim.cmd("sp")
            vim.cmd("e " .. working_directory .. curline)
        end, { buffer = true, noremap = true, silent = true })

        -- new tab (control+t)
        vim.keymap.set("n", "<C-t>", function()
            local curline = vim.api.nvim_get_current_line()
            local working_directory = vim.fn.getcwd() .. "/"
            vim.cmd("tabnew")
            vim.cmd("e " .. working_directory .. curline)
        end, { buffer = true, noremap = true, silent = true })
    end,
})
--[[
{
    projects = {
        ["/path/to/director"] = {
            term = {
                cmds = {
                }
                ... is there anything that could be options?
            },
            mark = {
                marks = {
                }
                ... is there anything that could be options?
            }
        }
    },
    ... high level settings
}
--]]
HarpoonConfig = HarpoonConfig or {}

-- tbl_deep_extend does not work the way you would think
local function merge_table_impl(t1, t2)
    for k, v in pairs(t2) do
        if type(v) == "table" then
            if type(t1[k]) == "table" then
                merge_table_impl(t1[k], v)
            else
                t1[k] = v
            end
        else
            t1[k] = v
        end
    end
end

local function mark_config_key(global_settings)
    global_settings = global_settings or M.get_global_settings()
    if global_settings.mark_branch then
        return utils.branch_key()
    else
        return utils.project_key()
    end
end

local function merge_tables(...)
    log.trace("_merge_tables()")
    local out = {}
    for i = 1, select("#", ...) do
        merge_table_impl(out, select(i, ...))
    end
    return out
end

local function ensure_correct_config(config)
    log.trace("_ensure_correct_config()")
    local projects = config.projects
    local mark_key = mark_config_key(config.global_settings)
    if projects[mark_key] == nil then
        log.debug("ensure_correct_config(): No config found for:", mark_key)
        projects[mark_key] = {
            mark = { marks = {} },
            term = {
                cmds = {},
            },
        }
    end

    local proj = projects[mark_key]
    if proj.mark == nil then
        log.debug("ensure_correct_config(): No marks found for", mark_key)
        proj.mark = { marks = {} }
    end

    if proj.term == nil then
        log.debug(
            "ensure_correct_config(): No terminal commands found for",
            mark_key
        )
        proj.term = { cmds = {} }
    end

    local marks = proj.mark.marks

    for idx, mark in pairs(marks) do
        if type(mark) == "string" then
            mark = { filename = mark }
            marks[idx] = mark
        end

        marks[idx].filename = utils.normalize_path(mark.filename)
    end

    return config
end

local function expand_dir(config)
    log.trace("_expand_dir(): Config pre-expansion:", config)

    local projects = config.projects or {}
    for k in pairs(projects) do
        local expanded_path = Path.new(k):expand()
        projects[expanded_path] = projects[k]
        if expanded_path ~= k then
            projects[k] = nil
        end
    end

    log.trace("_expand_dir(): Config post-expansion:", config)
    return config
end

function M.save()
    -- first refresh from disk everything but our project
    M.refresh_projects_b4update()

    log.trace("save(): Saving cache config to", cache_config)
    Path:new(cache_config):write(vim.fn.json_encode(HarpoonConfig), "w")
end

local function read_config(local_config)
    log.trace("_read_config():", local_config)
    return vim.json.decode(Path:new(local_config):read())
end

-- 1. saved.  Where do we save?
function M.setup(config)
    log.trace("setup(): Setting up...")

    if not config then
        config = {}
    end

    local ok, u_config = pcall(read_config, user_config)

    if not ok then
        log.debug("setup(): No user config present at", user_config)
        u_config = {}
    end

    local ok2, c_config = pcall(read_config, cache_config)

    if not ok2 then
        log.debug("setup(): No cache config present at", cache_config)
        c_config = {}
    end

    local complete_config = merge_tables({
        projects = {},
        global_settings = {
            ["save_on_toggle"] = false,
            ["save_on_change"] = true,
            ["enter_on_sendcmd"] = false,
            ["tmux_autoclose_windows"] = false,
            ["excluded_filetypes"] = { "harpoon" },
            ["mark_branch"] = false,
            ["tabline"] = false,
            ["tabline_suffix"] = "   ",
            ["tabline_prefix"] = "   ",
        },
    }, expand_dir(c_config), expand_dir(u_config), expand_dir(config))

    -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had
    -- an object for vim.loop.cwd()
    ensure_correct_config(complete_config)

    if complete_config.tabline then
        require("harpoon.tabline").setup(complete_config)
    end

    HarpoonConfig = complete_config

    log.debug("setup(): Complete config", HarpoonConfig)
    log.trace("setup(): log_key", Dev.get_log_key())
end

function M.get_global_settings()
    log.trace("get_global_settings()")
    return HarpoonConfig.global_settings
end

-- refresh all projects from disk, except our current one
function M.refresh_projects_b4update()
    log.trace(
        "refresh_projects_b4update(): refreshing other projects",
        cache_config
    )
    -- save current runtime version of our project config for merging back in later
    local cwd = mark_config_key()
    local current_p_config = {
        projects = {
            [cwd] = ensure_correct_config(HarpoonConfig).projects[cwd],
        },
    }

    -- erase all projects from global config, will be loaded back from disk
    HarpoonConfig.projects = nil

    -- this reads a stale version of our project but up-to-date versions
    -- of all other projects
    local ok2, c_config = pcall(read_config, cache_config)

    if not ok2 then
        log.debug(
            "refresh_projects_b4update(): No cache config present at",
            cache_config
        )
        c_config = { projects = {} }
    end
    -- don't override non-project config in HarpoonConfig later
    c_config = { projects = c_config.projects }

    -- erase our own project, will be merged in from current_p_config later
    c_config.projects[cwd] = nil

    local complete_config = merge_tables(
        HarpoonConfig,
        expand_dir(c_config),
        expand_dir(current_p_config)
    )

    -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had
    -- an object for vim.loop.cwd()
    ensure_correct_config(complete_config)

    HarpoonConfig = complete_config
    log.debug("refresh_projects_b4update(): Complete config", HarpoonConfig)
    log.trace("refresh_projects_b4update(): log_key", Dev.get_log_key())
end

function M.get_term_config()
    log.trace("get_term_config()")
    return ensure_correct_config(HarpoonConfig).projects[utils.project_key()].term
end

function M.get_mark_config()
    log.trace("get_mark_config()")
    return ensure_correct_config(HarpoonConfig).projects[mark_config_key()].mark
end

function M.get_menu_config()
    log.trace("get_menu_config()")
    return HarpoonConfig.menu or {}
end

-- should only be called for debug purposes
function M.print_config()
    print(vim.inspect(HarpoonConfig))
end

-- Sets a default config with no values
M.setup()

return M


================================================
FILE: lua/harpoon/mark.lua
================================================
local harpoon = require("harpoon")
local utils = require("harpoon.utils")
local log = require("harpoon.dev").log

-- I think that I may have to organize this better.  I am not the biggest fan
-- of procedural all the things
local M = {}
local callbacks = {}

-- I am trying to avoid over engineering the whole thing.  We will likely only
-- need one event emitted
local function emit_changed()
    log.trace("_emit_changed()")

    local global_settings = harpoon.get_global_settings()

    if global_settings.save_on_change then
        harpoon.save()
    end

    if global_settings.tabline then
        vim.cmd("redrawt")
    end

    if not callbacks["changed"] then
        log.trace("_emit_changed(): no callbacks for 'changed', returning")
        return
    end

    for idx, cb in pairs(callbacks["changed"]) do
        log.trace(
            string.format(
                "_emit_changed(): Running callback #%d for 'changed'",
                idx
            )
        )
        cb()
    end
end

local function filter_empty_string(list)
    log.trace("_filter_empty_string()")
    local next = {}
    for idx = 1, #list do
        if list[idx] ~= "" then
            table.insert(next, list[idx].filename)
        end
    end

    return next
end

local function get_first_empty_slot()
    log.trace("_get_first_empty_slot()")
    for idx = 1, M.get_length() do
        local filename = M.get_marked_file_name(idx)
        if filename == "" then
            return idx
        end
    end

    return M.get_length() + 1
end

local function get_buf_name(id)
    log.trace("_get_buf_name():", id)
    if id == nil then
        return utils.normalize_path(vim.api.nvim_buf_get_name(0))
    elseif type(id) == "string" then
        return utils.normalize_path(id)
    end

    local idx = M.get_index_of(id)
    if M.valid_index(idx) then
        return M.get_marked_file_name(idx)
    end
    --
    -- not sure what to do here...
    --
    return ""
end

local function create_mark(filename)
    local cursor_pos = vim.api.nvim_win_get_cursor(0)
    log.trace(
        string.format(
            "_create_mark(): Creating mark at row: %d, col: %d for %s",
            cursor_pos[1],
            cursor_pos[2],
            filename
        )
    )
    return {
        filename = filename,
        row = cursor_pos[1],
        col = cursor_pos[2],
    }
end

local function mark_exists(buf_name)
    log.trace("_mark_exists()")
    for idx = 1, M.get_length() do
        if M.get_marked_file_name(idx) == buf_name then
            log.debug("_mark_exists(): Mark exists", buf_name)
            return true
        end
    end

    log.debug("_mark_exists(): Mark doesn't exist", buf_name)
    return false
end

local function validate_buf_name(buf_name)
    log.trace("_validate_buf_name():", buf_name)
    if buf_name == "" or buf_name == nil then
        log.error(
            "_validate_buf_name(): Not a valid name for a mark,",
            buf_name
        )
        error("Couldn't find a valid file name to mark, sorry.")
        return
    end
end

local function filter_filetype()
    local current_filetype = vim.bo.filetype
    local excluded_filetypes = harpoon.get_global_settings().excluded_filetypes

    if current_filetype == "harpoon" then
        log.error("filter_filetype(): You can't add harpoon to the harpoon")
        error("You can't add harpoon to the harpoon")
        return
    end

    if vim.tbl_contains(excluded_filetypes, current_filetype) then
        log.error(
            'filter_filetype(): This filetype cannot be added or is included in the "excluded_filetypes" option'
        )
        error(
            'This filetype cannot be added or is included in the "excluded_filetypes" option'
        )
        return
    end
end

function M.get_index_of(item, marks)
    log.trace("get_index_of():", item)
    if item == nil then
        log.error(
            "get_index_of(): Function has been supplied with a nil value."
        )
        error(
            "You have provided a nil value to Harpoon, please provide a string rep of the file or the file idx."
        )
        return
    end

    if type(item) == "string" then
        local relative_item = utils.normalize_path(item)
        if marks == nil then
            marks = harpoon.get_mark_config().marks
        end
        for idx = 1, M.get_length(marks) do
            if marks[idx] and marks[idx].filename == relative_item then
                return idx
            end
        end

        return nil
    end

    -- TODO move this to a "harpoon_" prefix or global config?
    if vim.g.manage_a_mark_zero_index then
        item = item + 1
    end

    if item <= M.get_length() and item >= 1 then
        return item
    end

    log.debug("get_index_of(): No item found,", item)
    return nil
end

function M.status(bufnr)
    log.trace("status()")
    local buf_name
    if bufnr then
        buf_name = vim.api.nvim_buf_get_name(bufnr)
    else
        buf_name = vim.api.nvim_buf_get_name(0)
    end

    local norm_name = utils.normalize_path(buf_name)
    local idx = M.get_index_of(norm_name)

    if M.valid_index(idx) then
        return "M" .. idx
    end
    return ""
end

function M.valid_index(idx, marks)
    log.trace("valid_index():", idx)
    if idx == nil then
        return false
    end

    local file_name = M.get_marked_file_name(idx, marks)
    return file_name ~= nil and file_name ~= ""
end

function M.add_file(file_name_or_buf_id)
    filter_filetype()
    local buf_name = get_buf_name(file_name_or_buf_id)
    log.trace("add_file():", buf_name)

    if M.valid_index(M.get_index_of(buf_name)) then
        -- we don't alter file layout.
        return
    end

    validate_buf_name(buf_name)

    local found_idx = get_first_empty_slot()
    harpoon.get_mark_config().marks[found_idx] = create_mark(buf_name)
    M.remove_empty_tail(false)
    emit_changed()
end

-- _emit_on_changed == false should only be used internally
function M.remove_empty_tail(_emit_on_changed)
    log.trace("remove_empty_tail()")
    _emit_on_changed = _emit_on_changed == nil or _emit_on_changed
    local config = harpoon.get_mark_config()
    local found = false

    for i = M.get_length(), 1, -1 do
        local filename = M.get_marked_file_name(i)
        if filename ~= "" then
            return
        end

        if filename == "" then
            table.remove(config.marks, i)
            found = found or _emit_on_changed
        end
    end

    if found then
        emit_changed()
    end
end

function M.store_offset()
    log.trace("store_offset()")
    local ok, res = pcall(function()
        local marks = harpoon.get_mark_config().marks
        local buf_name = get_buf_name()
        local idx = M.get_index_of(buf_name, marks)
        if not M.valid_index(idx, marks) then
            return
        end

        local cursor_pos = vim.api.nvim_win_get_cursor(0)
        log.debug(
            string.format(
                "store_offset(): Stored row: %d, col: %d",
                cursor_pos[1],
                cursor_pos[2]
            )
        )
        marks[idx].row = cursor_pos[1]
        marks[idx].col = cursor_pos[2]
    end)

    if not ok then
        log.warn("store_offset(): Could not store offset:", res)
    end

    emit_changed()
end

function M.rm_file(file_name_or_buf_id)
    local buf_name = get_buf_name(file_name_or_buf_id)
    local idx = M.get_index_of(buf_name)
    log.trace("rm_file(): Removing mark at id", idx)

    if not M.valid_index(idx) then
        log.debug("rm_file(): No mark exists for id", file_name_or_buf_id)
        return
    end

    harpoon.get_mark_config().marks[idx] = create_mark("")
    M.remove_empty_tail(false)
    emit_changed()
end

function M.clear_all()
    harpoon.get_mark_config().marks = {}
    log.trace("clear_all(): Clearing all marks.")
    emit_changed()
end

--- ENTERPRISE PROGRAMMING
function M.get_marked_file(idxOrName)
    log.trace("get_marked_file():", idxOrName)
    if type(idxOrName) == "string" then
        idxOrName = M.get_index_of(idxOrName)
    end
    return harpoon.get_mark_config().marks[idxOrName]
end

function M.get_marked_file_name(idx, marks)
    local mark
    if marks ~= nil then
        mark = marks[idx]
    else
        mark = harpoon.get_mark_config().marks[idx]
    end
    log.trace("get_marked_file_name():", mark and mark.filename)
    return mark and mark.filename
end

function M.get_length(marks)
    if marks == nil then
        marks = harpoon.get_mark_config().marks
    end
    log.trace("get_length()")
    return table.maxn(marks)
end

function M.set_current_at(idx)
    filter_filetype()
    local buf_name = get_buf_name()
    log.trace("set_current_at(): Setting id", idx, buf_name)
    local config = harpoon.get_mark_config()
    local current_idx = M.get_index_of(buf_name)

    -- Remove it if it already exists
    if M.valid_index(current_idx) then
        config.marks[current_idx] = create_mark("")
    end

    config.marks[idx] = create_mark(buf_name)

    for i = 1, M.get_length() do
        if not config.marks[i] then
            config.marks[i] = create_mark("")
        end
    end

    emit_changed()
end

function M.to_quickfix_list()
    log.trace("to_quickfix_list(): Sending marks to quickfix list.")
    local config = harpoon.get_mark_config()
    local file_list = filter_empty_string(config.marks)
    local qf_list = {}
    for idx = 1, #file_list do
        local mark = M.get_marked_file(idx)
        qf_list[idx] = {
            text = string.format("%d: %s", idx, file_list[idx]),
            filename = mark.filename,
            row = mark.row,
            col = mark.col,
        }
    end
    log.debug("to_quickfix_list(): qf_list:", qf_list)
    vim.fn.setqflist(qf_list)
end

function M.set_mark_list(new_list)
    log.trace("set_mark_list(): New list:", new_list)

    local config = harpoon.get_mark_config()

    for k, v in pairs(new_list) do
        if type(v) == "string" then
            local mark = M.get_marked_file(v)
            if not mark then
                mark = create_mark(v)
            end

            new_list[k] = mark
        end
    end

    config.marks = new_list
    emit_changed()
end

function M.toggle_file(file_name_or_buf_id)
    local buf_name = get_buf_name(file_name_or_buf_id)
    log.trace("toggle_file():", buf_name)

    validate_buf_name(buf_name)

    if mark_exists(buf_name) then
        M.rm_file(buf_name)
        print("Mark removed")
        log.debug("toggle_file(): Mark removed")
    else
        M.add_file(buf_name)
        print("Mark added")
        log.debug("toggle_file(): Mark added")
    end
end

function M.get_current_index()
    log.trace("get_current_index()")
    return M.get_index_of(vim.api.nvim_buf_get_name(0))
end

function M.on(event, cb)
    log.trace("on():", event)
    if not callbacks[event] then
        log.debug("on(): no callbacks yet for", event)
        callbacks[event] = {}
    end

    table.insert(callbacks[event], cb)
    log.debug("on(): All callbacks:", callbacks)
end

return M


================================================
FILE: lua/harpoon/tabline.lua
================================================
local Dev = require("harpoon.dev")
local log = Dev.log

local M = {}

local function get_color(group, attr)
    return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), attr)
end


local function shorten_filenames(filenames)
    local shortened = {}

    local counts = {}
    for _, file in ipairs(filenames) do
        local name = vim.fn.fnamemodify(file.filename, ":t")
        counts[name] = (counts[name] or 0) + 1
    end

    for _, file in ipairs(filenames) do
        local name = vim.fn.fnamemodify(file.filename, ":t")

        if counts[name] == 1 then
            table.insert(shortened, { filename = vim.fn.fnamemodify(name, ":t") })
        else
            table.insert(shortened, { filename = file.filename })
        end
    end

    return shortened
end

function M.setup(opts)
    function _G.tabline()
        local tabs = shorten_filenames(require('harpoon').get_mark_config().marks)
        local tabline = ''

        local index = require('harpoon.mark').get_index_of(vim.fn.bufname())

        for i, tab in ipairs(tabs) do
            local is_current = i == index

            local label

            if tab.filename == "" or tab.filename == "(empty)" then
                label = "(empty)"
                is_current = false
            else
                label = tab.filename
            end


            if is_current then
                tabline = tabline ..
                    '%#HarpoonNumberActive#' .. (opts.tabline_prefix or '   ') .. i .. ' %*' .. '%#HarpoonActive#'
            else
                tabline = tabline ..
                    '%#HarpoonNumberInactive#' .. (opts.tabline_prefix or '   ') .. i .. ' %*' .. '%#HarpoonInactive#'
            end

            tabline = tabline .. label .. (opts.tabline_suffix or '   ') .. '%*'

            if i < #tabs then
                tabline = tabline .. '%T'
            end
        end

        return tabline
    end

    vim.opt.showtabline = 2

    vim.o.tabline = '%!v:lua.tabline()'

    vim.api.nvim_create_autocmd("ColorScheme", {
        group = vim.api.nvim_create_augroup("harpoon", { clear = true }),
        pattern = { "*" },
        callback = function()
            local color = get_color('HarpoonActive', 'bg#')

            if (color == "" or color == nil) then
                vim.api.nvim_set_hl(0, "HarpoonInactive", { link = "Tabline" })
                vim.api.nvim_set_hl(0, "HarpoonActive", { link = "TablineSel" })
                vim.api.nvim_set_hl(0, "HarpoonNumberActive", { link = "TablineSel" })
                vim.api.nvim_set_hl(0, "HarpoonNumberInactive", { link = "Tabline" })
            end
        end,
    })

    log.debug("setup(): Tabline Setup", opts)
end

return M


================================================
FILE: lua/harpoon/term.lua
================================================
local harpoon = require("harpoon")
local log = require("harpoon.dev").log
local global_config = harpoon.get_global_settings()

local M = {}
local terminals = {}

local function create_terminal(create_with)
    if not create_with then
        create_with = ":terminal"
    end
    log.trace("term: _create_terminal(): Init:", create_with)
    local current_id = vim.api.nvim_get_current_buf()

    vim.cmd(create_with)
    local buf_id = vim.api.nvim_get_current_buf()
    local term_id = vim.b.terminal_job_id

    if term_id == nil then
        log.error("_create_terminal(): term_id is nil")
        -- TODO: Throw an error?
        return nil
    end

    -- Make sure the term buffer has "hidden" set so it doesn't get thrown
    -- away and cause an error
    vim.api.nvim_buf_set_option(buf_id, "bufhidden", "hide")

    -- Resets the buffer back to the old one
    vim.api.nvim_set_current_buf(current_id)
    return buf_id, term_id
end

local function find_terminal(args)
    log.trace("term: _find_terminal(): Terminal:", args)
    if type(args) == "number" then
        args = { idx = args }
    end
    local term_handle = terminals[args.idx]
    if not term_handle or not vim.api.nvim_buf_is_valid(term_handle.buf_id) then
        local buf_id, term_id = create_terminal(args.create_with)
        if buf_id == nil then
            error("Failed to find and create terminal.")
            return
        end

        term_handle = {
            buf_id = buf_id,
            term_id = term_id,
        }
        terminals[args.idx] = term_handle
    end
    return term_handle
end

local function get_first_empty_slot()
    log.trace("_get_first_empty_slot()")
    for idx, cmd in pairs(harpoon.get_term_config().cmds) do
        if cmd == "" then
            return idx
        end
    end
    return M.get_length() + 1
end

function M.gotoTerminal(idx)
    log.trace("term: gotoTerminal(): Terminal:", idx)
    local term_handle = find_terminal(idx)

    vim.api.nvim_set_current_buf(term_handle.buf_id)
end

function M.sendCommand(idx, cmd, ...)
    log.trace("term: sendCommand(): Terminal:", idx)
    local term_handle = find_terminal(idx)

    if type(cmd) == "number" then
        cmd = harpoon.get_term_config().cmds[cmd]
    end

    if global_config.enter_on_sendcmd then
        cmd = cmd .. "\n"
    end

    if cmd then
        log.debug("sendCommand:", cmd)
        vim.api.nvim_chan_send(term_handle.term_id, string.format(cmd, ...))
    end
end

function M.clear_all()
    log.trace("term: clear_all(): Clearing all terminals.")
    for _, term in ipairs(terminals) do
        vim.api.nvim_buf_delete(term.buf_id, { force = true })
    end
    terminals = {}
end

function M.get_length()
    log.trace("_get_length()")
    return table.maxn(harpoon.get_term_config().cmds)
end

function M.valid_index(idx)
    if idx == nil or idx > M.get_length() or idx <= 0 then
        return false
    end
    return true
end

function M.emit_changed()
    log.trace("_emit_changed()")
    if harpoon.get_global_settings().save_on_change then
        harpoon.save()
    end
end

function M.add_cmd(cmd)
    log.trace("add_cmd()")
    local found_idx = get_first_empty_slot()
    harpoon.get_term_config().cmds[found_idx] = cmd
    M.emit_changed()
end

function M.rm_cmd(idx)
    log.trace("rm_cmd()")
    if not M.valid_index(idx) then
        log.debug("rm_cmd(): no cmd exists for index", idx)
        return
    end
    table.remove(harpoon.get_term_config().cmds, idx)
    M.emit_changed()
end

function M.set_cmd_list(new_list)
    log.trace("set_cmd_list(): New list:", new_list)
    for k in pairs(harpoon.get_term_config().cmds) do
        harpoon.get_term_config().cmds[k] = nil
    end
    for k, v in pairs(new_list) do
        harpoon.get_term_config().cmds[k] = v
    end
    M.emit_changed()
end

return M


================================================
FILE: lua/harpoon/test/manage-a-mark.lua
================================================
-- TODO: Harpooned
-- local Marker = require('harpoon.mark')
-- local eq = assert.are.same


================================================
FILE: lua/harpoon/test/manage_cmd_spec.lua
================================================
local harpoon = require("harpoon")
local term = require("harpoon.term")

local function assert_table_equals(tbl1, tbl2)
    if #tbl1 ~= #tbl2 then
        assert(false, "" .. #tbl1 .. " != " .. #tbl2)
    end
    for i = 1, #tbl1 do
        if tbl1[i] ~= tbl2[i] then
            assert.equals(tbl1[i], tbl2[i])
        end
    end
end

describe("basic functionalities", function()
    local emitted
    local cmds

    before_each(function()
        emitted = false
        cmds = {}
        harpoon.get_term_config = function()
            return {
                cmds = cmds,
            }
        end
        term.emit_changed = function()
            emitted = true
        end
    end)

    it("add_cmd for empty", function()
        term.add_cmd("cmake ..")
        local expected_result = {
            "cmake ..",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
    end)

    it("add_cmd for non_empty", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        local expected_result = {
            "cmake ..",
            "make",
            "ninja",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
    end)

    it("rm_cmd: removing a valid element", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        term.rm_cmd(2)
        local expected_result = {
            "cmake ..",
            "ninja",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
    end)

    it("rm_cmd: remove first element", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        term.rm_cmd(1)
        local expected_result = {
            "make",
            "ninja",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
    end)

    it("rm_cmd: remove last element", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        term.rm_cmd(3)
        local expected_result = {
            "cmake ..",
            "make",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
    end)

    it("rm_cmd: trying to remove invalid element", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        term.rm_cmd(5)
        local expected_result = {
            "cmake ..",
            "make",
            "ninja",
        }
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        assert.equals(emitted, true)
        term.rm_cmd(0)
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
        term.rm_cmd(-1)
        assert_table_equals(harpoon.get_term_config().cmds, expected_result)
    end)

    it("get_length", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        assert.equals(term.get_length(), 3)
    end)

    it("valid_index", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        assert(term.valid_index(1))
        assert(term.valid_index(2))
        assert(term.valid_index(3))
        assert(not term.valid_index(0))
        assert(not term.valid_index(-1))
        assert(not term.valid_index(4))
    end)

    it("set_cmd_list", function()
        term.add_cmd("cmake ..")
        term.add_cmd("make")
        term.add_cmd("ninja")
        term.set_cmd_list({ "make uninstall", "make install" })
        local expected_result = {
            "make uninstall",
            "make install",
        }
        assert_table_equals(expected_result, harpoon.get_term_config().cmds)
    end)
end)


================================================
FILE: lua/harpoon/tmux.lua
================================================
local harpoon = require("harpoon")
local log = require("harpoon.dev").log
local global_config = harpoon.get_global_settings()
local utils = require("harpoon.utils")

local M = {}
local tmux_windows = {}

if global_config.tmux_autoclose_windows then
    local harpoon_tmux_group = vim.api.nvim_create_augroup(
        "HARPOON_TMUX",
        { clear = true }
    )

    vim.api.nvim_create_autocmd("VimLeave", {
        callback = function()
            require("harpoon.tmux").clear_all()
        end,
        group = harpoon_tmux_group,
    })
end

local function create_terminal()
    log.trace("tmux: _create_terminal())")

    local window_id

    -- Create a new tmux window and store the window id
    local out, ret, _ = utils.get_os_command_output({
        "tmux",
        "new-window",
        "-P",
        "-F",
        "#{pane_id}",
    }, vim.loop.cwd())

    if ret == 0 then
        window_id = out[1]:sub(2)
    end

    if window_id == nil then
        log.error("tmux: _create_terminal(): window_id is nil")
        return nil
    end

    return window_id
end

-- Checks if the tmux window with the given window id exists
local function terminal_exists(window_id)
    log.trace("_terminal_exists(): Window:", window_id)

    local exists = false

    local window_list, _, _ = utils.get_os_command_output({
        "tmux",
        "list-windows",
    }, vim.loop.cwd())

    -- This has to be done this way because tmux has-session does not give
    -- updated results
    for _, line in pairs(window_list) do
        local window_info = utils.split_string(line, "@")[2]

        if string.find(window_info, string.sub(window_id, 2)) then
            exists = true
        end
    end

    return exists
end

local function find_terminal(args)
    log.trace("tmux: _find_terminal(): Window:", args)

    if type(args) == "string" then
        -- assume args is a valid tmux target identifier
        -- if invalid, the error returned by tmux will be thrown
        return {
            window_id = args,
            pane = true,
        }
    end

    if type(args) == "number" then
        args = { idx = args }
    end

    local window_handle = tmux_windows[args.idx]
    local window_exists

    if window_handle then
        window_exists = terminal_exists(window_handle.window_id)
    end

    if not window_handle or not window_exists then
        local window_id = create_terminal()

        if window_id == nil then
            error("Failed to find and create tmux window.")
            return
        end

        window_handle = {
            window_id = "%" .. window_id,
        }

        tmux_windows[args.idx] = window_handle
    end

    return window_handle
end

local function get_first_empty_slot()
    log.trace("_get_first_empty_slot()")
    for idx, cmd in pairs(harpoon.get_term_config().cmds) do
        if cmd == "" then
            return idx
        end
    end
    return M.get_length() + 1
end

function M.gotoTerminal(idx)
    log.trace("tmux: gotoTerminal(): Window:", idx)
    local window_handle = find_terminal(idx)

    local _, ret, stderr = utils.get_os_command_output({
        "tmux",
        window_handle.pane and "select-pane" or "select-window",
        "-t",
        window_handle.window_id,
    }, vim.loop.cwd())

    if ret ~= 0 then
        error("Failed to go to terminal." .. stderr[1])
    end
end

function M.sendCommand(idx, cmd, ...)
    log.trace("tmux: sendCommand(): Window:", idx)
    local window_handle = find_terminal(idx)

    if type(cmd) == "number" then
        cmd = harpoon.get_term_config().cmds[cmd]
    end

    if global_config.enter_on_sendcmd then
        cmd = cmd .. "\n"
    end

    if cmd then
        log.debug("sendCommand:", cmd)

        local _, ret, stderr = utils.get_os_command_output({
            "tmux",
            "send-keys",
            "-t",
            window_handle.window_id,
            string.format(cmd, ...),
        }, vim.loop.cwd())

        if ret ~= 0 then
            error("Failed to send command. " .. stderr[1])
        end
    end
end

function M.clear_all()
    log.trace("tmux: clear_all(): Clearing all tmux windows.")

    for _, window in pairs(tmux_windows) do
        -- Delete the current tmux window
        utils.get_os_command_output({
            "tmux",
            "kill-window",
            "-t",
            window.window_id,
        }, vim.loop.cwd())
    end

    tmux_windows = {}
end

function M.get_length()
    log.trace("_get_length()")
    return table.maxn(harpoon.get_term_config().cmds)
end

function M.valid_index(idx)
    if idx == nil or idx > M.get_length() or idx <= 0 then
        return false
    end
    return true
end

function M.emit_changed()
    log.trace("_emit_changed()")
    if harpoon.get_global_settings().save_on_change then
        harpoon.save()
    end
end

function M.add_cmd(cmd)
    log.trace("add_cmd()")
    local found_idx = get_first_empty_slot()
    harpoon.get_term_config().cmds[found_idx] = cmd
    M.emit_changed()
end

function M.rm_cmd(idx)
    log.trace("rm_cmd()")
    if not M.valid_index(idx) then
        log.debug("rm_cmd(): no cmd exists for index", idx)
        return
    end
    table.remove(harpoon.get_term_config().cmds, idx)
    M.emit_changed()
end

function M.set_cmd_list(new_list)
    log.trace("set_cmd_list(): New list:", new_list)
    for k in pairs(harpoon.get_term_config().cmds) do
        harpoon.get_term_config().cmds[k] = nil
    end
    for k, v in pairs(new_list) do
        harpoon.get_term_config().cmds[k] = v
    end
    M.emit_changed()
end

return M


================================================
FILE: lua/harpoon/ui.lua
================================================
local harpoon = require("harpoon")
local popup = require("plenary.popup")
local Marked = require("harpoon.mark")
local utils = require("harpoon.utils")
local log = require("harpoon.dev").log

local M = {}

Harpoon_win_id = nil
Harpoon_bufh = nil

-- We save before we close because we use the state of the buffer as the list
-- of items.
local function close_menu(force_save)
    force_save = force_save or false
    local global_config = harpoon.get_global_settings()

    if global_config.save_on_toggle or force_save then
        require("harpoon.ui").on_menu_save()
    end

    vim.api.nvim_win_close(Harpoon_win_id, true)

    Harpoon_win_id = nil
    Harpoon_bufh = nil
end

local function create_window()
    log.trace("_create_window()")
    local config = harpoon.get_menu_config()
    local width = config.width or 60
    local height = config.height or 10
    local borderchars = config.borderchars
        or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }
    local bufnr = vim.api.nvim_create_buf(false, false)

    local Harpoon_win_id, win = popup.create(bufnr, {
        title = "Harpoon",
        highlight = "HarpoonWindow",
        line = math.floor(((vim.o.lines - height) / 2) - 1),
        col = math.floor((vim.o.columns - width) / 2),
        minwidth = width,
        minheight = height,
        borderchars = borderchars,
    })

    vim.api.nvim_win_set_option(
        win.border.win_id,
        "winhl",
        "Normal:HarpoonBorder"
    )

    return {
        bufnr = bufnr,
        win_id = Harpoon_win_id,
    }
end

local function get_menu_items()
    log.trace("_get_menu_items()")
    local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true)
    local indices = {}

    for _, line in pairs(lines) do
        if not utils.is_white_space(line) then
            table.insert(indices, line)
        end
    end

    return indices
end

function M.toggle_quick_menu()
    log.trace("toggle_quick_menu()")
    if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then
        close_menu()
        return
    end

    local curr_file = utils.normalize_path(vim.api.nvim_buf_get_name(0))
    vim.cmd(
        string.format(
            "autocmd Filetype harpoon "
                .. "let path = '%s' | call clearmatches() | "
                -- move the cursor to the line containing the current filename
                .. "call search('\\V'.path.'\\$') | "
                -- add a hl group to that line
                .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')",
            curr_file:gsub("\\", "\\\\")
        )
    )

    local win_info = create_window()
    local contents = {}
    local global_config = harpoon.get_global_settings()

    Harpoon_win_id = win_info.win_id
    Harpoon_bufh = win_info.bufnr

    for idx = 1, Marked.get_length() do
        local file = Marked.get_marked_file_name(idx)
        if file == "" then
            file = "(empty)"
        end
        contents[idx] = string.format("%s", file)
    end

    vim.api.nvim_win_set_option(Harpoon_win_id, "number", true)
    vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu")
    vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents)
    vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon")
    vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite")
    vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete")
    vim.api.nvim_buf_set_keymap(
        Harpoon_bufh,
        "n",
        "q",
        "<Cmd>lua require('harpoon.ui').toggle_quick_menu()<CR>",
        { silent = true }
    )
    vim.api.nvim_buf_set_keymap(
        Harpoon_bufh,
        "n",
        "<ESC>",
        "<Cmd>lua require('harpoon.ui').toggle_quick_menu()<CR>",
        { silent = true }
    )
    vim.api.nvim_buf_set_keymap(
        Harpoon_bufh,
        "n",
        "<CR>",
        "<Cmd>lua require('harpoon.ui').select_menu_item()<CR>",
        {}
    )
    vim.cmd(
        string.format(
            "autocmd BufWriteCmd <buffer=%s> lua require('harpoon.ui').on_menu_save()",
            Harpoon_bufh
        )
    )
    if global_config.save_on_change then
        vim.cmd(
            string.format(
                "autocmd TextChanged,TextChangedI <buffer=%s> lua require('harpoon.ui').on_menu_save()",
                Harpoon_bufh
            )
        )
    end
    vim.cmd(
        string.format(
            "autocmd BufModifiedSet <buffer=%s> set nomodified",
            Harpoon_bufh
        )
    )
    vim.cmd(
        "autocmd BufLeave <buffer> ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()"
    )
end

function M.select_menu_item()
    local idx = vim.fn.line(".")
    close_menu(true)
    M.nav_file(idx)
end

function M.on_menu_save()
    log.trace("on_menu_save()")
    Marked.set_mark_list(get_menu_items())
end

local function get_or_create_buffer(filename)
    local buf_exists = vim.fn.bufexists(filename) ~= 0
    if buf_exists then
        return vim.fn.bufnr(filename)
    end

    return vim.fn.bufadd(filename)
end

function M.nav_file(id)
    log.trace("nav_file(): Navigating to", id)
    local idx = Marked.get_index_of(id)
    if not Marked.valid_index(idx) then
        log.debug("nav_file(): No mark exists for id", id)
        return
    end

    local mark = Marked.get_marked_file(idx)
    local filename = vim.fs.normalize(mark.filename)
    local buf_id = get_or_create_buffer(filename)
    local set_row = not vim.api.nvim_buf_is_loaded(buf_id)

    local old_bufnr = vim.api.nvim_get_current_buf()

    vim.api.nvim_set_current_buf(buf_id)
    vim.api.nvim_buf_set_option(buf_id, "buflisted", true)
    if set_row and mark.row and mark.col then
        vim.api.nvim_win_set_cursor(0, { mark.row, mark.col })
        log.debug(
            string.format(
                "nav_file(): Setting cursor to row: %d, col: %d",
                mark.row,
                mark.col
            )
        )
    end

    local old_bufinfo = vim.fn.getbufinfo(old_bufnr)
    if type(old_bufinfo) == "table" and #old_bufinfo >= 1 then
        old_bufinfo = old_bufinfo[1]
        local no_name = old_bufinfo.name == ""
        local one_line = old_bufinfo.linecount == 1
        local unchanged = old_bufinfo.changed == 0
        if no_name and one_line and unchanged then
            vim.api.nvim_buf_delete(old_bufnr, {})
        end
    end
end

function M.location_window(options)
    local default_options = {
        relative = "editor",
        style = "minimal",
        width = 30,
        height = 15,
        row = 2,
        col = 2,
    }
    options = vim.tbl_extend("keep", options, default_options)

    local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true)
    local win_id = vim.api.nvim_open_win(bufnr, true, options)

    return {
        bufnr = bufnr,
        win_id = win_id,
    }
end

function M.notification(text)
    local win_stats = vim.api.nvim_list_uis()[1]
    local win_width = win_stats.width

    local prev_win = vim.api.nvim_get_current_win()

    local info = M.location_window({
        width = 20,
        height = 2,
        row = 1,
        col = win_width - 21,
    })

    vim.api.nvim_buf_set_lines(
        info.bufnr,
        0,
        5,
        false,
        { "!!! Notification", text }
    )
    vim.api.nvim_set_current_win(prev_win)

    return {
        bufnr = info.bufnr,
        win_id = info.win_id,
    }
end

function M.close_notification(bufnr)
    vim.api.nvim_buf_delete(bufnr)
end

function M.nav_next()
    log.trace("nav_next()")
    local current_index = Marked.get_current_index()
    local number_of_items = Marked.get_length()

    if current_index == nil then
        current_index = 1
    else
        current_index = current_index + 1
    end

    if current_index > number_of_items then
        current_index = 1
    end
    M.nav_file(current_index)
end

function M.nav_prev()
    log.trace("nav_prev()")
    local current_index = Marked.get_current_index()
    local number_of_items = Marked.get_length()

    if current_index == nil then
        current_index = number_of_items
    else
        current_index = current_index - 1
    end

    if current_index < 1 then
        current_index = number_of_items
    end

    M.nav_file(current_index)
end

return M


================================================
FILE: lua/harpoon/utils.lua
================================================
local Path = require("plenary.path")
local data_path = vim.fn.stdpath("data")
local Job = require("plenary.job")

local M = {}

M.data_path = data_path

function M.project_key()
    return vim.loop.cwd()
end

function M.branch_key()
    local branch

    -- use tpope's fugitive for faster branch name resolution if available
    if vim.fn.exists("*FugitiveHead") == 1 then
        branch = vim.fn["FugitiveHead"]()
        -- return "HEAD" for parity with `git rev-parse` in detached head state
        if #branch == 0 then
            branch = "HEAD"
        end
    else
        -- `git branch --show-current` requires Git v2.22.0+ so going with more
        -- widely available command
        branch = M.get_os_command_output({
            "git",
            "rev-parse",
            "--abbrev-ref",
            "HEAD",
        })[1]
    end

    if branch then
        return vim.loop.cwd() .. "-" .. branch
    else
        return M.project_key()
    end
end

function M.normalize_path(item)
    return Path:new(item):make_relative(M.project_key())
end

function M.get_os_command_output(cmd, cwd)
    if type(cmd) ~= "table" then
        print("Harpoon: [get_os_command_output]: cmd has to be a table")
        return {}
    end
    local command = table.remove(cmd, 1)
    local stderr = {}
    local stdout, ret = Job
        :new({
            command = command,
            args = cmd,
            cwd = cwd,
            on_stderr = function(_, data)
                table.insert(stderr, data)
            end,
        })
        :sync()
    return stdout, ret, stderr
end

function M.split_string(str, delimiter)
    local result = {}
    for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do
        table.insert(result, match)
    end
    return result
end

function M.is_white_space(str)
    return str:gsub("%s", "") == ""
end

return M


================================================
FILE: lua/telescope/_extensions/harpoon.lua
================================================
local has_telescope, telescope = pcall(require, "telescope")

if not has_telescope then
    error("harpoon.nvim requires nvim-telescope/telescope.nvim")
end

return telescope.register_extension({
    exports = {
        marks = require("telescope._extensions.marks"),
    },
})


================================================
FILE: lua/telescope/_extensions/marks.lua
================================================
local action_state = require("telescope.actions.state")
local action_utils = require("telescope.actions.utils")
local entry_display = require("telescope.pickers.entry_display")
local finders = require("telescope.finders")
local pickers = require("telescope.pickers")
local conf = require("telescope.config").values
local harpoon = require("harpoon")
local harpoon_mark = require("harpoon.mark")

local function prepare_results(list)
    local next = {}
    for idx = 1, #list do
        if list[idx].filename ~= "" then
            list[idx].index = idx
            table.insert(next, list[idx])
        end
    end

    return next
end

local generate_new_finder = function()
    return finders.new_table({
        results = prepare_results(harpoon.get_mark_config().marks),
        entry_maker = function(entry)
            local line = entry.filename .. ":" .. entry.row .. ":" .. entry.col
            local displayer = entry_display.create({
                separator = " - ",
                items = {
                    { width = 2 },
                    { width = 50 },
                    { remaining = true },
                },
            })
            local make_display = function()
                return displayer({
                    tostring(entry.index),
                    line,
                })
            end
            return {
                value = entry,
                ordinal = line,
                display = make_display,
                lnum = entry.row,
                col = entry.col,
                filename = entry.filename,
            }
        end,
    })
end

local delete_harpoon_mark = function(prompt_bufnr)
    local confirmation = vim.fn.input(
        string.format("Delete current mark(s)? [y/n]: ")
    )
    if
        string.len(confirmation) == 0
        or string.sub(string.lower(confirmation), 0, 1) ~= "y"
    then
        print(string.format("Didn't delete mark"))
        return
    end

    local selection = action_state.get_selected_entry()
    harpoon_mark.rm_file(selection.filename)

    local function get_selections()
        local results = {}
        action_utils.map_selections(prompt_bufnr, function(entry)
            table.insert(results, entry)
        end)
        return results
    end

    local selections = get_selections()
    for _, current_selection in ipairs(selections) do
        harpoon_mark.rm_file(current_selection.filename)
    end

    local current_picker = action_state.get_current_picker(prompt_bufnr)
    current_picker:refresh(generate_new_finder(), { reset_prompt = true })
end

local move_mark_up = function(prompt_bufnr)
    local selection = action_state.get_selected_entry()
    local length = harpoon_mark.get_length()

    if selection.index == length then
        return
    end

    local mark_list = harpoon.get_mark_config().marks

    table.remove(mark_list, selection.index)
    table.insert(mark_list, selection.index + 1, selection.value)

    local current_picker = action_state.get_current_picker(prompt_bufnr)
    current_picker:refresh(generate_new_finder(), { reset_prompt = true })
end

local move_mark_down = function(prompt_bufnr)
    local selection = action_state.get_selected_entry()
    if selection.index == 1 then
        return
    end
    local mark_list = harpoon.get_mark_config().marks
    table.remove(mark_list, selection.index)
    table.insert(mark_list, selection.index - 1, selection.value)
    local current_picker = action_state.get_current_picker(prompt_bufnr)
    current_picker:refresh(generate_new_finder(), { reset_prompt = true })
end

return function(opts)
    opts = opts or {}

    pickers.new(opts, {
        prompt_title = "harpoon marks",
        finder = generate_new_finder(),
        sorter = conf.generic_sorter(opts),
        previewer = conf.grep_previewer(opts),
        attach_mappings = function(_, map)
            map("i", "<c-d>", delete_harpoon_mark)
            map("n", "<c-d>", delete_harpoon_mark)

            map("i", "<c-p>", move_mark_up)
            map("n", "<c-p>", move_mark_up)

            map("i", "<c-n>", move_mark_down)
            map("n", "<c-n>", move_mark_down)
            return true
        end,
    }):find()
end


================================================
FILE: scripts/tmux/switch-back-to-nvim
================================================
#!/usr/bin/env bash

# Make sure tmux is running
tmux_running=$(pgrep tmux)

if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then
    echo "tmux needs to be running"
    exit 1
fi

# Switch to a window called nvim in tmux - if it exists
session_name=$(tmux display-message -p "#S")

tmux switch-client -t "$session_name:nvim"
Download .txt
gitextract_l5y7hm4o/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── format.yml
│       └── lint.yml
├── .luacheckrc
├── .stylua.toml
├── LICENSE
├── Makefile
├── README.md
├── TODO.md
├── lua/
│   ├── harpoon/
│   │   ├── cmd-ui.lua
│   │   ├── dev.lua
│   │   ├── init.lua
│   │   ├── mark.lua
│   │   ├── tabline.lua
│   │   ├── term.lua
│   │   ├── test/
│   │   │   ├── manage-a-mark.lua
│   │   │   └── manage_cmd_spec.lua
│   │   ├── tmux.lua
│   │   ├── ui.lua
│   │   └── utils.lua
│   └── telescope/
│       └── _extensions/
│           ├── harpoon.lua
│           └── marks.lua
└── scripts/
    └── tmux/
        └── switch-back-to-nvim
Condensed preview — 26 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (74K chars).
[
  {
    "path": ".editorconfig",
    "chars": 183,
    "preview": "# top-most EditorConfig file\nroot = true\n\n# Unix-style newlines with a newline ending every file\n[*]\nend_of_line = lf\nin"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 21,
    "preview": "github: theprimeagen\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 343,
    "preview": "---\nname: Bug report\nabout: Found something wrong with Harpoon2?\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**WARNING**\nIf"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 262,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n**What issue "
  },
  {
    "path": ".github/workflows/format.yml",
    "chars": 665,
    "preview": "name: Format\n\non: [push, pull_request]\n\njobs:\n    format:\n        name: Stylua\n        runs-on: ubuntu-latest\n        st"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 418,
    "preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n    lint:\n        name: Luacheck\n        runs-on: ubuntu-latest\n        step"
  },
  {
    "path": ".luacheckrc",
    "chars": 192,
    "preview": "std = luajit\ncache = true\ncodes = true\n\nglobals = {\n    \"HarpoonConfig\",\n    \"Harpoon_bufh\",\n    \"Harpoon_win_id\",\n    \""
  },
  {
    "path": ".stylua.toml",
    "chars": 113,
    "preview": "column_width = 80\nline_endings = \"Unix\"\nindent_type = \"Spaces\"\nindent_width = 4\nquote_style = \"AutoPreferDouble\"\n"
  },
  {
    "path": "LICENSE",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2020 ThePrimeagen\n\nPermission is hereby granted, free of charge, to any person obtaining a co"
  },
  {
    "path": "Makefile",
    "chars": 146,
    "preview": "fmt:\n\techo \"===> Formatting\"\n\tstylua lua/ --config-path=.stylua.toml\n\nlint:\n\techo \"===> Linting\"\n\tluacheck lua/ --global"
  },
  {
    "path": "README.md",
    "chars": 8712,
    "preview": "<div align=\"center\">\n\n## ⇁  HARPOON 2\nThis is a deprecated and all future changes will be to the branch `harpoon2`.\n\n[Ha"
  },
  {
    "path": "TODO.md",
    "chars": 325,
    "preview": "### Manage A Mark 1.0\n* Logo\n* floating term / split term\n* TODO: Fill me in, that one really important thing....\n* READ"
  },
  {
    "path": "lua/harpoon/cmd-ui.lua",
    "chars": 4387,
    "preview": "local harpoon = require(\"harpoon\")\nlocal popup = require(\"plenary.popup\")\nlocal utils = require(\"harpoon.utils\")\nlocal l"
  },
  {
    "path": "lua/harpoon/dev.lua",
    "chars": 1073,
    "preview": "-- Don't include this file, we should manually include it via\n-- require(\"harpoon.dev\").reload();\n--\n-- A quick mapping "
  },
  {
    "path": "lua/harpoon/init.lua",
    "chars": 8937,
    "preview": "local Path = require(\"plenary.path\")\nlocal utils = require(\"harpoon.utils\")\nlocal Dev = require(\"harpoon.dev\")\nlocal log"
  },
  {
    "path": "lua/harpoon/mark.lua",
    "chars": 11092,
    "preview": "local harpoon = require(\"harpoon\")\nlocal utils = require(\"harpoon.utils\")\nlocal log = require(\"harpoon.dev\").log\n\n-- I t"
  },
  {
    "path": "lua/harpoon/tabline.lua",
    "chars": 2714,
    "preview": "local Dev = require(\"harpoon.dev\")\nlocal log = Dev.log\n\nlocal M = {}\n\nlocal function get_color(group, attr)\n    return v"
  },
  {
    "path": "lua/harpoon/term.lua",
    "chars": 3835,
    "preview": "local harpoon = require(\"harpoon\")\nlocal log = require(\"harpoon.dev\").log\nlocal global_config = harpoon.get_global_setti"
  },
  {
    "path": "lua/harpoon/test/manage-a-mark.lua",
    "chars": 91,
    "preview": "-- TODO: Harpooned\n-- local Marker = require('harpoon.mark')\n-- local eq = assert.are.same\n"
  },
  {
    "path": "lua/harpoon/test/manage_cmd_spec.lua",
    "chars": 3994,
    "preview": "local harpoon = require(\"harpoon\")\nlocal term = require(\"harpoon.term\")\n\nlocal function assert_table_equals(tbl1, tbl2)\n"
  },
  {
    "path": "lua/harpoon/tmux.lua",
    "chars": 5583,
    "preview": "local harpoon = require(\"harpoon\")\nlocal log = require(\"harpoon.dev\").log\nlocal global_config = harpoon.get_global_setti"
  },
  {
    "path": "lua/harpoon/ui.lua",
    "chars": 8290,
    "preview": "local harpoon = require(\"harpoon\")\nlocal popup = require(\"plenary.popup\")\nlocal Marked = require(\"harpoon.mark\")\nlocal u"
  },
  {
    "path": "lua/harpoon/utils.lua",
    "chars": 1862,
    "preview": "local Path = require(\"plenary.path\")\nlocal data_path = vim.fn.stdpath(\"data\")\nlocal Job = require(\"plenary.job\")\n\nlocal "
  },
  {
    "path": "lua/telescope/_extensions/harpoon.lua",
    "chars": 278,
    "preview": "local has_telescope, telescope = pcall(require, \"telescope\")\n\nif not has_telescope then\n    error(\"harpoon.nvim requires"
  },
  {
    "path": "lua/telescope/_extensions/marks.lua",
    "chars": 4212,
    "preview": "local action_state = require(\"telescope.actions.state\")\nlocal action_utils = require(\"telescope.actions.utils\")\nlocal en"
  },
  {
    "path": "scripts/tmux/switch-back-to-nvim",
    "chars": 323,
    "preview": "#!/usr/bin/env bash\n\n# Make sure tmux is running\ntmux_running=$(pgrep tmux)\n\nif [[ -z $TMUX ]] && [[ -z $tmux_running ]]"
  }
]

About this extraction

This page contains the full source code of the ThePrimeagen/harpoon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 26 files (67.5 KB), approximately 17.8k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!