agent: Differentiate @mentions from markdown links (#28073)
This ensures that we display @mentions and normal markdown links differently: <img width="670" alt="Screenshot 2025-04-04 at 11 07 51" src="https://github.com/user-attachments/assets/0a4d0881-abb9-42a8-b3fa-912cd6873ae0" /> Release Notes: - N/A
This commit is contained in:
committed by
Thomas Mickley-Doyle
parent
23ab3b833e
commit
579fc2aabc
@@ -245,6 +245,17 @@ fn render_markdown(
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
link_callback: Some(Rc::new(move |url, cx| {
|
||||
if MentionLink::is_valid(url) {
|
||||
let colors = cx.theme().colors();
|
||||
Some(TextStyleRefinement {
|
||||
background_color: Some(colors.element_background),
|
||||
..Default::default()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -320,6 +331,7 @@ fn open_markdown_link(
|
||||
});
|
||||
}
|
||||
}),
|
||||
Some(MentionLink::Fetch(url)) => cx.open_url(&url),
|
||||
None => cx.open_url(&text),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -609,24 +609,45 @@ fn fold_toggle(
|
||||
pub enum MentionLink {
|
||||
File(ProjectPath, Entry),
|
||||
Symbol(ProjectPath, String),
|
||||
Fetch(String),
|
||||
Thread(ThreadId),
|
||||
}
|
||||
|
||||
impl MentionLink {
|
||||
const FILE: &str = "@file";
|
||||
const SYMBOL: &str = "@symbol";
|
||||
const THREAD: &str = "@thread";
|
||||
const FETCH: &str = "@fetch";
|
||||
|
||||
const SEPARATOR: &str = ":";
|
||||
|
||||
pub fn is_valid(url: &str) -> bool {
|
||||
url.starts_with(Self::FILE)
|
||||
|| url.starts_with(Self::SYMBOL)
|
||||
|| url.starts_with(Self::FETCH)
|
||||
|| url.starts_with(Self::THREAD)
|
||||
}
|
||||
|
||||
pub fn for_file(file_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}](file:{})", file_name, full_path)
|
||||
format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
|
||||
}
|
||||
|
||||
pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
|
||||
format!("[@{}](symbol:{}:{})", symbol_name, full_path, symbol_name)
|
||||
format!(
|
||||
"[@{}]({}:{}:{})",
|
||||
symbol_name,
|
||||
Self::SYMBOL,
|
||||
full_path,
|
||||
symbol_name
|
||||
)
|
||||
}
|
||||
|
||||
pub fn for_fetch(url: &str) -> String {
|
||||
format!("[@{}]({})", url, url)
|
||||
format!("[@{}]({}:{})", url, Self::FETCH, url)
|
||||
}
|
||||
|
||||
pub fn for_thread(thread: &ThreadContextEntry) -> String {
|
||||
format!("[@{}](thread:{})", thread.summary, thread.id)
|
||||
format!("[@{}]({}:{})", thread.summary, Self::THREAD, thread.id)
|
||||
}
|
||||
|
||||
pub fn try_parse(link: &str, workspace: &Entity<Workspace>, cx: &App) -> Option<Self> {
|
||||
@@ -649,17 +670,10 @@ impl MentionLink {
|
||||
})
|
||||
}
|
||||
|
||||
let (prefix, link, target) = {
|
||||
let mut parts = link.splitn(3, ':');
|
||||
let prefix = parts.next();
|
||||
let link = parts.next();
|
||||
let target = parts.next();
|
||||
(prefix, link, target)
|
||||
};
|
||||
|
||||
match (prefix, link, target) {
|
||||
(Some("file"), Some(path), _) => {
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
let (prefix, argument) = link.split_once(Self::SEPARATOR)?;
|
||||
match prefix {
|
||||
Self::FILE => {
|
||||
let project_path = extract_project_path_from_link(argument, workspace, cx)?;
|
||||
let entry = workspace
|
||||
.read(cx)
|
||||
.project()
|
||||
@@ -667,14 +681,16 @@ impl MentionLink {
|
||||
.entry_for_path(&project_path, cx)?;
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
(Some("symbol"), Some(path), Some(symbol_name)) => {
|
||||
Self::SYMBOL => {
|
||||
let (path, symbol) = argument.split_once(Self::SEPARATOR)?;
|
||||
let project_path = extract_project_path_from_link(path, workspace, cx)?;
|
||||
Some(MentionLink::Symbol(project_path, symbol_name.to_string()))
|
||||
Some(MentionLink::Symbol(project_path, symbol.to_string()))
|
||||
}
|
||||
(Some("thread"), Some(thread_id), _) => {
|
||||
let thread_id = ThreadId::from(thread_id);
|
||||
Self::THREAD => {
|
||||
let thread_id = ThreadId::from(argument);
|
||||
Some(MentionLink::Thread(thread_id))
|
||||
}
|
||||
Self::FETCH => Some(MentionLink::Fetch(argument.to_string())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -932,22 +932,22 @@ mod tests {
|
||||
});
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](file:dir/a/one.txt)",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt)",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 36)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
cx.simulate_input(" ");
|
||||
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](file:dir/a/one.txt) ",);
|
||||
assert_eq!(editor.text(cx), "Lorem [@one.txt](@file:dir/a/one.txt) ",);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 36)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -956,12 +956,12 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum ",
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 36)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -970,12 +970,12 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum @file ",
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum @file ",
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![Point::new(0, 6)..Point::new(0, 36)]
|
||||
vec![Point::new(0, 6)..Point::new(0, 37)]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -988,14 +988,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 36),
|
||||
Point::new(0, 43)..Point::new(0, 69)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1005,14 +1005,14 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)\n@"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n@"
|
||||
);
|
||||
assert!(editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 36),
|
||||
Point::new(0, 43)..Point::new(0, 69)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71)
|
||||
]
|
||||
);
|
||||
});
|
||||
@@ -1026,15 +1026,15 @@ mod tests {
|
||||
editor.update(&mut cx, |editor, cx| {
|
||||
assert_eq!(
|
||||
editor.text(cx),
|
||||
"Lorem [@one.txt](file:dir/a/one.txt) Ipsum [@editor](file:dir/editor)\n[@seven.txt](file:dir/b/seven.txt)"
|
||||
"Lorem [@one.txt](@file:dir/a/one.txt) Ipsum [@editor](@file:dir/editor)\n[@seven.txt](@file:dir/b/seven.txt)"
|
||||
);
|
||||
assert!(!editor.has_visible_completions_menu());
|
||||
assert_eq!(
|
||||
crease_ranges(editor, cx),
|
||||
vec![
|
||||
Point::new(0, 6)..Point::new(0, 36),
|
||||
Point::new(0, 43)..Point::new(0, 69),
|
||||
Point::new(1, 0)..Point::new(1, 34)
|
||||
Point::new(0, 6)..Point::new(0, 37),
|
||||
Point::new(0, 44)..Point::new(0, 71),
|
||||
Point::new(1, 0)..Point::new(1, 35)
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -24,6 +24,10 @@ use util::{ResultExt, TryFutureExt};
|
||||
|
||||
use crate::parser::CodeBlockKind;
|
||||
|
||||
/// A callback function that can be used to customize the style of links based on the destination URL.
|
||||
/// If the callback returns `None`, the default link style will be used.
|
||||
type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownStyle {
|
||||
pub base_text_style: TextStyle,
|
||||
@@ -32,6 +36,7 @@ pub struct MarkdownStyle {
|
||||
pub inline_code: TextStyleRefinement,
|
||||
pub block_quote: TextStyleRefinement,
|
||||
pub link: TextStyleRefinement,
|
||||
pub link_callback: Option<LinkStyleCallback>,
|
||||
pub rule_color: Hsla,
|
||||
pub block_quote_border_color: Hsla,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
@@ -49,6 +54,7 @@ impl Default for MarkdownStyle {
|
||||
inline_code: Default::default(),
|
||||
block_quote: Default::default(),
|
||||
link: Default::default(),
|
||||
link_callback: None,
|
||||
rule_color: Default::default(),
|
||||
block_quote_border_color: Default::default(),
|
||||
syntax: Arc::new(SyntaxTheme::default()),
|
||||
@@ -679,7 +685,13 @@ impl Element for MarkdownElement {
|
||||
MarkdownTag::Link { dest_url, .. } => {
|
||||
if builder.code_block_stack.is_empty() {
|
||||
builder.push_link(dest_url.clone(), range.clone());
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
let style = self
|
||||
.style
|
||||
.link_callback
|
||||
.as_ref()
|
||||
.and_then(|callback| callback(dest_url, cx))
|
||||
.unwrap_or_else(|| self.style.link.clone());
|
||||
builder.push_text_style(style)
|
||||
}
|
||||
}
|
||||
MarkdownTag::MetadataBlock(_) => {}
|
||||
|
||||
Reference in New Issue
Block a user