Fix clicking on file links in editor (#25117)

Closes #18641
Contributes: #13194

Release Notes:

- Open LSP documentation file links in Zed not the system opener
- Render completion documentation markdown consistently with
documentation markdown
This commit is contained in:
Conrad Irwin 2025-02-18 22:54:35 -07:00 committed by GitHub
parent ebbc6a9752
commit 1678e3cbf1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 353 additions and 352 deletions

View file

@ -1,7 +1,6 @@
pub mod parser;
use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{
actions, point, quad, AnyElement, App, Bounds, ClipboardItem, CursorStyle, DispatchPhase,
Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla,
@ -12,7 +11,7 @@ use gpui::{
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_links_only, parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
use std::{collections::HashMap, iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::{prelude::*, Tooltip};
use util::{ResultExt, TryFutureExt};
@ -49,7 +48,7 @@ impl Default for MarkdownStyle {
}
pub struct Markdown {
source: String,
source: SharedString,
selection: Selection,
pressed_link: Option<RenderedLink>,
autoscroll_request: Option<usize>,
@ -60,6 +59,7 @@ pub struct Markdown {
focus_handle: FocusHandle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
open_url: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,
options: Options,
}
@ -73,7 +73,7 @@ actions!(markdown, [Copy]);
impl Markdown {
pub fn new(
source: String,
source: SharedString,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
@ -97,13 +97,24 @@ impl Markdown {
parse_links_only: false,
copy_code_block_buttons: true,
},
open_url: None,
};
this.parse(window, cx);
this
}
pub fn open_url(
self,
open_url: impl Fn(SharedString, &mut Window, &mut App) + 'static,
) -> Self {
Self {
open_url: Some(Box::new(open_url)),
..self
}
}
pub fn new_text(
source: String,
source: SharedString,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
@ -127,6 +138,7 @@ impl Markdown {
parse_links_only: true,
copy_code_block_buttons: true,
},
open_url: None,
};
this.parse(window, cx);
this
@ -137,11 +149,11 @@ impl Markdown {
}
pub fn append(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
self.source.push_str(text);
self.source = SharedString::new(self.source.to_string() + text);
self.parse(window, cx);
}
pub fn reset(&mut self, source: String, window: &mut Window, cx: &mut Context<Self>) {
pub fn reset(&mut self, source: SharedString, window: &mut Window, cx: &mut Context<Self>) {
if source == self.source() {
return;
}
@ -176,17 +188,38 @@ impl Markdown {
return;
}
let text = self.source.clone();
let source = self.source.clone();
let parse_text_only = self.options.parse_links_only;
let language_registry = self.language_registry.clone();
let fallback = self.fallback_code_block_language.clone();
let parsed = cx.background_spawn(async move {
let text = SharedString::from(text);
let events = match parse_text_only {
true => Arc::from(parse_links_only(text.as_ref())),
false => Arc::from(parse_markdown(text.as_ref())),
};
if parse_text_only {
return anyhow::Ok(ParsedMarkdown {
events: Arc::from(parse_links_only(source.as_ref())),
source,
languages: HashMap::default(),
});
}
let (events, language_names) = parse_markdown(&source);
let mut languages = HashMap::with_capacity(language_names.len());
for name in language_names {
if let Some(registry) = language_registry.as_ref() {
let language = if !name.is_empty() {
registry.language_for_name(&name)
} else if let Some(fallback) = &fallback {
registry.language_for_name(fallback)
} else {
continue;
};
if let Ok(language) = language.await {
languages.insert(name, language);
}
}
}
anyhow::Ok(ParsedMarkdown {
source: text,
events,
source,
events: Arc::from(events),
languages,
})
});
@ -217,12 +250,7 @@ impl Markdown {
impl Render for Markdown {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
MarkdownElement::new(
cx.entity().clone(),
self.style.clone(),
self.language_registry.clone(),
self.fallback_code_block_language.clone(),
)
MarkdownElement::new(cx.entity().clone(), self.style.clone())
}
}
@ -270,6 +298,7 @@ impl Selection {
pub struct ParsedMarkdown {
source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>,
languages: HashMap<SharedString, Arc<Language>>,
}
impl ParsedMarkdown {
@ -285,61 +314,11 @@ impl ParsedMarkdown {
pub struct MarkdownElement {
markdown: Entity<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
impl MarkdownElement {
fn new(
markdown: Entity<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
) -> Self {
Self {
markdown,
style,
language_registry,
fallback_code_block_language,
}
}
fn load_language(
&self,
name: &str,
window: &mut Window,
cx: &mut App,
) -> Option<Arc<Language>> {
let language_test = self.language_registry.as_ref()?.language_for_name(name);
let language_name = match language_test.now_or_never() {
Some(Ok(_)) => String::from(name),
Some(Err(_)) if !name.is_empty() && self.fallback_code_block_language.is_some() => {
self.fallback_code_block_language.clone().unwrap()
}
_ => String::new(),
};
let language = self
.language_registry
.as_ref()?
.language_for_name(language_name.as_str())
.map(|language| language.ok())
.shared();
match language.clone().now_or_never() {
Some(language) => language,
None => {
let markdown = self.markdown.downgrade();
window
.spawn(cx, |mut cx| async move {
language.await;
markdown.update(&mut cx, |_, cx| cx.notify())
})
.detach_and_log_err(cx);
None
}
}
fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self {
Self { markdown, style }
}
fn paint_selection(
@ -452,7 +431,7 @@ impl MarkdownElement {
pending: true,
};
window.focus(&markdown.focus_handle);
window.prevent_default()
window.prevent_default();
}
cx.notify();
@ -492,11 +471,15 @@ impl MarkdownElement {
});
self.on_mouse_event(window, cx, {
let rendered_text = rendered_text.clone();
move |markdown, event: &MouseUpEvent, phase, _, cx| {
move |markdown, event: &MouseUpEvent, phase, window, cx| {
if phase.bubble() {
if let Some(pressed_link) = markdown.pressed_link.take() {
if Some(&pressed_link) == rendered_text.link_for_position(event.position) {
cx.open_url(&pressed_link.destination_url);
if let Some(open_url) = markdown.open_url.as_mut() {
open_url(pressed_link.destination_url, window, cx);
} else {
cx.open_url(&pressed_link.destination_url);
}
}
}
} else if markdown.selection.pending {
@ -617,7 +600,7 @@ impl Element for MarkdownElement {
}
MarkdownTag::CodeBlock(kind) => {
let language = if let CodeBlockKind::Fenced(language) = kind {
self.load_language(language.as_ref(), window, cx)
parsed_markdown.languages.get(language).cloned()
} else {
None
};