Compare commits

...

71 Commits

Author SHA1 Message Date
Conrad Irwin
d3903957c8 Merge branch 'main' into selectable-popover-text 2024-07-10 16:22:48 -06:00
effdotsh
97cfe3a4b7 reverted minor accidental code changes 2024-07-04 20:38:12 -04:00
effdotsh
36f044f48e updated language match 2024-07-04 19:58:37 -04:00
effdotsh
874c1f773b added language fallback to markdown rendering 2024-07-04 17:09:58 -04:00
effdotsh
67229ea673 fixed remoting dialogue color 2024-07-04 15:30:25 -04:00
effdotsh
0423ad2c43 fixed code block font not being respected 2024-07-04 14:46:42 -04:00
effdotsh
948f16e2e3 replaced Rems(*_f32) with rems(*.) 2024-07-04 13:50:58 -04:00
effdotsh
66dce927dc Merge branch 'selectable-popover-text' of code:effdotsh/zed into selectable-popover-text 2024-07-04 13:43:25 -04:00
effdotsh
b0edf0ec7b removed commented code 2024-07-04 13:42:56 -04:00
Ephram
840c6e414a Merge branch 'main' into selectable-popover-text 2024-07-01 11:46:38 -06:00
effdotsh
3ef6efbb9d cleaned up diffs 2024-06-26 21:35:55 -04:00
effdotsh
a15673f778 split painting of text glyphs and decorations 2024-06-26 21:20:26 -04:00
effdotsh
b6e1acbf71 paint selections on top 2024-06-26 20:45:52 -04:00
effdotsh
b41f02d11a ignore new anchors when there's a keyboard grace 2024-06-25 20:52:29 -04:00
effdotsh
f78a566073 added keyboard grace 2024-06-25 20:25:38 -04:00
effdotsh
1574eddb46 Merge remote-tracking branch 'upstream/main' into selectable-popover-text 2024-06-21 21:13:25 -04:00
effdotsh
2d3e2a4af0 problem is background color taking precedence, not forground disapearing 2024-06-21 21:13:21 -04:00
effdotsh
f4615f3b3a cleaner loop 2024-06-20 19:34:53 -04:00
effdotsh
c6269f549d added transform_block test 2024-06-20 16:45:00 -04:00
effdotsh
c5d20e1215 break stylerefinement 2024-06-19 17:00:08 -06:00
effdotsh
15c3cc14e0 div on markdown break 2024-06-19 16:44:22 -06:00
effdotsh
d640da2b77 fixed get_rendered_text test function 2024-06-18 10:51:24 -06:00
effdotsh
6becb8c859 updated link color/underline and inline code background to match current look 2024-06-17 16:53:31 -06:00
effdotsh
6aa53496c3 updated examples to match 2024-06-14 23:02:55 -06:00
effdotsh
ba19bccd66 replaced pad_blocks with 2024-06-14 20:18:32 -06:00
effdotsh
6fad3883cf base style as textstyle 2024-06-14 14:04:02 -06:00
effdotsh
c1881b44ab added base text refinement style 2024-06-14 13:35:36 -06:00
effdotsh
b3646b32b2 added paragraph transformer to remove random line breaks 2024-06-14 11:04:14 -06:00
effdotsh
89c04a79ae removed trim codeblocks 2024-06-14 00:06:47 -06:00
effdotsh
667d9825b6 added code demoing markdown errors 2024-06-13 23:38:45 -06:00
effdotsh
f83f69a450 fixed spacing 2024-06-13 22:27:04 -06:00
effdotsh
8e1103a916 fixed line trimming, add language to codeblock if not provided 2024-06-13 13:04:36 -06:00
effdotsh
c7db6c4bf3 pushing again because ci froze 2024-06-11 22:16:58 -04:00
effdotsh
e4f0cf526e more code quality changes 2024-06-11 21:57:11 -04:00
effdotsh
8ec6b00106 added let statement to appease clippy 2024-06-11 21:45:14 -04:00
effdotsh
37d62cf590 removed debugging prints 2024-06-11 21:27:02 -04:00
effdotsh
b87b4fcfdf removed prints 2024-06-11 19:32:28 -04:00
effdotsh
9097c9151b fixed trimmer from overtrimming when not around codeblocks 2024-06-11 19:31:23 -04:00
effdotsh
54e48cf7da fixed line trimming 2024-06-11 18:56:25 -04:00
effdotsh
89726f6216 moved get_rendered_text to testing only 2024-06-11 17:35:56 -04:00
effdotsh
2fd07bfa46 removed unused imports 2024-06-11 17:33:05 -04:00
effdotsh
23d924c33f pass all tests except leading whitespace 2024-06-11 17:23:36 -04:00
effdotsh
e7a9ba9cb3 get rendered text from info popover 2024-06-11 17:08:50 -04:00
effdotsh
4397b8c6fc started adapting tests, fixed select color 2024-06-11 16:03:18 -04:00
effdotsh
965f2dbadb reverted lines that shouldnt change 2024-06-10 23:24:48 -04:00
effdotsh
3ee62fdf0c fixed cmd c close on link edge case 2024-06-10 23:20:21 -04:00
effdotsh
8b921f87d6 added hover popover focused check 2024-06-10 22:46:40 -04:00
effdotsh
00f78b94fe cmd c from popovers works 2024-06-10 19:16:57 -04:00
effdotsh
58165d8492 removed incomplete func 2024-06-10 18:51:46 -04:00
effdotsh
e5dd34ed5d copy on selection 2024-06-10 17:26:40 -04:00
effdotsh
4605db7523 fixed all warnings 2024-06-07 17:29:50 -04:00
effdotsh
6f3250ba9e cleared all warnings 2024-06-07 17:27:47 -04:00
effdotsh
998bd35d6f proper popover padding 2024-06-07 17:21:30 -04:00
effdotsh
87eaa625de markdownstyle from theme 2024-06-07 16:14:15 -04:00
effdotsh
4b99a94470 the formatting is strange but it works! 2024-06-07 15:10:47 -04:00
effdotsh
a4be555dc9 FOUND THE CULPRIT LINE 2024-06-06 20:11:06 -04:00
effdotsh
a06eb30f8d added background color for debugging 2024-06-06 19:53:33 -04:00
effdotsh
14a0a130f3 added debugging comment 2024-06-06 19:46:09 -04:00
effdotsh
e0b46a5060 removed middle object 2024-06-06 17:53:32 -04:00
effdotsh
8cea761693 no comprendo 2024-06-06 17:42:23 -04:00
effdotsh
e2087a0a84 it works... once 2024-06-06 15:32:14 -04:00
effdotsh
a901f9976b new markdown appears in popover 2024-06-06 14:27:22 -04:00
effdotsh
bfc7932024 Smaller window 2024-06-06 03:04:29 -04:00
effdotsh
1e825ffea5 second child appears 2024-06-06 02:43:31 -04:00
effdotsh
511463f574 new window on popover 2024-06-06 02:29:07 -04:00
effdotsh
7dc6ba4d66 added markdown as child demo 2024-06-06 02:07:36 -04:00
effdotsh
8781db6b68 merged 2024-06-06 00:08:09 -04:00
effdotsh
77809989aa white box popover 2024-06-05 19:39:07 -04:00
effdotsh
3125c98bb9 popover mousedown demo 2024-06-05 17:45:24 -04:00
effdotsh
8d1d06f0b3 import both markdowns 2024-06-05 17:13:03 -04:00
effdotsh
f52d4520e9 copy popover on click 2024-06-05 14:19:57 -04:00
11 changed files with 724 additions and 431 deletions

