1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
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)
}
}
|