feat: the basic list for harpoons

This commit is contained in:
mpaulson 2023-11-03 11:52:07 -06:00
parent 21d0d1bfa3
commit ccf5e1cc0a
14 changed files with 329 additions and 1757 deletions

90
HARPOON2.md Normal file
View File

@ -0,0 +1,90 @@
### Features
* select how to generate the list key
#### was
list_key = [cwd [+ git branch]]
* files
* terminals
* tmux
#### is
list_key = [key] + [list_name]
nil = default
false = turn off
listA = {
listLine({ ... })
{ ... }
{ ... }
{ ... }
}
harpoon.setup({
settings = {
jumpToFileLocation: boolean => defaults true
}
default = {
// defaults to json.parse
encode = function(obj) => string
decode = function(string) => object
key = function() ... end
display = function(listLine) => string
select = function(listLine) => void
equals = function(list_line_a, list_line_b) => boolean
# question mark: what does it take to support custom things in here?
# potentially subject to change
add = function() => void
}
frecency = {
... a file list that is generated by harpoon ...
... can be opened via viewer ...
}
events = {
on_change = function(operation, list, value)
}
project = {
//key = vim.loop.cwd
key = git origin?
}
specifics = {
key = vim.loop.cwd + git_branch
}
list_name = {
key = function() ... end
}
})
### Functionality
select by index
prev/next
addToBack
addToFront
checking for deleted files?
- perhaps this could be part of the default select operation and use error
select
- default file select should come with options so you can open split/tab as
well
harpoon.current = "default"
harpoon.current = "listName"
harpoon.set_current(list_name)
### LATER FEATUERS
frecency = later feature likely, but great idea
- https://github.com/agkozak/zsh-z
- https://en.wikipedia.org/wiki/Frecency
// i don't understand this one
harpoon -> qfix : qfix -> harpoon

View File

@ -1,160 +0,0 @@
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

68
lua/harpoon/config.lua Normal file
View File

@ -0,0 +1,68 @@
local M = {}
function M.get_config(config, name)
return vim.tbl_extend("force", {}, config.default, config[name] or {})
end
function M.get_default_config()
return {
settings = {
save_on_toggle = true,
jump_to_file_location = true,
},
default = {
---@param obj HarpoonListItem
---@return string
encode = function(obj)
return vim.json.encode(obj)
end,
---@param str string
---@return HarpoonListItem
decode = function(str)
return vim.json.decode(str)
end,
key = function()
return vim.loop.cwd()
end,
---@param list_item HarpoonListItem
display = function(list_item)
return list_item.value
end,
---@param list_item HarpoonListItem
select = function(list_item)
error("please implement select")
end,
---@param list_item_a HarpoonListItem
---@param list_item_b HarpoonListItem
equals = function(list_item_a, list_item_b)
return list_item_a.value == list_item_b.value
end,
add = function()
error("please implement add")
end,
}
}
end
function M.merge_config(partial_config, latest_config)
local config = latest_config or M.get_default_config()
for k, v in pairs(partial_config) do
if k == "settings" then
config.settings = vim.tbl_extend("force", config.settings, v)
elseif k == "default" then
config.default = vim.tbl_extend("force", config.default, v)
else
config[k] = vim.tbl_extend("force", config[k] or {}, v)
end
end
return config
end
return M

View File

@ -1,48 +0,0 @@
-- 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

View File

@ -1,268 +1,33 @@
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)
-- setup
-- read from a config file
--
---@alias HarpoonListItem {value: any, context: any}
---@class HarpoonPartialConfigItem
---@field encode? (fun(list_item: HarpoonListItem): string)
---@field decode? (fun(obj: string): any)
---@field key? (fun(): string)
---@field display? (fun(list_item: HarpoonListItem): string)
---@field select? (fun(list_item: HarpoonListItem): nil)
---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean)
---@field add? fun(): HarpoonListItem
---@class HarpoonSettings
---@field save_on_toggle boolean defaults to true
---@field jump_to_file_location boolean defaults to true
---@class HarpoonConfig
---@field default HarpoonPartialConfigItem
---@field settings HarpoonSettings
---@field [string] HarpoonPartialConfigItem
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,
})
--[[
{
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
---@param c HarpoonConfig
function config(c)
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,
},
}, 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)
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

