Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 46 additions & 7 deletions frontends/rioterm/src/hints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,16 +86,19 @@ impl HintState {
}
};

// Find regex matches if regex is specified
if let Some(regex_pattern) = &hint.regex {
if let Ok(regex) = onig::Regex::new(regex_pattern) {
self.find_regex_matches(term, &regex, hint.clone());
}
let regex = match hint.regex.as_deref().map(onig::Regex::new) {
Some(Ok(regex)) => Some(regex),
Some(Err(_)) => return,
None => None,
};

if let Some(regex) = &regex {
self.find_regex_matches(term, regex, hint.clone());
}

// Find OSC 8 hyperlinks if enabled
if hint.hyperlinks {
self.find_hyperlink_matches(term, hint.clone());
self.find_hyperlink_matches(term, regex.as_ref(), hint.clone());
}

// Cancel hint mode if no matches found
Expand Down Expand Up @@ -252,6 +255,7 @@ impl HintState {
fn find_hyperlink_matches<T: EventListener>(
&mut self,
term: &rio_backend::crosswords::Crosswords<T>,
regex: Option<&onig::Regex>,
hint: Rc<Hint>,
) {
// Walk the visible region looking for OSC 8 hyperlink spans.
Expand Down Expand Up @@ -295,6 +299,10 @@ impl HintState {

// Look up the URI once for the whole span.
if let Some(hyperlink) = term.cell_hyperlink(line, Column(start_col)) {
if !hyperlink_matches_rule(regex, hyperlink.uri()) {
col = end_col;
continue;
}
let mut uri = hyperlink.uri().to_string();
if hint.post_processing {
uri = post_process_hyperlink_uri(&uri);
Expand Down Expand Up @@ -322,7 +330,7 @@ impl HintState {

for col in 0..grid.columns() {
let cell = &grid[line][Column(col)];
text.push(cell.c());
text.push(cell_char_for_regex(cell.c()));
}

text.trim_end().to_string()
Expand All @@ -338,6 +346,18 @@ impl HintState {
}
}

fn cell_char_for_regex(c: char) -> char {
if c == '\0' {
' '
} else {
c
}
}

pub(crate) fn hyperlink_matches_rule(regex: Option<&onig::Regex>, uri: &str) -> bool {
regex.is_none_or(|regex| regex.find(uri).is_some())
}

/// Generates hint labels using the specified alphabet
struct LabelGenerator {
alphabet: Vec<char>,
Expand Down Expand Up @@ -753,6 +773,25 @@ mod tests {
use super::*;
use rio_backend::config::hints::{HintAction, HintInternalAction};

#[test]
fn empty_grid_cells_are_regex_whitespace() {
let line: String = "https://example.com"
.chars()
.chain(['\0', '\0'])
.map(cell_char_for_regex)
.collect();
let regex = onig::Regex::new(r"https?://[^\s]+").unwrap();

assert_eq!(regex.find_iter(&line).next(), Some((0, 19)));
}

#[test]
fn hyperlink_rule_filters_osc8_uri() {
let file = onig::Regex::new(r"^file://").unwrap();
assert!(hyperlink_matches_rule(Some(&file), "file://host/tmp/a.md"));
assert!(!hyperlink_matches_rule(Some(&file), "https://example.com"));
}

#[test]
fn test_label_generator() {
let mut gen = LabelGenerator::new("abc");
Expand Down
68 changes: 31 additions & 37 deletions frontends/rioterm/src/screen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2150,36 +2150,40 @@ impl Screen<'_> {
continue;
}

let regex = match hint_config.regex.as_deref() {
Some(pattern) => match self.compiled_hint_regex(pattern) {
Some(regex) => Some(regex),
None => continue,
},
None => None,
};

// Check hyperlinks if enabled
if hint_config.hyperlinks {
if let Some(hyperlink_match) =
self.find_hyperlink_at_point(terminal, point)
{
if let Some(hyperlink_match) = self.find_hyperlink_at_point(
terminal,
point,
regex.as_deref(),
hint_config.clone(),
) {
return Some(hyperlink_match);
}
}

// Check regex patterns if specified
if let Some(regex_pattern) = &hint_config.regex {
if let Some(regex) = self.compiled_hint_regex(regex_pattern) {
let line = logical_line.get_or_insert_with(|| {
crate::hints::LogicalLine::extract(terminal, point)
if let Some(regex) = &regex {
let line = logical_line.get_or_insert_with(|| {
crate::hints::LogicalLine::extract(terminal, point)
});
if let Some(m) = line.as_ref().and_then(|line| {
line.match_at(terminal, point, regex, hint_config.post_processing)
}) {
return Some(crate::hints::HintMatch {
text: m.text,
start: m.start,
end: m.end,
hint: hint_config.clone(),
});
if let Some(m) = line.as_ref().and_then(|line| {
line.match_at(
terminal,
point,
&regex,
hint_config.post_processing,
)
}) {
return Some(crate::hints::HintMatch {
text: m.text,
start: m.start,
end: m.end,
hint: hint_config.clone(),
});
}
}
}
}
Expand All @@ -2192,6 +2196,8 @@ impl Screen<'_> {
&self,
terminal: &rio_backend::crosswords::Crosswords<EventProxy>,
point: rio_backend::crosswords::pos::Pos,
regex: Option<&onig::Regex>,
hint_config: std::rc::Rc<rio_backend::config::hints::Hint>,
) -> Option<crate::hints::HintMatch> {
let grid = &terminal.grid;

Expand All @@ -2207,6 +2213,9 @@ impl Screen<'_> {
// a different id while belonging to the same link) to find the
// span boundaries.
let hyperlink = terminal.cell_hyperlink(point.row, point.col)?;
if !crate::hints::hyperlink_matches_rule(regex, hyperlink.uri()) {
return None;
}

let mut start_col = point.col;
let mut end_col = point.col;
Expand All @@ -2228,21 +2237,6 @@ impl Screen<'_> {
}
}

// Build a synthetic hint config so the rest of the hint
// pipeline (highlighting, click action) treats this just like
// a regex/url match.
let hint_config = std::rc::Rc::new(rio_backend::config::hints::Hint {
regex: None,
hyperlinks: true,
post_processing: true,
persist: false,
action: rio_backend::config::hints::HintAction::Action {
action: rio_backend::config::hints::HintInternalAction::Open,
},
mouse: rio_backend::config::hints::HintMouse::default(),
binding: None,
});

let mut uri = hyperlink.uri().to_string();
if hint_config.post_processing {
uri = post_process_hyperlink_uri(&uri);
Expand Down
Loading