1use std::path::Path;
8
9use nvim_oxi::Dictionary;
10use ytil_git::remote::GitProvider;
11use ytil_noxi::visual_selection::Bound;
12use ytil_noxi::visual_selection::Selection;
13
14pub fn dict() -> Dictionary {
16 dict! {
17 "get_link": fn_from!(get_link),
18 }
19}
20
21#[allow(clippy::needless_pass_by_value)]
23fn get_link((link_type, open): (String, Option<bool>)) -> Option<()> {
24 let selection = ytil_noxi::visual_selection::get(())?;
25
26 let repo = ytil_git::repo::discover(Path::new("."))
27 .inspect_err(|err| {
28 ytil_noxi::notify::error(err);
29 })
30 .ok()?;
31
32 let cur_buf = nvim_oxi::api::get_current_buf();
33 let abs_buf_path = ytil_noxi::buffer::get_absolute_path(Some(&cur_buf))
34 .ok_or_else(|| {
35 ytil_noxi::notify::error(format!(
36 "error getting absolute path for current_buffer | current_buffer={cur_buf:?}"
37 ));
38 })
39 .ok()?;
40 let current_buffer_path = ytil_git::repo::get_relative_path_to_repo(&abs_buf_path, &repo)
41 .inspect_err(|err| ytil_noxi::notify::error(err))
42 .ok()?;
43
44 let repo_urls = ytil_git::remote::get_https_urls(&repo)
45 .inspect_err(|err| {
46 ytil_noxi::notify::error(format!("error discovering git repo | error={err:#?}"));
47 })
48 .ok()?;
49
50 let mut repo_url = repo_urls.into_iter().next()?;
52
53 let current_commit_hash = ytil_git::get_current_commit_hash(&repo)
54 .inspect_err(|err| {
55 ytil_noxi::notify::error(format!("error getting current repo commit hash | error={err:#?}"));
56 })
57 .ok()?;
58
59 let git_provider = GitProvider::get(&repo_url)
60 .inspect_err(|err| {
61 ytil_noxi::notify::error(format!(
62 "error getting git provider for url | url={repo_url:#?} error={err:?}"
63 ));
64 })
65 .inspect(|gp| {
66 if gp.is_none() {
67 ytil_noxi::notify::error(format!("error no git provider found for url | url={repo_url:#?}"));
68 }
69 })
70 .ok()??;
71
72 match git_provider {
73 GitProvider::GitHub => build_github_file_url(
74 &mut repo_url,
75 &link_type,
76 ¤t_commit_hash,
77 ¤t_buffer_path,
78 &selection,
79 ),
80 GitProvider::GitLab => build_gitlab_file_url(
81 &mut repo_url,
82 &link_type,
83 ¤t_commit_hash,
84 ¤t_buffer_path,
85 &selection,
86 ),
87 }
88
89 if open.is_some_and(std::convert::identity) {
90 ytil_sys::open(&repo_url)
91 .inspect_err(|err| {
92 ytil_noxi::notify::error(format!("error opening file URL | repo_url={repo_url:?} error={err:#?}"));
93 })
94 .ok()?;
95 } else {
96 ytil_sys::file::cp_to_system_clipboard(&mut repo_url.as_bytes())
97 .inspect_err(|err| {
98 ytil_noxi::notify::error(format!(
99 "error copying content to system clipboard | content={repo_url:?} error={err:#?}"
100 ));
101 })
102 .ok()?;
103 nvim_oxi::print!("URL copied to clipboard:\n{repo_url}");
104 }
105
106 Some(())
107}
108
109fn build_github_file_url(
110 repo_url: &mut String,
111 link_type: &str,
112 commit_hash: &str,
113 current_buffer_path: &Path,
114 selection: &Selection,
115) {
116 fn add_github_lnum_and_col_to_url(repo_url: &mut String, bound: &Bound) {
117 repo_url.push('L');
118 append_lnum(repo_url, bound);
119 repo_url.push('C');
120 repo_url.push_str(&bound.col.to_string());
121 }
122
123 repo_url.push('/');
124 repo_url.push_str(link_type);
125 repo_url.push('/');
126 repo_url.push_str(commit_hash);
127 repo_url.push('/');
128 repo_url.push_str(current_buffer_path.to_string_lossy().trim_start_matches('/'));
129
130 repo_url.push_str("?plain=1");
131 repo_url.push('#');
132 add_github_lnum_and_col_to_url(repo_url, selection.start());
133 repo_url.push('-');
134 add_github_lnum_and_col_to_url(repo_url, selection.end());
135}
136
137fn build_gitlab_file_url(
138 repo_url: &mut String,
139 link_type: &str,
140 commit_hash: &str,
141 current_buffer_path: &Path,
142 selection: &Selection,
143) {
144 repo_url.push('/');
145 repo_url.push('-');
146
147 repo_url.push('/');
148 repo_url.push_str(link_type);
149 repo_url.push('/');
150 repo_url.push_str(commit_hash);
151 repo_url.push('/');
152 repo_url.push_str(current_buffer_path.to_string_lossy().trim_start_matches('/'));
153
154 repo_url.push('#');
155 repo_url.push('L');
156 append_lnum(repo_url, selection.start());
157 repo_url.push('-');
158 append_lnum(repo_url, selection.end());
159}
160
161fn append_lnum(repo_url: &mut String, bound: &Bound) {
162 repo_url.push_str(&bound.lnum.saturating_add(1).to_string());
163}
164
165#[cfg(test)]
166mod tests {
167 use rstest::rstest;
168
169 use super::*;
170
171 #[rstest]
172 #[case::single_line_selection(
173 "https://github.com/user/repo",
174 "blob",
175 "abc123",
176 "/src/main.rs",
177 Bound { lnum: 10, col: 5 },
178 Bound { lnum: 10, col: 10 },
179 "https://github.com/user/repo/blob/abc123/src/main.rs?plain=1#L11C5-L11C10"
180 )]
181 #[case::multi_line_selection(
182 "https://github.com/user/repo",
183 "blob",
184 "def456",
185 "/lib/utils.rs",
186 Bound { lnum: 1, col: 0 },
187 Bound { lnum: 3, col: 20 },
188 "https://github.com/user/repo/blob/def456/lib/utils.rs?plain=1#L2C0-L4C20"
189 )]
190 #[case::root_file(
191 "https://github.com/user/repo",
192 "tree",
193 "ghi789",
194 "/README.md",
195 Bound { lnum: 5, col: 2 },
196 Bound { lnum: 5, col: 2 },
197 "https://github.com/user/repo/tree/ghi789/README.md?plain=1#L6C2-L6C2"
198 )]
199 fn build_github_file_url_works_as_expected(
200 #[case] initial_repo_url: &str,
201 #[case] url_kind: &str,
202 #[case] commit_hash: &str,
203 #[case] file_path: &str,
204 #[case] start: Bound,
205 #[case] end: Bound,
206 #[case] expected: &str,
207 ) {
208 let mut repo_url = initial_repo_url.to_string();
209 let current_buffer_path = Path::new(file_path);
210 let selection = dummy_selection(start, end);
211
212 build_github_file_url(&mut repo_url, url_kind, commit_hash, current_buffer_path, &selection);
213
214 pretty_assertions::assert_eq!(repo_url, expected);
215 }
216
217 #[rstest]
218 #[case::single_line_selection(
219 "https://gitlab.com/user/repo",
220 "blob",
221 "abc123",
222 "/src/main.rs",
223 Bound { lnum: 10, col: 5 },
224 Bound { lnum: 10, col: 10 },
225 "https://gitlab.com/user/repo/-/blob/abc123/src/main.rs#L11-11"
226 )]
227 #[case::multi_line_selection(
228 "https://gitlab.com/user/repo",
229 "blob",
230 "def456",
231 "/lib/utils.rs",
232 Bound { lnum: 1, col: 0 },
233 Bound { lnum: 3, col: 20 },
234 "https://gitlab.com/user/repo/-/blob/def456/lib/utils.rs#L2-4"
235 )]
236 #[case::root_file(
237 "https://gitlab.com/user/repo",
238 "tree",
239 "ghi789",
240 "/README.md",
241 Bound { lnum: 5, col: 2 },
242 Bound { lnum: 5, col: 2 },
243 "https://gitlab.com/user/repo/-/tree/ghi789/README.md#L6-6"
244 )]
245 fn build_gitlab_file_url_works_as_expected(
246 #[case] initial_repo_url: &str,
247 #[case] url_kind: &str,
248 #[case] commit_hash: &str,
249 #[case] file_path: &str,
250 #[case] start: Bound,
251 #[case] end: Bound,
252 #[case] expected: &str,
253 ) {
254 let mut repo_url = initial_repo_url.to_string();
255 let current_buffer_path = Path::new(file_path);
256 let selection = dummy_selection(start, end);
257
258 build_gitlab_file_url(&mut repo_url, url_kind, commit_hash, current_buffer_path, &selection);
259
260 pretty_assertions::assert_eq!(repo_url, expected);
261 }
262
263 fn dummy_selection(start: Bound, end: Bound) -> Selection {
264 use ytil_noxi::visual_selection::SelectionBounds;
265 let bounds = SelectionBounds { buf_id: 1, start, end };
266 Selection::new(bounds, std::iter::empty::<nvim_oxi::String>())
267 }
268}