111
lua/harpoon/list.lua Normal file
View File

@ -0,0 +1,111 @@
local get_config = require "harpoon.config".get_config
-- TODO: Define the config object
--- @class Item
--- @field value string
--- @field context any
--- create a table object to be new'd
--- @class List
--- @field config any
--- @field name string
--- @field items Item[]
local List = {}
List.__index = List
function List:new(config, name, items)
return setmetatable({
items = items,
config = config,
name = name,
}, self)
end
function List:push(item)
table.insert(self.items, item)
end
function List:addToFront(item)
table.insert(self.items, 1, item)
end
function List:remove(item)
for i, v in ipairs(self.items) do
if get_config(self.config, self.name)(v, item) then
table.remove(self.items, i)
break
end
end
end
function List:removeAt(index)
table.remove(self.items, index)
end
function List:get(index)
return self.items[index]
end
--- much inefficiencies. dun care
---@param displayed string[]
function List:resolve_displayed(displayed)
local not_found = {}
local config = get_config(self.config, self.name)
for _, v in ipairs(displayed) do
local found = false
for _, in_table in ipairs(self.items) do
found = config.display(in_table, v)
break
end
if not found then
table.insert(not_found, v)
end
end
for _, v in ipairs(not_found) do
self:remove(v)
end
end
--- @return string[]
function List:display()
local out = {}
local config = get_config(self.config, self.name)
for _, v in ipairs(self.items) do
table.insert(out, config.display(v))
end
return out
end
--- @return string[]
function List:encode()
local out = {}
local config = get_config(self.config, self.name)
for _, v in ipairs(self.items) do
table.insert(out, config.encode(v))
end
return out
end
--- @return List
--- @param config HarpoonConfig
--- @param name string
--- @param items string[]
function List.decode(config, name, items)
local list_items = {}
local c = get_config(config, name)
for _, item in ipairs(items) do
table.insert(list_items, c.decode(item))
end
return List:new(config, name, list_items)
end
return List

View File

@ -1,420 +0,0 @@
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()")
if harpoon.get_global_settings().save_on_change then
harpoon.save()
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

View File

@ -1,146 +0,0 @@
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

View File

@ -0,0 +1,34 @@
local List = require("harpoon.list")
local Config = require("harpoon.config")
local eq = assert.are.same
describe("list", function()
it("decode", function()
local config = Config.merge_config({
foo = {
decode = function(item)
-- split item on :
local parts = vim.split(item, ":")
return {
value = parts,
context = nil,
}
end,
display = function(item)
return table.concat(item.value, "---")
end
}
})
local list = List.decode(config, "foo", {"foo:bar", "baz:qux"})
local displayed = list:display()
eq(displayed, {
"foo---bar",
"baz---qux",
})
end)
end)

View File

@ -1,3 +0,0 @@
-- TODO: Harpooned
-- local Marker = require('harpoon.mark')
-- local eq = assert.are.same

View File

@ -1,141 +0,0 @@
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)

View File

@ -1,230 +0,0 @@
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

View File

@ -1,284 +0,0 @@
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 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 = mark.filename
if filename:sub(1, 1) ~= "/" then
filename = vim.loop.cwd() .. "/" .. mark.filename
end
local buf_id = get_or_create_buffer(filename)
local set_row = not vim.api.nvim_buf_is_loaded(buf_id)
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.cmd(string.format(":call cursor(%d, %d)", mark.row, mark.col))
log.debug(
string.format(
"nav_file(): Setting cursor to row: %d, col: %d",
mark.row,
mark.col
)
)
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

View File

@ -1,64 +0,0 @@
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()
-- `git branch --show-current` requires Git v2.22.0+ so going with more
-- widely available command
local branch = M.get_os_command_output({
"git",
"rev-parse",
"--abbrev-ref",
"HEAD",
})[1]
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