1use std::fmt::Write;
8use std::path::Path;
9
10use nvim_oxi::Dictionary;
11use ytil_git::remote::GitProvider;
12use ytil_noxi::visual_selection::Bound;
13use ytil_noxi::visual_selection::Selection;
14
15pub fn dict() -> Dictionary {
17 dict! {
18 "get_link": fn_from!(get_link),
19 }
20}
21
22fn 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 let _ = write!(repo_url, "{}", bound.col);
122 }
123
124 repo_url.push('/');
125 repo_url.push_str(link_type);
126 repo_url.push('/');
127 repo_url.push_str(commit_hash);
128 repo_url.push('/');
129 repo_url.push_str(current_buffer_path.to_string_lossy().trim_start_matches('/'));
130
131 repo_url.push_str("?plain=1");
132 repo_url.push('#');
133 add_github_lnum_and_col_to_url(repo_url, selection.start());
134 repo_url.push('-');
135 add_github_lnum_and_col_to_url(repo_url, selection.end());
136}
137
138fn build_gitlab_file_url(
139 repo_url: &mut String,
140 link_type: &str,
141 commit_hash: &str,
142 current_buffer_path: &Path,
143 selection: &Selection,
144) {
145 repo_url.push('/');
146 repo_url.push('-');
147
148 repo_url.push('/');
149 repo_url.push_str(link_type);
150 repo_url.push('/');
151 repo_url.push_str(commit_hash);
152 repo_url.push('/');
153 repo_url.push_str(current_buffer_path.to_string_lossy().trim_start_matches('/'));
154
155 repo_url.push('#');
156 repo_url.push('L');
157 append_lnum(repo_url, selection.start());
158 repo_url.push('-');
159 append_lnum(repo_url, selection.end());
160}
161
162fn append_lnum(repo_url: &mut String, bound: &Bound) {
163 let _ = write!(repo_url, "{}", bound.lnum.saturating_add(1));
165}
166
167#[cfg(test)]
168mod tests {
169 use rstest::rstest;
170
171 use super::*;
172
173 #[rstest]
174 #[case::single_line_selection(
175 "https://github.com/user/repo",
176 "blob",
177 "abc123",
178 "/src/main.rs",
179 Bound { lnum: 10, col: 5 },
180 Bound { lnum: 10, col: 10 },
181 "https://github.com/user/repo/blob/abc123/src/main.rs?plain=1#L11C5-L11C10"
182 )]
183 #[case::multi_line_selection(
184 "https://github.com/user/repo",
185 "blob",
186 "def456",
187 "/lib/utils.rs",
188 Bound { lnum: 1, col: 0 },
189 Bound { lnum: 3, col: 20 },
190 "https://github.com/user/repo/blob/def456/lib/utils.rs?plain=1#L2C0-L4C20"
191 )]
192 #[case::root_file(
193 "https://github.com/user/repo",
194 "tree",
195 "ghi789",
196 "/README.md",
197 Bound { lnum: 5, col: 2 },
198 Bound { lnum: 5, col: 2 },
199 "https://github.com/user/repo/tree/ghi789/README.md?plain=1#L6C2-L6C2"
200 )]
201 fn build_github_file_url_works_as_expected(
202 #[case] initial_repo_url: &str,
203 #[case] url_kind: &str,
204 #[case] commit_hash: &str,
205 #[case] file_path: &str,
206 #[case] start: Bound,
207 #[case] end: Bound,
208 #[case] expected: &str,
209 ) {
210 let mut repo_url = initial_repo_url.to_string();
211 let current_buffer_path = Path::new(file_path);
212 let selection = dummy_selection(start, end);
213
214 build_github_file_url(&mut repo_url, url_kind, commit_hash, current_buffer_path, &selection);
215
216 pretty_assertions::assert_eq!(repo_url, expected);
217 }
218
219 #[rstest]
220 #[case::single_line_selection(
221 "https://gitlab.com/user/repo",
222 "blob",
223 "abc123",
224 "/src/main.rs",
225 Bound { lnum: 10, col: 5 },
226 Bound { lnum: 10, col: 10 },
227 "https://gitlab.com/user/repo/-/blob/abc123/src/main.rs#L11-11"
228 )]
229 #[case::multi_line_selection(
230 "https://gitlab.com/user/repo",
231 "blob",
232 "def456",
233 "/lib/utils.rs",
234 Bound { lnum: 1, col: 0 },
235 Bound { lnum: 3, col: 20 },
236 "https://gitlab.com/user/repo/-/blob/def456/lib/utils.rs#L2-4"
237 )]
238 #[case::root_file(
239 "https://gitlab.com/user/repo",
240 "tree",
241 "ghi789",
242 "/README.md",
243 Bound { lnum: 5, col: 2 },
244 Bound { lnum: 5, col: 2 },
245 "https://gitlab.com/user/repo/-/tree/ghi789/README.md#L6-6"
246 )]
247 fn build_gitlab_file_url_works_as_expected(
248 #[case] initial_repo_url: &str,
249 #[case] url_kind: &str,
250 #[case] commit_hash: &str,
251 #[case] file_path: &str,
252 #[case] start: Bound,
253 #[case] end: Bound,
254 #[case] expected: &str,
255 ) {
256 let mut repo_url = initial_repo_url.to_string();
257 let current_buffer_path = Path::new(file_path);
258 let selection = dummy_selection(start, end);
259
260 build_gitlab_file_url(&mut repo_url, url_kind, commit_hash, current_buffer_path, &selection);
261
262 pretty_assertions::assert_eq!(repo_url, expected);
263 }
264
265 fn dummy_selection(start: Bound, end: Bound) -> Selection {
266 use ytil_noxi::visual_selection::SelectionBounds;
267 let bounds = SelectionBounds { buf_id: 1, start, end };
268 Selection::new(bounds, std::iter::empty::<nvim_oxi::String>())
269 }
270}