Selectable popover text (#12918)

Release Notes:

- Fixed #5236
- Added the ability to select and copy text from information popovers



https://github.com/zed-industries/zed/assets/50590465/d5c86623-342b-474b-913e-d07cc3f76de4

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Antonio <ascii@zed.dev>
This commit is contained in:
Ephram 2024-07-10 23:14:34 -04:00 committed by GitHub
parent f1281c14dd
commit 945764e409
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 592 additions and 396 deletions

View file

@ -1,16 +1,17 @@
mod parser;
pub mod parser;
use crate::parser::CodeBlockKind;
use futures::FutureExt;
use gpui::{
actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
TextStyleRefinement, View,
Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
Point, Render, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
TextStyle, TextStyleRefinement, View,
};
use language::{Language, LanguageRegistry, Rope};
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
use theme::SyntaxTheme;
use ui::prelude::*;
@ -18,7 +19,8 @@ use util::{ResultExt, TryFutureExt};
#[derive(Clone)]
pub struct MarkdownStyle {
pub code_block: TextStyleRefinement,
pub base_text_style: TextStyle,
pub code_block: StyleRefinement,
pub inline_code: TextStyleRefinement,
pub block_quote: TextStyleRefinement,
pub link: TextStyleRefinement,
@ -26,8 +28,27 @@ pub struct MarkdownStyle {
pub block_quote_border_color: Hsla,
pub syntax: Arc<SyntaxTheme>,
pub selection_background_color: Hsla,
pub break_style: StyleRefinement,
pub heading: StyleRefinement,
}
impl Default for MarkdownStyle {
fn default() -> Self {
Self {
base_text_style: Default::default(),
code_block: Default::default(),
inline_code: Default::default(),
block_quote: Default::default(),
link: Default::default(),
rule_color: Default::default(),
block_quote_border_color: Default::default(),
syntax: Arc::new(SyntaxTheme::default()),
selection_background_color: Default::default(),
break_style: Default::default(),
heading: Default::default(),
}
}
}
pub struct Markdown {
source: String,
selection: Selection,
@ -39,6 +60,7 @@ pub struct Markdown {
pending_parse: Option<Task<Option<()>>>,
focus_handle: FocusHandle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
actions!(markdown, [Copy]);
@ -49,6 +71,7 @@ impl Markdown {
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
cx: &mut ViewContext<Self>,
fallback_code_block_language: Option<String>,
) -> Self {
let focus_handle = cx.focus_handle();
let mut this = Self {
@ -62,6 +85,7 @@ impl Markdown {
pending_parse: None,
focus_handle,
language_registry,
fallback_code_block_language,
};
this.parse(cx);
this
@ -89,7 +113,14 @@ impl Markdown {
&self.source
}
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
&self.parsed_markdown
}
fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
if self.selection.end <= self.selection.start {
return;
}
let text = text.text_for_range(self.selection.start..self.selection.end);
cx.write_to_clipboard(ClipboardItem::new(text));
}
@ -140,6 +171,7 @@ impl Render for Markdown {
cx.view().clone(),
self.style.clone(),
self.language_registry.clone(),
self.fallback_code_block_language.clone(),
)
}
}
@ -185,11 +217,21 @@ impl Selection {
}
#[derive(Clone)]
struct ParsedMarkdown {
pub struct ParsedMarkdown {
source: SharedString,
events: Arc<[(Range<usize>, MarkdownEvent)]>,
}
impl ParsedMarkdown {
pub fn source(&self) -> &SharedString {
&self.source
}
pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
return &self.events;
}
}
impl Default for ParsedMarkdown {
fn default() -> Self {
Self {
@ -203,6 +245,7 @@ pub struct MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
impl MarkdownElement {
@ -210,19 +253,31 @@ impl MarkdownElement {
markdown: View<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, cx: &mut WindowContext) -> 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(name)
.language_for_name(language_name.as_str())
.map(|language| language.ok())
.shared();
@ -417,7 +472,7 @@ impl MarkdownElement {
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
let text_style = cx.text_style();
let text_style = self.style.base_text_style.clone();
let font_id = cx.text_system().resolve_font(&text_style.font());
let font_size = text_style.font_size.to_pixels(cx.rem_size());
let em_width = cx
@ -462,14 +517,26 @@ impl Element for MarkdownElement {
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
let mut builder = MarkdownElementBuilder::new(
self.style.base_text_style.clone(),
self.style.syntax.clone(),
);
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
let markdown_end = if let Some(last) = parsed_markdown.events.last() {
last.0.end
} else {
0
};
for (range, event) in parsed_markdown.events.iter() {
match event {
MarkdownEvent::Start(tag) => {
match tag {
MarkdownTag::Paragraph => {
builder.push_div(div().mb_2().line_height(rems(1.3)));
builder.push_div(
div().mb_2().line_height(rems(1.3)),
range,
markdown_end,
);
}
MarkdownTag::Heading { level, .. } => {
let mut heading = div().mb_2();
@ -480,7 +547,11 @@ impl Element for MarkdownElement {
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
_ => heading,
};
builder.push_div(heading);
heading.style().refine(&self.style.heading);
builder.push_text_style(
self.style.heading.text_style().clone().unwrap_or_default(),
);
builder.push_div(heading, range, markdown_end);
}
MarkdownTag::BlockQuote => {
builder.push_text_style(self.style.block_quote.clone());
@ -490,6 +561,8 @@ impl Element for MarkdownElement {
.mb_2()
.border_l_4()
.border_color(self.style.block_quote_border_color),
range,
markdown_end,
);
}
MarkdownTag::CodeBlock(kind) => {
@ -499,17 +572,18 @@ impl Element for MarkdownElement {
None
};
let mut d = div().w_full().rounded_lg();
d.style().refine(&self.style.code_block);
if let Some(code_block_text_style) = &self.style.code_block.text {
builder.push_text_style(code_block_text_style.to_owned());
}
builder.push_code_block(language);
builder.push_text_style(self.style.code_block.clone());
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
self.style.code_block.background_color,
|div, color| div.bg(color),
));
builder.push_div(d, range, markdown_end);
}
MarkdownTag::HtmlBlock => builder.push_div(div()),
MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
MarkdownTag::List(bullet_index) => {
builder.push_list(*bullet_index);
builder.push_div(div().pl_4());
builder.push_div(div().pl_4(), range, markdown_end);
}
MarkdownTag::Item => {
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
@ -525,9 +599,11 @@ impl Element for MarkdownElement {
.items_start()
.gap_1()
.child(bullet),
range,
markdown_end,
);
// Without `w_0`, text doesn't wrap to the width of the container.
builder.push_div(div().flex_1().w_0());
builder.push_div(div().flex_1().w_0(), range, markdown_end);
}
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
font_style: Some(FontStyle::Italic),
@ -552,6 +628,7 @@ impl Element for MarkdownElement {
builder.push_text_style(self.style.link.clone())
}
}
MarkdownTag::MetadataBlock(_) => {}
_ => log::error!("unsupported markdown tag {:?}", tag),
}
}
@ -559,7 +636,10 @@ impl Element for MarkdownElement {
MarkdownTagEnd::Paragraph => {
builder.pop_div();
}
MarkdownTagEnd::Heading(_) => builder.pop_div(),
MarkdownTagEnd::Heading(_) => {
builder.pop_div();
builder.pop_text_style()
}
MarkdownTagEnd::BlockQuote => {
builder.pop_text_style();
builder.pop_div()
@ -567,8 +647,10 @@ impl Element for MarkdownElement {
MarkdownTagEnd::CodeBlock => {
builder.trim_trailing_newline();
builder.pop_div();
builder.pop_text_style();
builder.pop_code_block();
if self.style.code_block.text.is_some() {
builder.pop_text_style();
}
}
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
MarkdownTagEnd::List(_) => {
@ -609,18 +691,24 @@ impl Element for MarkdownElement {
.border_b_1()
.my_2()
.border_color(self.style.rule_color),
range,
markdown_end,
);
builder.pop_div()
}
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
MarkdownEvent::HardBreak => {
let mut d = div().py_3();
d.style().refine(&self.style.break_style);
builder.push_div(d, range, markdown_end);
builder.pop_div()
}
_ => log::error!("unsupported markdown event {:?}", event),
}
}
let mut rendered_markdown = builder.build();
let child_layout_id = rendered_markdown.element.request_layout(cx);
let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
let layout_id = cx.request_layout(gpui::Style::default(), [child_layout_id]);
(layout_id, rendered_markdown)
}
@ -732,8 +820,32 @@ impl MarkdownElementBuilder {
self.text_style_stack.pop();
}
fn push_div(&mut self, div: Div) {
fn push_div(&mut self, mut div: Div, range: &Range<usize>, markdown_end: usize) {
self.flush_text();
if range.start == 0 {
//first element, remove top margin
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(px(0.).into())),
left: None,
right: None,
bottom: None,
},
..Default::default()
});
}
if range.end == markdown_end {
div.style().refine(&StyleRefinement {
margin: gpui::EdgesRefinement {
top: None,
left: None,
right: None,
bottom: Some(Length::Definite(rems(0.).into())),
},
..Default::default()
});
}
self.div_stack.push(div);
}