diff options
author | Freya Murphy <freya@freyacat.org> | 2025-01-16 14:45:14 -0500 |
---|---|---|
committer | Freya Murphy <freya@freyacat.org> | 2025-01-16 14:45:14 -0500 |
commit | 60985236c7070425c28aa0c5ce675306d06ab123 (patch) | |
tree | 7448aef5dadf19d4504151ebc370f9e20757ef10 /src/model/plugin.rs | |
download | iris-main.tar.gz iris-main.tar.bz2 iris-main.zip |
Diffstat (limited to '')
-rw-r--r-- | src/model/plugin.rs | 283 |
1 files changed, 283 insertions, 0 deletions
diff --git a/src/model/plugin.rs b/src/model/plugin.rs new file mode 100644 index 0000000..bb097c6 --- /dev/null +++ b/src/model/plugin.rs @@ -0,0 +1,283 @@ +//! 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<Url> = 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<String>, + /// 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<String>, + /// Set of plugin ids this plugin should load before + pub run: Option<String>, + /// Handle to the local plugin repository. + pub before: HashSet<String>, + /// Set of plugin ids this plugin should load after + pub after: HashSet<String>, + /// 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<Self> { + // 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<Cow<'_, str>> { + // 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<Cow<'_, str>> { + // 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+=<path> + 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/<id>/init.lua or /lua/<id>.lua. If + /// both exist the former takes priority. + /// + /// lua: package.path = package.path .. ";<path>" + pub fn lua_package_path(&self) -> Option<PathBuf> { + let mut lua = self.repo_path().to_owned(); + lua.push("lua"); + lua.push(&self.id); + // /lua/<id>/init.lua + let module = lua.join("init.lua"); + if module.exists() { + return Some(module.with_file_name("?.lua")); + } + // /lua/<id>/.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/<id>.vim + /// + /// vim: source <path> + pub fn vim_source_file(&self) -> Option<PathBuf> { + 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/<id>.lua + /// + /// lua: dofile('<path>') + pub fn lua_source_file(&self) -> Option<PathBuf> { + 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/<id>.vim + /// + /// vim: source <path> + pub fn vim_source_after_file(&self) -> Option<PathBuf> { + 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/<id>.lua + /// + /// lua: dofile('<path>') + pub fn lua_source_after_file(&self) -> Option<PathBuf> { + 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<std::cmp::Ordering> { + 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) + } +} |