diff --git a/init.lua b/init.lua deleted file mode 100644 index b93939f..0000000 --- a/init.lua +++ /dev/null @@ -1,130 +0,0 @@ -local popup = require'plenary.popup' -local utils = require'presser.utils' - -local api = vim.api - -local presser = {} -- list of functions to be exported -local g = vim.g - -g.presser_buf_ctx = {} -- setup a new global context manager for windows/buffers - --- :@Dev: close all Presser windows that may be open for all contexts -presser.close = function () - for _type, ctx in pairs( g.presser_buf_ctx ) do - for _, win_id in pairs( ctx ) do - api.nvim_win_close( win_id, true ) -- buffer contents are irrelevant in this context to save - end - end - - g.presser_buf_ctx = {} -- clear the global context manager -end - --- @Description: Construct a new instance of a window and buffer using the 'plenary.popup' module. --- @Params: --- + `_type` ~ string denoting which context manager is owner of the window. --- + `placeholder` ~ string allowing for placeholder text to be placed within the buffer when constructed. --- + `opts` ~ a table of additional options to be provided when constructing the window. --- @Returns: nil. Constructs a new window and stores the window's ID in a global table. --- --- @Dev: This function is to be as generic for creating a window whilst allowing for as much customisation over the --- window/buffer which are to be constructed. Users are to access this function so is not part of the module export. --- However, it's used by all internal built-ins which are meant to be used by users. Consider a class constructor in --- C++, this function is akin to that behaviour and is responsible for constructing the window/buffer and ensuring it --- can be tracked by Vim itself. -local new = function ( _type, placeholder, opts ) - -- create new manager for handling windows in the - local manager = g.presser_buf_ctx - -- create specific context manager if it doesn't exist - if not manager[_type] then - manager[_type] = {} - g.presser_buf_ctx = manager - end - - local opts = opts or {} - local placeholder = placeholder or "" - - local buf_opts = { - minwidth = 80, -- getwin_w() / 2 - ((getwin_w() % 2) / 2), - borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, - } - - if opts.window then - for k,v in pairs( opts.window ) do - buf_opts[k] = v - end - end - - local obj = popup.create( placeholder, buf_opts ) - table.insert( manager[_type], obj ) - g.presser_buf_ctx = manager - - -- :@Dev: handle for keybindings (TODO: any future stuff below here once window is made) - local buf_id = function () - return api.nvim_win_get_buf(obj) - end - - -- handle for key bindings - if opts.keybinds then - for mode, mode_map in pairs( opts.keybinds ) do - mode = string.lower(mode) - - for key_bind, key_action in pairs(mode_map) do - local key_bind = api.nvim_replace_termcodes(key_bind, true, false, true), - api.nvim_buf_set_keymap(buf_id(), mode, key_bind, key_action, - { noremap = true, silent = true } - ) - end - end - end -end - --- @Description: Find and replace words within the current buffer. --- @Params: --- @Returns: nil. --- --- @Dev: function is responsible to creating all required buffers to allow full user interaction. --- --- @Future: implementation may allow for greater user customisation similar to what's found with --- extensions such as Telescope. For now, it should provide a concrete UI for purpose of design. -function presser.find_replace() - -- define the context which these windows will belong to in the context manager. - local ctx = "find_replace" - - local keymap = { - n = { - [""] = "lua require'presser'.close()", - }, - } - - -- options for the title buffer of the built-in - local opts = { - window = { - line = 12, - border = false, - minwidth = 82, - padding = { 0, 1, 1, 1 } - }, - keybinds = keymap, - } - local tab_title = new( ctx, "Find & Replace", opts ) - - -- options for the replace buffer - local opts = { - window = { - line = 18, - title = "Replace", - }, - keybinds = keymap, - } - local replace = new( ctx, nil, opts ) - - -- modify options for find buffer - opts["window"]["line"] = 15 - opts["window"]["title"] = "Find" - local find = new ( ctx, nil, opts ) - - api.nvim_feedkeys('A', 'n', false) -end - - -return presser diff --git a/lua/presser/actions/init.lua b/lua/presser/actions/init.lua new file mode 100644 index 0000000..f2e09db --- /dev/null +++ b/lua/presser/actions/init.lua @@ -0,0 +1,72 @@ +local gcm = require'presser.context_manager' +local utils = require'presser.utils' + +local a = vim.api + +local actions = {} + +-- :@Dev: close all Presser windows that may be open for all contexts +actions.close = function () + gcm.flush() -- invoke the context manager to flush the table +end + +actions.move_next_buffer = function () + local ctxs = 0 + local c = gcm.get_global_context_manager() + local buf = a.nvim_get_current_buf() + + for _, v in pairs( c ) do for idx, _ in pairs( v ) do ctxs = idx end end + + for _, ctx_obj in pairs( c ) do + for idx, obj in pairs( ctx_obj ) do + if a.nvim_win_get_buf( obj.id ) == buf then + -- get the next window, and put cursor there + local s = idx + repeat + local win = ctx_obj[(idx % ctxs) + 1] + if win.allowed then + a.nvim_set_current_win( ctx_obj[(idx % ctxs) + 1].id ) + break + end + idx = (idx % ctxs) + 1 + until s == idx + end + end + end +end + +local read_buffer = function () + local c = vim.g.presser_buf_ctx + + local b = {} + for _, ctx_obj in pairs( c ) do + for _, obj in pairs( ctx_obj ) do + if obj.allowed then + local buf = a.nvim_win_get_buf( obj.id ) + table.insert(b, utils.clean_buf( a.nvim_buf_get_lines( buf, 0, -1, false )[1] )) + end + end + end + + return b +end + +actions.execute = function () + local result = read_buffer() + actions.close() + + local cmd = ":%s/" .. result[1] .. "/" .. result[2] .. "/g" + a.nvim_exec( cmd, false ) +end + +actions.buf_put_curser_at = function ( label ) + for _, ctx_obj in pairs( vim.g.presser_buf_ctx ) do + for _, record in pairs( ctx_obj ) do + if record.what == label then + a.nvim_set_current_win( record.id ) + end + end + end +end + +return actions diff --git a/lua/presser/builtins/__modules.lua b/lua/presser/builtins/__modules.lua new file mode 100644 index 0000000..b02fa9e --- /dev/null +++ b/lua/presser/builtins/__modules.lua @@ -0,0 +1,83 @@ +local gcm = require'presser.context_manager' +local steamers = require'presser.steamers' +local actions = require'presser.actions' + +local a = vim.api + +local modules = {} + +-- @Description: Find and replace words within the current buffer. +-- @Params: +-- @Returns: nil. +-- +-- @Dev: function is responsible to creating all required buffers to allow full user interaction. +-- +-- @Future: implementation may allow for greater user customisation similar to what's found with +-- extensions such as Telescope. For now, it should provide a concrete UI for purpose of design. +modules.find_replace = function () + -- define the context which these windows will belong to in the context manager. + local ctx = "find_replace" + gcm.create( ctx ) + + local keymap = { + n = { + [""] = "lua require'presser.actions'.close()", + [""] = "lua require'presser.actions'.execute()", + }, + i = { + [""] = "lua require'presser.actions'.move_next_buffer()", + [""] = "lua require'presser.actions'.execute()", + } + } + + -- get the centre of the current buffer + local c_cols = math.floor( a.nvim_win_get_width(0) / 2 ) + local c_lines = math.floor( a.nvim_win_get_height(0) / 2 ) + + -- create a title buffer for the steamer + steamers + .new( ctx, { + label = "presser_title", + steamer = { + placeholder = "Find & Replace", + allowed = false, + window = { + line = c_lines - 3, + border = false, + minwidth = 82, + padding = { 0, 1, 1, 1 } + } + } + } ) + + -- create a new buffer for find pattern + steamers + .new ( ctx, { + label = "find_buf", + steamer = { + window = { + line = c_lines, + title = "Find", + }, + keybinds = keymap, + } + } ) + + -- create a new buffer for replace pattern + steamers + .new( ctx, { + label = "replace_buf", + steamer = { + window = { + line = c_lines + 3, + title = "Replace", + }, + keybinds = keymap, + } + } ) + + local result = actions.buf_put_curser_at( "find_buf" ) + a.nvim_feedkeys('A', 'n', false) +end + +return modules diff --git a/lua/presser/builtins/init.lua b/lua/presser/builtins/init.lua new file mode 100644 index 0000000..6726383 --- /dev/null +++ b/lua/presser/builtins/init.lua @@ -0,0 +1,16 @@ +local builtins = {} + +local function require_on_exported_call (require_path) + return setmetatable({}, { + __index = function(_, k) + return function(...) + return require(require_path)[k](...) + end + end, + }) +end + + +builtins.find_replace = require_on_exported_call("presser.builtins.__modules").find_replace + +return builtins diff --git a/lua/presser/context_manager/init.lua b/lua/presser/context_manager/init.lua new file mode 100644 index 0000000..d49847e --- /dev/null +++ b/lua/presser/context_manager/init.lua @@ -0,0 +1,120 @@ +local a = vim.api +local g = vim.g + +-- @Dev: The following table layout is the structure of contexts stored in the global manager. +-- +-- vim.g.presser_buf_ctx = { +-- = { +-- { +-- id = , +-- what = "Name of window" | id, +-- allowed = true | false, +-- }, +-- ... +-- } +-- } +-- + +-- setup a new global context manager for windows/buffers +g.presser_buf_ctx = g.presser_buf_ctx or {} + +local M = {} + +-- @TODO: getters which might be useful +-- + get_win_by_name +-- + get_idx_from_win_id +-- + get_next_allowed_win +-- + get_prev_allowed_win + + +-- @Description: Create a new context in the global context manager if it doesn't already exist. +-- @Params: +-- + `ctx` ~ A string which represents the name of the new context to be created. +-- @Returns: nil. Updates the global context manager with a new context. +-- +-- @Dev: note that this function shouldn't be responsible for multiple contexts existing in the global manager. Whilst +-- it's possible that multiple contexts could exist, it would imply multiple UIs open at once, so unideal. +M.create = function ( ctx ) + local c = g.presser_buf_ctx + -- create specific context manager if it doesn't exist + if not c[ctx] then + c[ctx] = {} + g.presser_buf_ctx = c + end +end + + +-- @Description: Update a context within the global context manager with a new buffer record. If the context doesn't +-- exist, this function will guarantee that it does. If an ID for the window to be recorded in the context manager isn't +-- provided, then this function errors. The table passed to `obj` must contain the key `id` for the function to succeed. +-- The fields are as followed: +-- + `id` ~ A number which is given when a new buffer window is constructed. +-- + `what` ~ A string which gives the ID meaning; a label for the window (not related to buffer). +-- + `allowed` ~ A boolean value which tells the context manager if the cursor is allowed in a window (virtually +-- disables the buffer). +-- +-- `id` is the unique value to the record. Whilst `what` is a guaranteed value, its uniqueness isn't guaranteed in a +-- given context. Since `what` can be used to fetch the associated `id` of the record, it should be unique from other +-- records to prevent conflicts within a context to multiple window IDs. +-- @Params: +-- + `ctx` ~ A string representing the context which the record will be added to in the global context manager. +-- + `obj` ~ A table holding one argument and two optionals. +-- @Returns: bool. +-- +-- @Dev: this function should require proper error handling to alert a dev when something really should be guaranteed, +-- such as ID. (bool is fine to return, so perhaps error handling should be considered here for benefits). +M.update = function ( ctx, obj ) + -- ensure that the context exists +-- if not g.presser_buf_ctx[ctx] then +-- M.create( ctx ) +-- end + + if not obj.id then + print("[ERROR - context_manager] cannot allocate window. No ID was provided.") + return false + end + + if obj.allowed == nil then + obj.allowed = true + end + + -- construct new buffer record for context + local data = { + id = obj.id, + what = obj.what or obj.id, + allowed = obj.allowed, + } + + -- read the global context buffer and add new buffer to context + local c = g.presser_buf_ctx + table.insert(c[ctx], data) + + g.presser_buf_ctx = c + return true +end + + +-- @Description: Flush the contents of the global context manager and destroy all associated windows and buffers to a +-- context. Any information stored in the buffers is not saved during this process. Any such buffer contents should be +-- handled before calling to flush. +-- @Params: nil. +-- @Returns: nil. Destroys all windows/buffers and clears the global context manager +-- +-- @Dev: This is why it's important to guarantee that the IDs are stored in the context manager. They are required here +-- to close the windows using the vim API calls, and force closing them too. +M.flush = function () + for _, ctx_tbl in pairs( g.presser_buf_ctx ) do + for _, data in pairs( ctx_tbl ) do + --print("Deleting:", _, data.id) + a.nvim_win_close( data.id, true ) -- buffer contents are irrelevant in this context to save + end + end + + g.presser_buf_ctx = {} -- clear the global context manager +end + +M.get_global_context_manager = function () + return g.presser_buf_ctx +end + +return M diff --git a/lua/presser/init.lua b/lua/presser/init.lua new file mode 100644 index 0000000..6c959fa --- /dev/null +++ b/lua/presser/init.lua @@ -0,0 +1,34 @@ +local builtins = require'presser.builtins' + +local a = vim.api + +local presser = {} -- list of functions to be exported + +presser.setup = function ( opts ) + local opts = opts or {} + -- @Dev: TODO - construct setup function for configs + -- + + --map("n", "h", "lua require'presser'.find_replace()") -- global replace with last buffer item + + a.nvim_create_user_command( + 'Presser', + function ( o ) + local args = {} + for arg in string.gmatch( o.args, "%S+" ) do + table.insert( args, arg ) + end + builtins[args[1]]() + end, + { + nargs = 1, + complete = function (ArgLead, CmdLine, CursorPos) + return { 'find_replace' } + end, + } + ) + + +end + +return presser diff --git a/lua/presser/mappings/init.lua b/lua/presser/mappings/init.lua new file mode 100644 index 0000000..e69de29 diff --git a/lua/presser/steamers.lua b/lua/presser/steamers.lua new file mode 100644 index 0000000..e35a93e --- /dev/null +++ b/lua/presser/steamers.lua @@ -0,0 +1,78 @@ +local popup = require'plenary.popup' +local gcm = require'presser.context_manager' + +local a = vim.api + +local steamers = {} + +local Steamer = {} +Steamer.__index = Steamer + +-- @Description: Construct a new instance of a window and buffer using the 'plenary.popup' module. +-- @Params: +-- + `_type` ~ string denoting which context manager is owner of the window. +-- + `placeholder` ~ string allowing for placeholder text to be placed within the buffer when constructed. +-- + `opts` ~ a table of additional options to be provided when constructing the window. +-- @Returns: nil. Constructs a new window and stores the window's ID in a global table. +-- +-- @Dev: This function is to be as generic for creating a window whilst allowing for as much customisation over the +-- window/buffer which are to be constructed. Users are to access this function so is not part of the module export. +-- However, it's used by all internal built-ins which are meant to be used by users. Consider a class constructor in +-- C++, this function is akin to that behaviour and is responsible for constructing the window/buffer and ensuring it +-- can be tracked by Vim itself. +function Steamer:new ( _ctx, label, opts ) + local opts = opts or {} + if not opts.placeholder then + opts["placeholder"] = "" + end + + local buf_opts = { + minwidth = 80, -- getwin_w() / 2 - ((getwin_w() % 2) / 2), + borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, + } + + if opts.window then + for k,v in pairs( opts.window ) do + buf_opts[k] = v + end + end + + print("building window: ", opts.placeholder, buf_opts ) + local obj = popup.create( opts.placeholder, buf_opts ) + gcm.update( _ctx, { id = obj, what = label, allowed = opts.allowed } ) + -- :TODO: call update function in context manager +-- table.insert( manager[_type], obj ) +-- g.presser_buf_ctx = manager + + -- :@Dev: handle for keybindings (TODO: any future stuff below here once window is made) + local buf_id = function () + return a.nvim_win_get_buf(obj) + end + + -- handle for key bindings + if opts.keybinds then + for mode, mode_map in pairs( opts.keybinds ) do + mode = string.lower(mode) + + for key_bind, key_action in pairs(mode_map) do + local key_bind = a.nvim_replace_termcodes(key_bind, true, false, true), + a.nvim_buf_set_keymap(buf_id(), mode, key_bind, key_action, + { noremap = true, silent = true } + ) + end + end + end +end + +steamers.new = function ( ctx, opts ) + -- error if the provided option is not type string + assert( type(opts.label) == "string", "Should be string, found type: " .. type(opts.label) ) + + -- @Dev: function can provide greater wrapper options in future. + + return Steamer:new( ctx, opts.label, opts.steamer ) +end + +steamers._Steamer = Steamer + +return steamers diff --git a/utils.lua b/lua/presser/utils.lua similarity index 90% rename from utils.lua rename to lua/presser/utils.lua index 98573fc..13583c6 100644 --- a/utils.lua +++ b/lua/presser/utils.lua @@ -28,8 +28,7 @@ end -- :@dev: clean the text once it has been fetched from buffer M.clean_buf = function ( text ) if not type(text) == "string" or text == nil then - print("[DEBUG - clean_buf] Given input is not of type string.") - return + return "" end return escape_chars( text )