summaryrefslogtreecommitdiff
path: root/src/model/plugin.rs
diff options
context:
space:
mode:
authorFreya Murphy <freya@freyacat.org>2025-01-16 14:45:14 -0500
committerFreya Murphy <freya@freyacat.org>2025-01-16 14:45:14 -0500
commit60985236c7070425c28aa0c5ce675306d06ab123 (patch)
tree7448aef5dadf19d4504151ebc370f9e20757ef10 /src/model/plugin.rs
downloadiris-60985236c7070425c28aa0c5ce675306d06ab123.tar.gz
iris-60985236c7070425c28aa0c5ce675306d06ab123.tar.bz2
iris-60985236c7070425c28aa0c5ce675306d06ab123.zip
initialHEADmain
Diffstat (limited to 'src/model/plugin.rs')
-rw-r--r--src/model/plugin.rs283
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)
+ }
+}