There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
421 lines
16 KiB
Rust
421 lines
16 KiB
Rust
use futures::FutureExt;
|
|
use gpui::{
|
|
AnyElement, AnyView, App, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText,
|
|
IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window,
|
|
};
|
|
use language::{HighlightId, Language, LanguageRegistry};
|
|
use std::{ops::Range, sync::Arc};
|
|
use theme::ActiveTheme;
|
|
use ui::LinkPreview;
|
|
use util::RangeExt;
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Highlight {
|
|
Code,
|
|
Id(HighlightId),
|
|
InlineCode(bool),
|
|
Highlight(HighlightStyle),
|
|
Mention,
|
|
SelfMention,
|
|
}
|
|
|
|
impl From<HighlightStyle> for Highlight {
|
|
fn from(style: HighlightStyle) -> Self {
|
|
Self::Highlight(style)
|
|
}
|
|
}
|
|
|
|
impl From<HighlightId> for Highlight {
|
|
fn from(style: HighlightId) -> Self {
|
|
Self::Id(style)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Default)]
|
|
pub struct RichText {
|
|
pub text: SharedString,
|
|
pub highlights: Vec<(Range<usize>, Highlight)>,
|
|
pub link_ranges: Vec<Range<usize>>,
|
|
pub link_urls: Arc<[String]>,
|
|
|
|
pub custom_ranges: Vec<Range<usize>>,
|
|
custom_ranges_tooltip_fn:
|
|
Option<Arc<dyn Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView>>>,
|
|
}
|
|
|
|
/// Allows one to specify extra links to the rendered markdown, which can be used
|
|
/// for e.g. mentions.
|
|
#[derive(Debug)]
|
|
pub struct Mention {
|
|
pub range: Range<usize>,
|
|
pub is_self_mention: bool,
|
|
}
|
|
|
|
impl RichText {
|
|
pub fn new(
|
|
block: String,
|
|
mentions: &[Mention],
|
|
language_registry: &Arc<LanguageRegistry>,
|
|
) -> Self {
|
|
let mut text = String::new();
|
|
let mut highlights = Vec::new();
|
|
let mut link_ranges = Vec::new();
|
|
let mut link_urls = Vec::new();
|
|
render_markdown_mut(
|
|
&block,
|
|
mentions,
|
|
language_registry,
|
|
None,
|
|
&mut text,
|
|
&mut highlights,
|
|
&mut link_ranges,
|
|
&mut link_urls,
|
|
);
|
|
text.truncate(text.trim_end().len());
|
|
|
|
RichText {
|
|
text: SharedString::from(text),
|
|
link_urls: link_urls.into(),
|
|
link_ranges,
|
|
highlights,
|
|
custom_ranges: Vec::new(),
|
|
custom_ranges_tooltip_fn: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_tooltip_builder_for_custom_ranges(
|
|
&mut self,
|
|
f: impl Fn(usize, Range<usize>, &mut Window, &mut App) -> Option<AnyView> + 'static,
|
|
) {
|
|
self.custom_ranges_tooltip_fn = Some(Arc::new(f));
|
|
}
|
|
|
|
pub fn element(&self, id: ElementId, window: &mut Window, cx: &mut App) -> AnyElement {
|
|
let theme = cx.theme();
|
|
let code_background = theme.colors().surface_background;
|
|
|
|
InteractiveText::new(
|
|
id,
|
|
StyledText::new(self.text.clone()).with_highlights(
|
|
&window.text_style(),
|
|
self.highlights.iter().map(|(range, highlight)| {
|
|
(
|
|
range.clone(),
|
|
match highlight {
|
|
Highlight::Code => HighlightStyle {
|
|
background_color: Some(code_background),
|
|
..Default::default()
|
|
},
|
|
Highlight::Id(id) => HighlightStyle {
|
|
background_color: Some(code_background),
|
|
..id.style(theme.syntax()).unwrap_or_default()
|
|
},
|
|
Highlight::InlineCode(link) => {
|
|
if *link {
|
|
HighlightStyle {
|
|
background_color: Some(code_background),
|
|
underline: Some(UnderlineStyle {
|
|
thickness: 1.0.into(),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}
|
|
} else {
|
|
HighlightStyle {
|
|
background_color: Some(code_background),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
Highlight::Highlight(highlight) => *highlight,
|
|
Highlight::Mention => HighlightStyle {
|
|
font_weight: Some(FontWeight::BOLD),
|
|
..Default::default()
|
|
},
|
|
Highlight::SelfMention => HighlightStyle {
|
|
font_weight: Some(FontWeight::BOLD),
|
|
..Default::default()
|
|
},
|
|
},
|
|
)
|
|
}),
|
|
),
|
|
)
|
|
.on_click(self.link_ranges.clone(), {
|
|
let link_urls = self.link_urls.clone();
|
|
move |ix, _, cx| {
|
|
let url = &link_urls[ix];
|
|
if url.starts_with("http") {
|
|
cx.open_url(url);
|
|
}
|
|
}
|
|
})
|
|
.tooltip({
|
|
let link_ranges = self.link_ranges.clone();
|
|
let link_urls = self.link_urls.clone();
|
|
let custom_tooltip_ranges = self.custom_ranges.clone();
|
|
let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone();
|
|
move |idx, window, cx| {
|
|
for (ix, range) in link_ranges.iter().enumerate() {
|
|
if range.contains(&idx) {
|
|
return Some(LinkPreview::new(&link_urls[ix], cx));
|
|
}
|
|
}
|
|
for range in &custom_tooltip_ranges {
|
|
if range.contains(&idx) {
|
|
if let Some(f) = &custom_tooltip_fn {
|
|
return f(idx, range.clone(), window, cx);
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
})
|
|
.into_any_element()
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn render_markdown_mut(
|
|
block: &str,
|
|
mut mentions: &[Mention],
|
|
language_registry: &Arc<LanguageRegistry>,
|
|
language: Option<&Arc<Language>>,
|
|
text: &mut String,
|
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
|
link_ranges: &mut Vec<Range<usize>>,
|
|
link_urls: &mut Vec<String>,
|
|
) {
|
|
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd};
|
|
|
|
let mut bold_depth = 0;
|
|
let mut italic_depth = 0;
|
|
let mut strikethrough_depth = 0;
|
|
let mut link_url = None;
|
|
let mut current_language = None;
|
|
let mut list_stack = Vec::new();
|
|
|
|
let mut options = Options::all();
|
|
options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST);
|
|
|
|
for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() {
|
|
let prev_len = text.len();
|
|
match event {
|
|
Event::Text(t) => {
|
|
if let Some(language) = ¤t_language {
|
|
render_code(text, highlights, t.as_ref(), language);
|
|
} else {
|
|
while let Some(mention) = mentions.first() {
|
|
if !source_range.contains_inclusive(&mention.range) {
|
|
break;
|
|
}
|
|
mentions = &mentions[1..];
|
|
let range = (prev_len + mention.range.start - source_range.start)
|
|
..(prev_len + mention.range.end - source_range.start);
|
|
highlights.push((
|
|
range.clone(),
|
|
if mention.is_self_mention {
|
|
Highlight::SelfMention
|
|
} else {
|
|
Highlight::Mention
|
|
},
|
|
));
|
|
}
|
|
|
|
text.push_str(t.as_ref());
|
|
let mut style = HighlightStyle::default();
|
|
if bold_depth > 0 {
|
|
style.font_weight = Some(FontWeight::BOLD);
|
|
}
|
|
if italic_depth > 0 {
|
|
style.font_style = Some(FontStyle::Italic);
|
|
}
|
|
if strikethrough_depth > 0 {
|
|
style.strikethrough = Some(StrikethroughStyle {
|
|
thickness: 1.0.into(),
|
|
..Default::default()
|
|
});
|
|
}
|
|
let last_run_len = if let Some(link_url) = link_url.clone() {
|
|
link_ranges.push(prev_len..text.len());
|
|
link_urls.push(link_url);
|
|
style.underline = Some(UnderlineStyle {
|
|
thickness: 1.0.into(),
|
|
..Default::default()
|
|
});
|
|
prev_len
|
|
} else {
|
|
// Manually scan for links
|
|
let mut finder = linkify::LinkFinder::new();
|
|
finder.kinds(&[linkify::LinkKind::Url]);
|
|
let mut last_link_len = prev_len;
|
|
for link in finder.links(&t) {
|
|
let start = link.start();
|
|
let end = link.end();
|
|
let range = (prev_len + start)..(prev_len + end);
|
|
link_ranges.push(range.clone());
|
|
link_urls.push(link.as_str().to_string());
|
|
|
|
// If there is a style before we match a link, we have to add this to the highlighted ranges
|
|
if style != HighlightStyle::default() && last_link_len < link.start() {
|
|
highlights.push((
|
|
last_link_len..link.start(),
|
|
Highlight::Highlight(style),
|
|
));
|
|
}
|
|
|
|
highlights.push((
|
|
range,
|
|
Highlight::Highlight(HighlightStyle {
|
|
underline: Some(UnderlineStyle {
|
|
thickness: 1.0.into(),
|
|
..Default::default()
|
|
}),
|
|
..style
|
|
}),
|
|
));
|
|
|
|
last_link_len = end;
|
|
}
|
|
last_link_len
|
|
};
|
|
|
|
if style != HighlightStyle::default() && last_run_len < text.len() {
|
|
let mut new_highlight = true;
|
|
if let Some((last_range, last_style)) = highlights.last_mut() {
|
|
if last_range.end == last_run_len
|
|
&& last_style == &Highlight::Highlight(style)
|
|
{
|
|
last_range.end = text.len();
|
|
new_highlight = false;
|
|
}
|
|
}
|
|
if new_highlight {
|
|
highlights
|
|
.push((last_run_len..text.len(), Highlight::Highlight(style)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Event::Code(t) => {
|
|
text.push_str(t.as_ref());
|
|
let is_link = link_url.is_some();
|
|
|
|
if let Some(link_url) = link_url.clone() {
|
|
link_ranges.push(prev_len..text.len());
|
|
link_urls.push(link_url);
|
|
}
|
|
|
|
highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link)))
|
|
}
|
|
Event::Start(tag) => match tag {
|
|
Tag::Paragraph => new_paragraph(text, &mut list_stack),
|
|
Tag::Heading { .. } => {
|
|
new_paragraph(text, &mut list_stack);
|
|
bold_depth += 1;
|
|
}
|
|
Tag::CodeBlock(kind) => {
|
|
new_paragraph(text, &mut list_stack);
|
|
current_language = if let CodeBlockKind::Fenced(language) = kind {
|
|
language_registry
|
|
.language_for_name(language.as_ref())
|
|
.now_or_never()
|
|
.and_then(Result::ok)
|
|
} else {
|
|
language.cloned()
|
|
}
|
|
}
|
|
Tag::Emphasis => italic_depth += 1,
|
|
Tag::Strong => bold_depth += 1,
|
|
Tag::Strikethrough => strikethrough_depth += 1,
|
|
Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()),
|
|
Tag::List(number) => {
|
|
list_stack.push((number, false));
|
|
}
|
|
Tag::Item => {
|
|
let len = list_stack.len();
|
|
if let Some((list_number, has_content)) = list_stack.last_mut() {
|
|
*has_content = false;
|
|
if !text.is_empty() && !text.ends_with('\n') {
|
|
text.push('\n');
|
|
}
|
|
for _ in 0..len - 1 {
|
|
text.push_str(" ");
|
|
}
|
|
if let Some(number) = list_number {
|
|
text.push_str(&format!("{}. ", number));
|
|
*number += 1;
|
|
*has_content = false;
|
|
} else {
|
|
text.push_str("- ");
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
},
|
|
Event::End(tag) => match tag {
|
|
TagEnd::Heading(_) => bold_depth -= 1,
|
|
TagEnd::CodeBlock => current_language = None,
|
|
TagEnd::Emphasis => italic_depth -= 1,
|
|
TagEnd::Strong => bold_depth -= 1,
|
|
TagEnd::Strikethrough => strikethrough_depth -= 1,
|
|
TagEnd::Link => link_url = None,
|
|
TagEnd::List(_) => drop(list_stack.pop()),
|
|
_ => {}
|
|
},
|
|
Event::HardBreak => text.push('\n'),
|
|
Event::SoftBreak => text.push('\n'),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn render_code(
|
|
text: &mut String,
|
|
highlights: &mut Vec<(Range<usize>, Highlight)>,
|
|
content: &str,
|
|
language: &Arc<Language>,
|
|
) {
|
|
let prev_len = text.len();
|
|
text.push_str(content);
|
|
let mut offset = 0;
|
|
for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) {
|
|
if range.start > offset {
|
|
highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code));
|
|
}
|
|
highlights.push((
|
|
prev_len + range.start..prev_len + range.end,
|
|
Highlight::Id(highlight_id),
|
|
));
|
|
offset = range.end;
|
|
}
|
|
if offset < content.len() {
|
|
highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code));
|
|
}
|
|
}
|
|
|
|
pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option<u64>, bool)>) {
|
|
let mut is_subsequent_paragraph_of_list = false;
|
|
if let Some((_, has_content)) = list_stack.last_mut() {
|
|
if *has_content {
|
|
is_subsequent_paragraph_of_list = true;
|
|
} else {
|
|
*has_content = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if !text.is_empty() {
|
|
if !text.ends_with('\n') {
|
|
text.push('\n');
|
|
}
|
|
text.push('\n');
|
|
}
|
|
for _ in 0..list_stack.len().saturating_sub(1) {
|
|
text.push_str(" ");
|
|
}
|
|
if is_subsequent_paragraph_of_list {
|
|
text.push_str(" ");
|
|
}
|
|
}
|