@@ -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(_) => {}