diff --git a/lua/harpoon2/buffer.lua b/lua/harpoon2/buffer.lua new file mode 100644 index 0000000..4ba6d79 --- /dev/null +++ b/lua/harpoon2/buffer.lua @@ -0,0 +1,121 @@ +local utils = require("harpoon2.utils") +local M = {} + +local HARPOON_MENU = "__harpoon-menu__" + +-- simple reason here is that if we are deving harpoon, we will create several +-- ui objects, each with their own buffer, which will cause the name to be duplicated and then we will get a vim error on nvim_buf_set_name +local harpoon_menu_id = 0 + +local function get_harpoon_menu_name() + harpoon_menu_id = harpoon_menu_id + 1 + return HARPOON_MENU .. harpoon_menu_id +end + +---TODO: I don't know how to do what i want to do, but i want to be able to +---make this so we use callbacks for these buffer actions instead of using +---strings back into the ui. it feels gross and it puts odd coupling +---@param bufnr number +function M.setup_autocmds_and_keymaps(bufnr) + --[[ + -- TODO: Do the highlighting better + local curr_file = vim.api.nvim_buf_get_name(0) + local 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("\\", "\\\\") + ) + print(cmd) + vim.cmd(cmd) + --]] + + if vim.api.nvim_buf_get_name(bufnr) == "" then + vim.api.nvim_buf_set_name(bufnr, get_harpoon_menu_name()) + end + + vim.api.nvim_buf_set_option(bufnr, "filetype", "harpoon") + vim.api.nvim_buf_set_option(bufnr, "buftype", "acwrite") + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "delete") + + --[[ + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "z", + "lua print('WTF')", + { silent = true } + ) + --]] + + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "q", + "lua require('harpoon2').ui:toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + "lua require('harpoon2').ui:toggle_quick_menu()", + { silent = true } + ) + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + "lua require('harpoon2').ui:select_menu_item()", + {} + ) + -- TODO: Update these to use the new autocmd api + vim.cmd( + string.format( + "autocmd BufWriteCmd lua require('harpoon2').ui:on_menu_save()", + bufnr + ) + ) + -- TODO: Do we want this? is this a thing? + -- its odd... why save on text change? shouldn't we wait until close / w / esc? + --[[ + if global_config.save_on_change then + vim.cmd( + string.format( + "autocmd TextChanged,TextChangedI lua require('harpoon2').ui:on_menu_save()", + bufnr + ) + ) + end + --]] + vim.cmd( + string.format( + "autocmd BufModifiedSet set nomodified", + bufnr + ) + ) + vim.cmd( + "autocmd BufLeave ++nested ++once silent lua require('harpoon2').ui:toggle_quick_menu()" + ) + +end + +---@param bufnr number +function M.get_contents(bufnr) + local lines = vim.api.nvim_buf_get_lines(bufnr, 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 + +return M diff --git a/lua/harpoon2/config.lua b/lua/harpoon2/config.lua index 1365cb9..f3ec2da 100644 --- a/lua/harpoon2/config.lua +++ b/lua/harpoon2/config.lua @@ -1,3 +1,4 @@ +local utils = require("harpoon2.utils") local M = {} ---@alias HarpoonListItem {value: any, context: any} @@ -12,6 +13,12 @@ local M = {} ---@field add? fun(item: any?): HarpoonListItem ---@field BufLeave? fun(evt: any, list: HarpoonList): nil ---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil +---@field get_root_dir? fun(): string + +---@class HarpoonWindowSettings +---@field width number +---@field height number + ---notehunthoeunthoeunthoeunthoeunthoeunth ---@class HarpoonSettings @@ -108,10 +115,21 @@ function M.get_default_config() return list_item_a.value == list_item_b.value end, + get_root_dir = function() + return vim.loop.cwd() + end, + ---@param name any ---@return HarpoonListItem add = function(name) - name = name or vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + name = name or + -- TODO: should we do path normalization??? + -- i know i have seen sometimes it becoming an absolute + -- path, if that is the case we can use the context to + -- store the bufname and then have value be the normalized + -- value + vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) + local bufnr = vim.fn.bufnr(name, false) local pos = {1, 0} diff --git a/lua/harpoon2/init.lua b/lua/harpoon2/init.lua index 517a886..593b8b5 100644 --- a/lua/harpoon2/init.lua +++ b/lua/harpoon2/init.lua @@ -1,3 +1,4 @@ +local Ui = require("harpoon2.ui") local Data = require("harpoon2.data") local Config = require("harpoon2.config") local List = require("harpoon2.list") @@ -12,6 +13,7 @@ local DEFAULT_LIST = "__harpoon_files" ---@class Harpoon ---@field config HarpoonConfig +---@field ui HarpoonUI ---@field data HarpoonData ---@field lists {[string]: {[string]: HarpoonList}} ---@field hooks_setup boolean @@ -23,18 +25,24 @@ Harpoon.__index = Harpoon function Harpoon:new() local config = Config.get_default_config() - return setmetatable({ + local harpoon = setmetatable({ config = config, data = Data.Data:new(), + ui = Ui:new(config.settings), lists = {}, hooks_setup = false, }, self) + + return harpoon end ---@param partial_config HarpoonPartialConfig ---@return Harpoon function Harpoon:setup(partial_config) self.config = Config.merge_config(partial_config, self.config) + self.ui:configure(self.config.settings) + + ---TODO: should we go through every seen list and update its config? if self.hooks_setup == false then local augroup = vim.api.nvim_create_augroup @@ -44,6 +52,7 @@ function Harpoon:setup(partial_config) group = HarpoonGroup, pattern = '*', callback = function(ev) + --[[ self:_for_each_list(function(list, config) local fn = config[ev.event] @@ -55,6 +64,7 @@ function Harpoon:setup(partial_config) self:sync() end end) + --]] end, }) @@ -129,6 +139,16 @@ function Harpoon:dump() return self.data._data end -return Harpoon:new() +function Harpoon:__debug_reset() + require("plenary.reload").reload_module("harpoon2") +end +local harpoon = Harpoon:new() +HARPOON_DEBUG_VAR = HARPOON_DEBUG_VAR or 0 +if HARPOON_DEBUG_VAR == 0 then + harpoon.ui:toggle_quick_menu(harpoon:list()) + HARPOON_DEBUG_VAR = 1 +end +-- leave this undone, i sometimes use this for debugging +return harpoon diff --git a/lua/harpoon2/list.lua b/lua/harpoon2/list.lua index 8bb588c..21d8681 100644 --- a/lua/harpoon2/list.lua +++ b/lua/harpoon2/list.lua @@ -18,6 +18,7 @@ end --- @class HarpoonList --- @field config HarpoonPartialConfigItem --- @field name string +--- @field _index number --- @field items HarpoonItem[] local HarpoonList = {} @@ -27,9 +28,15 @@ function HarpoonList:new(config, name, items) items = items, config = config, name = name, + _index = 1, }, self) end +---@return number +function HarpoonList:length() + return #self.items +end + ---@return HarpoonList function HarpoonList:append(item) item = item or self.config.add() @@ -113,6 +120,24 @@ function HarpoonList:select(index, options) end end +function HarpoonList:next() + self._index = self._index + 1 + if self._index > #self.items then + self._index = 1 + end + + self:select(self._index) +end + +function HarpoonList:prev() + self._index = self._index - 1 + if self._index < 1 then + self._index = #self.items + end + + self:select(self._index) +end + --- @return string[] function HarpoonList:display() local out = {} diff --git a/lua/harpoon2/test/ui_spec.lua b/lua/harpoon2/test/ui_spec.lua new file mode 100644 index 0000000..5ec055f --- /dev/null +++ b/lua/harpoon2/test/ui_spec.lua @@ -0,0 +1,18 @@ +local utils = require("harpoon2.test.utils") + +local eq = assert.are.same + +describe("harpoon", function() + + before_each(utils.before_each) + + it("open the ui without any items in the list", function() + local harpoon = require("harpoon2") + harpoon.ui:toggle_quick_menu(harpoon:list()) + + -- no test, just wanted it to run without error'ing + end) + +end) + + diff --git a/lua/harpoon2/test/utils.lua b/lua/harpoon2/test/utils.lua index ecee9c1..25e0668 100644 --- a/lua/harpoon2/test/utils.lua +++ b/lua/harpoon2/test/utils.lua @@ -1,8 +1,27 @@ +local Data = require("harpoon2.data") local M = {} M.created_files = {} +function M.before_each() + Data.set_data_path("/tmp/harpoon2.json") + Data.__dangerously_clear_data() + require("plenary.reload").reload_module("harpoon2") + Data = require("harpoon2.data") + Data.set_data_path("/tmp/harpoon2.json") + local harpoon = require("harpoon2") + M.clean_files() + + harpoon:setup({ + settings = { + key = function() + return "testies" + end + } + }) +end + function M.clean_files() for _, bufnr in ipairs(M.created_files) do vim.api.nvim_buf_delete(bufnr, {force = true}) diff --git a/lua/harpoon2/ui.lua b/lua/harpoon2/ui.lua index a59651e..3b525ad 100644 --- a/lua/harpoon2/ui.lua +++ b/lua/harpoon2/ui.lua @@ -1,42 +1,57 @@ --- TODO: This is just the UI from the previous harpoon. --- it needs to be cleaned up and converted to the new api -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 popup = require("plenary").popup +local Buffer = require("harpoon2.buffer") +local DEFAULT_WINDOW_WIDTH = 69 -- nice -local M = {} +---@class HarpoonUI +---@field win_id number +---@field bufnr number +---@field settings HarpoonSettings +---@field active_list HarpoonList +local HarpoonUI = {} -Harpoon_win_id = nil -Harpoon_bufh = nil +HarpoonUI.__index = HarpoonUI --- 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 +---@param settings HarpoonSettings +---@return HarpoonUI +function HarpoonUI:new(settings) + return setmetatable({ + win_id = nil, + bufnr = nil, + active_list = nil, + settings = settings, + }, self) 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) +function HarpoonUI:close_menu() + print("CLOSING MENU") + if self.win_id ~= nil and vim.api.nvim_win_is_valid(self.win_id) then + vim.api.nvim_win_close(self.win_id, true) + end - local Harpoon_win_id, win = popup.create(bufnr, { + if self.bufnr ~= nil and vim.api.nvim_buf_is_valid(self.bufnr) then + vim.api.nvim_buf_delete(self.bufnr, { force = true }) + end + + self.active_list = nil + self.win_id = nil + self.bufnr = nil +end + +---@return number,number +function HarpoonUI:_create_window() + local win = vim.api.nvim_list_uis() + + local width = DEFAULT_WINDOW_WIDTH + if #win > 0 then + -- no ackshual reason for 0.62569, just looks complicated, and i want + -- to make my boss think i am smart + width = math.floor(win[1].width * 0.62569) + end + + local height = 8 + local borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } + local bufnr = vim.api.nvim_create_buf(false, false) + local win_id, _ = popup.create(bufnr, { title = "Harpoon", highlight = "HarpoonWindow", line = math.floor(((vim.o.lines - height) / 2) - 1), @@ -45,180 +60,62 @@ local function create_window() minheight = height, borderchars = borderchars, }) + Buffer.setup_autocmds_and_keymaps(bufnr) + self.win_id = win_id + vim.api.nvim_win_set_option(self.win_id, "number", true) vim.api.nvim_win_set_option( - win.border.win_id, + win_id, "winhl", "Normal:HarpoonBorder" ) - return { - bufnr = bufnr, - win_id = Harpoon_win_id, - } + return win_id, bufnr 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 = {} +local count = 0 - for _, line in pairs(lines) do - if not utils.is_white_space(line) then - table.insert(indices, line) - end - end +---@param list HarpoonList +function HarpoonUI:toggle_quick_menu(list) - return indices -end + count = count + 1 + print("toggle?", self.win_id, self.bufnr, count) -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() + if list == nil or self.win_id ~= nil then + self: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_id, bufnr = self:_create_window() - local win_info = create_window() - local contents = {} - local global_config = harpoon.get_global_settings() + print("_create_window_results", win_id, bufnr, count) + self.win_id = win_id + self.bufnr = bufnr + self.active_list = list - 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", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').toggle_quick_menu()", - { silent = true } - ) - vim.api.nvim_buf_set_keymap( - Harpoon_bufh, - "n", - "", - "lua require('harpoon.ui').select_menu_item()", - {} - ) - vim.cmd( - string.format( - "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - if global_config.save_on_change then - vim.cmd( - string.format( - "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", - Harpoon_bufh - ) - ) - end - vim.cmd( - string.format( - "autocmd BufModifiedSet set nomodified", - Harpoon_bufh - ) - ) - vim.cmd( - "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" - ) + local contents = self.active_list:display() + vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, contents) end -function M.select_menu_item() +function HarpoonUI:select_menu_item() + error("select_menu_item...?") local idx = vim.fn.line(".") - close_menu(true) - M.nav_file(idx) + self.active_list:select(idx) + self:close_menu() end -function M.on_menu_save() - log.trace("on_menu_save()") - Marked.set_mark_list(get_menu_items()) +function HarpoonUI:on_menu_save() + error("saving...?") + local list = Buffer.get_contents(self.bufnr) + self.active_list:resolve_displayed(list) 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.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 - - 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 +---@param settings HarpoonSettings +function HarpoonUI:configure(settings) + self.settings = settings end +--[[ function M.location_window(options) local default_options = { relative = "editor", @@ -239,6 +136,7 @@ function M.location_window(options) } end +-- TODO: What is this used for? function M.notification(text) local win_stats = vim.api.nvim_list_uis()[1] local win_width = win_stats.width @@ -270,41 +168,6 @@ 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 - +return HarpoonUI diff --git a/lua/harpoon2/utils.lua b/lua/harpoon2/utils.lua new file mode 100644 index 0000000..8dfa86a --- /dev/null +++ b/lua/harpoon2/utils.lua @@ -0,0 +1,12 @@ +local Path = require("plenary.path") + +local M = {} +function M.normalize_path(item) + return Path:new(item):make_relative(M.project_key()) +end + +function M.is_white_space(str) + return str:gsub("%s", "") == "" +end + +return M