Compare commits
71 Commits
fix-git-ht
...
selectable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3903957c8 | ||
|
|
97cfe3a4b7 | ||
|
|
36f044f48e | ||
|
|
874c1f773b | ||
|
|
67229ea673 | ||
|
|
0423ad2c43 | ||
|
|
948f16e2e3 | ||
|
|
66dce927dc | ||
|
|
b0edf0ec7b | ||
|
|
840c6e414a | ||
|
|
3ef6efbb9d | ||
|
|
a15673f778 | ||
|
|
b6e1acbf71 | ||
|
|
b41f02d11a | ||
|
|
f78a566073 | ||
|
|
1574eddb46 | ||
|
|
2d3e2a4af0 | ||
|
|
f4615f3b3a | ||
|
|
c6269f549d | ||
|
|
c5d20e1215 | ||
|
|
15c3cc14e0 | ||
|
|
d640da2b77 | ||
|
|
6becb8c859 | ||
|
|
6aa53496c3 | ||
|
|
ba19bccd66 | ||
|
|
6fad3883cf | ||
|
|
c1881b44ab | ||
|
|
b3646b32b2 | ||
|
|
89c04a79ae | ||
|
|
667d9825b6 | ||
|
|
f83f69a450 | ||
|
|
8e1103a916 | ||
|
|
c7db6c4bf3 | ||
|
|
e4f0cf526e | ||
|
|
8ec6b00106 | ||
|
|
37d62cf590 | ||
|
|
b87b4fcfdf | ||
|
|
9097c9151b | ||
|
|
54e48cf7da | ||
|
|
89726f6216 | ||
|
|
2fd07bfa46 | ||
|
|
23d924c33f | ||
|
|
e7a9ba9cb3 | ||
|
|
4397b8c6fc | ||
|
|
965f2dbadb | ||
|
|
3ee62fdf0c | ||
|
|
8b921f87d6 | ||
|
|
00f78b94fe | ||
|
|
58165d8492 | ||
|
|
e5dd34ed5d | ||
|
|
4605db7523 | ||
|
|
6f3250ba9e | ||
|
|
998bd35d6f | ||
|
|
87eaa625de | ||
|
|
4b99a94470 | ||
|
|
a4be555dc9 | ||
|
|
a06eb30f8d | ||
|
|
14a0a130f3 | ||
|
|
e0b46a5060 | ||
|
|
8cea761693 | ||
|
|
e2087a0a84 | ||
|
|
a901f9976b | ||
|
|
bfc7932024 | ||
|
|
1e825ffea5 | ||
|
|
511463f574 | ||
|
|
7dc6ba4d66 | ||
|
|
8781db6b68 | ||
|
|
77809989aa | ||
|
|
3125c98bb9 | ||
|
|
8d1d06f0b3 | ||
|
|
f52d4520e9 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3605,6 +3605,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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
|
||||
- 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"
|
||||
);
|
||||
|
||||
@@ -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(())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
119
crates/markdown/examples/markdown_as_child.rs
Normal file
119
crates/markdown/examples/markdown_as_child.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user