// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2020 The Gleam contributors use crate::{ ast::SrcSpan, build, config::{PackageConfig, Repository}, line_numbers::LineNumbers, paths::ProjectPaths, }; use camino::{Utf8Component, Utf8Path, Utf8PathBuf}; pub struct SourceLinker { line_numbers: LineNumbers, url_pattern: Option<(String, String)>, } impl SourceLinker { pub fn new( paths: &ProjectPaths, project_config: &PackageConfig, module: &build::Module, ) -> Self { let path = paths .src_directory() .join(module.name.as_str()) .strip_prefix(paths.root()) .expect("path is not in root") .with_extension("gleam"); let path_in_repo = match project_config .repository .as_ref() .map(|r| r.path()) .unwrap_or_default() { Some(repo_path) => to_url_path(&Utf8PathBuf::from(repo_path).join(path)), _ => to_url_path(&path), } .unwrap_or_default(); let tag = project_config.tag_for_version(&project_config.version); let url_pattern = project_config .repository .as_ref() .map(|r| match r { Repository::GitHub { user, repo, .. } => Some(( format!("https://github.com/{user}/{repo}/blob/{tag}/{path_in_repo}#L"), "-L".into(), )), Repository::GitLab { user, repo, .. } => Some(( format!("https://gitlab.com/{user}/{repo}/-/blob/{tag}/{path_in_repo}#L"), "-".into(), )), Repository::BitBucket { user, repo, .. } => Some(( format!("https://bitbucket.com/{user}/{repo}/src/{tag}/{path_in_repo}#lines-"), ":".into(), )), Repository::Codeberg { user, repo, .. } => Some(( format!("https://codeberg.org/{user}/{repo}/src/tag/{tag}/{path_in_repo}#L"), "-".into(), )), Repository::SourceHut { user, repo, .. } => Some(( format!("https://git.sr.ht/~{user}/{repo}/tree/{tag}/item/{path_in_repo}#L"), "-".into(), )), Repository::Tangled { user, repo, .. } => Some(( format!("https://tangled.sh/{user}/{repo}/tree/{tag}/{path_in_repo}#L"), "-".into(), )), Repository::Gitea { user, repo, host, .. } | Repository::Forgejo { user, repo, host, .. } => { let string_host = host.to_string(); let cleaned_host = string_host.trim_end_matches('/'); Some(( format!("{cleaned_host}/{user}/{repo}/src/tag/{tag}/{path_in_repo}#L",), "-L".into(), )) } Repository::Custom { .. } => None, }) .unwrap_or_default(); SourceLinker { line_numbers: LineNumbers::new(&module.code), url_pattern, } } pub fn url(&self, span: SrcSpan) -> String { match &self.url_pattern { Some((base, line_sep)) => { let start_line = self.line_numbers.line_number(span.start); let end_line = self.line_numbers.line_number(span.end); if start_line == end_line { format!("{base}{start_line}") } else { format!("{base}{start_line}{line_sep}{end_line}") } } None => "".into(), } } } fn to_url_path(path: &Utf8Path) -> Option { let mut buf = String::new(); for c in path.components() { if let Utf8Component::Normal(s) = c { buf.push_str(s); } buf.push('/'); } let _ = buf.pop(); Some(buf) }