use super::*;
pub(super) fn file_ref(text: &str, col: usize) -> Option<(String, u32)> {
let token = token_at(text, col, |c| c.is_alphanumeric() || "_./-:#".contains(c))?;
let token = token.trim_end_matches(['.', ',', ':', '-']);
let (path, rest) = token.split_once("#L").or_else(|| token.split_once(':'))?;
let line: u32 = rest.split(|c: char| !c.is_ascii_digit()).next()?.parse().ok()?;
let file = path.rsplit('/').next().is_some_and(|f| f.contains('.') && !f.starts_with('.') || path.contains('/'));
(file && line > 0 && !path.chars().all(|c| c.is_ascii_digit())).then(|| (path.to_owned(), line))
}
fn token_at(text: &str, col: usize, ok: impl Fn(char) -> bool) -> Option<String> {
let chars: Vec<char> = text.chars().collect();
if !chars.get(col).is_some_and(|&c| ok(c)) {
return None;
}
let a = (0..col).rev().take_while(|&i| ok(chars[i])).last().unwrap_or(col);
let b = (col..chars.len()).take_while(|&i| ok(chars[i])).last()? + 1;
Some(chars[a..b].iter().collect())
}
pub(super) const DEF_WORDS: &str = "fn|struct|enum|trait|type|union|mod|macro_rules!|const|static|def|class|function|func|interface";
pub(super) fn ident_at(text: &str, col: usize, in_code: bool) -> Option<String> {
let w = token_at(text, col, |c| c.is_alphanumeric() || c == '_')?;
let code = in_code || w.contains('_') || w.chars().skip(1).any(char::is_uppercase) || w.chars().any(|c| c.is_ascii_digit());
(code && !w.starts_with(|c: char| c.is_ascii_digit())).then_some(w)
}
impl App {
pub(super) fn link_at_cursor(&self) -> Option<&str> {
let col = self.head.col;
self.rows[self.head.row].links.iter().find(|(a, b, _)| *a <= col && col < *b).map(|l| l.2.as_str())
}
pub(super) fn open_link(&mut self) {
let row = &self.rows[self.head.row];
if let Some((t, c)) = row.goes {
return self.goto_comment(t, c);
}
let has_link = row.thread.is_some_and(|t| self.threads[t].claude.is_none())
|| row.file == INFO && row.info.and_then(|i| self.info.get(i.checked_sub(1)?)).is_some_and(|h| h.heading && !h.url.is_empty());
let comment = self.cursor_ref().filter(|u| has_link && u.starts_with("https://"));
let on_author = row.line.kind == Kind::Thread;
let url = self.link_at_cursor().map(str::to_owned).or_else(|| comment.clone().filter(|_| on_author));
let Some(url) = url else {
if let Some((path, line)) = file_ref(&self.rows[self.head.row].line.text, self.word_col()) {
return self.goto_file_line(&path, line);
}
return match (self.name_at_cursor(), comment) {
(Some(name), _) => self.goto_definition(&name),
(None, Some(u)) => self.open_url(&u),
(None, None) if self.rows[self.head.row].file < self.files.len() => self.action = self.open_target().map(Action::Open),
(None, None) => self.msg = "no link, file:line or name under the cursor".into(),
};
};
self.open_url(&url);
}
pub(super) fn open_url(&mut self, url: &str) {
self.msg = open_url(url);
}
pub(super) fn goto_file_line(&mut self, path: &str, line: u32) {
let path = path.trim_start_matches("./");
let ends = |p: &str| p == path || p.ends_with(&format!("/{path}"));
let found = match self.root.join(path).is_file() {
true => Some(path.to_owned()),
false => self.files.iter().map(|f| f.path.clone()).find(|p| ends(p)).or_else(|| {
let all = diff::git(&["ls-files"]).unwrap_or_default();
let mut hits = all.lines().filter(|p| ends(p));
match (hits.next(), hits.next()) {
(Some(p), None) => Some(p.to_owned()),
_ => None,
}
}),
};
let Some(rel) = found else { return self.msg = format!("no file {path} here") };
if self.collapsed.remove(&rel) {
let keep = self.cursor_key();
self.rebuild(keep);
}
self.jump_loc(&crate::lsp::Loc { path: self.root.join(&rel), line: line.saturating_sub(1), character: 0 });
}
fn word_col(&self) -> usize {
let on_space = self.rows[self.head.row].line.text.chars().nth(self.head.col).is_none_or(char::is_whitespace);
if on_space && self.anchor.row == self.head.row { self.anchor.col } else { self.head.col }
}
pub(super) fn name_at_cursor(&self) -> Option<String> {
let (text, col) = (&self.rows[self.head.row].line.text, self.word_col());
let raw = self.ui.md.raw.fg;
let in_code = raw.is_some() && self.rows[self.head.row].syn.get(col).is_some_and(|s| s.and_then(|s| s.fg) == raw);
ident_at(text, col, in_code)
}
pub(super) fn goto_definition(&mut self, name: &str) {
let pat = format!("(^|[^[:alnum:]_])({DEF_WORDS})[[:space:]]+{name}([^[:alnum:]_]|$)");
let root = self.root.display().to_string();
let out = diff::git(&["-C", &root, "grep", "-n", "-z", "-I", "--untracked", "-E", &pat]).unwrap_or_default();
let mut locs: Vec<crate::lsp::Loc> = out
.lines()
.filter_map(|l| {
let mut parts = l.splitn(3, '\0');
let (path, line, text) = (parts.next()?, parts.next()?.parse::<u32>().ok()?, parts.next()?);
let character = text.find(name).map_or(0, |i| text[..i].encode_utf16().count()) as u32;
Some(crate::lsp::Loc { path: self.root.join(path), line: line - 1, character })
})
.collect();
let in_diff: HashSet<PathBuf> = self.files.iter().map(|f| self.root.join(&f.path)).collect();
locs.sort_by_key(|l| !in_diff.contains(&l.path));
locs.truncate(50);
if locs.is_empty() {
return self.msg = format!("no definition of {name} found");
}
self.show_locations(locs);
}
}
#[test]
fn idents() {
let at = |text: &str, needle: &str| ident_at(text, text.find(needle).unwrap(), false);
let t = "half of next_returns_none_after_error, so inline parseArgs or Foo::bar.";
assert_eq!(at(t, "none").as_deref(), Some("next_returns_none_after_error")); assert_eq!(at(t, "parse").as_deref(), Some("parseArgs"));
assert_eq!(at(t, "half"), None); assert_eq!(at(t, "Foo"), None); assert_eq!(at(t, " so"), None);
assert_eq!(ident_at(t, t.find("bar").unwrap(), true).as_deref(), Some("bar")); }
#[test]
fn file_refs() {
let at = |text: &str, needle: &str| file_ref(text, text.find(needle).unwrap());
let t = "see src/view/mod.rs:1213 and a.rs:12-15, also b.rs#L7.";
assert_eq!(at(t, "mod.rs"), Some(("src/view/mod.rs".into(), 1213)));
assert_eq!(at(t, "1213"), Some(("src/view/mod.rs".into(), 1213))); assert_eq!(at(t, "a.rs"), Some(("a.rs".into(), 12)));
assert_eq!(at(t, "b.rs"), Some(("b.rs".into(), 7)));
assert_eq!(at(t, "see"), None);
assert_eq!(at("at 12:30 today", "12"), None); assert_eq!(at("note: this", "note"), None);
assert_eq!(at("x.rs:12:5,", "x.rs"), Some(("x.rs".into(), 12)));
}
pub(crate) fn open_url(url: &str) -> String {
if cfg!(test) {
return format!("opened {url}"); }
if !(url.starts_with("https://") || url.starts_with("http://")) {
return format!("not opened: only http(s) links are ({url})");
}
let opener = if cfg!(target_os = "macos") { "open" } else { "xdg-open" };
let spawned = std::process::Command::new(opener)
.arg(url)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn();
match spawned {
Ok(_) => format!("opened {url}"),
Err(e) => format!("{opener}: {e}"),
}
}