diff --git a/Cargo.lock b/Cargo.lock index 29509ad4b5..ffd69fb1df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1546,7 +1546,7 @@ dependencies = [ "sum_tree", "sysinfo", "tempfile", - "text", + "text2", "thiserror", "time", "tiny_http", @@ -3133,7 +3133,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "text", + "text2", "time", "util", ] @@ -3425,6 +3425,26 @@ dependencies = [ "url", ] +[[package]] +name = "git3" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "clock", + "collections", + "futures 0.3.28", + "git2", + "lazy_static", + "log", + "parking_lot 0.11.2", + "smol", + "sum_tree", + "text2", + "unindent", + "util", +] + [[package]] name = "glob" version = "0.3.1" @@ -4399,7 +4419,7 @@ dependencies = [ "env_logger 0.9.3", "futures 0.3.28", "fuzzy2", - "git", + "git3", "globset", "gpui2", "indoc", @@ -4421,7 +4441,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", - "text", + "text2", "theme2", "tree-sitter", "tree-sitter-elixir", @@ -5136,7 +5156,7 @@ dependencies = [ "ctor", "env_logger 0.9.3", "futures 0.3.28", - "git", + "git3", "gpui2", "indoc", "itertools 0.10.5", @@ -5150,7 +5170,7 @@ dependencies = [ "project2", "pulldown-cmark", "rand 0.8.5", - "rich_text", + "rich_text2", "schemars", "serde", "serde_derive", @@ -5159,7 +5179,7 @@ dependencies = [ "smol", "snippet", "sum_tree", - "text", + "text2", "theme2", "tree-sitter", "tree-sitter-html", @@ -6339,8 +6359,8 @@ dependencies = [ "fsevent", "futures 0.3.28", "fuzzy2", - "git", "git2", + "git3", "globset", "gpui2", "ignore", @@ -6368,7 +6388,7 @@ dependencies = [ "sum_tree", "tempdir", "terminal2", - "text", + "text2", "thiserror", "toml 0.5.11", "unindent", @@ -6982,6 +7002,24 @@ dependencies = [ "util", ] +[[package]] +name = "rich_text2" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "futures 0.3.28", + "gpui2", + "language2", + "lazy_static", + "pulldown-cmark", + "smallvec", + "smol", + "sum_tree", + "theme2", + "util", +] + [[package]] name = "ring" version = "0.16.20" @@ -8911,7 +8949,7 @@ name = "theme2" version = "0.1.0" dependencies = [ "anyhow", - "fs", + "fs2", "gpui2", "indexmap 1.9.3", "parking_lot 0.11.2", diff --git a/crates/client2/Cargo.toml b/crates/client2/Cargo.toml index 45e1f618d2..ace229bc21 100644 --- a/crates/client2/Cargo.toml +++ b/crates/client2/Cargo.toml @@ -17,7 +17,7 @@ db = { package = "db2", path = "../db2" } gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } rpc = { package = "rpc2", path = "../rpc2" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2" } sum_tree = { path = "../sum_tree" } diff --git a/crates/fs2/Cargo.toml b/crates/fs2/Cargo.toml index 636def05ec..ca525afe5f 100644 --- a/crates/fs2/Cargo.toml +++ b/crates/fs2/Cargo.toml @@ -10,7 +10,7 @@ path = "src/fs2.rs" [dependencies] collections = { path = "../collections" } rope = { path = "../rope" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } diff --git a/crates/git3/Cargo.toml b/crates/git3/Cargo.toml new file mode 100644 index 0000000000..e88fa6574d --- /dev/null +++ b/crates/git3/Cargo.toml @@ -0,0 +1,30 @@ +[package] +# git2 was already taken. +name = "git3" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/git.rs" + +[dependencies] +anyhow.workspace = true +clock = { path = "../clock" } +lazy_static.workspace = true +sum_tree = { path = "../sum_tree" } +text = { package = "text2", path = "../text2" } +collections = { path = "../collections" } +util = { path = "../util" } +log.workspace = true +smol.workspace = true +parking_lot.workspace = true +async-trait.workspace = true +futures.workspace = true +git2.workspace = true + +[dev-dependencies] +unindent.workspace = true + +[features] +test-support = [] diff --git a/crates/git3/src/diff.rs b/crates/git3/src/diff.rs new file mode 100644 index 0000000000..39383cfc78 --- /dev/null +++ b/crates/git3/src/diff.rs @@ -0,0 +1,412 @@ +use std::{iter, ops::Range}; +use sum_tree::SumTree; +use text::{Anchor, BufferSnapshot, OffsetRangeExt, Point}; + +pub use git2 as libgit; +use libgit::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DiffHunkStatus { + Added, + Modified, + Removed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DiffHunk { + pub buffer_range: Range, + pub diff_base_byte_range: Range, +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added + } else if self.buffer_range.is_empty() { + DiffHunkStatus::Removed + } else { + DiffHunkStatus::Modified + } + } +} + +impl sum_tree::Item for DiffHunk { + type Summary = DiffHunkSummary; + + fn summary(&self) -> Self::Summary { + DiffHunkSummary { + buffer_range: self.buffer_range.clone(), + } + } +} + +#[derive(Debug, Default, Clone)] +pub struct DiffHunkSummary { + buffer_range: Range, +} + +impl sum_tree::Summary for DiffHunkSummary { + type Context = text::BufferSnapshot; + + fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { + self.buffer_range.start = self + .buffer_range + .start + .min(&other.buffer_range.start, buffer); + self.buffer_range.end = self.buffer_range.end.max(&other.buffer_range.end, buffer); + } +} + +#[derive(Clone)] +pub struct BufferDiff { + last_buffer_version: Option, + tree: SumTree>, +} + +impl BufferDiff { + pub fn new() -> BufferDiff { + BufferDiff { + last_buffer_version: None, + tree: SumTree::new(), + } + } + + pub fn is_empty(&self) -> bool { + self.tree.is_empty() + } + + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let start = buffer.anchor_before(Point::new(range.start, 0)); + let end = buffer.anchor_after(Point::new(range.end, 0)); + + self.hunks_intersecting_range(start..end, buffer) + } + + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + let anchor_iter = std::iter::from_fn(move || { + cursor.next(buffer); + cursor.item() + }) + .flat_map(move |hunk| { + [ + (&hunk.buffer_range.start, hunk.diff_base_byte_range.start), + (&hunk.buffer_range.end, hunk.diff_base_byte_range.end), + ] + .into_iter() + }); + + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); + iter::from_fn(move || { + let (start_point, start_base) = summaries.next()?; + let (end_point, end_base) = summaries.next()?; + + let end_row = if end_point.column > 0 { + end_point.row + 1 + } else { + end_point.row + }; + + Some(DiffHunk { + buffer_range: start_point.row..end_row, + diff_base_byte_range: start_base..end_base, + }) + }) + } + + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a BufferSnapshot, + ) -> impl 'a + Iterator> { + let mut cursor = self.tree.filter::<_, DiffHunkSummary>(move |summary| { + let before_start = summary.buffer_range.end.cmp(&range.start, buffer).is_lt(); + let after_end = summary.buffer_range.start.cmp(&range.end, buffer).is_gt(); + !before_start && !after_end + }); + + std::iter::from_fn(move || { + cursor.prev(buffer); + + let hunk = cursor.item()?; + let range = hunk.buffer_range.to_point(buffer); + let end_row = if range.end.column > 0 { + range.end.row + 1 + } else { + range.end.row + }; + + Some(DiffHunk { + buffer_range: range.start.row..end_row, + diff_base_byte_range: hunk.diff_base_byte_range.clone(), + }) + }) + } + + pub fn clear(&mut self, buffer: &text::BufferSnapshot) { + self.last_buffer_version = Some(buffer.version().clone()); + self.tree = SumTree::new(); + } + + pub async fn update(&mut self, diff_base: &str, buffer: &text::BufferSnapshot) { + let mut tree = SumTree::new(); + + let buffer_text = buffer.as_rope().to_string(); + let patch = Self::diff(&diff_base, &buffer_text); + + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = Self::process_patch_hunk(&patch, hunk_index, buffer, &mut divergence); + tree.push(hunk, buffer); + } + } + + self.tree = tree; + self.last_buffer_version = Some(buffer.version().clone()); + } + + #[cfg(test)] + fn hunks<'a>(&'a self, text: &'a BufferSnapshot) -> impl 'a + Iterator> { + let start = text.anchor_before(Point::new(0, 0)); + let end = text.anchor_after(Point::new(u32::MAX, u32::MAX)); + self.hunks_intersecting_range(start..end, text) + } + + fn diff<'a>(head: &'a str, current: &'a str) -> Option> { + let mut options = GitOptions::default(); + options.context_lines(0); + + let patch = GitPatch::from_buffers( + head.as_bytes(), + None, + current.as_bytes(), + None, + Some(&mut options), + ); + + match patch { + Ok(patch) => Some(patch), + + Err(err) => { + log::error!("`GitPatch::from_buffers` failed: {}", err); + None + } + } + } + + fn process_patch_hunk<'a>( + patch: &GitPatch<'a>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, + ) -> DiffHunk { + let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap(); + assert!(line_item_count > 0); + + let mut first_deletion_buffer_row: Option = None; + let mut buffer_row_range: Option> = None; + let mut diff_base_byte_range: Option> = None; + + for line_index in 0..line_item_count { + let line = patch.line_in_hunk(hunk_index, line_index).unwrap(); + let kind = line.origin_value(); + let content_offset = line.content_offset() as isize; + let content_len = line.content().len() as isize; + + if kind == GitDiffLineType::Addition { + *buffer_row_divergence += 1; + let row = line.new_lineno().unwrap().saturating_sub(1); + + match &mut buffer_row_range { + Some(buffer_row_range) => buffer_row_range.end = row + 1, + None => buffer_row_range = Some(row..row + 1), + } + } + + if kind == GitDiffLineType::Deletion { + let end = content_offset + content_len; + + match &mut diff_base_byte_range { + Some(head_byte_range) => head_byte_range.end = end as usize, + None => diff_base_byte_range = Some(content_offset as usize..end as usize), + } + + if first_deletion_buffer_row.is_none() { + let old_row = line.old_lineno().unwrap().saturating_sub(1); + let row = old_row as i64 + *buffer_row_divergence; + first_deletion_buffer_row = Some(row as u32); + } + + *buffer_row_divergence -= 1; + } + } + + //unwrap_or deletion without addition + let buffer_row_range = buffer_row_range.unwrap_or_else(|| { + //we cannot have an addition-less hunk without deletion(s) or else there would be no hunk + let row = first_deletion_buffer_row.unwrap(); + row..row + }); + + //unwrap_or addition without deletion + let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0); + + let start = Point::new(buffer_row_range.start, 0); + let end = Point::new(buffer_row_range.end, 0); + let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end); + DiffHunk { + buffer_range, + diff_base_byte_range, + } + } +} + +/// Range (crossing new lines), old, new +#[cfg(any(test, feature = "test-support"))] +#[track_caller] +pub fn assert_hunks( + diff_hunks: Iter, + buffer: &BufferSnapshot, + diff_base: &str, + expected_hunks: &[(Range, &str, &str)], +) where + Iter: Iterator>, +{ + let actual_hunks = diff_hunks + .map(|hunk| { + ( + hunk.buffer_range.clone(), + &diff_base[hunk.diff_base_byte_range], + buffer + .text_for_range( + Point::new(hunk.buffer_range.start, 0) + ..Point::new(hunk.buffer_range.end, 0), + ) + .collect::(), + ) + }) + .collect::>(); + + let expected_hunks: Vec<_> = expected_hunks + .iter() + .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .collect(); + + assert_eq!(actual_hunks, expected_hunks); +} + +#[cfg(test)] +mod tests { + use std::assert_eq; + + use super::*; + use text::Buffer; + use unindent::Unindent as _; + + #[test] + fn test_buffer_diff_simple() { + let diff_base = " + one + two + three + " + .unindent(); + + let buffer_text = " + one + HELLO + three + " + .unindent(); + + let mut buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(1..2, "two\n", "HELLO\n")], + ); + + buffer.edit([(0..0, "point five\n")]); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_hunks( + diff.hunks(&buffer), + &buffer, + &diff_base, + &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + ); + + diff.clear(&buffer); + assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]); + } + + #[test] + fn test_buffer_diff_range() { + let diff_base = " + one + two + three + four + five + six + seven + eight + nine + ten + " + .unindent(); + + let buffer_text = " + A + one + B + two + C + three + HELLO + four + five + SIXTEEN + seven + eight + WORLD + nine + + ten + + " + .unindent(); + + let buffer = Buffer::new(0, 0, buffer_text); + let mut diff = BufferDiff::new(); + smol::block_on(diff.update(&diff_base, &buffer)); + assert_eq!(diff.hunks(&buffer).count(), 8); + + assert_hunks( + diff.hunks_in_row_range(7..12, &buffer), + &buffer, + &diff_base, + &[ + (6..7, "", "HELLO\n"), + (9..10, "six\n", "SIXTEEN\n"), + (12..13, "", "WORLD\n"), + ], + ); + } +} diff --git a/crates/git3/src/git.rs b/crates/git3/src/git.rs new file mode 100644 index 0000000000..b1b885eca2 --- /dev/null +++ b/crates/git3/src/git.rs @@ -0,0 +1,11 @@ +use std::ffi::OsStr; + +pub use git2 as libgit; +pub use lazy_static::lazy_static; + +pub mod diff; + +lazy_static! { + pub static ref DOT_GIT: &'static OsStr = OsStr::new(".git"); + pub static ref GITIGNORE: &'static OsStr = OsStr::new(".gitignore"); +} diff --git a/crates/language2/Cargo.toml b/crates/language2/Cargo.toml index bd43465b55..2ffcbc62cf 100644 --- a/crates/language2/Cargo.toml +++ b/crates/language2/Cargo.toml @@ -25,13 +25,13 @@ test-support = [ clock = { path = "../clock" } collections = { path = "../collections" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } lsp = { package = "lsp2", path = "../lsp2" } rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } @@ -65,7 +65,7 @@ client = { package = "client2", path = "../client2", features = ["test-support"] collections = { path = "../collections", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } ctor.workspace = true diff --git a/crates/multi_buffer2/Cargo.toml b/crates/multi_buffer2/Cargo.toml index a57ef29531..98b96dfa1d 100644 --- a/crates/multi_buffer2/Cargo.toml +++ b/crates/multi_buffer2/Cargo.toml @@ -23,15 +23,15 @@ test-support = [ client = { package = "client2", path = "../client2" } clock = { path = "../clock" } collections = { path = "../collections" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" } -rich_text = { path = "../rich_text" } +rich_text = { package = "rich_text2", path = "../rich_text2" } settings = { package = "settings2", path = "../settings2" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } -text = { path = "../text" } +text = { package = "text2", path = "../text2" } theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } @@ -60,7 +60,7 @@ tree-sitter-typescript = { workspace = true, optional = true } [dev-dependencies] copilot = { package = "copilot2", path = "../copilot2", features = ["test-support"] } -text = { path = "../text", features = ["test-support"] } +text = { package = "text2", path = "../text2", features = ["test-support"] } language = { package = "language2", path = "../language2", features = ["test-support"] } lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } diff --git a/crates/project2/Cargo.toml b/crates/project2/Cargo.toml index 7aae9fb007..892ddb91c7 100644 --- a/crates/project2/Cargo.toml +++ b/crates/project2/Cargo.toml @@ -20,7 +20,7 @@ test-support = [ ] [dependencies] -text = { path = "../text" } +text = { package = "text2", path = "../text2" } copilot = { package = "copilot2", path = "../copilot2" } client = { package = "client2", path = "../client2" } clock = { path = "../clock" } @@ -29,7 +29,7 @@ db = { package = "db2", path = "../db2" } fs = { package = "fs2", path = "../fs2" } fsevent = { path = "../fsevent" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -git = { path = "../git" } +git = { package = "git3", path = "../git3" } gpui = { package = "gpui2", path = "../gpui2" } language = { package = "language2", path = "../language2" } lsp = { package = "lsp2", path = "../lsp2" } diff --git a/crates/rich_text2/Cargo.toml b/crates/rich_text2/Cargo.toml new file mode 100644 index 0000000000..4eee1e107b --- /dev/null +++ b/crates/rich_text2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "rich_text2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/rich_text.rs" +doctest = false + +[features] +test-support = [ + "gpui/test-support", + "util/test-support", +] + +[dependencies] +collections = { path = "../collections" } +gpui = { package = "gpui2", path = "../gpui2" } +sum_tree = { path = "../sum_tree" } +theme = { package = "theme2", path = "../theme2" } +language = { package = "language2", path = "../language2" } +util = { path = "../util" } +anyhow.workspace = true +futures.workspace = true +lazy_static.workspace = true +pulldown-cmark = { version = "0.9.2", default-features = false } +smallvec.workspace = true +smol.workspace = true diff --git a/crates/rich_text2/src/rich_text.rs b/crates/rich_text2/src/rich_text.rs new file mode 100644 index 0000000000..48b530b7c5 --- /dev/null +++ b/crates/rich_text2/src/rich_text.rs @@ -0,0 +1,373 @@ +use std::{ops::Range, sync::Arc}; + +use anyhow::bail; +use futures::FutureExt; +use gpui::{AnyElement, FontStyle, FontWeight, HighlightStyle, UnderlineStyle}; +use language::{HighlightId, Language, LanguageRegistry}; +use util::RangeExt; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Highlight { + Id(HighlightId), + Highlight(HighlightStyle), + Mention, + SelfMention, +} + +impl From for Highlight { + fn from(style: HighlightStyle) -> Self { + Self::Highlight(style) + } +} + +impl From for Highlight { + fn from(style: HighlightId) -> Self { + Self::Id(style) + } +} + +#[derive(Debug, Clone)] +pub struct RichText { + pub text: String, + pub highlights: Vec<(Range, Highlight)>, + pub region_ranges: Vec>, + pub regions: Vec, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BackgroundKind { + Code, + /// A mention background for non-self user. + Mention, + SelfMention, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RenderedRegion { + pub background_kind: Option, + pub link_url: Option, +} + +/// Allows one to specify extra links to the rendered markdown, which can be used +/// for e.g. mentions. +pub struct Mention { + pub range: Range, + pub is_self_mention: bool, +} + +impl RichText { + pub fn element( + &self, + // syntax: Arc, + // style: RichTextStyle, + // cx: &mut ViewContext, + ) -> AnyElement { + todo!(); + + // let mut region_id = 0; + // let view_id = cx.view_id(); + + // let regions = self.regions.clone(); + + // enum Markdown {} + // Text::new(self.text.clone(), style.text.clone()) + // .with_highlights( + // self.highlights + // .iter() + // .filter_map(|(range, highlight)| { + // let style = match highlight { + // Highlight::Id(id) => id.style(&syntax)?, + // Highlight::Highlight(style) => style.clone(), + // Highlight::Mention => style.mention_highlight, + // Highlight::SelfMention => style.self_mention_highlight, + // }; + // Some((range.clone(), style)) + // }) + // .collect::>(), + // ) + // .with_custom_runs(self.region_ranges.clone(), move |ix, bounds, cx| { + // region_id += 1; + // let region = regions[ix].clone(); + // if let Some(url) = region.link_url { + // cx.scene().push_cursor_region(CursorRegion { + // bounds, + // style: CursorStyle::PointingHand, + // }); + // cx.scene().push_mouse_region( + // MouseRegion::new::(view_id, region_id, bounds) + // .on_click::(MouseButton::Left, move |_, _, cx| { + // cx.platform().open_url(&url) + // }), + // ); + // } + // if let Some(region_kind) = ®ion.background_kind { + // let background = match region_kind { + // BackgroundKind::Code => style.code_background, + // BackgroundKind::Mention => style.mention_background, + // BackgroundKind::SelfMention => style.self_mention_background, + // }; + // if background.is_some() { + // cx.scene().push_quad(gpui::Quad { + // bounds, + // background, + // border: Default::default(), + // corner_radii: (2.0).into(), + // }); + // } + // } + // }) + // .with_soft_wrap(true) + // .into_any() + } + + pub fn add_mention( + &mut self, + range: Range, + is_current_user: bool, + mention_style: HighlightStyle, + ) -> anyhow::Result<()> { + if range.end > self.text.len() { + bail!( + "Mention in range {range:?} is outside of bounds for a message of length {}", + self.text.len() + ); + } + + if is_current_user { + self.region_ranges.push(range.clone()); + self.regions.push(RenderedRegion { + background_kind: Some(BackgroundKind::Mention), + link_url: None, + }); + } + self.highlights + .push((range, Highlight::Highlight(mention_style))); + Ok(()) + } +} + +pub fn render_markdown_mut( + block: &str, + mut mentions: &[Mention], + language_registry: &Arc, + language: Option<&Arc>, + data: &mut RichText, +) { + use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag}; + + let mut bold_depth = 0; + let mut italic_depth = 0; + let mut link_url = None; + let mut current_language = None; + let mut list_stack = Vec::new(); + + let options = Options::all(); + for (event, source_range) in Parser::new_ext(&block, options).into_offset_iter() { + let prev_len = data.text.len(); + match event { + Event::Text(t) => { + if let Some(language) = ¤t_language { + render_code(&mut data.text, &mut data.highlights, t.as_ref(), language); + } else { + if let Some(mention) = mentions.first() { + if source_range.contains_inclusive(&mention.range) { + mentions = &mentions[1..]; + let range = (prev_len + mention.range.start - source_range.start) + ..(prev_len + mention.range.end - source_range.start); + data.highlights.push(( + range.clone(), + if mention.is_self_mention { + Highlight::SelfMention + } else { + Highlight::Mention + }, + )); + data.region_ranges.push(range); + data.regions.push(RenderedRegion { + background_kind: Some(if mention.is_self_mention { + BackgroundKind::SelfMention + } else { + BackgroundKind::Mention + }), + link_url: None, + }); + } + } + + data.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 let Some(link_url) = link_url.clone() { + data.region_ranges.push(prev_len..data.text.len()); + data.regions.push(RenderedRegion { + link_url: Some(link_url), + background_kind: None, + }); + style.underline = Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }); + } + + if style != HighlightStyle::default() { + let mut new_highlight = true; + if let Some((last_range, last_style)) = data.highlights.last_mut() { + if last_range.end == prev_len + && last_style == &Highlight::Highlight(style) + { + last_range.end = data.text.len(); + new_highlight = false; + } + } + if new_highlight { + data.highlights + .push((prev_len..data.text.len(), Highlight::Highlight(style))); + } + } + } + } + Event::Code(t) => { + data.text.push_str(t.as_ref()); + data.region_ranges.push(prev_len..data.text.len()); + if link_url.is_some() { + data.highlights.push(( + prev_len..data.text.len(), + Highlight::Highlight(HighlightStyle { + underline: Some(UnderlineStyle { + thickness: 1.0.into(), + ..Default::default() + }), + ..Default::default() + }), + )); + } + data.regions.push(RenderedRegion { + background_kind: Some(BackgroundKind::Code), + link_url: link_url.clone(), + }); + } + Event::Start(tag) => match tag { + Tag::Paragraph => new_paragraph(&mut data.text, &mut list_stack), + Tag::Heading(_, _, _) => { + new_paragraph(&mut data.text, &mut list_stack); + bold_depth += 1; + } + Tag::CodeBlock(kind) => { + new_paragraph(&mut data.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::Link(_, url, _) => link_url = Some(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 !data.text.is_empty() && !data.text.ends_with('\n') { + data.text.push('\n'); + } + for _ in 0..len - 1 { + data.text.push_str(" "); + } + if let Some(number) = list_number { + data.text.push_str(&format!("{}. ", number)); + *number += 1; + *has_content = false; + } else { + data.text.push_str("- "); + } + } + } + _ => {} + }, + Event::End(tag) => match tag { + Tag::Heading(_, _, _) => bold_depth -= 1, + Tag::CodeBlock(_) => current_language = None, + Tag::Emphasis => italic_depth -= 1, + Tag::Strong => bold_depth -= 1, + Tag::Link(_, _, _) => link_url = None, + Tag::List(_) => drop(list_stack.pop()), + _ => {} + }, + Event::HardBreak => data.text.push('\n'), + Event::SoftBreak => data.text.push(' '), + _ => {} + } + } +} + +pub fn render_markdown( + block: String, + mentions: &[Mention], + language_registry: &Arc, + language: Option<&Arc>, +) -> RichText { + let mut data = RichText { + text: Default::default(), + highlights: Default::default(), + region_ranges: Default::default(), + regions: Default::default(), + }; + + render_markdown_mut(&block, mentions, language_registry, language, &mut data); + + data.text = data.text.trim().to_string(); + + data +} + +pub fn render_code( + text: &mut String, + highlights: &mut Vec<(Range, Highlight)>, + content: &str, + language: &Arc, +) { + let prev_len = text.len(); + text.push_str(content); + for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { + highlights.push(( + prev_len + range.start..prev_len + range.end, + Highlight::Id(highlight_id), + )); + } +} + +pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, 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(" "); + } +} diff --git a/crates/theme2/Cargo.toml b/crates/theme2/Cargo.toml index a051468b00..5a8448372c 100644 --- a/crates/theme2/Cargo.toml +++ b/crates/theme2/Cargo.toml @@ -17,7 +17,7 @@ doctest = false [dependencies] anyhow.workspace = true -fs = { path = "../fs" } +fs = { package = "fs2", path = "../fs2" } gpui = { package = "gpui2", path = "../gpui2" } indexmap = "1.6.2" parking_lot.workspace = true @@ -32,5 +32,5 @@ util = { path = "../util" } [dev-dependencies] gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -fs = { path = "../fs", features = ["test-support"] } +fs = { package = "fs2", path = "../fs2", features = ["test-support"] } settings = { package = "settings2", path = "../settings2", features = ["test-support"] } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 20a06d1658..9da9123a2f 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -622,7 +622,7 @@ impl StatusItemView for PanelButtons { _active_pane_item: Option<&dyn crate::ItemHandle>, _cx: &mut ViewContext, ) { - todo!() + // todo!(This is empty in the old `workspace::dock`) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index bf4ed73c27..8a80ef328a 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1044,9 +1044,9 @@ impl Workspace { // dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); // } - // pub fn status_bar(&self) -> &View { - // &self.status_bar - // } + pub fn status_bar(&self) -> &View { + &self.status_bar + } pub fn app_state(&self) -> &Arc { &self.app_state diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index cd52ea33e1..4224baadb8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -242,7 +242,7 @@ pub fn build_window_options( focus: false, show: false, kind: WindowKind::Normal, - is_movable: false, + is_movable: true, display_id: display.map(|display| display.id()), } } @@ -317,16 +317,16 @@ pub fn initialize_workspace( // feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) // }); // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - // workspace.status_bar().update(cx, |status_bar, cx| { - // status_bar.add_left_item(diagnostic_summary, cx); - // status_bar.add_left_item(activity_indicator, cx); + workspace.status_bar().update(cx, |status_bar, cx| { + // status_bar.add_left_item(diagnostic_summary, cx); + // status_bar.add_left_item(activity_indicator, cx); - // status_bar.add_right_item(feedback_button, cx); - // status_bar.add_right_item(copilot, cx); - // status_bar.add_right_item(active_buffer_language, cx); - // status_bar.add_right_item(vim_mode_indicator, cx); - // status_bar.add_right_item(cursor_position, cx); - // }); + // status_bar.add_right_item(feedback_button, cx); + // status_bar.add_right_item(copilot, cx); + // status_bar.add_right_item(active_buffer_language, cx); + // status_bar.add_right_item(vim_mode_indicator, cx); + // status_bar.add_right_item(cursor_position, cx); + }); // auto_update::notify_of_any_new_update(cx.weak_handle(), cx);