Skip to content

Commit

Permalink
feat!: improved autoloading by cwd (#63)
Browse files Browse the repository at this point in the history
This extends the concept of "loading cwd sessions" by also considering any named session that has the same cwd as the one returned by getcwd() - previously only session with the name matching cwd would be loaded (so the one saved by PossessionSaveCwd. There are different strategies for autoloading on VimEnter controlled by the "autoload" setup option (last/auto_cwd/last_cwd). 

This is a breaking change because PossessionLoadCwd now works differently - loads newest session with data.cwd matching getcwd(). It takes optional argument with autocomplete of sessions matching cwd.
  • Loading branch information
josh-nz authored Jun 25, 2024
1 parent 5e91dc3 commit 2d51ce2
Show file tree
Hide file tree
Showing 11 changed files with 201 additions and 56 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,7 @@ require('possession').setup {
on_load = true,
on_quit = true,
},
autoload = {
cwd = false, -- or fun(): boolean
},
autoload = false, -- or 'last' or 'auto_cwd' or 'last_cwd' or fun(): string
commands = {
save = 'PossessionSave',
load = 'PossessionLoad',
Expand All @@ -89,6 +87,7 @@ require('possession').setup {
delete = 'PossessionDelete',
show = 'PossessionShow',
list = 'PossessionList',
list_cwd = 'PossessionListCwd',
migrate = 'PossessionMigrate',
},
hooks = {
Expand Down Expand Up @@ -192,6 +191,9 @@ require('telescope').load_extension('possession')
Then use `:Telescope possession list` or `require('telescope').extensions.possession.list()`
The default action will load selected session.

Alternatively, use `:Telescope possession list only_cwd=true` or `require('telescope').extensions.possession.list({only_cwd=true})`
This will limit the displayed sessions to those related to the current working directory.

![telescope](./img/telescope.png)

## Auto-save
Expand Down
40 changes: 29 additions & 11 deletions doc/possession.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,32 @@ currently loaded session is used (unless noted otherwise).

Save the current session information under `session_dir`. Use
`:PossessionSave! [{name}]` to avoid asking for confirmation when overwriting
existing session file. If [{name}] is not provided and a session does not
existing session file. If [{name}] is omitted and a session does not
already exist, you will be prompted for a name.

*:PossessionLoad*
:PossessionLoad [{name}]~

Load given session from disk. When `name` is not ommitted, then use last
Load given session from disk. If `name` is omitted then use last
loaded session name.

*:PossessionSaveCwd*
:PossessionSaveCwd

Save session for current working directory. Use `!` to avoid confirmation
dialog.
dialog. The session name will be the current working directory.

*:PossessionLoadCwd*
:PossessionLoadCwd
:PossessionLoadCwd [{name}]

Load session for current working directory.
Load given session for current working directory. If `name` is omitted
then use last session name for the current working directory.

*:PossessionRename*
:PossessionRename [{name} [{new_name}]]~

Rename session `name` to `new_name`. If `new_name` is ommited then it is taken
from |vim.ui.input|. If `name` is ommited then current session name is used.
Rename session `name` to `new_name`. If `new_name` is omitted then it is taken
from |vim.ui.input|. If `name` is omitted then current session name is used.

*:PossessionClose*
:PossessionClose~
Expand All @@ -59,6 +60,14 @@ Show given session info.
List available sessions. `:PossessionList!` will not hide the `vimscript`
field that contains commands generated by |:mksession|.


:PossessionListCwd [{dir}]~

List available sessions with a cwd that matches the specified `dir`.
If `dir` is omitted the current working directory is used.
`:PossessionListCwd!` will not hide the `vimscript` field that contains
commands generated by |:mksession|.

*:PossessionMigrate*
:PossessionMigrate {dir_or_file}~

Expand Down Expand Up @@ -146,10 +155,19 @@ autosave.on_quit~
way the current window layout will be preserved.

*possession-autoload*
autoload.cwd~
`boolean | function(): boolean`
Automatically load a session for current working directory (created by
`autosave.cwd`) on VimEnter if such a session exists.
autoload~
`string | function(): string
Automatically load a session. Valid string values are:
- `last` loads the last saved session
- `auto_cwd` loads the session saved by `autosave.cwd` or `:PossessionSaveCwd`
- `last_cwd` loads the last session for the current working directory

If a function is provided, it needs to return the name of a session file
or a directory. If it is a directory, the last session for that directory
is loaded. The function can also return one of the above strings, in
which case the behaviour is as defined for that string.
If any files or folders are passed as command line arguments to Neovim,
autoload is always skipped.

*possession-hooks*
hooks.before_save~
Expand Down
8 changes: 6 additions & 2 deletions lua/possession.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ local function setup(opts)
local names = config.commands
local commands = require('possession.commands')
local complete = commands.complete_session
local cwd_complete = commands.cwd_complete_session

cmd(names.save, 'name?', { nargs = '?', complete = complete, bang = true }, function(o)
commands.save(o.fargs[1], o.bang)
Expand All @@ -23,8 +24,8 @@ local function setup(opts)
cmd(names.save_cwd, '', { nargs = 0, bang = true }, function(o)
commands.save_cwd(o.bang)
end)
cmd(names.load_cwd, '', { nargs = 0 }, function(o)
commands.load_cwd()
cmd(names.load_cwd, 'name?', { nargs = '?', complete = cwd_complete }, function(o)
commands.load_cwd(o.fargs[1])
end)
cmd(names.rename, 'old_name? new_name?', { nargs = '*', complete = complete }, function(o)
commands.rename(o.fargs[1], o.fargs[2])
Expand All @@ -41,6 +42,9 @@ local function setup(opts)
cmd(names.list, '', { nargs = 0, bang = true }, function(o)
commands.list(o.bang)
end)
cmd(names.list_cwd, 'dir?', { nargs = '?', complete = 'dir', bang = true }, function(o)
commands.list_cwd(o.fargs[1], o.bang)
end)
cmd(names.migrate, 'dir_or_file', { nargs = 1, complete = 'file' }, function(o)
commands.migrate(o.fargs[1])
end)
Expand Down
94 changes: 81 additions & 13 deletions lua/possession/commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ local function complete_list(candidates, opts)
end
end

-- Limits filesystem access by caching the session names per command line access
---@type table<string, string>?
-- Limits filesystem access by caching the session data per command line access
---@type table<string, { name: string, cwd: string }>?
local cached_names
vim.api.nvim_create_autocmd('CmdlineLeave', {
group = vim.api.nvim_create_augroup('possession.commands.complete', { clear = true }),
Expand All @@ -49,13 +49,27 @@ local function get_session_names()
if not cached_names then
cached_names = {}
for file, data in pairs(session.list()) do
cached_names[file] = data.name
cached_names[file] = { name = data.name, cwd = data.cwd }
end
end
return cached_names
end

M.complete_session = complete_list(get_session_names)
M.complete_session = complete_list(function()
return vim.tbl_map(function(s)
return s.name
end, get_session_names())
end)

M.cwd_complete_session = complete_list(function()
local cwd = vim.fn.getcwd()
local cwd_sessions = vim.tbl_filter(function(s)
return s.cwd == cwd
end, get_session_names())
return vim.tbl_map(function(s)
return s.name
end, cwd_sessions)
end)

local function get_current()
local name = session.get_session_name()
Expand All @@ -66,15 +80,17 @@ local function get_current()
return name
end

local function get_last()
local sessions = query.as_list()
---@param dir string dir to get sessions for
local function get_sessions_for_dir(dir)
return query.filter_by(query.as_list(), { cwd = paths.absolute_dir(dir) })
end

---@param sessions? table[] list of sessions from `as_list`
local function get_last(sessions)
sessions = sessions or query.as_list()
query.sort_by(sessions, 'mtime', true)
local last_session = sessions and sessions[1]
if not last_session then
utils.error('Cannot find last loaded session - specify session name as an argument')
return nil
end
return last_session.name
return last_session and last_session.name
end

local function name_or(name, getter)
Expand All @@ -100,6 +116,8 @@ function M.load(name)
name = name_or(name, get_last)
if name then
session.load(name)
else
utils.error('Cannot find last loaded session - specify session name as an argument')
end
end

Expand All @@ -108,8 +126,52 @@ function M.save_cwd(no_confirm)
session.save(paths.cwd_session_name(), { no_confirm = no_confirm })
end

function M.load_cwd()
session.load(paths.cwd_session_name())
---@param name? string
function M.load_cwd(name)
local last = function()
return get_last(get_sessions_for_dir(vim.fn.getcwd()))
end

name = name_or(name, last)
if name then
session.load(name)
else
utils.error('Cannot find last loaded cwd session - specify session name as an argument')
end
end

---@param session_type string
function M.load_last(session_type)
local last
if session_type == 'last' then
last = get_last()
elseif session_type == 'auto_cwd' then
last = paths.cwd_session_name()
elseif session_type == 'last_cwd' then
last = get_last(get_sessions_for_dir(vim.fn.getcwd()))
elseif session_type then
-- Something was returned from custom config function.
if vim.fn.isdirectory(vim.fn.fnamemodify(session_type, ':p')) == 1 then
local abs = paths.absolute_dir(session_type)
last = get_last(get_sessions_for_dir(abs))
else
-- Try to load returned string as literal session name.

-- Futher down the `session.load` call stack will error
-- if `session_type` ends with `.json`. Strip if off, it
-- will get added back when needed.
last = string.gsub(session_type, '.json$', '')
end
else
utils.error('Possession.nvim: Unknown `autoload` config value `' .. session_type .. '`')
return
end

if last then
session.load(last, { skip_autosave = true })
return last
end
utils.info('No session found to autoload')
end

local function maybe_input(value, opts, callback)
Expand Down Expand Up @@ -174,6 +236,12 @@ function M.list(full)
display.echo_sessions { vimscript = full }
end

---@param full? boolean
function M.list_cwd(dir, full)
dir = dir or vim.fn.getcwd()
display.echo_sessions { vimscript = full, sessions = get_sessions_for_dir(dir) }
end

---@param path string
function M.migrate(path)
if vim.fn.getftype(path) == 'file' then
Expand Down
15 changes: 10 additions & 5 deletions lua/possession/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ local function defaults()
on_load = true,
on_quit = true,
},
autoload = {
cwd = false, -- or fun(): boolean
},
autoload = false, -- or 'last' or 'auto_cwd' or 'last_cwd' or fun(): string
commands = {
save = 'PossessionSave',
load = 'PossessionLoad',
Expand All @@ -35,6 +33,7 @@ local function defaults()
delete = 'PossessionDelete',
show = 'PossessionShow',
list = 'PossessionList',
list_cwd = 'PossessionListCwd',
migrate = 'PossessionMigrate',
},
hooks = {
Expand Down Expand Up @@ -151,13 +150,19 @@ local function fix_compatibility(opts)
enable = opts.telescope.previewer,
}
end

local autoload = vim.tbl_get(opts, 'autoload')
if type(autoload) == 'table' then
vim.deprecate('`setup.autoload.cwd = true`', '`autoload = "..."`', 'in the future', 'possession')
opts.autoload = autoload.cwd and 'auto_cwd' or false
end
end

function M.setup(opts)
warn_on_unknown_keys(opts)

fix_compatibility(opts)

warn_on_unknown_keys(opts)

local new_config = vim.tbl_deep_extend('force', {}, defaults(), opts or {})
-- Do _not_ replace the table pointer with `config = ...` because this
-- wouldn't change the tables that have already been `require`d by other
Expand Down
4 changes: 4 additions & 0 deletions lua/possession/display.lua
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ function M.echo_sessions(opts)
}, opts or {})

local sessions = opts.sessions or query.as_list()
if #sessions == 0 then
utils.info('No sessions found')
return
end

local info = {}
if opts.buffers or opts.tab_cwd then
Expand Down
9 changes: 9 additions & 0 deletions lua/possession/paths.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,13 @@ function M.cwd_session_name()
return vim.fn.fnamemodify(global_cwd, ':~')
end

--- Vim expands the given dir, then converts it to an absolute path
function M.absolute_dir(dir)
local p = Path:new(vim.fn.expand(dir)):absolute()
if vim.endswith(p, Path.path.sep) then
p = p:sub(1, #p - 1)
end
return p
end

return M
11 changes: 10 additions & 1 deletion lua/possession/query.lua
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ local config = require('possession.config')
---@param sessions? table<string, table> like from possession.session.list()
---@return table[] list of session data with additional `file` key
function M.as_list(sessions)
sessions = sessions or session.list() --[[@as table<string, table> ]]
sessions = sessions or session.list()
local list = {}
for file, data in pairs(sessions) do
if data.file then
Expand All @@ -21,6 +21,15 @@ function M.as_list(sessions)
return list
end

--- Filters a list of sessions
---@param sessions table[] list of sessions from `as_list`
---@param opts { cwd: string }
function M.filter_by(sessions, opts)
return vim.tbl_filter(function(s)
return s.cwd == opts.cwd
end, sessions)
end

---@alias possession.QuerySortKey 'name'|'atime'|'mtime'|'ctime'

--- Sort a list of sessions in-place
Expand Down
Loading

0 comments on commit 2d51ce2

Please sign in to comment.