diff --git a/README.md b/README.md index 4fd3382..322f2cb 100644 --- a/README.md +++ b/README.md @@ -114,7 +114,7 @@ use({ local switcher = require("projections.switcher") vim.api.nvim_create_autocmd({ "VimEnter" }, { callback = function() - if vim.fn.argc() == 0 then switcher.switch(vim.loop.cwd()) end + if vim.fn.argc() == 0 then switcher:switch(vim.loop.cwd()) end end, }) end @@ -140,13 +140,14 @@ If you are using the recommended configuration, make sure to remove the -- If in some project's root, attempt to restore that project's session -- If not, restore last session -- If no sessions, do nothing +local Switcher = require("projections.switcher") local Session = require("projections.session") vim.api.nvim_create_autocmd({ "VimEnter" }, { callback = function() if vim.fn.argc() ~= 0 then return end local session_info = Session.info(vim.loop.cwd()) if session_info == nil then - Session.restore_latest() + Switcher.last() else Session.restore(vim.loop.cwd()) end diff --git a/lua/projections/project.lua b/lua/projections/project.lua index d025902..5215389 100644 --- a/lua/projections/project.lua +++ b/lua/projections/project.lua @@ -20,6 +20,7 @@ end ---@return Path ---@nodiscard function Project:path() + if not self.workspace or not self.name then return Path.new("") end return self.workspace.path .. self.name end diff --git a/lua/projections/session.lua b/lua/projections/session.lua index 171495f..5704e7e 100644 --- a/lua/projections/session.lua +++ b/lua/projections/session.lua @@ -19,14 +19,7 @@ function Session.info(spath) local path = Path.new(spath) local project_name = path:basename() local workspace_path = path:parent() - local all_workspaces = Workspace.get_workspaces() - local workspace = nil - for _, ws in ipairs(all_workspaces) do - if workspace_path == ws.path then - workspace = ws - break - end - end + local workspace = Workspace.find(workspace_path) if workspace == nil or not workspace:is_project(project_name) then return nil end local filename = Session.session_filename(tostring(workspace_path), project_name) @@ -58,7 +51,11 @@ end function Session.store(spath) Session._ensure_sessions_directory() local session_info = Session.info(spath) - if session_info == nil then return false end + local Switcher = require("projections.switcher") + -- Don't store session if no session info or currently not in a project + if session_info == nil or not Switcher:in_project() then + return false + end return Session.store_to_session_file(tostring(session_info.path)) end @@ -112,12 +109,4 @@ function Session.latest() return latest_session end --- Restore latest session ----@return boolean -function Session.restore_latest() - local latest_session = Session.latest() - if latest_session == nil then return false end - return Session.restore_from_session_file(tostring(latest_session)) -end - return Session diff --git a/lua/projections/switcher.lua b/lua/projections/switcher.lua index 0c681e3..4b35aef 100644 --- a/lua/projections/switcher.lua +++ b/lua/projections/switcher.lua @@ -1,28 +1,109 @@ local Session = require("projections.session") +local Project = require("projections.project") local utils = require("projections.utils") local M = {} +M._current = Project; + +-- Attempts to return the current active project +----@return ProjectInfo +function M:get_current() + return self._current; +end + +-- Returns whether or not we are currently in a project +function M:in_project() + return (#tostring(self._current:path()) > 0) +end + +-- Attempts to set the current active project, with no args passed, unsets current project +-- @param project_info ProjectInfo table of information about the project to set as the current one +----@return boolean +function M:set_current(project_info) + if project_info == nil then + self._current = Project + else + self._current = project_info + end + return true +end + +-- Attempts to switch to the last loaded project +---@return boolean +function M:last() + local latest_session = Session.latest() + if latest_session ~= nil then + local project_dir = utils.project_dir_from_session_file(tostring(latest_session)) + -- "expand" for OS compatiblity (Windows) + project_dir = vim.fn.expand(project_dir) + return self:switch(project_dir) + end + return false +end + + -- Attempts to switch projects and load the session file. ----@param spath string Path to project root +---@param new_project Project table describing project to switch to ---@return boolean -M.switch = function(spath) - if utils._unsaved_buffers_present() then - vim.notify("projections: Unsaved buffers. Unable to switch projects", vim.log.levels.WARN) - return false - end +function M:switch_project(new_project) + local new_path = vim.fn.expand(tostring(new_project:path())) + local current_path = vim.fn.expand(tostring(self._current and self._current:path() or Path.new(""))) + if #new_path == 0 or new_path == current_path then return false end + + if utils._unsaved_buffers_present() then + vim.notify("projections: Unsaved buffers. Unable to switch projects", vim.log.levels.WARN) + return false + end - local session_info = Session.info(spath) - if session_info == nil then return false end + -- Store current session before moving on to the new project + if new_path ~= vim.loop.cwd() then + Session.store(vim.loop.cwd()) + end - if vim.loop.cwd() ~= spath then Session.store(vim.loop.cwd()) end - vim.cmd("noautocmd cd " .. spath) + -- Close any existing buffers + if utils._num_valid_buffers() > 0 then vim.cmd [[ - silent! %bdelete - clearjumps - ]] - Session.restore(spath) - return true + silent! %bdelete + clearjumps + ]] + end + + -- If there's a session for the project we're switching to, attempt to restore it + local session_info = Session.info(new_path) + if session_info ~= nil then + if Session.restore(new_path) then + vim.schedule(function() vim.notify("[projections.nvim]: Restored session for project - " .. new_project.name, vim.log.levels.INFO) end) + else + -- Something went wrong restoring session + return false + end + else + -- Otherwise, project with no existing session + -- Update current dir to new project dir + vim.cmd("noautocmd cd " .. new_path) + end + + -- Formally set the current project + self._current = new_project + + return true end +-- Attempts to switch projects using only a path to the desired project dir +---@param spath string Path to project root +---@return boolean +function M:switch(spath) + local Workspace = require("projections.workspace") + -- check if path is some project's root + local path = Path.new(spath) + local project_name = path:basename() + local workspace_path = path:parent() + local workspace = Workspace.find(workspace_path) + if workspace == nil or not workspace:is_project(project_name) then return nil end + + return self:switch_project(Project.new(project_name, workspace)) +end + + return M diff --git a/lua/projections/utils.lua b/lua/projections/utils.lua index 6cecb59..09235eb 100644 --- a/lua/projections/utils.lua +++ b/lua/projections/utils.lua @@ -1,5 +1,29 @@ local M = {} +-- Splits a string at all instances of provided 'separator' +---@param input_string string String to separate +---@param sep string Separator to split on +---@return Table +local split = function(input_string, sep) + local fields = {} + local pattern = string.format("([^%s]+)", sep) + local _ = string.gsub(input_string, pattern, function(c) + fields[#fields + 1] = c + end) + + return fields +end + +-- Gets a project directory from a session file +---@param filepath string Filepath for session file +---@return string +M.project_dir_from_session_file = function(filepath) + local session = vim.fn.readfile(filepath) + -- Directory for session is found on line 6. It is preceded by "cd ", so we take a substring + local project_dir = string.sub(session[6], 4, -1) + return project_dir +end + -- Checks if unsaved buffers are present ---@return boolean ---@nodiscard @@ -12,6 +36,16 @@ M._unsaved_buffers_present = function() return false end +-- Gets number of valid buffers currently open +---@return integer +M._num_valid_buffers = function() + local get_ls = vim.tbl_filter(function(buf) + return vim.api.nvim_buf_is_valid(buf) + and vim.api.nvim_buf_get_option(buf, 'buflisted') + end, vim.api.nvim_list_bufs()) + return #get_ls +end + -- Calculate fnv1a hash ---@param s string String to hash ---@return integer diff --git a/lua/projections/workspace.lua b/lua/projections/workspace.lua index db8ae7f..b35c6bf 100644 --- a/lua/projections/workspace.lua +++ b/lua/projections/workspace.lua @@ -153,6 +153,22 @@ function Workspace.get_workspaces() return utils._unique_workspaces(workspaces) end +-- Returns the workspace for a given workspace path +---@param workspace_path Path Workspace path of workspace to search for +---@return Workspace[] +---@nodiscard +function Workspace.find(workspace_path) + local all_workspaces = Workspace.get_workspaces() + local workspace = nil + for _, ws in ipairs(all_workspaces) do + if workspace_path == ws.path then + workspace = ws + break + end + end + return workspace +end + -- Add workspace to workspaces file ---@param spath string String representation of path. Can be unnormalized ---@param patterns Patterns The patterns for workspace diff --git a/lua/telescope/_extensions/projections.lua b/lua/telescope/_extensions/projections.lua index f491c7b..e237aa5 100644 --- a/lua/telescope/_extensions/projections.lua +++ b/lua/telescope/_extensions/projections.lua @@ -65,7 +65,7 @@ local find_projects = function(opts) if opts.action == nil then opts.action = function(selected) if selected ~= nil and selected.value ~= vim.loop.cwd() then - switcher.switch(selected.value) + switcher:switch(selected.value) end end end