use compact_str::CompactString;
use crate::{SelectionMode, TerminalGrid, gl::CellQuery, position::CursorPosition, select};
pub struct UrlMatch {
pub query: CellQuery,
pub url: CompactString,
}
fn is_url_char(ch: char) -> bool {
ch.is_ascii_alphanumeric()
|| matches!(
ch,
'-' | '.'
| '_'
| '~'
| ':'
| '/'
| '?'
| '#'
| '['
| ']'
| '@'
| '!'
| '$'
| '&'
| '\''
| '('
| ')'
| '*'
| '+'
| ','
| ';'
| '='
| '%'
)
}
fn is_trailing_punctuation(ch: char) -> bool {
matches!(ch, '.' | ',' | ';' | ':' | '!' | '?')
}
#[must_use]
pub fn find_url_at_cursor(cursor: CursorPosition, grid: &TerminalGrid) -> Option<UrlMatch> {
let cols = grid.terminal_size().cols;
let scheme_start = find_scheme_start(cursor, grid, cols)?;
let scheme_len = if matches_sequence(grid, scheme_start, "https://", cols) {
8
} else if matches_sequence(grid, scheme_start, "http://", cols) {
7
} else {
return None;
};
let after_scheme = scheme_start.move_right(scheme_len, cols)?;
let (raw_end, paren_balance) = scan_url_extent(after_scheme, grid, cols);
let url_end = trim_url_end(scheme_start, raw_end, paren_balance, grid);
if cursor.col < scheme_start.col || cursor.col > url_end.col {
return None;
}
let query = select(SelectionMode::Linear)
.start((scheme_start.col, scheme_start.row))
.end((url_end.col, url_end.row));
let url = grid.get_text(query);
Some(UrlMatch { query, url })
}
fn find_scheme_start(
cursor: CursorPosition,
grid: &TerminalGrid,
cols: u16,
) -> Option<CursorPosition> {
let mut pos = cursor;
loop {
if grid.get_ascii_char_at(pos) == Some('h')
&& (matches_sequence(grid, pos, "https://", cols)
|| matches_sequence(grid, pos, "http://", cols))
{
return Some(pos);
}
pos = pos.move_left(1)?;
}
}
fn matches_sequence(grid: &TerminalGrid, start: CursorPosition, seq: &str, cols: u16) -> bool {
let mut pos = start;
let char_count = seq.chars().count();
for (i, ch) in seq.chars().enumerate() {
if grid.get_ascii_char_at(pos) != Some(ch) {
return false;
}
if i < char_count - 1 {
match pos.move_right(1, cols) {
Some(next) => pos = next,
None => return false, }
}
}
true
}
fn scan_url_extent(start: CursorPosition, grid: &TerminalGrid, cols: u16) -> (CursorPosition, i32) {
let mut pos = start;
let mut paren_balance: i32 = 0;
let mut last_valid = start;
loop {
match grid.get_ascii_char_at(pos) {
Some(ch) if is_url_char(ch) => {
if ch == '(' {
paren_balance += 1;
} else if ch == ')' {
paren_balance -= 1;
}
last_valid = pos;
},
_ => break,
}
match pos.move_right(1, cols) {
Some(next) => pos = next,
None => break,
}
}
(last_valid, paren_balance)
}
fn trim_url_end(
start: CursorPosition,
mut end: CursorPosition,
mut paren_balance: i32,
grid: &TerminalGrid,
) -> CursorPosition {
while end.col > start.col {
let Some(ch) = grid.get_ascii_char_at(end) else {
break;
};
if is_trailing_punctuation(ch) {
end = end.move_left(1).unwrap_or(end);
} else if ch == ')' && paren_balance < 0 {
paren_balance += 1;
end = end.move_left(1).unwrap_or(end);
} else {
break;
}
}
end
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_url_char() {
assert!(is_url_char('a'));
assert!(is_url_char('Z'));
assert!(is_url_char('0'));
assert!(is_url_char('-'));
assert!(is_url_char('.'));
assert!(is_url_char('/'));
assert!(is_url_char('?'));
assert!(is_url_char('='));
assert!(is_url_char('&'));
assert!(is_url_char('('));
assert!(is_url_char(')'));
assert!(!is_url_char(' '));
assert!(!is_url_char('\n'));
assert!(!is_url_char('<'));
assert!(!is_url_char('>'));
assert!(!is_url_char('"'));
}
#[test]
fn test_is_trailing_punctuation() {
assert!(is_trailing_punctuation('.'));
assert!(is_trailing_punctuation(','));
assert!(is_trailing_punctuation(';'));
assert!(is_trailing_punctuation(':'));
assert!(is_trailing_punctuation('!'));
assert!(is_trailing_punctuation('?'));
assert!(!is_trailing_punctuation('/'));
assert!(!is_trailing_punctuation('-'));
assert!(!is_trailing_punctuation('a'));
}
}