1
Cargo.lock generated
View File

@@ -3605,6 +3605,7 @@ dependencies = [
"linkify",
"log",
"lsp",
"markdown",
"multi_buffer",
"ordered-float 2.10.0",
"parking_lot",

View File

@@ -49,6 +49,7 @@ lazy_static.workspace = true
linkify.workspace = true
log.workspace = true
lsp.workspace = true
markdown.workspace = true
multi_buffer.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true

View File

@@ -2359,7 +2359,9 @@ impl Editor {
drop(context_menu);
}
hide_hover(self, cx);
if !self.hover_state.focused(cx) {
hide_hover(self, cx);
}
if old_cursor_position.to_display_point(&display_map).row()
!= new_cursor_position.to_display_point(&display_map).row()
@@ -2862,8 +2864,10 @@ impl Editor {
return true;
}
if hide_hover(self, cx) {
return true;
if !self.hover_state.focused(cx) {
if hide_hover(self, cx) {
return true;
}
}
if self.hide_context_menu(cx).is_some() {
@@ -11604,8 +11608,11 @@ impl Editor {
if let Some(blame) = self.blame.as_ref() {
blame.update(cx, GitBlame::blur)
}
if !self.hover_state.focused(cx) {
hide_hover(self, cx);
}
self.hide_context_menu(cx);
hide_hover(self, cx);
cx.emit(EditorEvent::Blurred);
cx.notify();
}

View File

@@ -423,7 +423,9 @@ pub fn update_inlay_link_and_hover_points(
editor.hide_hovered_link(cx)
}
if !hover_updated {
hover_popover::hover_at(editor, None, cx);
if !editor.hover_state.focused(cx) {
hover_popover::hover_at(editor, None, cx);
}
}
}

View File

@@ -5,24 +5,24 @@ use crate::{
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
EditorStyle, Hover, RangeToAnchorExt,
};
use futures::{stream::FuturesUnordered, FutureExt};
use gpui::{
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
Task, ViewContext, WeakView,
div, px, AnyElement, AsyncWindowContext, CursorStyle, Hsla, InteractiveElement, IntoElement,
Length, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
StatefulInteractiveElement, Styled, Task, View, ViewContext, WeakView,
};
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
use language::{DiagnosticEntry, Language, LanguageRegistry};
use lsp::DiagnosticSeverity;
use markdown::{Markdown, MarkdownStyle};
use multi_buffer::ToOffset;
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
use project::{HoverBlock, InlayHintLabelPart};
use settings::Settings;
use smol::stream::StreamExt;
use std::cell::RefCell;
use std::rc::Rc;
use std::{ops::Range, sync::Arc, time::Duration};
use theme::ThemeSettings;
use ui::{prelude::*, window_is_transparent, Tooltip};
use util::TryFutureExt;
use workspace::Workspace;
pub const HOVER_DELAY_MILLIS: u64 = 350;
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
@@ -40,14 +40,31 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
/// depending on whether a point to hover over is provided.
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
if EditorSettings::get_global(cx).hover_popover_enabled {
if let Some(anchor) = anchor {
show_hover(editor, anchor, false, cx);
} else {
hide_hover(editor, cx);
let old_hover_shown = show_old_hover(editor, cx);
if !old_hover_shown {
if let Some(anchor) = anchor {
show_hover(editor, anchor, false, cx);
} else {
hide_hover(editor, cx);
}
}
}
}
pub fn show_old_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
let info_popovers = editor.hover_state.info_popovers.clone();
for p in info_popovers {
let keyboard_grace = p.keyboard_grace.borrow();
if *keyboard_grace {
if let Some(anchor) = p.anchor {
show_hover(editor, anchor, false, cx);
return true;
}
}
}
return false;
}
pub struct InlayHover {
pub range: InlayHighlight,
pub tooltip: HoverBlock,
@@ -113,12 +130,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
let blocks = vec![inlay_hover.tooltip];
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
let hover_popover = InfoPopover {
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
parsed_content,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(false)),
anchor: None,
};
this.update(&mut cx, |this, cx| {
@@ -291,39 +310,40 @@ fn show_hover(
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
let mut info_popovers = Vec::with_capacity(hovers_response.len());
let mut info_popover_tasks = hovers_response
.into_iter()
.map(|hover_result| async {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = hover_result
.range
.and_then(|range| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)?;
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
Some(start..end)
})
.unwrap_or_else(|| anchor..anchor);
for hover_result in hovers_response {
// Create symbol range of anchors for highlighting and filtering of future requests.
let range = hover_result
.range
.and_then(|range| {
let start = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.start)?;
let end = snapshot
.buffer_snapshot
.anchor_in_excerpt(excerpt_id, range.end)?;
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
Some(start..end)
})
.unwrap_or_else(|| anchor..anchor);
(
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
},
)
})
.collect::<FuturesUnordered<_>>();
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
let blocks = hover_result.contents;
let language = hover_result.language;
let parsed_content =
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
info_popover_tasks.push((
range.clone(),
InfoPopover {
symbol_range: RangeInEditor::Text(range),
parsed_content,
scroll_handle: ScrollHandle::new(),
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
anchor: Some(anchor),
},
));
}
for (highlight_range, info_popover) in info_popover_tasks {
hover_highlights.push(highlight_range);
info_popovers.push(info_popover);
}
@@ -353,76 +373,122 @@ fn show_hover(
editor.hover_state.info_task = Some(task);
}
fn transform_codeblock(mut input: String) -> String {
input = input.replace("\\n", "\n").trim().to_string();
let lines: Vec<&str> = input.lines().collect();
let mut result: Vec<String> = Vec::new();
let mut i = 0;
let mut open_block = false;
while i < lines.len() {
let mut line = lines[i].to_string();
if line.starts_with("```") {
open_block = !open_block;
} else {
//remove mid-sentence single linebreaks
while i + 1 < lines.len()
&& !lines[i + 1].trim().is_empty()
&& !open_block
&& !lines[i + 1].contains("```")
{
i += 1;
if line.contains('\n') {
break;
}
line.push(' ');
line.push_str(lines[i]);
}
}
result.push(line);
i += 1;
}
let r = result.join("\n");
r
}
async fn parse_blocks(
blocks: &[HoverBlock],
language_registry: &Arc<LanguageRegistry>,
language: Option<Arc<Language>>,
) -> markdown::ParsedMarkdown {
let mut text = String::new();
let mut highlights = Vec::new();
let mut region_ranges = Vec::new();
let mut regions = Vec::new();
cx: &mut AsyncWindowContext,
) -> Option<View<Markdown>> {
let mut combined_text = String::new();
let fallback_language_name = if let Some(ref l) = language {
let l = Arc::clone(l);
Some(l.lsp_id().clone())
} else {
None
};
for block in blocks {
match &block.kind {
HoverBlockKind::PlainText => {
markdown::new_paragraph(&mut text, &mut Vec::new());
text.push_str(&block.text.replace("\\n", "\n"));
}
let text = transform_codeblock(block.clone().text);
HoverBlockKind::Markdown => {
markdown::parse_markdown_block(
&block.text.replace("\\n", "\n"),
language_registry,
language.clone(),
&mut text,
&mut highlights,
&mut region_ranges,
&mut regions,
)
.await
}
HoverBlockKind::Code { language } => {
if let Some(language) = language_registry
.language_for_name(language)
.now_or_never()
.and_then(Result::ok)
{
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
} else {
text.push_str(&block.text);
}
}
}
combined_text.push_str(text.as_str());
}
let rendered_block = cx
.new_view(|cx| {
let settings = ThemeSettings::get_global(cx);
let buffer_font_family = settings.buffer_font.family.clone();
let mut base_style = cx.text_style();
base_style.refine(&gpui::TextStyleRefinement {
font_family: Some(buffer_font_family.clone()),
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
});
let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
if leading_space > 0 {
highlights = highlights
.into_iter()
.map(|(range, style)| {
(
range.start.saturating_sub(leading_space)
..range.end.saturating_sub(leading_space),
style,
)
})
.collect();
region_ranges = region_ranges
.into_iter()
.map(|range| {
range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
})
.collect();
}
let markdown_style = MarkdownStyle {
base_text_style: base_style,
code_block: gpui::StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some(buffer_font_family.clone()),
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
}),
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(rems(1.).into())),
left: None,
right: None,
bottom: Some(Length::Definite(rems(1.).into())),
},
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some(buffer_font_family.clone()),
background_color: Some(cx.theme().colors().background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
font_family: Some(buffer_font_family.clone()),
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(cx.theme().colors().editor_foreground),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(cx.theme().colors().editor_foreground),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: { cx.theme().players().local().selection },
break_style: Default::default(),
};
ParsedMarkdown {
text: text.trim().to_string(),
highlights,
region_ranges,
regions,
}
Markdown::new(
combined_text,
markdown_style.clone(),
Some(language_registry.clone()),
cx,
fallback_language_name,
)
})
.ok();
rendered_block
}
#[derive(Default, Debug)]
@@ -444,7 +510,7 @@ impl HoverState {
style: &EditorStyle,
visible_rows: Range<DisplayRow>,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
_workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
// If there is a diagnostic, position the popovers based on that.
@@ -482,29 +548,39 @@ impl HoverState {
elements.push(diagnostic_popover.render(style, max_size, cx));
}
for info_popover in &mut self.info_popovers {
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
elements.push(info_popover.render(max_size, cx));
}
Some((point, elements))
}
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
let mut hover_popover_is_focused = false;
for info_popover in &self.info_popovers {
for markdown_view in &info_popover.parsed_content {
if markdown_view.focus_handle(cx).is_focused(cx) {
hover_popover_is_focused = true;
}
}
}
return hover_popover_is_focused;
}
}
#[derive(Clone, Debug)]
#[derive(Debug, Clone)]
pub struct InfoPopover {
pub symbol_range: RangeInEditor,
pub parsed_content: ParsedMarkdown,
pub parsed_content: Option<View<Markdown>>,
pub scroll_handle: ScrollHandle,
pub keyboard_grace: Rc<RefCell<bool>>,
pub anchor: Option<Anchor>,
}
impl InfoPopover {
pub fn render(
&mut self,
style: &EditorStyle,
max_size: Size<Pixels>,
workspace: Option<WeakView<Workspace>>,
cx: &mut ViewContext<Editor>,
) -> AnyElement {
div()
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
let keyboard_grace = Rc::clone(&self.keyboard_grace);
let mut d = div()
.id("info_popover")
.elevation_2(cx)
.overflow_y_scroll()
@@ -514,15 +590,17 @@ impl InfoPopover {
// Prevent a mouse down/move on the popover from being propagated to the editor,
// because that would dismiss the popover.
.on_mouse_move(|_, cx| cx.stop_propagation())
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
.child(div().p_2().child(crate::render_parsed_markdown(
"content",
&self.parsed_content,
style,
workspace,
cx,
)))
.into_any_element()
.on_mouse_down(MouseButton::Left, move |_, cx| {
let mut keyboard_grace = keyboard_grace.borrow_mut();
*keyboard_grace = false;
cx.stop_propagation();
})
.p_2();
if let Some(markdown) = &self.parsed_content {
d = d.child(markdown.clone());
}
d.into_any_element()
}
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
@@ -642,17 +720,33 @@ mod tests {
InlayId, PointForPosition,
};
use collections::BTreeSet;
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
use indoc::indoc;
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
use lsp::LanguageServerId;
use project::{HoverBlock, HoverBlockKind};
use markdown::parser::MarkdownEvent;
use smol::stream::StreamExt;
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
use text::Bias;
use unindent::Unindent;
use util::test::marked_text_ranges;
impl InfoPopover {
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
let mut rendered_text = String::new();
if let Some(parsed_content) = self.parsed_content.clone() {
let markdown = parsed_content.read(cx);
let text = markdown.parsed_markdown().source().to_string();
let data = markdown.parsed_markdown().events();
let slice = data;
for (range, event) in slice.iter() {
if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
rendered_text.push_str(&text[range.clone()])
}
}
}
rendered_text
}
}
#[gpui::test]
async fn test_mouse_hover_info_popover_with_autocomplete_popover(
@@ -736,7 +830,7 @@ mod tests {
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -744,14 +838,13 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// check that the completion menu is still visible and that there still has only been 1 completion request
@@ -777,7 +870,7 @@ mod tests {
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
//verify the information popover is still visible and unchanged
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -785,14 +878,14 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// Mouse moved with no hover response dismisses
@@ -870,7 +963,7 @@ mod tests {
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
requests.next().await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert!(editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
@@ -878,14 +971,14 @@ mod tests {
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some basic docs".to_string())
});
// Mouse moved with no hover response dismisses
@@ -931,34 +1024,49 @@ mod tests {
let symbol_range = cx.lsp_range(indoc! {"
«fn» test() { println!(); }
"});
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some other basic docs".to_string(),
}),
range: Some(symbol_range),
}))
})
.next()
.await;
cx.editor(|editor, _cx| {
assert!(!editor.hover_state.visible());
assert_eq!(
editor.hover_state.info_popovers.len(),
0,
"Expected no hovers but got but got: {:?}",
editor.hover_state.info_popovers
);
});
let mut requests =
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
Ok(Some(lsp::Hover {
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: "some other basic docs".to_string(),
}),
range: Some(symbol_range),
}))
});
requests.next().await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
assert_eq!(rendered.text, "some other basic docs".to_string())
.get_rendered_text(cx);
assert_eq!(rendered_text, "some other basic docs".to_string())
});
}
@@ -998,30 +1106,38 @@ mod tests {
})
.next()
.await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
.get_rendered_text(cx);
assert_eq!(
rendered.text,
rendered_text,
"regular text for hover to show".to_string(),
"No empty string hovers should be shown"
);
});
}
#[gpui::test]
async fn test_transform_info_popover_markdown(_cx: &mut gpui::TestAppContext) {
//test removing middle of the block line-breaks
let input =String::from( "```rust\nfn\n```\n\n---\n\nA function or function pointer.\n\nFunctions are the primary way code is executed within Rust. Function blocks, usually just\ncalled functions, can be defined in a variety of different places and be assigned many\ndifferent attributes and modifiers.\n\nStandalone functions that just sit within a module not attached to anything else are common,\nbut most functions will end up being inside [`impl`](https://doc.rust-lang.org/stable/std/keyword.impl.html) blocks, either on another type itself, or\nas a trait impl for that type.\n\n```rust\nfn standalone_function() {\n // code\n}\n\npub fn public_thing(argument: bool) -> String {\n // code\n}\n\nstruct Thing {\n foo: i32,\n}\n\nimpl Thing {\n pub fn new() -> Self {\n Self {\n foo: 42,\n }\n }\n}\n```\n\nIn addition to presenting fixed types in the form of `fn name(arg: type, ..) -> return_type`,\nfunctions can also declare a list of type parameters along with trait bounds that they fall\ninto.\n\n```rust\nfn generic_function<T: Clone>(x: T) -> (T, T, T) {\n (x.clone(), x.clone(), x.clone())\n}\n\nfn generic_where<T>(x: T) -> T\n where T: std::ops::Add<Output = T> + Copy\n{\n x + x + x\n}\n```\n\nDeclaring trait bounds in the angle brackets is functionally identical to using a `where`\nclause. It's up to the programmer to decide which works better in each situation, but `where`\ntends to be better when things get longer than one line.\n\nAlong with being made public via `pub`, `fn` can also have an [`extern`](https://doc.rust-lang.org/stable/std/keyword.extern.html) added for use in\nFFI.\n\nFor more information on the various types of functions and how they're used, consult the [Rust\nbook](https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html) or the [Reference](https://doc.rust-lang.org/stable/reference/items/functions.html).");
let target_output = String::from("```rust\nfn\n```\n ---\n A function or function pointer.\n Functions are the primary way code is executed within Rust. Function blocks, usually just called functions, can be defined in a variety of different places and be assigned many different attributes and modifiers.\n Standalone functions that just sit within a module not attached to anything else are common, but most functions will end up being inside [`impl`](https://doc.rust-lang.org/stable/std/keyword.impl.html) blocks, either on another type itself, or as a trait impl for that type.\n\n```rust\nfn standalone_function() {\n // code\n}\n\npub fn public_thing(argument: bool) -> String {\n // code\n}\n\nstruct Thing {\n foo: i32,\n}\n\nimpl Thing {\n pub fn new() -> Self {\n Self {\n foo: 42,\n }\n }\n}\n```\n In addition to presenting fixed types in the form of `fn name(arg: type, ..) -> return_type`, functions can also declare a list of type parameters along with trait bounds that they fall into.\n\n```rust\nfn generic_function<T: Clone>(x: T) -> (T, T, T) {\n (x.clone(), x.clone(), x.clone())\n}\n\nfn generic_where<T>(x: T) -> T\n where T: std::ops::Add<Output = T> + Copy\n{\n x + x + x\n}\n```\n Declaring trait bounds in the angle brackets is functionally identical to using a `where` clause. It's up to the programmer to decide which works better in each situation, but `where` tends to be better when things get longer than one line.\n Along with being made public via `pub`, `fn` can also have an [`extern`](https://doc.rust-lang.org/stable/std/keyword.extern.html) added for use in FFI.\n For more information on the various types of functions and how they're used, consult the [Rust book](https://doc.rust-lang.org/stable/book/ch03-03-how-functions-work.html) or the [Reference](https://doc.rust-lang.org/stable/reference/items/functions.html).");
let output = transform_codeblock(input);
assert_eq!(output, target_output);
}
#[gpui::test]
async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -1063,24 +1179,25 @@ mod tests {
.next()
.await;
cx.dispatch_action(Hover);
cx.condition(|editor, _| editor.hover_state.visible()).await;
cx.editor(|editor, _| {
cx.editor(|editor, cx| {
assert_eq!(
editor.hover_state.info_popovers.len(),
1,
"Expected exactly one hover but got: {:?}",
editor.hover_state.info_popovers
);
let rendered = editor
let rendered_text = editor
.hover_state
.info_popovers
.first()
.cloned()
.unwrap()
.parsed_content;
.get_rendered_text(cx);
assert_eq!(
rendered.text,
code_str.trim(),
rendered_text, code_str,
"Should not have extra line breaks at end of rendered hover"
);
});
@@ -1156,153 +1273,6 @@ mod tests {
});
}
#[gpui::test]
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
let editor = cx.add_window(|cx| Editor::single_line(cx));
editor
.update(cx, |editor, _cx| {
let style = editor.style.clone().unwrap();
struct Row {
blocks: Vec<HoverBlock>,
expected_marked_text: String,
expected_styles: Vec<HighlightStyle>,
}
let rows = &[
// Strong emphasis
Row {
blocks: vec![HoverBlock {
text: "one **two** three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
font_weight: Some(FontWeight::BOLD),
..Default::default()
}],
},
// Links
Row {
blocks: vec![HoverBlock {
text: "one [two](https://the-url) three".to_string(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "one «two» three".to_string(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Lists
Row {
blocks: vec![HoverBlock {
text: "
lists:
* one
- a
- b
* two
- [c](https://the-url)
- d"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
lists:
- one
- a
- b
- two
- «c»
- d"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
// Multi-paragraph list items
Row {
blocks: vec![HoverBlock {
text: "
* one two
three
* four five
* six seven
eight
nine
* ten
* six"
.unindent(),
kind: HoverBlockKind::Markdown,
}],
expected_marked_text: "
- one two three
- four five
- six seven eight
nine
- ten
- six"
.unindent(),
expected_styles: vec![HighlightStyle {
underline: Some(UnderlineStyle {
thickness: 1.0.into(),
..Default::default()
}),
..Default::default()
}],
},
];
for Row {
blocks,
expected_marked_text,
expected_styles,
} in &rows[0..]
{
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
let expected_highlights = ranges
.into_iter()
.zip(expected_styles.iter().cloned())
.collect::<Vec<_>>();
assert_eq!(
rendered.text, expected_text,
"wrong text for input {blocks:?}"
);
let rendered_highlights: Vec<_> = rendered
.highlights
.iter()
.filter_map(|(range, highlight)| {
let highlight = highlight.to_highlight_style(&style.syntax)?;
Some((range.clone(), highlight))
})
.collect();
assert_eq!(
rendered_highlights, expected_highlights,
"wrong highlights for input {blocks:?}"
);
}
})
.unwrap();
}
#[gpui::test]
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
init_test(cx, |settings| {
@@ -1546,9 +1516,8 @@ mod tests {
"Popover range should match the new type label part"
);
assert_eq!(
popover.parsed_content.text,
format!("A tooltip for `{new_type_label}`"),
"Rendered text should not anyhow alter backticks"
popover.get_rendered_text(cx),
format!("A tooltip for {new_type_label}"),
);
});
@@ -1602,7 +1571,7 @@ mod tests {
"Popover range should match the struct label part"
);
assert_eq!(
popover.parsed_content.text,
popover.get_rendered_text(cx),
format!("A tooltip for {struct_label}"),
"Rendered markdown element should remove backticks from text"
);

View File

@@ -108,6 +108,32 @@ fn paint_line(
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext,
) -> Result<()> {
paint_line_decorations(
origin,
layout,
line_height,
decoration_runs,
wrap_boundaries,
cx,
)?;
paint_line_glyphs(
origin,
layout,
line_height,
decoration_runs,
wrap_boundaries,
cx,
)?;
Ok(())
}
fn paint_line_decorations(
origin: Point<Pixels>,
layout: &LineLayout,
line_height: Pixels,
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext,
) -> Result<()> {
let line_bounds = Bounds::new(origin, size(layout.width, line_height));
cx.paint_layer(line_bounds, |cx| {
@@ -116,16 +142,12 @@ fn paint_line(
let mut decoration_runs = decoration_runs.iter();
let mut wraps = wrap_boundaries.iter().peekable();
let mut run_end = 0;
let mut color = black();
let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
let text_system = cx.text_system().clone();
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
for (run_ix, run) in layout.runs.iter().enumerate() {
let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
@@ -224,7 +246,6 @@ fn paint_line(
}
run_end += style_run.len as usize;
color = style_run.color;
} else {
run_end = layout.len;
finished_background = current_background.take();
@@ -258,31 +279,6 @@ fn paint_line(
&strikethrough_style,
);
}
let max_glyph_bounds = Bounds {
origin: glyph_origin,
size: max_glyph_size,
};
let content_mask = cx.content_mask();
if max_glyph_bounds.intersects(&content_mask.bounds) {
if glyph.is_emoji {
cx.paint_emoji(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
)?;
} else {
cx.paint_glyph(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
color,
)?;
}
}
}
}
@@ -322,3 +318,73 @@ fn paint_line(
Ok(())
})
}
fn paint_line_glyphs(
origin: Point<Pixels>,
layout: &LineLayout,
line_height: Pixels,
decoration_runs: &[DecorationRun],
wrap_boundaries: &[WrapBoundary],
cx: &mut WindowContext,
) -> Result<()> {
let line_bounds = Bounds::new(origin, size(layout.width, line_height));
cx.paint_layer(line_bounds, |cx| {
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
let baseline_offset = point(px(0.), padding_top + layout.ascent);
let mut decoration_runs = decoration_runs.iter();
let mut wraps = wrap_boundaries.iter().peekable();
let mut run_end = 0;
let mut color = black();
let text_system = cx.text_system().clone();
let mut glyph_origin = origin;
let mut prev_glyph_position = Point::default();
for (run_ix, run) in layout.runs.iter().enumerate() {
let max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
glyph_origin.x += glyph.position.x - prev_glyph_position.x;
if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
wraps.next();
glyph_origin.x = origin.x;
glyph_origin.y += line_height;
}
prev_glyph_position = glyph.position;
if glyph.index >= run_end {
if let Some(style_run) = decoration_runs.next() {
run_end += style_run.len as usize;
color = style_run.color;
} else {
run_end = layout.len;
}
}
let max_glyph_bounds = Bounds {
origin: glyph_origin,
size: max_glyph_size,
};
let content_mask = cx.content_mask();
if max_glyph_bounds.intersects(&content_mask.bounds) {
if glyph.is_emoji {
cx.paint_emoji(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
)?;
} else {
cx.paint_glyph(
glyph_origin + baseline_offset,
run.font_id,
glyph.id,
layout.font_size,
color,
)?;
}
}
}
}
Ok(())
})
}

View File

@@ -1,5 +1,5 @@
use assets::Assets;
use gpui::{prelude::*, App, KeyBinding, Task, View, WindowOptions};
use gpui::{prelude::*, App, KeyBinding, Length, StyleRefinement, Task, View, WindowOptions};
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime;
@@ -105,44 +105,58 @@ pub fn main() {
cx.activate(true);
cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| {
let markdown_style = MarkdownStyle {
base_text_style: gpui::TextStyle {
font_family: "Zed Plex Mono".into(),
color: cx.theme().colors().terminal_ansi_black,
..Default::default()
},
code_block: StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
..Default::default()
}),
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(rems(4.).into())),
left: Some(Length::Definite(rems(4.).into())),
right: Some(Length::Definite(rems(4.).into())),
bottom: Some(Length::Definite(rems(4.).into())),
},
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
break_style: Default::default(),
};
MarkdownExample::new(
MARKDOWN_EXAMPLE.to_string(),
MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
// @nate: Could we add inline-code specific styles to the theme?
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
},
markdown_style,
language_registry,
cx,
)
@@ -163,7 +177,8 @@ impl MarkdownExample {
language_registry: Arc<LanguageRegistry>,
cx: &mut WindowContext,
) -> Self {
let markdown = cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx));
let markdown =
cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None));
Self { markdown }
}
}

