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:
parent
ebbc6a9752
commit
1678e3cbf1
16 changed files with 353 additions and 352 deletions
|
@ -20,7 +20,6 @@ test-support = [
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
linkify.workspace = true
|
||||
|
@ -34,7 +33,7 @@ util.workspace = true
|
|||
assets.workspace = true
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
languages.workspace = true
|
||||
languages = { workspace = true, features = ["load-grammars"] }
|
||||
node_runtime.workspace = true
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -15,84 +15,12 @@ const MARKDOWN_EXAMPLE: &str = r#"
|
|||
## Headings
|
||||
Headings are created by adding one or more `#` symbols before your heading text. The number of `#` you use will determine the size of the heading.
|
||||
|
||||
```rust
|
||||
gpui::window::ViewContext
|
||||
impl<'a, V> ViewContext<'a, V>
|
||||
pub fn on_blur(&mut self, handle: &FocusHandle, listener: impl FnMut(&mut V, &mut iewContext<V>) + 'static) -> Subscription
|
||||
where
|
||||
// Bounds from impl:
|
||||
V: 'static,
|
||||
```
|
||||
function a(b: T) {
|
||||
|
||||
## Emphasis
|
||||
Emphasis can be added with italics or bold. *This text will be italic*. _This will also be italic_
|
||||
|
||||
## Lists
|
||||
|
||||
### Unordered Lists
|
||||
Unordered lists use asterisks `*`, plus `+`, or minus `-` as list markers.
|
||||
|
||||
* Item 1
|
||||
* Item 2
|
||||
* Item 2a
|
||||
* Item 2b
|
||||
|
||||
### Ordered Lists
|
||||
Ordered lists use numbers followed by a period.
|
||||
|
||||
1. Item 1
|
||||
2. Item 2
|
||||
3. Item 3
|
||||
1. Item 3a
|
||||
2. Item 3b
|
||||
|
||||
## Links
|
||||
Links are created using the format [http://zed.dev](https://zed.dev).
|
||||
|
||||
They can also be detected automatically, for example https://zed.dev/blog.
|
||||
|
||||
They may contain dollar signs:
|
||||
|
||||
[https://svelte.dev/docs/svelte/$state](https://svelte.dev/docs/svelte/$state)
|
||||
|
||||
https://svelte.dev/docs/svelte/$state
|
||||
|
||||
## Images
|
||||
Images are like links, but with an exclamation mark `!` in front.
|
||||
|
||||
```markdown
|
||||

|
||||
```
|
||||
|
||||
## Code
|
||||
Inline `code` can be wrapped with backticks `` ` ``.
|
||||
|
||||
```markdown
|
||||
Inline `code` has `back-ticks around` it.
|
||||
```
|
||||
|
||||
Code blocks can be created by indenting lines by four spaces or with triple backticks ```.
|
||||
|
||||
```javascript
|
||||
function test() {
|
||||
console.log("notice the blank line before this function?");
|
||||
}
|
||||
```
|
||||
|
||||
## Blockquotes
|
||||
Blockquotes are created with `>`.
|
||||
|
||||
> This is a blockquote.
|
||||
|
||||
## Horizontal Rules
|
||||
Horizontal rules are created using three or more asterisks `***`, dashes `---`, or underscores `___`.
|
||||
|
||||
## Line breaks
|
||||
This is a
|
||||
\
|
||||
line break!
|
||||
|
||||
---
|
||||
|
||||
Remember, markdown processors may have slight differences and extensions, so always refer to the specific documentation or guides relevant to your platform or editor for the best practices and additional features.
|
||||
"#;
|
||||
|
@ -161,7 +89,7 @@ pub fn main() {
|
|||
};
|
||||
|
||||
MarkdownExample::new(
|
||||
MARKDOWN_EXAMPLE.to_string(),
|
||||
MARKDOWN_EXAMPLE.into(),
|
||||
markdown_style,
|
||||
language_registry,
|
||||
window,
|
||||
|
@ -179,14 +107,22 @@ struct MarkdownExample {
|
|||
|
||||
impl MarkdownExample {
|
||||
pub fn new(
|
||||
text: String,
|
||||
text: SharedString,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let markdown =
|
||||
cx.new(|cx| Markdown::new(text, style, Some(language_registry), None, window, cx));
|
||||
let markdown = cx.new(|cx| {
|
||||
Markdown::new(
|
||||
text,
|
||||
style,
|
||||
Some(language_registry),
|
||||
Some("TypeScript".to_string()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
Self { markdown }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -2,15 +2,16 @@ use gpui::SharedString;
|
|||
use linkify::LinkFinder;
|
||||
pub use pulldown_cmark::TagEnd as MarkdownTagEnd;
|
||||
use pulldown_cmark::{Alignment, HeadingLevel, LinkType, MetadataBlockKind, Options, Parser};
|
||||
use std::ops::Range;
|
||||
use std::{collections::HashSet, ops::Range};
|
||||
|
||||
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
pub fn parse_markdown(text: &str) -> (Vec<(Range<usize>, MarkdownEvent)>, HashSet<SharedString>) {
|
||||
let mut options = Options::all();
|
||||
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
|
||||
options.remove(pulldown_cmark::Options::ENABLE_YAML_STYLE_METADATA_BLOCKS);
|
||||
options.remove(pulldown_cmark::Options::ENABLE_MATH);
|
||||
|
||||
let mut events = Vec::new();
|
||||
let mut languages = HashSet::new();
|
||||
let mut within_link = false;
|
||||
let mut within_metadata = false;
|
||||
for (pulldown_event, mut range) in Parser::new_ext(text, options).into_offset_iter() {
|
||||
|
@ -27,6 +28,11 @@ pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
|||
match tag {
|
||||
pulldown_cmark::Tag::Link { .. } => within_link = true,
|
||||
pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = true,
|
||||
pulldown_cmark::Tag::CodeBlock(pulldown_cmark::CodeBlockKind::Fenced(
|
||||
ref language,
|
||||
)) => {
|
||||
languages.insert(SharedString::from(language.to_string()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
events.push((range, MarkdownEvent::Start(tag.into())))
|
||||
|
@ -102,7 +108,7 @@ pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
|||
pulldown_cmark::Event::InlineMath(_) | pulldown_cmark::Event::DisplayMath(_) => {}
|
||||
}
|
||||
}
|
||||
events
|
||||
(events, languages)
|
||||
}
|
||||
|
||||
pub fn parse_links_only(mut text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue