├── .gitignore ├── stylua.toml ├── lua ├── spec │ ├── session_test_file │ ├── is_in_list_spec.lua │ ├── session_files_spec.lua │ └── sort_spec.lua └── nvim-possession │ ├── sorting.lua │ ├── ui.lua │ ├── config.lua │ ├── utils.lua │ └── init.lua ├── .pre-commit-config.yaml ├── .github └── workflows │ ├── panvimdoc.yml │ └── release.yml ├── LICENSE ├── README.md └── doc └── possession.txt /.gitignore: -------------------------------------------------------------------------------- 1 | doc/tags 2 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | indent_type = "Tabs" 2 | indent_width = 4 3 | -------------------------------------------------------------------------------- /lua/spec/session_test_file: -------------------------------------------------------------------------------- 1 | cd ~/nvim-possession 2 | badd +15 lua/nvim-possession/init.lua 3 | badd +9 ~/nvim-possession/./lua/spec/session_files_spec.lua 4 | badd +4 ~/nvim-possession/./lua/nvim-possession/utils.lua 5 | argglobal 6 | %argdel 7 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/JohnnyMorganz/StyLua 3 | rev: v0.14.3 4 | hooks: 5 | - id: stylua 6 | - repo: local 7 | hooks: 8 | - id: unittest 9 | name: unittest 10 | entry: busted 11 | language: system 12 | args: [-C=lua] 13 | pass_filenames: false 14 | -------------------------------------------------------------------------------- /lua/spec/is_in_list_spec.lua: -------------------------------------------------------------------------------- 1 | describe("test matching is_in_list", function() 2 | local utils 3 | local s, l, r 4 | 5 | setup(function() 6 | utils = require("nvim-possession.utils") 7 | s = "lua" 8 | r = "rust" 9 | l = { "python", "lua", "go", "vim" } 10 | end) 11 | 12 | teardown(function() 13 | utils = nil 14 | end) 15 | 16 | it("positive match", function() 17 | assert.is_true(utils.is_in_list(s, l)) 18 | end) 19 | it("negative match", function() 20 | assert.is_false(utils.is_in_list(r, l)) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /lua/nvim-possession/sorting.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | ---sort sessions by last updated 4 | ---@param a table 5 | ---@param b table 6 | ---@return boolean 7 | M.time_sort = function(a, b) 8 | if a.mtime.sec ~= b.mtime.sec then 9 | return a.mtime.sec > b.mtime.sec 10 | end 11 | if a.mtime.nsec ~= b.mtime.nsec then 12 | return a.mtime.nsec > b.mtime.nsec 13 | end 14 | return M.alpha_sort(a, b) 15 | end 16 | 17 | ---sort sessions by name 18 | ---@param a table 19 | ---@param b table 20 | ---@return boolean 21 | M.alpha_sort = function(a, b) 22 | return a.name < b.name 23 | end 24 | 25 | return M 26 | -------------------------------------------------------------------------------- /lua/spec/session_files_spec.lua: -------------------------------------------------------------------------------- 1 | describe("test matching session files", function() 2 | local utils 3 | local test_file, session_files 4 | 5 | setup(function() 6 | utils = require("nvim-possession.utils") 7 | test_file = "spec/session_test_file" 8 | session_files = { 9 | "lua/nvim-possession/init.lua", 10 | "lua/spec/session_files_spec.lua", 11 | "lua/nvim-possession/utils.lua", 12 | } 13 | end) 14 | 15 | teardown(function() 16 | utils = nil 17 | end) 18 | 19 | it("positive match", function() 20 | assert.same(session_files, utils.session_files(test_file)) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /lua/spec/sort_spec.lua: -------------------------------------------------------------------------------- 1 | describe("test matching sorting functions", function() 2 | local sort 3 | local sessions, time_sorted_sessions 4 | 5 | setup(function() 6 | sort = require("nvim-possession.sorting") 7 | sessions = { 8 | { name = "aaa", mtime = { sec = 0, nsec = 1 } }, 9 | { name = "zzz", mtime = { sec = 0, nsec = 2 } }, 10 | } 11 | time_sorted_sessions = 12 | { { name = "zzz", mtime = { sec = 0, nsec = 2 } }, { name = "aaa", mtime = { sec = 0, nsec = 1 } } } 13 | end) 14 | 15 | teardown(function() 16 | sort = nil 17 | end) 18 | 19 | it("time sorting", function() 20 | table.sort(sessions, sort.time_sort) 21 | assert.same(time_sorted_sessions, sessions) 22 | end) 23 | end) 24 | -------------------------------------------------------------------------------- /.github/workflows/panvimdoc.yml: -------------------------------------------------------------------------------- 1 | name: panvimdoc 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - '**' 8 | paths: 9 | - "README.md" 10 | - "**.yml" 11 | 12 | jobs: 13 | docs: 14 | runs-on: ubuntu-latest 15 | name: pandoc to vimdoc 16 | permissions: 17 | contents: write 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: panvimdoc 22 | uses: kdheepak/panvimdoc@main 23 | with: 24 | vimdoc: possession 25 | version: "Neovim >= 0.8.0" 26 | demojify: true 27 | treesitter: true 28 | - uses: stefanzweifel/git-auto-commit-action@v4 29 | if: startsWith(github.ref, 'refs/heads/') 30 | with: 31 | commit_message: "docs: auto generate vim documentation" 32 | branch: ${{ github.ref_name }} 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - "v*.*.*" 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | name: Github release 13 | permissions: 14 | contents: write 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | ref: ${{ github.head_ref || github.ref }} 20 | - name: Release 21 | uses: softprops/action-gh-release@v1 22 | 23 | luarocks-release: 24 | runs-on: ubuntu-latest 25 | name: LuaRocks upload 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | with: 30 | ref: ${{ github.head_ref || github.ref }} 31 | - name: LuaRocks Upload 32 | uses: nvim-neorocks/luarocks-tag-release@v2.1.0 33 | env: 34 | LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }} 35 | with: 36 | dependencies: | 37 | fzf-lua 38 | copy_directories: | 39 | doc 40 | license: "MIT" 41 | -------------------------------------------------------------------------------- /lua/nvim-possession/ui.lua: -------------------------------------------------------------------------------- 1 | local utils = require("nvim-possession.utils") 2 | 3 | local builtin_ok, builtin = pcall(require, "fzf-lua.previewer.builtin") 4 | if not builtin_ok then 5 | return 6 | end 7 | 8 | --- extend fzf builtin previewer 9 | local M = {} 10 | M.session_previewer = builtin.base:extend() 11 | 12 | M.session_previewer.new = function(self, o, opts, fzf_win) 13 | M.session_previewer.super.new(self, o, opts, fzf_win) 14 | setmetatable(self, M.session_previewer) 15 | return self 16 | end 17 | 18 | M.session_previewer.populate_preview_buf = function(self, entry_str) 19 | local tmpbuf = self:get_tmp_buffer() 20 | local files = utils.session_files(self.opts.user_config.sessions.sessions_path .. entry_str) 21 | 22 | vim.api.nvim_buf_set_lines(tmpbuf, 0, -1, false, files) 23 | self:set_preview_buf(tmpbuf) 24 | self.win:update_preview_scrollbar() 25 | end 26 | 27 | M.session_previewer.gen_winopts = function(self) 28 | local new_winopts = { 29 | wrap = false, 30 | number = false, 31 | } 32 | return vim.tbl_extend("force", self.winopts, new_winopts) 33 | end 34 | return M 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Gennaro Tedesco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lua/nvim-possession/config.lua: -------------------------------------------------------------------------------- 1 | local sort = require("nvim-possession.sorting") 2 | 3 | local M = {} 4 | 5 | M.sessions = { 6 | sessions_path = vim.fn.stdpath("data") .. "/sessions/", 7 | sessions_variable = "session", 8 | sessions_icon = "📌", 9 | sessions_prompt = "sessions:", 10 | } 11 | 12 | ---@type boolean 13 | M.autoload = false 14 | ---@type boolean 15 | M.autoprompt = false 16 | ---@type boolean 17 | M.autosave = true 18 | M.autoswitch = { 19 | enable = false, 20 | exclude_ft = {}, 21 | } 22 | 23 | ---@type function 24 | M.save_hook = nil 25 | ---@type function 26 | M.post_hook = nil 27 | 28 | ---@class possession.Hls 29 | ---@field normal? string hl group bg session window 30 | ---@field preview_normal? string hl group bg preview window 31 | ---@field border? string hl group border session window 32 | ---@field preview_border? string hl group border preview window 33 | M.fzf_hls = { 34 | normal = "Normal", 35 | preview_normal = "Normal", 36 | border = "Constant", 37 | preview_border = "Constant", 38 | } 39 | 40 | ---@class possession.Winopts 41 | ---@field border? string Any of the options of nvim_win_open.border 42 | ---@field height? number Height of the fzf window 43 | ---@field width? number Width of the fzf window 44 | ---@field preview? table 45 | M.fzf_winopts = { 46 | title = " sessions 📌 ", 47 | title_pos = "center", 48 | border = "rounded", 49 | height = 0.5, 50 | width = 0.25, 51 | preview = { 52 | hidden = "nohidden", 53 | horizontal = "down:40%", 54 | }, 55 | } 56 | 57 | ---@class possession.Mapopts 58 | ---@field delete? string 59 | ---@field rename? string 60 | ---@field new? string 61 | M.mappings = { 62 | action_delete = "ctrl-x", 63 | action_rename = "ctrl-r", 64 | action_new = "ctrl-n", 65 | } 66 | 67 | ---@type function 68 | M.sort = sort.alpha_sort 69 | 70 | return M 71 | -------------------------------------------------------------------------------- /lua/nvim-possession/utils.lua: -------------------------------------------------------------------------------- 1 | local sort = require("nvim-possession.sorting") 2 | local M = {} 3 | 4 | ---return the list of available sessions 5 | ---@param user_config table 6 | ---@return table 7 | M.list_sessions = function(user_config, cwd) 8 | local sessions = {} 9 | for name, type in vim.fs.dir(user_config.sessions.sessions_path) do 10 | if type == "file" then 11 | local stat = vim.uv.fs_stat(user_config.sessions.sessions_path .. name) 12 | if stat then 13 | table.insert(sessions, { name = name, mtime = stat.mtime }) 14 | end 15 | end 16 | end 17 | table.sort(sessions, function(a, b) 18 | if type(user_config.sort) == "function" then 19 | return user_config.sort(a, b) 20 | else 21 | return sort.alpha_sort(a, b) 22 | end 23 | end) 24 | local session_names = {} 25 | for _, sess in ipairs(sessions) do 26 | if cwd then 27 | if M.is_in_cwd(sess.name, user_config) then 28 | table.insert(session_names, sess.name) 29 | end 30 | else 31 | table.insert(session_names, sess.name) 32 | end 33 | end 34 | return session_names 35 | end 36 | 37 | ---return the list of files in the session 38 | ---@param file string 39 | ---@return table 40 | M.session_files = function(file) 41 | if vim.fn.isdirectory(file) == 1 then 42 | return {} 43 | end 44 | local lines = {} 45 | local cwd, cwd_pat = "", "^cd%s*" 46 | local buf_pat = "^badd%s*%+%d+%s*" 47 | for line in io.lines(file) do 48 | if string.find(line, cwd_pat) then 49 | cwd = line:gsub("%p", "%%%1") 50 | end 51 | if string.find(line, buf_pat) then 52 | lines[#lines + 1] = line 53 | end 54 | end 55 | local buffers = {} 56 | for k, v in pairs(lines) do 57 | buffers[k] = v:gsub(buf_pat, ""):gsub(cwd:gsub("cd%s*", ""), ""):gsub("^/?%.?/", "") 58 | end 59 | return buffers 60 | end 61 | 62 | ---checks whether a session is contained in the cwd 63 | ---@param session string 64 | ---@param user_config table 65 | ---@return boolean 66 | M.is_in_cwd = function(session, user_config) 67 | local session_dir, dir_pat = "", "^cd%s*" 68 | for file, type in vim.fs.dir(user_config.sessions.sessions_path) do 69 | if type == "file" and file == session then 70 | for line in io.lines(user_config.sessions.sessions_path .. file) do 71 | if string.find(line, dir_pat) then 72 | session_dir = vim.uv.fs_realpath(vim.fs.normalize((line:gsub("cd%s*", "")))) 73 | if session_dir == vim.fn.getcwd() then 74 | return true 75 | end 76 | end 77 | end 78 | end 79 | end 80 | return false 81 | end 82 | 83 | ---check if an item is in a list 84 | ---@param value string 85 | ---@param list table 86 | ---@return boolean 87 | M.is_in_list = function(value, list) 88 | for _, v in pairs(list) do 89 | if v == value then 90 | return true 91 | end 92 | end 93 | return false 94 | end 95 | 96 | ---check if a session is loaded and save it automatically 97 | ---without asking for prompt 98 | ---@param config table 99 | M.autosave = function(config) 100 | local cur_session = vim.g[config.sessions.sessions_variable] 101 | if type(config.save_hook) == "function" then 102 | config.save_hook() 103 | end 104 | if cur_session ~= nil then 105 | vim.cmd.mksession({ args = { config.sessions.sessions_path .. cur_session }, bang = true }) 106 | end 107 | end 108 | 109 | ---before switching session perform the following: 110 | ---1) autosave current session 111 | ---2) save and close all modifiable buffers 112 | ---@param config table 113 | M.autoswitch = function(config) 114 | if vim.api.nvim_buf_get_name(0) ~= "" then 115 | vim.cmd.write() 116 | end 117 | M.autosave(config) 118 | vim.cmd([[silent! bufdo if expand('%') !=# '' | edit | endif]]) 119 | local buf_list = vim.tbl_filter(function(buf) 120 | return vim.api.nvim_buf_is_valid(buf) 121 | and vim.bo[buf].buflisted 122 | and vim.bo[buf].modifiable 123 | and not M.is_in_list(vim.bo[buf].filetype, config.autoswitch.exclude_ft) 124 | end, vim.api.nvim_list_bufs()) 125 | for _, buf in pairs(buf_list) do 126 | vim.cmd("bd " .. buf) 127 | end 128 | end 129 | 130 | return M 131 | -------------------------------------------------------------------------------- /lua/nvim-possession/init.lua: -------------------------------------------------------------------------------- 1 | local config = require("nvim-possession.config") 2 | local ui = require("nvim-possession.ui") 3 | local utils = require("nvim-possession.utils") 4 | 5 | local M = {} 6 | 7 | ---expose the following interfaces: 8 | ---require("nvim-possession").new() 9 | ---require("nvim-possession").list() 10 | ---require("nvim-possession").update() 11 | ---require("nvim-possession").status() 12 | ---@param user_opts table 13 | M.setup = function(user_opts) 14 | local user_config = vim.tbl_deep_extend("force", config, user_opts or {}) 15 | 16 | local notification_title = user_config.sessions.sessions_icon .. " nvim-possession" 17 | local fzf_ok, fzf = pcall(require, "fzf-lua") 18 | if not fzf_ok then 19 | vim.notify("fzf-lua required as dependency", vim.log.levels.WARN, { title = notification_title }) 20 | return 21 | end 22 | 23 | ---get global variable with session name: useful for statusbar components 24 | ---@return string|nil 25 | M.status = function() 26 | local cur_session = vim.g[user_config.sessions.sessions_variable] 27 | return cur_session ~= nil and user_config.sessions.sessions_icon .. cur_session or nil 28 | end 29 | 30 | ---save current session if session path exists 31 | ---return if path does not exist 32 | M.new = function() 33 | if vim.fn.finddir(user_config.sessions.sessions_path) == "" then 34 | vim.notify("sessions_path does not exist", vim.log.levels.WARN, { title = notification_title }) 35 | return 36 | end 37 | 38 | local name = vim.fn.input("name: ") 39 | if name ~= "" then 40 | if next(vim.fs.find(name, { path = user_config.sessions.sessions_path })) == nil then 41 | vim.cmd.mksession({ args = { user_config.sessions.sessions_path .. name } }) 42 | vim.g[user_config.sessions.sessions_variable] = vim.fs.basename(name) 43 | vim.notify( 44 | "saved in: " .. user_config.sessions.sessions_path .. name, 45 | vim.log.levels.INFO, 46 | { title = notification_title } 47 | ) 48 | else 49 | vim.notify("session already exists", vim.log.levels.INFO, { title = notification_title }) 50 | end 51 | end 52 | end 53 | fzf.config.set_action_helpstr(M.new, "new-session") 54 | 55 | ---update loaded session with current status 56 | M.update = function() 57 | local cur_session = vim.g[user_config.sessions.sessions_variable] 58 | if cur_session ~= nil then 59 | local confirm = vim.fn.confirm("overwrite session?", "&Yes\n&No", 2) 60 | if confirm == 1 then 61 | if type(user_config.save_hook) == "function" then 62 | user_config.save_hook() 63 | end 64 | vim.cmd.mksession({ args = { user_config.sessions.sessions_path .. cur_session }, bang = true }) 65 | vim.notify("updated session: " .. cur_session, vim.log.levels.INFO, { title = notification_title }) 66 | end 67 | else 68 | vim.notify("no session loaded", vim.log.levels.INFO, { title = notification_title }) 69 | end 70 | end 71 | 72 | ---load selected session 73 | ---@param selected string 74 | M.load = function(selected) 75 | local session = user_config.sessions.sessions_path .. selected[1] 76 | if user_config.autoswitch.enable and vim.g[user_config.sessions.sessions_variable] ~= nil then 77 | utils.autoswitch(user_config) 78 | end 79 | vim.cmd.source(session) 80 | vim.g[user_config.sessions.sessions_variable] = vim.fs.basename(session) 81 | if type(user_config.post_hook) == "function" then 82 | user_config.post_hook() 83 | end 84 | end 85 | fzf.config.set_action_helpstr(M.load, "load-session") 86 | 87 | ---delete selected session 88 | ---@param selected string 89 | M.delete_selected = function(selected) 90 | local session = user_config.sessions.sessions_path .. selected[1] 91 | local confirm = vim.fn.confirm("delete session?", "&Yes\n&No", 2) 92 | if confirm == 1 then 93 | os.remove(session) 94 | vim.notify("deleted " .. session, vim.log.levels.INFO, { title = notification_title }) 95 | if vim.g[user_config.sessions.sessions_variable] == vim.fs.basename(session) then 96 | vim.g[user_config.sessions.sessions_variable] = nil 97 | end 98 | end 99 | end 100 | fzf.config.set_action_helpstr(M.delete_selected, "delete-session") 101 | 102 | ---delete current active session 103 | M.delete = function() 104 | local cur_session = vim.g[user_config.sessions.sessions_variable] 105 | if cur_session ~= nil then 106 | local confirm = vim.fn.confirm("delete session " .. cur_session .. "?", "&Yes\n&No", 2) 107 | if confirm == 1 then 108 | local session_path = user_config.sessions.sessions_path .. cur_session 109 | os.remove(session_path) 110 | vim.notify("deleted " .. session_path, vim.log.levels.INFO, { title = notification_title }) 111 | if vim.g[user_config.sessions.sessions_variable] == vim.fs.basename(session_path) then 112 | vim.g[user_config.sessions.sessions_variable] = nil 113 | end 114 | end 115 | else 116 | vim.notify("no active session", vim.log.levels.WARN, { title = notification_title }) 117 | end 118 | end 119 | 120 | ---rename selected session 121 | ---@param selected string 122 | M.rename_selected = function(selected) 123 | local session_path = user_config.sessions.sessions_path 124 | local old_name = selected[1] 125 | local old_file_path = session_path .. old_name 126 | local new_name = vim.fn.input("Enter new name for the session: ", old_name) 127 | 128 | if new_name and new_name ~= "" then 129 | local new_file_path = session_path .. new_name 130 | os.rename(old_file_path, new_file_path) 131 | vim.notify( 132 | "Session renamed from " .. old_name .. " to " .. new_name, 133 | vim.log.levels.INFO, 134 | { title = notification_title } 135 | ) 136 | else 137 | vim.notify("New name cannot be empty", vim.log.levels.WARN, { title = notification_title }) 138 | end 139 | end 140 | fzf.config.set_action_helpstr(M.rename_selected, "rename-session") 141 | 142 | ---list all existing sessions and their files 143 | ---@param cwd boolean|nil 144 | M.list = function(cwd) 145 | local iter = vim.uv.fs_scandir(user_config.sessions.sessions_path) 146 | if iter == nil then 147 | vim.notify( 148 | "session folder " .. user_config.sessions.sessions_path .. " does not exist", 149 | vim.log.levels.WARN, 150 | { title = notification_title } 151 | ) 152 | return 153 | end 154 | local next_dir = vim.uv.fs_scandir_next(iter) 155 | if next_dir == nil then 156 | vim.notify("no saved sessions", vim.log.levels.WARN, { title = notification_title }) 157 | return 158 | end 159 | 160 | local opts = { 161 | user_config = user_config, 162 | prompt = user_config.sessions.sessions_icon .. user_config.sessions.sessions_prompt, 163 | cwd_prompt = false, 164 | file_icons = false, 165 | git_icons = false, 166 | cwd_header = false, 167 | no_header = true, 168 | 169 | previewer = ui.session_previewer, 170 | hls = user_config.fzf_hls, 171 | winopts = user_config.fzf_winopts, 172 | cwd = user_config.sessions.sessions_path, 173 | actions = { 174 | ["enter"] = M.load, 175 | [user_config.mappings.action_delete] = { 176 | fn = function(selected) 177 | M.delete_selected(selected) 178 | M.list() 179 | end, 180 | header = "delete session", 181 | desc = "delete-session", 182 | }, 183 | [user_config.mappings.action_rename] = { 184 | fn = function(selected) 185 | M.rename_selected(selected) 186 | M.list() 187 | end, 188 | header = "rename session", 189 | desc = "rename-session", 190 | }, 191 | [user_config.mappings.action_new] = { fn = M.new, header = "new session", desc = "new-session" }, 192 | }, 193 | } 194 | opts = require("fzf-lua.config").normalize_opts(opts, {}) 195 | opts = require("fzf-lua.core").set_header(opts) 196 | 197 | ---autoload mechanism 198 | if cwd then 199 | local sessions_in_cwd = utils.list_sessions(user_config, cwd) 200 | if next(sessions_in_cwd) == nil then 201 | return nil 202 | elseif #sessions_in_cwd == 1 or not user_config.autoprompt then 203 | vim.schedule(function() 204 | vim.cmd.source(user_config.sessions.sessions_path .. sessions_in_cwd[1]) 205 | vim.g[user_config.sessions.sessions_variable] = vim.fs.basename(sessions_in_cwd[1]) 206 | if type(user_config.post_hook) == "function" then 207 | user_config.post_hook() 208 | end 209 | end) 210 | return nil 211 | end 212 | end 213 | ---standard list load mechanism 214 | fzf.fzf_exec(function(fzf_cb) 215 | for _, sess in ipairs(utils.list_sessions(user_config, cwd)) do 216 | fzf_cb(sess) 217 | end 218 | fzf_cb() 219 | end, opts) 220 | end 221 | 222 | if user_config.autoload and vim.fn.argc() == 0 then 223 | M.list(true) 224 | end 225 | 226 | if user_config.autosave then 227 | local autosave_possession = vim.api.nvim_create_augroup("AutosavePossession", {}) 228 | vim.api.nvim_clear_autocmds({ group = autosave_possession }) 229 | vim.api.nvim_create_autocmd("VimLeave", { 230 | group = autosave_possession, 231 | desc = "📌 save session on VimLeave", 232 | callback = function() 233 | utils.autosave(user_config) 234 | end, 235 | }) 236 | end 237 | end 238 | 239 | return M 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | 4 |
5 | nvim-possession 6 |
7 |

8 | 9 |

10 | 11 | luarocks 12 | 13 | 14 | releases 15 | 16 |

17 | 18 |

No-nonsense session manager

19 | 20 | You are puzzled by neovim sessions and are not using them, are you? Start your pos-sessions journey, fear no more! 21 | 22 | This plugin is a no-nonsense session manager built on top of [fzf-lua](https://github.com/ibhagwan/fzf-lua) (required) that makes managing sessions quick and visually appealing: dynamically browse through your existing sessions, create new ones, update and delete with a statusline component to remind you of where you are. See for yourself: 23 | 24 | ![demo](https://user-images.githubusercontent.com/15387611/211946693-7c0a8f00-4ed8-4142-a8aa-a4dc75f42841.gif) 25 | 26 | ## 🔌 Installation and quickstart 27 | 28 | Install `nvim-possession` with your favourite plugin manager (`fzf-lua` is required) and invoke `require("nvim-possession").setup({})`; in order to avoid conflicts with your own keymaps we do not set any mappings but only expose the interfaces, which means you would need to define them yourself. The suggested quickstart configuration is, for instance 29 | 30 | - with [lazy.nvim](https://github.com/folke/lazy.nvim) 31 | 32 | ```lua 33 | { 34 | "gennaro-tedesco/nvim-possession", 35 | dependencies = { 36 | "ibhagwan/fzf-lua", 37 | }, 38 | config = true, 39 | keys = { 40 | { "sl", function() require("nvim-possession").list() end, desc = "📌list sessions", }, 41 | { "sn", function() require("nvim-possession").new() end, desc = "📌create new session", }, 42 | { "su", function() require("nvim-possession").update() end, desc = "📌update current session", }, 43 | { "sd", function() require("nvim-possession").delete() end, desc = "📌delete selected session"}, 44 | }, 45 | } 46 | ``` 47 | 48 | Exposed interfaces 49 | 50 | | function | description | interaction | 51 | | :------------------ | :-------------------------------------------------------------------------- | :------------------------------------------------------------------ | 52 | | possession.list() | list all the existing sessions with fzf-lua; preview shows files in session | `` load selected session
`` delete selection session
`` rename selected session
`` create new session | 53 | | possession.new() | prompt for name to create new session | session folder must alredy exist, return a message error otherwise | 54 | | possession.update() | update current session (if new buffers are open) | do nothing if no session is loaded | 55 | | possession.delete() | delete current session (without prompt) | do nothing if no session is loaded | 56 | 57 | ## 🛠 Usage and advanced configuration 58 | 59 | As shown above the main use of the plugin is to show all existing sessions (say via `sl`) and load the selected one upon ``. Once a session is loaded a global variable is defined containing the session name (to display in a statusline - see below - or to validate which session is currently active). New sessions can also be created and updated on the fly, and they will show when you next invoke the list. 60 | 61 | Default configurations can be found in the [config](https://github.com/gennaro-tedesco/nvim-possession/blob/main/lua/nvim-possession/config.lua) and can be overriden at will by passing them to the `setup({})` function: in particular the default location folder for sessions is `vim.fn.stdpath("data") .. "/sessions/",`. You should not need to change any of the default settings, however if you really want to do so: 62 | 63 | ```lua 64 | 65 | require("nvim-possession").setup({ 66 | sessions = { 67 | sessions_path = ... -- folder to look for sessions, must be a valid existing path 68 | sessions_variable = ... -- defines vim.g[sessions_variable] when a session is loaded 69 | sessions_icon = ...-- string: shows icon both in the prompt and in the statusline 70 | sessions_prompt = ... -- fzf prompt string 71 | }, 72 | 73 | autoload = false, -- whether to autoload sessions in the cwd at startup 74 | autosave = true, -- whether to autosave loaded sessions before quitting 75 | autoswitch = { 76 | enable = false -- whether to enable autoswitch 77 | exclude_ft = {}, -- list of filetypes to exclude from autoswitch 78 | } 79 | 80 | save_hook = nil -- callback, function to execute before saving a session 81 | -- useful to update or cleanup global variables for example 82 | post_hook = nil -- callback, function to execute after loading a session 83 | -- useful to restore file trees, file managers or terminals 84 | -- function() 85 | -- require('FTerm').open() 86 | -- require("nvim-tree.api").tree.toggle() 87 | -- end 88 | 89 | ---@type possession.Hls 90 | fzf_hls = { -- highlight groups for the sessions and preview windows 91 | normal = "Normal", 92 | preview_normal = "Normal", 93 | border = "Todo", 94 | preview_border = "Constant", 95 | }, 96 | ---@type possession.Winopts 97 | fzf_winopts = { 98 | -- any valid fzf-lua winopts options, for instance 99 | width = 0.5, 100 | preview = { 101 | vertical = "right:30%" 102 | } 103 | } 104 | ---@type possession.Mapopts 105 | mappings = { -- configure action keymaps on possession.list() picker 106 | action_delete = "ctrl-x", 107 | action_rename = "ctrl-r", 108 | action_new = "ctrl-n", 109 | } 110 | sort = require("nvim-possession.sorting").alpha_sort -- callback, sorting function to list sessions 111 | -- require("nvim-possession.sorting").time_sort 112 | -- to sort by last updated instead 113 | }) 114 | ``` 115 | 116 | ### 🪄 Automagic 117 | 118 | If you want to automatically load sessions defined for the current working directory at startup, specify 119 | 120 | ```lua 121 | require("nvim-possession").setup({ 122 | autoload = true -- default false 123 | }) 124 | ``` 125 | 126 | This autoloads sessions when starting neovim without file arguments (i. e. `$ nvim `) and in case such sessions explicitly contain a reference to the current working directory (you must have `vim.go.ssop+=curdir`). If more than one session in the current working directory exists, you can either specify 127 | 128 | ```lua 129 | require("nvim-possession").setup({ 130 | autoprompt = true -- default false 131 | }) 132 | ``` 133 | 134 | to be presented with a fuzzy prompt or default automatically to load the topmost sorted session (see `sort = require("nvim-possession.sorting")` in [Usage](#-usage-and-advanced-configuration)). 135 | 136 | Sessions are automatically saved before quitting, should buffers be added or removed to them. This defaults to `true` (as it is generally expected behaviour), if you want to opt-out specify 137 | 138 | ```lua 139 | require("nvim-possession").setup({ 140 | autosave = false -- default true 141 | }) 142 | ``` 143 | 144 | When switching between sessions it is often desirable to remove pending buffers belonging to the previous one, so that only buffers with the new session files are loaded. In order to achieve this behaviour specify 145 | 146 | ```lua 147 | require("nvim-possession").setup({ 148 | autoswitch = { 149 | enable = true, -- default false 150 | } 151 | }) 152 | ``` 153 | 154 | this option autosaves the previous session and deletes all its buffers before switching to a new one. If there are some filetypes you want to always keep, you may indicate them in 155 | 156 | ```lua 157 | require("nvim-possession").setup({ 158 | autoswitch = { 159 | enable = true, -- default false 160 | exclude_ft = {"...", "..."}, -- list of filetypes to exclude from deletion 161 | } 162 | }) 163 | ``` 164 | 165 | A note on lazy loading: this plugin is extremely light weight and it generally loads in no time: practically speaking there should not be any need to lazy load it on events. If you are however opting in the `autoload = true` feature, notice that by definition such a feature loads the existing session buffers in memory at start-up, thereby also triggering all other buffer related events (especially treesitter); this may result in higher start-up times but is independent of the plugin (you would get the same loading times by manually sourcing the session files). 166 | 167 | ### Save-hook 168 | 169 | Before saving a session, you can run actions that may update state or perform cleanup before updating the session. 170 | 171 | For example, you may want to only save visible buffers to the session. This could be useful if loading a lot of buffers leads to slow startup. Or maybe you want to keep the tabline clean. To do so, you can use: 172 | 173 | ```lua 174 | require("nvim-possession").setup({ 175 | save_hook = function() 176 | -- Get visible buffers 177 | local visible_buffers = {} 178 | for _, win in ipairs(vim.api.nvim_list_wins()) do 179 | visible_buffers[vim.api.nvim_win_get_buf(win)] = true 180 | end 181 | 182 | local buflist = vim.api.nvim_list_bufs() 183 | for _, bufnr in ipairs(buflist) do 184 | if visible_buffers[bufnr] == nil then -- Delete buffer if not visible 185 | vim.cmd("bd " .. bufnr) 186 | end 187 | end 188 | end 189 | }) 190 | 191 | ``` 192 | 193 | ### Post-hook 194 | 195 | After loading a session you may want to specify additional actions to run that may not be have been saved in the session content: this is often the case for restoring file tree or file managers, or open up terminal windows or fuzzy finders or set specific options. To do so you can use 196 | 197 | ```lua 198 | 199 | require("nvim-possession").setup({ 200 | post_hook = function() 201 | require("FTerm").open() 202 | require("nvim-tree.api").tree.toggle() 203 | vim.lsp.buf.format() 204 | end 205 | }) 206 | ``` 207 | 208 | ## 🚥 Statusline 209 | 210 | You can call `require("nvim-possession").status()` as component in your statusline, for example with `lualine` you would have 211 | 212 | ```lua 213 | 214 | lualine.setup({ 215 | sections = { 216 | lualine_a = ... 217 | lualine_b = ... 218 | lualine_c = { 219 | { "filename", path = 1 }, 220 | { 221 | require("nvim-possession").status, 222 | cond = function() 223 | return require("nvim-possession").status() ~= nil 224 | end, 225 | }, 226 | }, 227 | } 228 | }) 229 | ``` 230 | 231 | to display 232 | 233 |

234 | 235 |

236 | 237 | the component automatically disappears or changes if you delete the current session or switch to another one. 238 | 239 | ## Feedback 240 | 241 | If you find this plugin useful consider awarding it a ⭐, it is a great way to give feedback! Otherwise, any additional suggestions or merge request is warmly welcome! 242 | -------------------------------------------------------------------------------- /doc/possession.txt: -------------------------------------------------------------------------------- 1 | *possession.txt* For Neovim >= 0.8.0 Last change: 2025 September 04 2 | 3 | ============================================================================== 4 | Table of Contents *possession-table-of-contents* 5 | 6 | - Installation and quickstart |possession-installation-and-quickstart| 7 | - Usage and advanced configuration|possession-usage-and-advanced-configuration| 8 | - Statusline |possession-statusline| 9 | - Feedback |possession-feedback| 10 | 1. Links |possession-links| 11 | 12 | 13 | 14 | nvim-possession 15 | 16 | 17 | 18 | 19 | 20 | No-nonsense session managerYou are puzzled by neovim sessions and are not using them, are you? Start your 21 | pos-sessions journey, fear no more! 22 | 23 | This plugin is a no-nonsense session manager built on top of fzf-lua 24 | (required) that makes managing sessions 25 | quick and visually appealing: dynamically browse through your existing 26 | sessions, create new ones, update and delete with a statusline component to 27 | remind you of where you are. See for yourself: 28 | 29 | 30 | INSTALLATION AND QUICKSTART *possession-installation-and-quickstart* 31 | 32 | Install `nvim-possession` with your favourite plugin manager (`fzf-lua` is 33 | required) and invoke `require("nvim-possession").setup({})`; in order to avoid 34 | conflicts with your own keymaps we do not set any mappings but only expose the 35 | interfaces, which means you would need to define them yourself. The suggested 36 | quickstart configuration is, for instance 37 | 38 | - with lazy.nvim 39 | 40 | >lua 41 | { 42 | "gennaro-tedesco/nvim-possession", 43 | dependencies = { 44 | "ibhagwan/fzf-lua", 45 | }, 46 | config = true, 47 | keys = { 48 | { "sl", function() require("nvim-possession").list() end, desc = "📌list sessions", }, 49 | { "sn", function() require("nvim-possession").new() end, desc = "📌create new session", }, 50 | { "su", function() require("nvim-possession").update() end, desc = "📌update current session", }, 51 | { "sd", function() require("nvim-possession").delete() end, desc = "📌delete selected session"}, 52 | }, 53 | } 54 | < 55 | 56 | Exposed interfaces 57 | 58 | ------------------------------------------------------------------------------------- 59 | function description interaction 60 | --------------------- --------------------------------- ----------------------------- 61 | possession.list() list all the existing sessions load selected 62 | with fzf-lua; preview shows files session delete 63 | in session selection session 64 | rename selected 65 | session create new 66 | session 67 | 68 | possession.new() prompt for name to create new session folder must alredy 69 | session exist, return a message error 70 | otherwise 71 | 72 | possession.update() update current session (if new do nothing if no session is 73 | buffers are open) loaded 74 | 75 | possession.delete() delete current session (without do nothing if no session is 76 | prompt) loaded 77 | ------------------------------------------------------------------------------------- 78 | 79 | USAGE AND ADVANCED CONFIGURATION *possession-usage-and-advanced-configuration* 80 | 81 | As shown above the main use of the plugin is to show all existing sessions (say 82 | via `sl`) and load the selected one upon ``. Once a session is 83 | loaded a global variable is defined containing the session name (to display in 84 | a statusline - see below - or to validate which session is currently active). 85 | New sessions can also be created and updated on the fly, and they will show 86 | when you next invoke the list. 87 | 88 | Default configurations can be found in the config 89 | 90 | and can be overriden at will by passing them to the `setup({})` function: in 91 | particular the default location folder for sessions is `vim.fn.stdpath("data") 92 | .. "/sessions/",`. You should not need to change any of the default settings, 93 | however if you really want to do so: 94 | 95 | >lua 96 | 97 | require("nvim-possession").setup({ 98 | sessions = { 99 | sessions_path = ... -- folder to look for sessions, must be a valid existing path 100 | sessions_variable = ... -- defines vim.g[sessions_variable] when a session is loaded 101 | sessions_icon = ...-- string: shows icon both in the prompt and in the statusline 102 | sessions_prompt = ... -- fzf prompt string 103 | }, 104 | 105 | autoload = false, -- whether to autoload sessions in the cwd at startup 106 | autosave = true, -- whether to autosave loaded sessions before quitting 107 | autoswitch = { 108 | enable = false -- whether to enable autoswitch 109 | exclude_ft = {}, -- list of filetypes to exclude from autoswitch 110 | } 111 | 112 | save_hook = nil -- callback, function to execute before saving a session 113 | -- useful to update or cleanup global variables for example 114 | post_hook = nil -- callback, function to execute after loading a session 115 | -- useful to restore file trees, file managers or terminals 116 | -- function() 117 | -- require('FTerm').open() 118 | -- require("nvim-tree.api").tree.toggle() 119 | -- end 120 | 121 | ---@type possession.Hls 122 | fzf_hls = { -- highlight groups for the sessions and preview windows 123 | normal = "Normal", 124 | preview_normal = "Normal", 125 | border = "Todo", 126 | preview_border = "Constant", 127 | }, 128 | ---@type possession.Winopts 129 | fzf_winopts = { 130 | -- any valid fzf-lua winopts options, for instance 131 | width = 0.5, 132 | preview = { 133 | vertical = "right:30%" 134 | } 135 | } 136 | ---@type possession.Mapopts 137 | mappings = { -- configure action keymaps on possession.list() picker 138 | action_delete = "ctrl-x", 139 | action_rename = "ctrl-r", 140 | action_new = "ctrl-n", 141 | } 142 | sort = require("nvim-possession.sorting").alpha_sort -- callback, sorting function to list sessions 143 | -- require("nvim-possession.sorting").time_sort 144 | -- to sort by last updated instead 145 | }) 146 | < 147 | 148 | 149 | AUTOMAGIC ~ 150 | 151 | If you want to automatically load sessions defined for the current working 152 | directory at startup, specify 153 | 154 | >lua 155 | require("nvim-possession").setup({ 156 | autoload = true -- default false 157 | }) 158 | < 159 | 160 | This autoloads sessions when starting neovim without file arguments (i. e. `$ 161 | nvim`) and in case such sessions explicitly contain a reference to the current 162 | working directory (you must have `vim.go.ssop+=curdir`). If more than one 163 | session in the current working directory exists, you can either specify 164 | 165 | >lua 166 | require("nvim-possession").setup({ 167 | autoprompt = true -- default false 168 | }) 169 | < 170 | 171 | to be presented with a fuzzy prompt or default automatically to load the 172 | topmost sorted session (see `sort = require("nvim-possession.sorting")` in 173 | |possession-usage|). 174 | 175 | Sessions are automatically saved before quitting, should buffers be added or 176 | removed to them. This defaults to `true` (as it is generally expected 177 | behaviour), if you want to opt-out specify 178 | 179 | >lua 180 | require("nvim-possession").setup({ 181 | autosave = false -- default true 182 | }) 183 | < 184 | 185 | When switching between sessions it is often desirable to remove pending buffers 186 | belonging to the previous one, so that only buffers with the new session files 187 | are loaded. In order to achieve this behaviour specify 188 | 189 | >lua 190 | require("nvim-possession").setup({ 191 | autoswitch = { 192 | enable = true, -- default false 193 | } 194 | }) 195 | < 196 | 197 | this option autosaves the previous session and deletes all its buffers before 198 | switching to a new one. If there are some filetypes you want to always keep, 199 | you may indicate them in 200 | 201 | >lua 202 | require("nvim-possession").setup({ 203 | autoswitch = { 204 | enable = true, -- default false 205 | exclude_ft = {"...", "..."}, -- list of filetypes to exclude from deletion 206 | } 207 | }) 208 | < 209 | 210 | A note on lazy loading: this plugin is extremely light weight and it generally 211 | loads in no time: practically speaking there should not be any need to lazy 212 | load it on events. If you are however opting in the `autoload = true` feature, 213 | notice that by definition such a feature loads the existing session buffers in 214 | memory at start-up, thereby also triggering all other buffer related events 215 | (especially treesitter); this may result in higher start-up times but is 216 | independent of the plugin (you would get the same loading times by manually 217 | sourcing the session files). 218 | 219 | 220 | SAVE-HOOK ~ 221 | 222 | Before saving a session, you can run actions that may update state or perform 223 | cleanup before updating the session. 224 | 225 | For example, you may want to only save visible buffers to the session. This 226 | could be useful if loading a lot of buffers leads to slow startup. Or maybe you 227 | want to keep the tabline clean. To do so, you can use: 228 | 229 | >lua 230 | require("nvim-possession").setup({ 231 | save_hook = function() 232 | -- Get visible buffers 233 | local visible_buffers = {} 234 | for _, win in ipairs(vim.api.nvim_list_wins()) do 235 | visible_buffers[vim.api.nvim_win_get_buf(win)] = true 236 | end 237 | 238 | local buflist = vim.api.nvim_list_bufs() 239 | for _, bufnr in ipairs(buflist) do 240 | if visible_buffers[bufnr] == nil then -- Delete buffer if not visible 241 | vim.cmd("bd " .. bufnr) 242 | end 243 | end 244 | end 245 | }) 246 | < 247 | 248 | 249 | POST-HOOK ~ 250 | 251 | After loading a session you may want to specify additional actions to run that 252 | may not be have been saved in the session content: this is often the case for 253 | restoring file tree or file managers, or open up terminal windows or fuzzy 254 | finders or set specific options. To do so you can use 255 | 256 | >lua 257 | 258 | require("nvim-possession").setup({ 259 | post_hook = function() 260 | require("FTerm").open() 261 | require("nvim-tree.api").tree.toggle() 262 | vim.lsp.buf.format() 263 | end 264 | }) 265 | < 266 | 267 | 268 | STATUSLINE *possession-statusline* 269 | 270 | You can call `require("nvim-possession").status()` as component in your 271 | statusline, for example with `lualine` you would have 272 | 273 | >lua 274 | 275 | lualine.setup({ 276 | sections = { 277 | lualine_a = ... 278 | lualine_b = ... 279 | lualine_c = { 280 | { "filename", path = 1 }, 281 | { 282 | require("nvim-possession").status, 283 | cond = function() 284 | return require("nvim-possession").status() ~= nil 285 | end, 286 | }, 287 | }, 288 | } 289 | }) 290 | < 291 | 292 | to display 293 | 294 | the component automatically disappears or changes if you delete the current 295 | session or switch to another one. 296 | 297 | 298 | FEEDBACK *possession-feedback* 299 | 300 | If you find this plugin useful consider awarding it a , it is a great way to 301 | give feedback! Otherwise, any additional suggestions or merge request is warmly 302 | welcome! 303 | 304 | ============================================================================== 305 | 1. Links *possession-links* 306 | 307 | 1. *demo*: https://user-images.githubusercontent.com/15387611/211946693-7c0a8f00-4ed8-4142-a8aa-a4dc75f42841.gif 308 | 309 | Generated by panvimdoc 310 | 311 | vim:tw=78:ts=8:noet:ft=help:norl: 312 | --------------------------------------------------------------------------------