View File

@@ -0,0 +1,119 @@
use assets::Assets;
use gpui::*;
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
use markdown::{Markdown, MarkdownStyle};
use node_runtime::FakeNodeRuntime;
use settings::SettingsStore;
use std::sync::Arc;
use theme::LoadThemes;
use ui::div;
use ui::prelude::*;
const MARKDOWN_EXAMPLE: &'static str = r#"
this text should be selectable
wow so cool
## Heading 2
"#;
pub fn main() {
env_logger::init();
App::new().with_assets(Assets).run(|cx| {
let store = SettingsStore::test(cx);
cx.set_global(store);
language::init(cx);
SettingsStore::update(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
});
cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
let node_runtime = FakeNodeRuntime::new();
let language_registry = Arc::new(LanguageRegistry::new(
Task::ready(()),
cx.background_executor().clone(),
));
languages::init(language_registry.clone(), node_runtime, cx);
theme::init(LoadThemes::JustBase, cx);
Assets.load_fonts(cx).unwrap();
cx.activate(true);
let _ = cx.open_window(WindowOptions::default(), |cx| {
cx.new_view(|cx| {
let markdown_style = MarkdownStyle {
base_text_style: gpui::TextStyle {
font_family: "Zed Mono".into(),
color: cx.theme().colors().text,
..Default::default()
},
code_block: StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
}),
margin: gpui::EdgesRefinement {
top: Some(Length::Definite(rems(4.).into())),
left: Some(Length::Definite(rems(4.).into())),
right: Some(Length::Definite(rems(4.).into())),
bottom: Some(Length::Definite(rems(4.).into())),
},
..Default::default()
},
inline_code: gpui::TextStyleRefinement {
font_family: Some("Zed Mono".into()),
background_color: Some(cx.theme().colors().editor_background),
..Default::default()
},
rule_color: Color::Muted.color(cx),
block_quote_border_color: Color::Muted.color(cx),
block_quote: gpui::TextStyleRefinement {
color: Some(Color::Muted.color(cx)),
..Default::default()
},
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
underline: Some(gpui::UnderlineStyle {
thickness: px(1.),
color: Some(Color::Accent.color(cx)),
wavy: false,
}),
..Default::default()
},
syntax: cx.theme().syntax().clone(),
selection_background_color: {
let mut selection = cx.theme().players().local().selection;
selection.fade_out(0.7);
selection
},
break_style: Default::default(),
};
let markdown = cx.new_view(|cx| {
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None)
});
HelloWorld { markdown }
})
});
});
}
struct HelloWorld {
markdown: View<Markdown>,
}
impl Render for HelloWorld {
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.flex()
.bg(rgb(0x2e7d32))
.size(Length::Definite(Pixels(700.0).into()))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(div().child(self.markdown.clone()).p_20())
}
}

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,25 @@ pub struct MarkdownStyle {
pub block_quote_border_color: Hsla,
pub syntax: Arc<SyntaxTheme>,
pub selection_background_color: Hsla,
pub break_style: 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(),
}
}
}
pub struct Markdown {
source: String,
selection: Selection,
@@ -39,6 +58,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 +69,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 +83,7 @@ impl Markdown {
pending_parse: None,
focus_handle,
language_registry,
fallback_code_block_language,
};
this.parse(cx);
this
@@ -89,7 +111,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 +169,7 @@ impl Render for Markdown {
cx.view().clone(),
self.style.clone(),
self.language_registry.clone(),
self.fallback_code_block_language.clone(),
)
}
}
@@ -185,11 +215,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 +243,7 @@ pub struct MarkdownElement {
markdown: View<Markdown>,
style: MarkdownStyle,
language_registry: Option<Arc<LanguageRegistry>>,
fallback_code_block_language: Option<String>,
}
impl MarkdownElement {
@@ -210,19 +251,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 +470,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 +515,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 +545,7 @@ impl Element for MarkdownElement {
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
_ => heading,
};
builder.push_div(heading);
builder.push_div(heading, range, markdown_end);
}
MarkdownTag::BlockQuote => {
builder.push_text_style(self.style.block_quote.clone());
@@ -490,6 +555,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 +566,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 +593,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),
@@ -567,8 +637,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 +681,29 @@ 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 => {
let mut d = div().py_3();
d.style().refine(&self.style.break_style.clone());
builder.push_div(d, range, markdown_end);
builder.pop_div()
}
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 +815,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);
}

