Skip to content

feat: make rg search more convenient#3549

Open
Cachey4467 wants to merge 1 commit intosxyazi:mainfrom
Cachey4467:easy_rg
Open

feat: make rg search more convenient#3549
Cachey4467 wants to merge 1 commit intosxyazi:mainfrom
Cachey4467:easy_rg

Conversation

@Cachey4467
Copy link

@Cachey4467 Cachey4467 commented Jan 11, 2026

Amazing tool! Try to improve the rg search experience.

Which issue does this PR resolve?

Resolves #3521

Rationale of this PR

See the issue for more details.

  • URL supports #line:column in domain
    • Added Url::line() and Url::column() methods to parse line/column from search URL fragments (e.g., keyword#123:45)
    • Updated scheme::encode::domain() to include # in the encode set
  • Enhanced rg plugin
    • Changed rg args to --line-number --no-heading --column for detailed match positions
    • Added parse_rg_line_column() parser function
    • Search results now include line and column in the URL
  • Preview improvements
    • When opening a file with line info, preview automatically scrolls to show the matched line at the top
  • Search reveal support
    • Added Reveal::reveal_search() to handle revealing files within search results
  • Splatter format support
    • Added %l/%L for line number
    • Added %c/%C for column number
  • Lua bindings
    • Added file:line() method to get line number from URL

I changed the reveal logic to account for Search URL.

if !cx.tab().preview.same_url(&hovered.url) {
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_default();
// set matched line on the top of preview window
cx.tab_mut().preview.skip = folder.map(|f| f.0).unwrap_or_else(|| hovered.url.as_url().line().map_or(0, |l| l.saturating_sub(1)));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled in the seek() method of code.lua: when the URL contains line numbers, the unit for seek() should be 1, meaning it should jump between matching lines, then, peek() should get the N-th line number by skip.

succ!();
}

fn reveal_search(cx: &mut Ctx, opt: RevealOpt) -> Result<Data> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a new method needed?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we use a search URL to create a File in rg.rs, to_search() ends up zeroing out the URN and URI, which results in the placeholder file issue in the reveal scenario. We’re using search URLs in the first place because regular URLs don't have a domain field.

let Some((fp, l, c)) = parse_rg_line_column(&line) else { continue };

let url = cwd.try_join(fp)
	.ok()
	.and_then(|u| u.to_search(&format!("{}#{}:{}", subject, l, c)).ok()); // URI set to 0 causing the placeholder file issue

let Some(url) = url else { continue };

if let Ok(file) = File::new(url).await {
	tx.send(file).ok();
}

Ok(yazi_config::THEME.icon.matches(me).map(Icon::from))
});

$methods.add_method("line", |_, me, ()| {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled in the peek() method of code.lua

Some('d') | Some('D') => self.visit_dirname(it, buf),
Some('t') | Some('T') => self.visit_tab(it, buf),
Some('y') | Some('Y') => self.visit_yanked(it, buf),
Some('l') | Some('L') => self.visit_line_number(it, buf),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason l and c are needed?

function Entity:highlights()
local name, p = self._file.name, ui.printable

local ok, line = pcall(function() return self._file:line() end)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the file has multiple lines matched?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implementation is designed to achieve a #1095 like rg search experience, where each match in the same file is treated as an individual entry.

image

So 1 match = 1 "file"

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you want to show the same file as multiple entries? Is there any advantage to this compared to showing it as a single entry?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well... It feels natural to me (that's how nvim + telescope handles it as seen in #1095 ). I haven't dug into the project too much yet, but would the line and column information be accessible in the context for opening files in the Openers? And I understand if you think it's bit complex or disruptive to the module design.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's possible, but it's better to discuss it in a separate issue on how to attach line numbers to the file path and keep this PR focused on preview matched lines

let path = lock.url.as_url().unified_path();
let inner = match Highlighter::new(path).highlight(lock.skip, area.size()).await {
Ok(text) => text,
Err(e @ PeekError::Exceed(max)) => return (e.to_string(), max).into_lua_multi(&lua),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this change made?

#[inline]
pub fn domain<'s>(s: &'s str) -> PercentEncode<'s> {
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':');
const SET: &AsciiSet = &CONTROLS.add(b'/').add(b':').add(b'#');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not right - SET here is used to encode the entire domain, not just the keyword part in the domain, so .add(b'#') will not work if you intend to use it as the separator between the keyword and the line/col numbers

Comment on lines +90 to +117
pub fn line(self) -> Option<usize> {
let Self::Search { domain, .. } = self else { return None };
let Some(hash_pos) = domain.find('#') else {
return None
};

let frag = &domain[hash_pos + 1..];
if let Some(colon_pos) = frag.find(':') {
frag[..colon_pos].parse::<usize>().ok()
} else {
frag.parse::<usize>().ok()
}
}

#[inline]
pub fn column(self) -> Option<usize> {
let Self::Search { domain, .. } = self else { return None };
let Some(hash_pos) = domain.find('#') else {
return None
};

let frag = &domain[hash_pos + 1..];
if let Some(colon_pos) = frag.find(':') {
frag[colon_pos + 1..].parse::<usize>().ok()
} else {
None
}
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be handled in the respective previewer so that different previewers can use different formats for different search metadata

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Url enhancement: support line and column addressing for more precise rg searches

2 participants