use agent_client_protocol::schema::v1 as acp;use anyhow::{Context as _, Result, bail};use file_icons::FileIcons;use serde::{Deserialize, Serialize};use std::{ borrow::Cow, fmt, ops::RangeInclusive, path::{Path, PathBuf},};use ui::{App, IconName, SharedString};use url::Url;use urlencoding::decode;use util::{ ResultExt, paths::{PathStyle, PathWithPosition, is_absolute},}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]pub enum MentionUri { File { abs_path: PathBuf, }, PastedImage { name: String, }, Directory { abs_path: PathBuf, }, Symbol { abs_path: PathBuf, name: String, line_range: RangeInclusive<u32>, }, Thread { id: acp::SessionId, name: String, }, /// Deprecated: kept so threads from before rules became skills still /// deserialize. `id` (an opaque `prompt_store::PromptId`) is preserved /// verbatim so re-saved threads stay loadable by older Zed versions. Rule { #[serde(default = "default_deprecated_rule_id")] id: serde_json::Value, name: String, }, Diagnostics { #[serde(default = "default_include_errors")] include_errors: bool, #[serde(default)] include_warnings: bool, }, Selection { #[serde(default, skip_serializing_if = "Option::is_none")] abs_path: Option<PathBuf>, line_range: RangeInclusive<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] column: Option<u32>, }, Fetch { url: Url, }, TerminalSelection { line_count: u32, }, GitDiff { base_ref: String, }, MergeConflict { file_path: String, }, Skill { name: String, source: String, skill_file_path: PathBuf, },} impl MentionUri { pub fn parse(input: &str, path_style: PathStyle) -> Result<Self> { let input = input .strip_prefix('`') .and_then(|input| input.strip_suffix('`')) .unwrap_or(input); let parse_column = |input: Option<String>| -> Option<u32> { input?.parse::<u32>().ok()?.checked_sub(1) }; let validate_query_params = |url: &Url, allowed: &[&str]| -> Result<()> { for (key, _) in url.query_pairs() { if !allowed.contains(&key.as_ref()) { bail!("invalid query parameter") } } Ok(()) }; if is_absolute(input, path_style) && !input.contains("://") { return parse_absolute_path(input) .with_context(|| format!("Invalid absolute path mention URI: {input}")); } let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let trimmed = if path_style.is_windows() { path.trim_start_matches("/") } else { path }; let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed)); let normalized: Cow<str> = if path_style.is_windows() { match to_native_windows_path(&decoded) { Some(native) => Cow::Owned(native), None => decoded, } } else { decoded }; let path = normalized.as_ref(); if let Some(fragment) = url.fragment() { validate_query_params(&url, &["symbol", "column"])?; let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1); let column = parse_column(query_param(&url, "column")); if let Some(name) = query_param(&url, "symbol") { Ok(Self::Symbol { name, abs_path: path.into(), line_range, }) } else { Ok(Self::Selection { abs_path: Some(path.into()), line_range, column, }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path.into(), }) } else { Ok(Self::File { abs_path: path.into(), }) } } "zed" => { if let Some(thread_id) = path.strip_prefix("/agent/thread/") { let name = single_query_param(&url, "name")?.context("Missing thread name")?; Ok(Self::Thread { id: acp::SessionId::new(thread_id), name, }) } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") { // Deprecated: parses legacy rule mentions. let name = single_query_param(&url, "name")?.context("Missing rule name")?; let id = if rule_id.is_empty() { default_deprecated_rule_id() } else { serde_json::json!({ "User": { "uuid": rule_id } }) }; Ok(Self::Rule { id, name }) } else if path == "/agent/diagnostics" { let mut include_errors = default_include_errors(); let mut include_warnings = false; for (key, value) in url.query_pairs() { match key.as_ref() { "include_warnings" => include_warnings = value == "true", "include_errors" => include_errors = value == "true", _ => bail!("invalid query parameter"), } } Ok(Self::Diagnostics { include_errors, include_warnings, }) } else if path.starts_with("/agent/pasted-image") { let name = single_query_param(&url, "name")?.unwrap_or_else(|| "Image".to_string()); Ok(Self::PastedImage { name }) } else if path.starts_with("/agent/untitled-buffer") { let fragment = url .fragment() .context("Missing fragment for untitled buffer selection")?; let line_range = parse_line_range(fragment)?; validate_query_params(&url, &["column"])?; Ok(Self::Selection { abs_path: None, line_range, column: parse_column(query_param(&url, "column")), }) } else if let Some(name) = path.strip_prefix("/agent/symbol/") { let fragment = url .fragment() .context("Missing fragment for untitled buffer selection")?; let line_range = parse_line_range(fragment)?; let path = single_query_param(&url, "path")?.context("Missing path for symbol")?; Ok(Self::Symbol { name: name.to_string(), abs_path: path.into(), line_range, }) } else if path.starts_with("/agent/file") { let path = single_query_param(&url, "path")?.context("Missing path for file")?; Ok(Self::File { abs_path: path.into(), }) } else if path.starts_with("/agent/directory") { let path = single_query_param(&url, "path")?.context("Missing path for directory")?; Ok(Self::Directory { abs_path: path.into(), }) } else if path.starts_with("/agent/selection") { validate_query_params(&url, &["path", "column"])?; let fragment = url.fragment().context("Missing fragment for selection")?; let line_range = parse_line_range(fragment)?; let column = parse_column(query_param(&url, "column")); let path = query_param(&url, "path").context("Missing path for selection")?; Ok(Self::Selection { abs_path: Some(path.into()), line_range, column, }) } else if path.starts_with("/agent/terminal-selection") { let line_count = single_query_param(&url, "lines")? .unwrap_or_else(|| "0".to_string()) .parse::<u32>() .unwrap_or(0); Ok(Self::TerminalSelection { line_count }) } else if path.starts_with("/agent/git-diff") { let base_ref = single_query_param(&url, "base")?.unwrap_or_else(|| "main".to_string()); Ok(Self::GitDiff { base_ref }) } else if path.starts_with("/agent/merge-conflict") { let file_path = single_query_param(&url, "path")?.unwrap_or_default(); Ok(Self::MergeConflict { file_path }) } else if path.starts_with("/agent/skill") { let mut name = None; let mut source = None; let mut skill_file_path = None; for (key, value) in url.query_pairs() { match key.as_ref() { "name" => { if name.replace(value.to_string()).is_some() { bail!("duplicate skill name query parameter"); } } "source" => { if source.replace(value.to_string()).is_some() { bail!("duplicate skill source query parameter"); } } "path" => { if skill_file_path .replace(PathBuf::from(value.to_string())) .is_some() { bail!("duplicate skill file path query parameter"); } } _ => bail!("invalid query parameter"), } } Ok(Self::Skill { name: name.context("missing skill name")?, source: source.context("missing skill source")?, skill_file_path: skill_file_path.context("missing skill file path")?, }) } else { bail!("invalid zed url: {:?}", input); } } "http" | "https" => Ok(MentionUri::Fetch { url }), other => bail!("unrecognized scheme {:?}", other), } } /// Parses a hyperlink target from agent-authored Markdown. /// /// Unlike [`MentionUri::parse`] — which stays strict so canonical mention /// URIs round-trip verbatim — bare path targets are normalized first: /// percent escapes are decoded (see [`decode_path_escapes`]) and /// Windows-compatible spellings like `/C:/foo` or `/c/foo` become native /// paths (see [`to_native_windows_path`]). pub fn parse_hyperlink(input: &str, path_style: PathStyle) -> Result<Self> { if let Some(target) = bare_path_target(input, path_style) { return parse_hyperlink_path(target, path_style, DecodePercentEscapes::Yes) .with_context(|| format!("Invalid hyperlink path target: {input}")); } Self::parse(input, path_style) } /// Returns the literal (un-decoded) interpretation of a bare-path /// hyperlink target, for files whose names literally contain an escape /// sequence (e.g. `a%20b.rs`). Returns `None` when this wouldn't differ /// from [`MentionUri::parse_hyperlink`], including for URLs, whose /// escapes are unambiguous. pub fn parse_hyperlink_literal(input: &str, path_style: PathStyle) -> Option<Self> { let target = bare_path_target(input, path_style)?; let (path_input, _) = split_path_fragment(target); if !matches!(decode_path_escapes(path_input), Cow::Owned(_)) { return None; } parse_hyperlink_path(target, path_style, DecodePercentEscapes::No).ok() } /// The absolute path this mention refers to, if it refers to one. pub fn abs_path(&self) -> Option<&Path> { match self { MentionUri::File { abs_path } | MentionUri::Directory { abs_path } | MentionUri::Symbol { abs_path, .. } => Some(abs_path), MentionUri::Selection { abs_path, .. } => abs_path.as_deref(), MentionUri::Skill { skill_file_path, .. } => Some(skill_file_path), MentionUri::PastedImage { .. } | MentionUri::Thread { .. } | MentionUri::Rule { .. } | MentionUri::Diagnostics { .. } | MentionUri::Fetch { .. } | MentionUri::TerminalSelection { .. } | MentionUri::GitDiff { .. } | MentionUri::MergeConflict { .. } => None, } } pub fn name(&self) -> String { match self { MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() .into_owned(), MentionUri::PastedImage { name } => name.clone(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Diagnostics { .. } => "Diagnostics".to_string(), MentionUri::TerminalSelection { line_count } => { if *line_count == 1 { "Terminal (1 line)".to_string() } else { format!("Terminal ({} lines)", line_count) } } MentionUri::GitDiff { base_ref } => format!("Branch Diff ({})", base_ref), MentionUri::MergeConflict { file_path } => { let name = Path::new(file_path) .file_name() .unwrap_or_default() .to_string_lossy(); format!("Merge Conflict ({name})") } MentionUri::Selection { abs_path: path, line_range, .. } => selection_name(path.as_deref(), line_range), MentionUri::Fetch { url } => url.to_string(), MentionUri::Skill { name, .. } => name.clone(), } } /// Returns a label for this mention at the given disambiguation `detail` /// level. `detail == 0` is the base name returned by [`Self::name`]; higher /// levels include progressively more context (e.g. additional parent path /// components for files, or the source for skills) until a fixed point is /// reached. Intended to be driven by [`util::disambiguate::compute_disambiguation_details`]. pub fn disambiguated_name(&self, detail: usize) -> String { if detail == 0 { return self.name(); } match self { MentionUri::Skill { name, source, .. } => { if source.is_empty() { // Must match `SkillSource::display_label()` in agent_skills. format!("{} (global)", name) } else { format!("{} ({})", name, source) } } MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => { project::path_suffix(abs_path, detail) } _ => self.name(), } } pub fn tooltip_text(&self) -> Option<SharedString> { match self { MentionUri::File { abs_path } | MentionUri::Directory { abs_path } => { Some(abs_path.to_string_lossy().into_owned().into()) } MentionUri::Symbol { abs_path, line_range, .. } => Some( format!( "{}:{}-{}", abs_path.display(), line_range.start(), line_range.end() ) .into(), ), MentionUri::Selection { abs_path: Some(path), line_range, .. } => Some( format!( "{}:{}-{}", path.display(), line_range.start(), line_range.end() ) .into(), ), MentionUri::Skill { skill_file_path, .. } => Some(skill_file_path.to_string_lossy().into_owned().into()), _ => None, } } pub fn icon_path(&self, cx: &mut App) -> SharedString { match self { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } MentionUri::PastedImage { .. } => IconName::Image.path().into(), MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), MentionUri::Thread { .. } => IconName::Thread.path().into(), MentionUri::Rule { .. } => IconName::Reader.path().into(), MentionUri::Diagnostics { .. } => IconName::Warning.path().into(), MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(), MentionUri::Selection { .. } => IconName::Reader.path().into(), MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), MentionUri::GitDiff { .. } => IconName::GitBranch.path().into(), MentionUri::MergeConflict { .. } => IconName::GitMergeConflict.path().into(), MentionUri::Skill { .. } => IconName::Sparkle.path().into(), } } pub fn as_link<'a>(&'a self) -> MentionLink<'a> { MentionLink(self) } pub fn to_uri(&self) -> Url { match self { MentionUri::File { abs_path } => { let mut url = Url::parse("file:///").unwrap(); url.set_path(&abs_path.to_string_lossy()); url } MentionUri::PastedImage { name } => { let mut url = Url::parse("zed:///agent/pasted-image").unwrap(); url.query_pairs_mut().append_pair("name", name); url } MentionUri::Directory { abs_path } => { let mut url = Url::parse("file:///").unwrap(); let mut path = abs_path.to_string_lossy().into_owned(); if !path.ends_with('/') && !path.ends_with('\\') { path.push('/'); } url.set_path(&path); url } MentionUri::Symbol { abs_path, name, line_range, .. } => { let mut url = Url::parse("file:///").unwrap(); url.set_path(&abs_path.to_string_lossy()); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", line_range.start() + 1, line_range.end() + 1 ))); url } MentionUri::Selection { abs_path, line_range,Lines 1–500 of 1773. Load more
use agent_client_protocol::schema::v1 as acp; use anyhow::{Context as _, Result, bail}; use file_icons::FileIcons; use serde::{Deserialize, Serialize}; use std::{ borrow::Cow, fmt, ops::RangeInclusive, path::{Path, PathBuf}, }; use ui::{App, IconName, SharedString}; use url::Url; use urlencoding::decode; use util::{ ResultExt, paths::{PathStyle, PathWithPosition, is_absolute}, }; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] pub enum MentionUri { File { abs_path: PathBuf, }, PastedImage { name: String, }, Directory { abs_path: PathBuf, }, Symbol { abs_path: PathBuf, name: String, line_range: RangeInclusive<u32>, }, Thread { id: acp::SessionId, name: String, }, /// Deprecated: kept so threads from before rules became skills still /// deserialize. `id` (an opaque `prompt_store::PromptId`) is preserved /// verbatim so re-saved threads stay loadable by older Zed versions. Rule { #[serde(default = "default_deprecated_rule_id")] id: serde_json::Value, name: String, }, Diagnostics { #[serde(default = "default_include_errors")] include_errors: bool, #[serde(default)] include_warnings: bool, }, Selection { #[serde(default, skip_serializing_if = "Option::is_none")] abs_path: Option<PathBuf>, line_range: RangeInclusive<u32>, #[serde(default, skip_serializing_if = "Option::is_none")] column: Option<u32>, }, Fetch { url: Url, }, TerminalSelection { line_count: u32, }, GitDiff { base_ref: String, }, MergeConflict { file_path: String, }, Skill { name: String, source: String, skill_file_path: PathBuf, }, } impl MentionUri { pub fn parse(input: &str, path_style: PathStyle) -> Result<Self> { let input = input .strip_prefix('`') .and_then(|input| input.strip_suffix('`')) .unwrap_or(input); let parse_column = |input: Option<String>| -> Option<u32> { input?.parse::<u32>().ok()?.checked_sub(1) }; let validate_query_params = |url: &Url, allowed: &[&str]| -> Result<()> { for (key, _) in url.query_pairs() { if !allowed.contains(&key.as_ref()) { bail!("invalid query parameter") } } Ok(()) }; if is_absolute(input, path_style) && !input.contains("://") { return parse_absolute_path(input) .with_context(|| format!("Invalid absolute path mention URI: {input}")); } let url = url::Url::parse(input)?; let path = url.path(); match url.scheme() { "file" => { let trimmed = if path_style.is_windows() { path.trim_start_matches("/") } else { path }; let decoded = decode(trimmed).unwrap_or(Cow::Borrowed(trimmed)); let normalized: Cow<str> = if path_style.is_windows() { match to_native_windows_path(&decoded) { Some(native) => Cow::Owned(native), None => decoded, } } else { decoded }; let path = normalized.as_ref(); if let Some(fragment) = url.fragment() { validate_query_params(&url, &["symbol", "column"])?; let line_range = parse_line_range(fragment).log_err().unwrap_or(1..=1); let column = parse_column(query_param(&url, "column")); if let Some(name) = query_param(&url, "symbol") { Ok(Self::Symbol { name, abs_path: path.into(), line_range, }) } else { Ok(Self::Selection { abs_path: Some(path.into()), line_range, column, }) } } else if input.ends_with("/") { Ok(Self::Directory { abs_path: path.into(), }) } else { Ok(Self::File { abs_path: path.into(), }) } } "zed" => { if let Some(thread_id) = path.strip_prefix("/agent/thread/") { let name = single_query_param(&url, "name")?.context("Missing thread name")?; Ok(Self::Thread { id: acp::SessionId::new(thread_id), name, }) } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") { // Deprecated: parses legacy rule mentions. let name = single_query_param(&url, "name")?.context("Missing rule name")?; let id = if rule_id.is_empty() { default_deprecated_rule_id() } else { serde_json::json!({ "User": { "uuid": rule_id } }) }; Ok(Self::Rule { id, name }) } else if path == "/agent/diagnostics" { let mut include_errors = default_include_errors(); let mut include_warnings = false; for (key, value) in url.query_pairs() { match key.as_ref() { "include_warnings" => include_warnings = value == "true", "include_errors" => include_errors = value == "true", _ => bail!("invalid query parameter"), } } Ok(Self::Diagnostics { include_errors, include_warnings, }) } else if path.starts_with("/agent/pasted-image") { let name = single_query_param(&url, "name")?.unwrap_or_else(|| "Image".to_string()); Ok(Self::PastedImage { name }) } else if path.starts_with("/agent/untitled-buffer") { let fragment = url .fragment() .context("Missing fragment for untitled buffer selection")?; let line_range = parse_line_range(fragment)?; validate_query_params(&url, &["column"])?; Ok(Self::Selection { abs_path: None, line_range, column: parse_column(query_param(&url, "column")), }) } else if let Some(name) = path.strip_prefix("/agent/symbol/") { let fragment = url .fragment() .context("Missing fragment for untitled buffer selection")?; let line_range = parse_line_range(fragment)?; let path = single_query_param(&url, "path")?.context("Missing path for symbol")?; Ok(Self::Symbol { name: name.to_string(), abs_path: path.into(), line_range, }) } else if path.starts_with("/agent/file") { let path = single_query_param(&url, "path")?.context("Missing path for file")?; Ok(Self::File { abs_path: path.into(), }) } else if path.starts_with("/agent/directory") { let path = single_query_param(&url, "path")?.context("Missing path for directory")?; Ok(Self::Directory { abs_path: path.into(), }) } else if path.starts_with("/agent/selection") { validate_query_params(&url, &["path", "column"])?; let fragment = url.fragment().context("Missing fragment for selection")?; let line_range = parse_line_range(fragment)?; let column = parse_column(query_param(&url, "column")); let path = query_param(&url, "path").context("Missing path for selection")?; Ok(Self::Selection { abs_path: Some(path.into()), line_range, column, }) } else if path.starts_with("/agent/terminal-selection") { let line_count = single_query_param(&url, "lines")? .unwrap_or_else(|| "0".to_string()) .parse::<u32>() .unwrap_or(0); Ok(Self::TerminalSelection { line_count }) } else if path.starts_with("/agent/git-diff") { let base_ref = single_query_param(&url, "base")?.unwrap_or_else(|| "main".to_string()); Ok(Self::GitDiff { base_ref }) } else if path.starts_with("/agent/merge-conflict") { let file_path = single_query_param(&url, "path")?.unwrap_or_default(); Ok(Self::MergeConflict { file_path }) } else if path.starts_with("/agent/skill") { let mut name = None; let mut source = None; let mut skill_file_path = None; for (key, value) in url.query_pairs() { match key.as_ref() { "name" => { if name.replace(value.to_string()).is_some() { bail!("duplicate skill name query parameter"); } } "source" => { if source.replace(value.to_string()).is_some() { bail!("duplicate skill source query parameter"); } } "path" => { if skill_file_path .replace(PathBuf::from(value.to_string())) .is_some() { bail!("duplicate skill file path query parameter"); } } _ => bail!("invalid query parameter"), } } Ok(Self::Skill { name: name.context("missing skill name")?, source: source.context("missing skill source")?, skill_file_path: skill_file_path.context("missing skill file path")?, }) } else { bail!("invalid zed url: {:?}", input); } } "http" | "https" => Ok(MentionUri::Fetch { url }), other => bail!("unrecognized scheme {:?}", other), } } /// Parses a hyperlink target from agent-authored Markdown. /// /// Unlike [`MentionUri::parse`] — which stays strict so canonical mention /// URIs round-trip verbatim — bare path targets are normalized first: /// percent escapes are decoded (see [`decode_path_escapes`]) and /// Windows-compatible spellings like `/C:/foo` or `/c/foo` become native /// paths (see [`to_native_windows_path`]). pub fn parse_hyperlink(input: &str, path_style: PathStyle) -> Result<Self> { if let Some(target) = bare_path_target(input, path_style) { return parse_hyperlink_path(target, path_style, DecodePercentEscapes::Yes) .with_context(|| format!("Invalid hyperlink path target: {input}")); } Self::parse(input, path_style) } /// Returns the literal (un-decoded) interpretation of a bare-path /// hyperlink target, for files whose names literally contain an escape /// sequence (e.g. `a%20b.rs`). Returns `None` when this wouldn't differ /// from [`MentionUri::parse_hyperlink`], including for URLs, whose /// escapes are unambiguous. pub fn parse_hyperlink_literal(input: &str, path_style: PathStyle) -> Option<Self> { let target = bare_path_target(input, path_style)?; let (path_input, _) = split_path_fragment(target); if !matches!(decode_path_escapes(path_input), Cow::Owned(_)) { return None; } parse_hyperlink_path(target, path_style, DecodePercentEscapes::No).ok() } /// The absolute path this mention refers to, if it refers to one. pub fn abs_path(&self) -> Option<&Path> { match self { MentionUri::File { abs_path } | MentionUri::Directory { abs_path } | MentionUri::Symbol { abs_path, .. } => Some(abs_path), MentionUri::Selection { abs_path, .. } => abs_path.as_deref(), MentionUri::Skill { skill_file_path, .. } => Some(skill_file_path), MentionUri::PastedImage { .. } | MentionUri::Thread { .. } | MentionUri::Rule { .. } | MentionUri::Diagnostics { .. } | MentionUri::Fetch { .. } | MentionUri::TerminalSelection { .. } | MentionUri::GitDiff { .. } | MentionUri::MergeConflict { .. } => None, } } pub fn name(&self) -> String { match self { MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path .file_name() .unwrap_or_default() .to_string_lossy() .into_owned(), MentionUri::PastedImage { name } => name.clone(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), MentionUri::Diagnostics { .. } => "Diagnostics".to_string(), MentionUri::TerminalSelection { line_count } => { if *line_count == 1 { "Terminal (1 line)".to_string() } else { format!("Terminal ({} lines)", line_count) } } MentionUri::GitDiff { base_ref } => format!("Branch Diff ({})", base_ref), MentionUri::MergeConflict { file_path } => { let name = Path::new(file_path) .file_name() .unwrap_or_default() .to_string_lossy(); format!("Merge Conflict ({name})") } MentionUri::Selection { abs_path: path, line_range, .. } => selection_name(path.as_deref(), line_range), MentionUri::Fetch { url } => url.to_string(), MentionUri::Skill { name, .. } => name.clone(), } } /// Returns a label for this mention at the given disambiguation `detail` /// level. `detail == 0` is the base name returned by [`Self::name`]; higher /// levels include progressively more context (e.g. additional parent path /// components for files, or the source for skills) until a fixed point is /// reached. Intended to be driven by [`util::disambiguate::compute_disambiguation_details`]. pub fn disambiguated_name(&self, detail: usize) -> String { if detail == 0 { return self.name(); } match self { MentionUri::Skill { name, source, .. } => { if source.is_empty() { // Must match `SkillSource::display_label()` in agent_skills. format!("{} (global)", name) } else { format!("{} ({})", name, source) } } MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => { project::path_suffix(abs_path, detail) } _ => self.name(), } } pub fn tooltip_text(&self) -> Option<SharedString> { match self { MentionUri::File { abs_path } | MentionUri::Directory { abs_path } => { Some(abs_path.to_string_lossy().into_owned().into()) } MentionUri::Symbol { abs_path, line_range, .. } => Some( format!( "{}:{}-{}", abs_path.display(), line_range.start(), line_range.end() ) .into(), ), MentionUri::Selection { abs_path: Some(path), line_range, .. } => Some( format!( "{}:{}-{}", path.display(), line_range.start(), line_range.end() ) .into(), ), MentionUri::Skill { skill_file_path, .. } => Some(skill_file_path.to_string_lossy().into_owned().into()), _ => None, } } pub fn icon_path(&self, cx: &mut App) -> SharedString { match self { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } MentionUri::PastedImage { .. } => IconName::Image.path().into(), MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), MentionUri::Thread { .. } => IconName::Thread.path().into(), MentionUri::Rule { .. } => IconName::Reader.path().into(), MentionUri::Diagnostics { .. } => IconName::Warning.path().into(), MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(), MentionUri::Selection { .. } => IconName::Reader.path().into(), MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), MentionUri::GitDiff { .. } => IconName::GitBranch.path().into(), MentionUri::MergeConflict { .. } => IconName::GitMergeConflict.path().into(), MentionUri::Skill { .. } => IconName::Sparkle.path().into(), } } pub fn as_link<'a>(&'a self) -> MentionLink<'a> { MentionLink(self) } pub fn to_uri(&self) -> Url { match self { MentionUri::File { abs_path } => { let mut url = Url::parse("file:///").unwrap(); url.set_path(&abs_path.to_string_lossy()); url } MentionUri::PastedImage { name } => { let mut url = Url::parse("zed:///agent/pasted-image").unwrap(); url.query_pairs_mut().append_pair("name", name); url } MentionUri::Directory { abs_path } => { let mut url = Url::parse("file:///").unwrap(); let mut path = abs_path.to_string_lossy().into_owned(); if !path.ends_with('/') && !path.ends_with('\\') { path.push('/'); } url.set_path(&path); url } MentionUri::Symbol { abs_path, name, line_range, .. } => { let mut url = Url::parse("file:///").unwrap(); url.set_path(&abs_path.to_string_lossy()); url.query_pairs_mut().append_pair("symbol", name); url.set_fragment(Some(&format!( "L{}:{}", line_range.start() + 1, line_range.end() + 1 ))); url } MentionUri::Selection { abs_path, line_range,