View File

@@ -5801,7 +5801,7 @@ impl Project {
.await
.into_iter()
.filter_map(|hover| remove_empty_hover_blocks(hover?))
.collect()
.collect::<Vec<Hover>>()
})
} else if let Some(project_id) = self.remote_id() {
let request_task = self.client().request(proto::MultiLspQuery {

View File

@@ -114,25 +114,31 @@ impl DevServerProjects {
cx.notify();
});
let mut base_style = cx.text_style();
base_style.refine(&gpui::TextStyleRefinement {
color: Some(cx.theme().colors().editor_foreground),
..Default::default()
});
let markdown_style = MarkdownStyle {
code_block: gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
color: Some(cx.theme().colors().editor_foreground),
background_color: Some(cx.theme().colors().editor_background),
base_text_style: base_style,
code_block: gpui::StyleRefinement {
text: Some(gpui::TextStyleRefinement {
font_family: Some("Zed Plex Mono".into()),
..Default::default()
}),
..Default::default()
},
inline_code: Default::default(),
block_quote: Default::default(),
link: gpui::TextStyleRefinement {
color: Some(Color::Accent.color(cx)),
..Default::default()
},
rule_color: Default::default(),
block_quote_border_color: Default::default(),
syntax: cx.theme().syntax().clone(),
selection_background_color: cx.theme().players().local().selection,
..Default::default()
};
let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
let markdown =
cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
Self {
mode: Mode::Default(None),