//! Defines of an iris [plugin][Plugin]. use crate::{error::error, path, Repository, Result}; use log::{debug, trace, warn}; use std::{borrow::Cow, cmp::Ordering, collections::HashSet, path::PathBuf}; use url::Url; /// Returns the url to the fallback git server. If a repository url is provided /// and it does not contain a git server (just author and repo), this url will /// be used in its absence. fn fallback_git_server() -> &'static Url { use std::sync::OnceLock; static FALLBACK: OnceLock = OnceLock::new(); FALLBACK.get_or_init(|| { Url::parse("https://github.com") .expect("!! IRIS BUG !! invalid fallback git server url") }) } /// # Plugin /// /// A structure representing an iris plugin specification. /// /// An iris plugin has two parts. /// /// - [id], must be a unique identifier for this plugin and the plugins /// module name /// - [args], a set of arguments to configure this plugin /// /// [id]: Self::id /// [args]: Self::args #[derive(Debug, Eq)] pub struct Plugin { /// The plugins unique identifier. Used for the vim module name and /// repository checkout foler. pub id: String, /// The commit oid (if set) that the local repository will be forced to /// track. If not set then the repository will track the latest commit in /// the current tracking branch. pub commit: Option, /// The branch name (if set) that the local repository will be forced to /// track. If not set then the repository will track the repositories /// default branch. pub branch: Option, /// Set of plugin ids this plugin should load before pub run: Option, /// Handle to the local plugin repository. pub before: HashSet, /// Set of plugin ids this plugin should load after pub after: HashSet, /// The command to run when vim starts up repo: Repository, } impl Plugin { /// Create a new iris plugin. /// /// Creates a plugin from the git repo at `url` with the unique id `id`. /// The plugin will be unlocked meaning it will always track the latest /// commit until locked. A handle to a local repository will be created but /// not loaded until called upon. pub fn new(id: String, url: &str) -> Result { // resolve and parse url let url = { // try url as is Url::parse(url) // try url with fallback .or_else(|_| fallback_git_server().join(url)) // invalid git url! .map_err(|_| error!("invalid git url {url}")) }?; // get the path to the local repository checkout let repo_path = path::plugin_dir().join(&id); // create repository handle let repo = Repository::new(url, repo_path); debug!("found plugin '{id}'"); Ok(Self { id, commit: None, branch: None, run: None, before: HashSet::new(), after: HashSet::new(), repo, }) } /// The url to the remote git repository. pub const fn url(&self) -> &Url { &self.repo.url } /// The path to the local repository pub const fn repo_path(&self) -> &PathBuf { &self.repo.path } /// The commit oid that the plugin repository is currently tracking fn commit(&self) -> Result> { // returned locked commit if set if let Some(commit) = &self.commit { return Ok(Cow::Borrowed(commit)); } // get commit from repo let branch = self.branch()?; let commit = self.repo.latest_commit(&branch)?; Ok(Cow::Owned(commit)) } /// The branch name that the plugin repository is currently tracking fn branch(&self) -> Result> { // return locked branch if set if let Some(branch) = &self.branch { return Ok(Cow::Borrowed(branch)); } // get branch from repo let branch = self.repo.default_branch()?; Ok(Cow::Owned(branch)) } /// Fetches latest changes in the local repository but does not update /// checkout. pub fn fetch(&self) -> Result<()> { trace!("fetching {}", self.id); let branch = self.branch()?; self.repo.fetch(&branch)?; Ok(()) } /// Checkout plugin repository using their current refs pub fn switch(&self) -> Result<()> { trace!("switching {}", self.id); let commit = self.commit()?; self.repo.checkout(&commit)?; Ok(()) } /// Update plugin ref to point to the latest commit pub fn lock(&mut self) -> Result<()> { trace!("locking {}", self.id); let branch = self.branch()?; self.repo.fetch(&branch)?; let commit = self.repo.latest_commit(&branch)?; self.commit = Some(commit); Ok(()) } /// Runtime path /// /// This this the plugins root directory, and is used by vim/neovim to /// find the vim/lua runtime scripts. /// /// vim: set runtimepath+= pub fn runtime_path(&self) -> &PathBuf { self.repo_path() } /// Lua package path /// /// This is the path to the lua module if the plugin has one. If so /// it needs to be added to the lua package path. The path is located /// in the repos lua directory at /lua//init.lua or /lua/.lua. If /// both exist the former takes priority. /// /// lua: package.path = package.path .. ";" pub fn lua_package_path(&self) -> Option { let mut lua = self.repo_path().to_owned(); lua.push("lua"); lua.push(&self.id); // /lua//init.lua let module = lua.join("init.lua"); if module.exists() { return Some(module.with_file_name("?.lua")); } // /lua//.lua let module = lua.with_extension("lua"); if module.exists() { return Some(module.with_file_name("?.lua")); } // no lua package path found None } /// Vim source file /// /// Path to a vim file in the /plugin directory that needs to be /// sourced. This is located at /plugin/.vim /// /// vim: source pub fn vim_source_file(&self) -> Option { let mut path = self.repo_path().to_owned(); path.push("plugin"); path.push(&self.id); path.set_extension("vim"); Some(path).filter(|path| path.exists()) } /// Lua source file /// /// Path to a lua file in the /plugin directory that needs to be /// sourced. This is located at /plugin/.lua /// /// lua: dofile('') pub fn lua_source_file(&self) -> Option { let mut path = self.repo_path().to_owned(); path.push("plugin"); path.push(&self.id); path.set_extension("lua"); Some(path).filter(|path| path.exists()) } /// Vim source after file /// /// Path to a vim file in the /after/plugin directory that needs to be /// sourced last. This is located at /after/plugin/.vim /// /// vim: source pub fn vim_source_after_file(&self) -> Option { let mut path = self.repo_path().to_owned(); path.push("after/plugin"); path.push(&self.id); path.set_extension("vim"); Some(path).filter(|path| path.exists()) } /// Lua source after file /// /// Path to a lua file in the /after/plugin directory that needs to be /// sourced last. This is located at /after/plugin/.lua /// /// lua: dofile('') pub fn lua_source_after_file(&self) -> Option { let mut path = self.repo_path().to_owned(); path.push("after/plugin"); path.push(&self.id); path.set_extension("lua"); Some(path).filter(|path| path.exists()) } } impl PartialEq for Plugin { fn eq(&self, other: &Self) -> bool { self.id.eq(&other.id) } } impl PartialOrd for Plugin { fn partial_cmp(&self, other: &Self) -> Option { let is_after = other.before.contains(&self.id) || self.after.contains(&other.id); let is_before = other.after.contains(&self.id) || self.before.contains(&other.id); match (is_before, is_after) { // equal, no ordering (false, false) => Some(Ordering::Equal), // before (true, false) => Some(Ordering::Less), // after (false, true) => Some(Ordering::Greater), // cycle detected (true, true) => { warn!( "plugin dependency cycle detected between '{}' and '{}'", &self.id, &other.id ); None } } } } impl Ord for Plugin { fn cmp(&self, other: &Self) -> Ordering { self.partial_cmp(other).unwrap_or(Ordering::Equal) } }