diff --git a/Cargo.lock b/Cargo.lock index 452490795480d1f7c73c1bbd28206495bda75587..eb872d156e57c747e717f64525112280323d8828 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,6 +2024,24 @@ dependencies = [ "serde", ] +[[package]] +name = "buffer_diff" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures 0.3.31", + "git2", + "gpui", + "language", + "pretty_assertions", + "rope", + "serde_json", + "sum_tree", + "text", + "unindent", + "util", +] + [[package]] name = "built" version = "0.7.5" @@ -2742,6 +2760,7 @@ dependencies = [ "axum", "axum-extra", "base64 0.22.1", + "buffer_diff", "call", "channel", "chrono", @@ -2753,7 +2772,6 @@ dependencies = [ "ctor", "dashmap 6.1.0", "derive_more", - "diff 0.1.0", "editor", "env_logger 0.11.6", "envy", @@ -3860,24 +3878,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "diff" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "git2", - "gpui", - "language", - "log", - "pretty_assertions", - "rope", - "serde_json", - "sum_tree", - "text", - "unindent", - "util", -] - [[package]] name = "diff" version = "0.1.13" @@ -4041,6 +4041,7 @@ dependencies = [ "aho-corasick", "anyhow", "assets", + "buffer_diff", "chrono", "client", "clock", @@ -4048,7 +4049,6 @@ dependencies = [ "convert_case 0.7.1", "ctor", "db", - "diff 0.1.0", "emojis", "env_logger 0.11.6", "file_icons", @@ -5347,9 +5347,9 @@ name = "git_ui" version = "0.1.0" dependencies = [ "anyhow", + "buffer_diff", "collections", "db", - "diff 0.1.0", "editor", "feature_flags", "futures 0.3.31", @@ -5546,6 +5546,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "refineable", + "reqwest_client", "resvg", "schemars", "seahash", @@ -7980,10 +7981,10 @@ name = "multi_buffer" version = "0.1.0" dependencies = [ "anyhow", + "buffer_diff", "clock", "collections", "ctor", - "diff 0.1.0", "env_logger 0.11.6", "futures 0.3.31", "gpui", @@ -9995,7 +9996,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ - "diff 0.1.13", + "diff", "yansi", ] @@ -10088,10 +10089,10 @@ dependencies = [ "aho-corasick", "anyhow", "async-trait", + "buffer_diff", "client", "clock", "collections", - "diff 0.1.0", "env_logger 0.11.6", "fancy-regex 0.14.0", "fs", @@ -12035,7 +12036,6 @@ dependencies = [ "indoc", "inventory", "log", - "migrator", "paths", "pretty_assertions", "release_channel", @@ -16647,6 +16647,7 @@ dependencies = [ "markdown", "markdown_preview", "menu", + "migrator", "mimalloc", "nix", "node_runtime", diff --git a/Cargo.toml b/Cargo.toml index 935b05337814c9032d268598c200050a6772e395..ec893364e23b1d8704e97e64b747b679119a8594 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,7 @@ members = [ "crates/db", "crates/deepseek", "crates/diagnostics", - "crates/diff", + "crates/buffer_diff", "crates/docs_preprocessor", "crates/editor", "crates/evals", @@ -235,7 +235,7 @@ copilot = { path = "crates/copilot" } db = { path = "crates/db" } deepseek = { path = "crates/deepseek" } diagnostics = { path = "crates/diagnostics" } -diff = { path = "crates/diff" } +buffer_diff = { path = "crates/buffer_diff" } editor = { path = "crates/editor" } extension = { path = "crates/extension" } extension_host = { path = "crates/extension_host" } diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index ffed3d680dd6a5686c6963c4074e7f2bff382fd7..f73cd708f18794bb10874633a1a71e33bb836162 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -113,8 +113,8 @@ "lua": "lua", "m4a": "audio", "m4v": "video", - "markdown": "document", - "md": "document", + "markdown": "markdown", + "md": "markdown", "mdb": "storage", "mdf": "storage", "mdx": "document", @@ -186,7 +186,7 @@ "sh": "terminal", "sql": "storage", "sqlite": "storage", - "svelte": "template", + "svelte": "svelte", "svg": "image", "swift": "swift", "tcl": "tcl", diff --git a/assets/icons/zed_predict_down.svg b/assets/icons/zed_predict_down.svg new file mode 100644 index 0000000000000000000000000000000000000000..4532ad7e26cab76bc4e52a68ea5c766a1ffdca81 --- /dev/null +++ b/assets/icons/zed_predict_down.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/zed_predict_up.svg b/assets/icons/zed_predict_up.svg new file mode 100644 index 0000000000000000000000000000000000000000..61ec143022b4f785affa5183d549a750bd741ab2 --- /dev/null +++ b/assets/icons/zed_predict_up.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 48eebbeaef5af04bc861bcf95616a9cdec30cfde..ef9ab3f369397ec7fb194a7cefd426b1400ca778 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -31,7 +31,7 @@ "ctrl-,": "zed::OpenSettings", "ctrl-q": "zed::Quit", "f11": "zed::ToggleFullScreen", - "ctrl-alt-z": "zeta::RateCompletions", + "ctrl-alt-z": "edit_prediction::RateCompletions", "ctrl-shift-i": "edit_prediction::ToggleMenu" } }, @@ -502,17 +502,22 @@ "tab": "editor::ComposeCompletion" } }, + // Bindings for accepting edit predictions + // + // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This is + // because alt-tab may not be available, as it is often used for window switching. { "context": "Editor && edit_prediction", "bindings": { - // Changing the modifier currently breaks accepting while you also an LSP completions menu open - "alt-enter": "editor::AcceptEditPrediction" + "alt-tab": "editor::AcceptEditPrediction", + "alt-l": "editor::AcceptEditPrediction" } }, { "context": "Editor && edit_prediction && !edit_prediction_requires_modifier", "bindings": { - "tab": "editor::AcceptEditPrediction" + "tab": "editor::AcceptEditPrediction", + "alt-l": "editor::AcceptEditPrediction" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 5fa14b940c592f19f708b198ebe47dc41d917174..f2b37819d3e8e8973fdbab01ba9cc3b762e1fa2d 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -39,7 +39,7 @@ "cmd-m": "zed::Minimize", "fn-f": "zed::ToggleFullScreen", "ctrl-cmd-f": "zed::ToggleFullScreen", - "ctrl-cmd-z": "zeta::RateCompletions", + "ctrl-cmd-z": "edit_prediction::RateCompletions", "ctrl-cmd-i": "edit_prediction::ToggleMenu" } }, @@ -583,7 +583,6 @@ { "context": "Editor && edit_prediction", "bindings": { - // Changing the modifier currently breaks accepting while you also an LSP completions menu open "alt-tab": "editor::AcceptEditPrediction" } }, diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index aa3e44892c18a30878091cbe9e59c9b616879200..111d4c8181f111dca294f2d7dc1eeccab9f1373e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -694,5 +694,22 @@ "shift-x": "git::StageAll", "shift-u": "git::UnstageAll" } + }, + { + "context": "edit_prediction && !edit_prediction_requires_modifier", + "bindings": { + // This is identical to the binding in the base keymap, but the vim bindings above to + // "vim::Tab" shadow it, so it needs to be bound again. + "tab": "editor::AcceptEditPrediction" + } + }, + { + "context": "os != macos && edit_prediction", + "bindings": { + // alt-l is provided as an alternative to tab/alt-tab. and will be displayed in the UI. This + // is because alt-tab may not be available, as it is often used for window switching on Linux + // and Windows. + "alt-l": "editor::AcceptEditPrediction" + } } ] diff --git a/crates/diff/Cargo.toml b/crates/buffer_diff/Cargo.toml similarity index 80% rename from crates/diff/Cargo.toml rename to crates/buffer_diff/Cargo.toml index 7a4186f6e5aaf29dfa58a0b39b765c5f57dcd796..d4cac616d0e5eb5f372e1b5585e321f39e398b91 100644 --- a/crates/diff/Cargo.toml +++ b/crates/buffer_diff/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "diff" +name = "buffer_diff" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,17 +9,17 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/diff.rs" +path = "src/buffer_diff.rs" [features] test-support = [] [dependencies] +anyhow.workspace = true futures.workspace = true git2.workspace = true gpui.workspace = true language.workspace = true -log.workspace = true rope.workspace = true sum_tree.workspace = true text.workspace = true @@ -29,4 +29,5 @@ util.workspace = true pretty_assertions.workspace = true serde_json.workspace = true text = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } unindent.workspace = true diff --git a/crates/diff/LICENSE-GPL b/crates/buffer_diff/LICENSE-GPL similarity index 100% rename from crates/diff/LICENSE-GPL rename to crates/buffer_diff/LICENSE-GPL diff --git a/crates/diff/src/diff.rs b/crates/buffer_diff/src/buffer_diff.rs similarity index 50% rename from crates/diff/src/diff.rs rename to crates/buffer_diff/src/buffer_diff.rs index adb25417a713048715df7bfc85ac4f09ab651458..772835f9a93f382934b91e016ba4c8c2cb91fef7 100644 --- a/crates/diff/src/diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -1,20 +1,60 @@ use futures::{channel::oneshot, future::OptionFuture}; use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch}; -use gpui::{App, Context, Entity, EventEmitter}; +use gpui::{App, AsyncApp, Context, Entity, EventEmitter}; use language::{Language, LanguageRegistry}; use rope::Rope; use std::{cmp, future::Future, iter, ops::Range, sync::Arc}; use sum_tree::SumTree; -use text::{Anchor, BufferId, OffsetRangeExt, Point}; +use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point}; use util::ResultExt; -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct BufferDiff { + pub buffer_id: BufferId, + inner: BufferDiffInner, + secondary_diff: Option>, +} + +#[derive(Clone)] +pub struct BufferDiffSnapshot { + inner: BufferDiffInner, + secondary_diff: Option>, +} + +#[derive(Clone)] +struct BufferDiffInner { + hunks: SumTree, + base_text: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DiffHunkStatus { - Added, - Modified, - Removed, + Added(DiffHunkSecondaryStatus), + Modified(DiffHunkSecondaryStatus), + Removed(DiffHunkSecondaryStatus), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum DiffHunkSecondaryStatus { + HasSecondaryHunk, + OverlapsWithSecondaryHunk, + None, } +// to stage a hunk: +// - assume hunk starts out as not staged +// - hunk exists with the same buffer range in the unstaged diff and the uncommitted diff +// - we want to construct a "version" of the file that +// - starts from the index base text +// - has the single hunk applied to it +// - the hunk is the one from the UNSTAGED diff, so that the diff base offset range is correct to apply to that diff base +// - write that new version of the file into the index + +// to unstage a hunk +// - no hunk in the unstaged diff intersects this hunk from the uncommitted diff +// - we want to compute the hunk that +// - we can apply to the index text +// - at the end of applying it, + /// A diff hunk resolved to rows in the buffer. #[derive(Debug, Clone, PartialEq, Eq)] pub struct DiffHunk { @@ -24,6 +64,7 @@ pub struct DiffHunk { pub buffer_range: Range, /// The range in the buffer's diff base text to which this hunk corresponds. pub diff_base_byte_range: Range, + pub secondary_status: DiffHunkSecondaryStatus, } /// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range. @@ -64,13 +105,17 @@ impl sum_tree::Summary for DiffHunkSummary { } } -#[derive(Clone)] -pub struct BufferDiffSnapshot { - hunks: SumTree, - pub base_text: Option, +impl<'a> sum_tree::SeekTarget<'a, DiffHunkSummary, DiffHunkSummary> for Anchor { + fn cmp( + &self, + cursor_location: &DiffHunkSummary, + buffer: &text::BufferSnapshot, + ) -> cmp::Ordering { + self.cmp(&cursor_location.buffer_range.end, buffer) + } } -impl std::fmt::Debug for BufferDiffSnapshot { +impl std::fmt::Debug for BufferDiffInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferDiffSnapshot") .field("hunks", &self.hunks) @@ -79,142 +124,56 @@ impl std::fmt::Debug for BufferDiffSnapshot { } impl BufferDiffSnapshot { - pub fn new(buffer: &text::BufferSnapshot) -> BufferDiffSnapshot { - BufferDiffSnapshot { - hunks: SumTree::new(buffer), - base_text: None, - } + pub fn is_empty(&self) -> bool { + self.inner.hunks.is_empty() } - pub fn new_with_single_insertion(cx: &mut App) -> Self { - let base_text = language::Buffer::build_empty_snapshot(cx); - Self { - hunks: SumTree::from_item( - InternalDiffHunk { - buffer_range: Anchor::MIN..Anchor::MAX, - diff_base_byte_range: 0..0, - }, - &base_text, - ), - base_text: Some(base_text), - } + pub fn secondary_diff(&self) -> Option<&BufferDiffSnapshot> { + self.secondary_diff.as_deref() } - #[cfg(any(test, feature = "test-support"))] - pub fn build_sync( - buffer: text::BufferSnapshot, - diff_base: String, - cx: &mut gpui::TestAppContext, - ) -> Self { - let snapshot = - cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx)); - cx.executor().block(snapshot) + pub fn hunks_intersecting_range<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + let unstaged_counterpart = self.secondary_diff.as_ref().map(|diff| &diff.inner); + self.inner + .hunks_intersecting_range(range, buffer, unstaged_counterpart) } - pub fn build( - buffer: text::BufferSnapshot, - diff_base: Option>, - language: Option>, - language_registry: Option>, - cx: &mut App, - ) -> impl Future { - let base_text_snapshot = diff_base.as_ref().map(|base_text| { - language::Buffer::build_snapshot( - Rope::from(base_text.as_str()), - language.clone(), - language_registry.clone(), - cx, - ) - }); - let base_text_snapshot = cx - .background_executor() - .spawn(OptionFuture::from(base_text_snapshot)); - - let hunks = cx.background_executor().spawn({ - let buffer = buffer.clone(); - async move { Self::recalculate_hunks(diff_base, buffer) } - }); - - async move { - let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); - Self { base_text, hunks } - } + pub fn hunks_intersecting_range_rev<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + ) -> impl 'a + Iterator { + self.inner.hunks_intersecting_range_rev(range, buffer) } - pub fn build_with_base_buffer( - buffer: text::BufferSnapshot, - diff_base: Option>, - diff_base_buffer: Option, - cx: &App, - ) -> impl Future { - cx.background_executor().spawn({ - let buffer = buffer.clone(); - async move { - let hunks = Self::recalculate_hunks(diff_base, buffer); - Self { - hunks, - base_text: diff_base_buffer, - } - } - }) + pub fn base_text(&self) -> Option<&language::BufferSnapshot> { + self.inner.base_text.as_ref() } - fn recalculate_hunks( - diff_base: Option>, - buffer: text::BufferSnapshot, - ) -> SumTree { - let mut tree = SumTree::new(&buffer); - - if let Some(diff_base) = diff_base { - let buffer_text = buffer.as_rope().to_string(); - let patch = Self::diff(&diff_base, &buffer_text); - - // A common case in Zed is that the empty buffer is represented as just a newline, - // but if we just compute a naive diff you get a "preserved" line in the middle, - // which is a bit odd. - if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 { - tree.push( - InternalDiffHunk { - buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0), - diff_base_byte_range: 0..diff_base.len() - 1, - }, - &buffer, - ); - return tree; - } - - 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); - } + pub fn base_texts_eq(&self, other: &Self) -> bool { + match (other.base_text(), self.base_text()) { + (None, None) => true, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(old), Some(new)) => { + let (old_id, old_empty) = (old.remote_id(), old.is_empty()); + let (new_id, new_empty) = (new.remote_id(), new.is_empty()); + new_id == old_id || (new_empty && old_empty) } } - - tree - } - - pub fn is_empty(&self) -> bool { - self.hunks.is_empty() - } - - pub fn hunks_in_row_range<'a>( - &'a self, - range: Range, - buffer: &'a text::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>( +impl BufferDiffInner { + fn hunks_intersecting_range<'a>( &'a self, range: Range, buffer: &'a text::BufferSnapshot, + secondary: Option<&'a Self>, ) -> impl 'a + Iterator { let range = range.to_offset(buffer); @@ -244,6 +203,12 @@ impl BufferDiffSnapshot { ] }); + let mut secondary_cursor = secondary.as_ref().map(|diff| { + let mut cursor = diff.hunks.cursor::(buffer); + cursor.next(buffer); + cursor + }); + let mut summaries = buffer.summaries_for_anchors_with_payload::(anchor_iter); iter::from_fn(move || loop { let (start_point, (start_anchor, start_base)) = summaries.next()?; @@ -259,15 +224,35 @@ impl BufferDiffSnapshot { end_anchor = buffer.anchor_before(end_point); } + let mut secondary_status = DiffHunkSecondaryStatus::None; + if let Some(secondary_cursor) = secondary_cursor.as_mut() { + if start_anchor + .cmp(&secondary_cursor.start().buffer_range.start, buffer) + .is_gt() + { + secondary_cursor.seek_forward(&end_anchor, Bias::Left, buffer); + } + + if let Some(secondary_hunk) = secondary_cursor.item() { + let secondary_range = secondary_hunk.buffer_range.to_point(buffer); + if secondary_range == (start_point..end_point) { + secondary_status = DiffHunkSecondaryStatus::HasSecondaryHunk; + } else if secondary_range.start <= end_point { + secondary_status = DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk; + } + } + } + return Some(DiffHunk { row_range: start_point.row..end_point.row, diff_base_byte_range: start_base..end_base, buffer_range: start_anchor..end_anchor, + secondary_status, }); }) } - pub fn hunks_intersecting_range_rev<'a>( + fn hunks_intersecting_range_rev<'a>( &'a self, range: Range, buffer: &'a text::BufferSnapshot, @@ -295,15 +280,13 @@ impl BufferDiffSnapshot { row_range: range.start.row..end_row, diff_base_byte_range: hunk.diff_base_byte_range.clone(), buffer_range: hunk.buffer_range.clone(), + // The secondary status is not used by callers of this method. + secondary_status: DiffHunkSecondaryStatus::None, }) }) } - pub fn compare( - &self, - old: &Self, - new_snapshot: &text::BufferSnapshot, - ) -> Option> { + fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option> { let mut new_cursor = self.hunks.cursor::<()>(new_snapshot); let mut old_cursor = old.hunks.cursor::<()>(new_snapshot); old_cursor.next(new_snapshot); @@ -365,174 +348,370 @@ impl BufferDiffSnapshot { start.zip(end).map(|(start, end)| start..end) } +} - #[cfg(test)] - fn clear(&mut self, buffer: &text::BufferSnapshot) { - self.hunks = SumTree::new(buffer); - } +fn compute_hunks( + diff_base: Option>, + buffer: text::BufferSnapshot, +) -> SumTree { + let mut tree = SumTree::new(&buffer); - #[cfg(test)] - fn hunks<'a>(&'a self, text: &'a text::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) - } + if let Some(diff_base) = diff_base { + let buffer_text = buffer.as_rope().to_string(); - 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(), + diff_base.as_bytes(), None, - current.as_bytes(), + buffer_text.as_bytes(), None, Some(&mut options), - ); - - match patch { - Ok(patch) => Some(patch), + ) + .log_err(); + + // A common case in Zed is that the empty buffer is represented as just a newline, + // but if we just compute a naive diff you get a "preserved" line in the middle, + // which is a bit odd. + if buffer_text == "\n" && diff_base.ends_with("\n") && diff_base.len() > 1 { + tree.push( + InternalDiffHunk { + buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0), + diff_base_byte_range: 0..diff_base.len() - 1, + }, + &buffer, + ); + return tree; + } - Err(err) => { - log::error!("`GitPatch::from_buffers` failed: {}", err); - None + if let Some(patch) = patch { + let mut divergence = 0; + for hunk_index in 0..patch.num_hunks() { + let hunk = process_patch_hunk(&patch, hunk_index, &buffer, &mut divergence); + tree.push(hunk, &buffer); } } } - fn process_patch_hunk( - patch: &GitPatch<'_>, - hunk_index: usize, - buffer: &text::BufferSnapshot, - buffer_row_divergence: &mut i64, - ) -> InternalDiffHunk { - 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); - } + tree +} - *buffer_row_divergence -= 1; +fn process_patch_hunk( + patch: &GitPatch<'_>, + hunk_index: usize, + buffer: &text::BufferSnapshot, + buffer_row_divergence: &mut i64, +) -> InternalDiffHunk { + 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), } } - //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 - }); + 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), + } - //unwrap_or addition without deletion - let diff_base_byte_range = diff_base_byte_range.unwrap_or(0..0); + 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); + } - 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); - InternalDiffHunk { - buffer_range, - diff_base_byte_range, + *buffer_row_divergence -= 1; } } -} -pub struct BufferDiff { - pub buffer_id: BufferId, - pub snapshot: BufferDiffSnapshot, - pub unstaged_diff: Option>, + //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); + InternalDiffHunk { + buffer_range, + diff_base_byte_range, + } } impl std::fmt::Debug for BufferDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("BufferChangeSet") .field("buffer_id", &self.buffer_id) - .field("snapshot", &self.snapshot) + .field("snapshot", &self.inner) .finish() } } pub enum BufferDiffEvent { - DiffChanged { changed_range: Range }, + DiffChanged { + changed_range: Option>, + }, LanguageChanged, } impl EventEmitter for BufferDiff {} impl BufferDiff { - pub fn set_state( + #[cfg(test)] + fn build_sync( + buffer: text::BufferSnapshot, + diff_base: String, + cx: &mut gpui::TestAppContext, + ) -> BufferDiffInner { + let snapshot = + cx.update(|cx| Self::build(buffer, Some(Arc::new(diff_base)), None, None, cx)); + cx.executor().block(snapshot) + } + + fn build( + buffer: text::BufferSnapshot, + diff_base: Option>, + language: Option>, + language_registry: Option>, + cx: &mut App, + ) -> impl Future { + let base_text_snapshot = diff_base.as_ref().map(|base_text| { + language::Buffer::build_snapshot( + Rope::from(base_text.as_str()), + language.clone(), + language_registry.clone(), + cx, + ) + }); + let base_text_snapshot = cx + .background_executor() + .spawn(OptionFuture::from(base_text_snapshot)); + + let hunks = cx.background_executor().spawn({ + let buffer = buffer.clone(); + async move { compute_hunks(diff_base, buffer) } + }); + + async move { + let (base_text, hunks) = futures::join!(base_text_snapshot, hunks); + BufferDiffInner { base_text, hunks } + } + } + + fn build_with_base_buffer( + buffer: text::BufferSnapshot, + diff_base: Option>, + diff_base_buffer: Option, + cx: &App, + ) -> impl Future { + cx.background_executor().spawn(async move { + BufferDiffInner { + hunks: compute_hunks(diff_base, buffer), + base_text: diff_base_buffer, + } + }) + } + + fn build_empty(buffer: &text::BufferSnapshot) -> BufferDiffInner { + BufferDiffInner { + hunks: SumTree::new(buffer), + base_text: None, + } + } + + pub fn build_with_single_insertion( + insertion_present_in_secondary_diff: bool, + cx: &mut App, + ) -> BufferDiffSnapshot { + let base_text = language::Buffer::build_empty_snapshot(cx); + let hunks = SumTree::from_item( + InternalDiffHunk { + buffer_range: Anchor::MIN..Anchor::MAX, + diff_base_byte_range: 0..0, + }, + &base_text, + ); + BufferDiffSnapshot { + inner: BufferDiffInner { + hunks: hunks.clone(), + base_text: Some(base_text.clone()), + }, + secondary_diff: if insertion_present_in_secondary_diff { + Some(Box::new(BufferDiffSnapshot { + inner: BufferDiffInner { + hunks, + base_text: Some(base_text), + }, + secondary_diff: None, + })) + } else { + None + }, + } + } + + pub fn set_secondary_diff(&mut self, diff: Entity) { + self.secondary_diff = Some(diff); + } + + pub fn secondary_diff(&self) -> Option> { + Some(self.secondary_diff.as_ref()?.clone()) + } + + pub fn range_to_hunk_range( + &self, + range: Range, + buffer: &text::BufferSnapshot, + cx: &App, + ) -> Option> { + let start = self + .hunks_intersecting_range(range.clone(), &buffer, cx) + .next()? + .buffer_range + .start; + let end = self + .hunks_intersecting_range_rev(range.clone(), &buffer) + .next()? + .buffer_range + .end; + Some(start..end) + } + + #[allow(clippy::too_many_arguments)] + pub async fn update_diff( + this: Entity, + buffer: text::BufferSnapshot, + base_text: Option>, + base_text_changed: bool, + language_changed: bool, + language: Option>, + language_registry: Option>, + cx: &mut AsyncApp, + ) -> anyhow::Result>> { + let snapshot = if base_text_changed || language_changed { + cx.update(|cx| { + Self::build( + buffer.clone(), + base_text, + language.clone(), + language_registry.clone(), + cx, + ) + })? + .await + } else { + this.read_with(cx, |this, cx| { + Self::build_with_base_buffer( + buffer.clone(), + base_text, + this.base_text().cloned(), + cx, + ) + })? + .await + }; + + this.update(cx, |this, _| this.set_state(snapshot, &buffer)) + } + + pub fn update_diff_from( &mut self, - snapshot: BufferDiffSnapshot, buffer: &text::BufferSnapshot, + other: &Entity, cx: &mut Context, - ) { - if let Some(base_text) = snapshot.base_text.as_ref() { - let changed_range = if Some(base_text.remote_id()) - != self - .snapshot - .base_text - .as_ref() - .map(|buffer| buffer.remote_id()) - { - Some(text::Anchor::MIN..text::Anchor::MAX) - } else { - snapshot.compare(&self.snapshot, buffer) - }; - if let Some(changed_range) = changed_range { - cx.emit(BufferDiffEvent::DiffChanged { changed_range }); + ) -> Option> { + let other = other.read(cx).inner.clone(); + self.set_state(other, buffer) + } + + fn set_state( + &mut self, + inner: BufferDiffInner, + buffer: &text::BufferSnapshot, + ) -> Option> { + let changed_range = match (self.inner.base_text.as_ref(), inner.base_text.as_ref()) { + (None, None) => None, + (Some(old), Some(new)) if old.remote_id() == new.remote_id() => { + inner.compare(&self.inner, buffer) } + _ => Some(text::Anchor::MIN..text::Anchor::MAX), + }; + self.inner = inner; + changed_range + } + + pub fn base_text(&self) -> Option<&language::BufferSnapshot> { + self.inner.base_text.as_ref() + } + + pub fn snapshot(&self, cx: &App) -> BufferDiffSnapshot { + BufferDiffSnapshot { + inner: self.inner.clone(), + secondary_diff: self + .secondary_diff + .as_ref() + .map(|diff| Box::new(diff.read(cx).snapshot(cx))), } - self.snapshot = snapshot; } - pub fn diff_hunks_intersecting_range<'a>( + pub fn hunks_intersecting_range<'a>( &'a self, range: Range, buffer_snapshot: &'a text::BufferSnapshot, + cx: &'a App, ) -> impl 'a + Iterator { - self.snapshot - .hunks_intersecting_range(range, buffer_snapshot) + let unstaged_counterpart = self + .secondary_diff + .as_ref() + .map(|diff| &diff.read(cx).inner); + self.inner + .hunks_intersecting_range(range, buffer_snapshot, unstaged_counterpart) } - pub fn diff_hunks_intersecting_range_rev<'a>( + pub fn hunks_intersecting_range_rev<'a>( &'a self, range: Range, buffer_snapshot: &'a text::BufferSnapshot, ) -> impl 'a + Iterator { - self.snapshot + self.inner .hunks_intersecting_range_rev(range, buffer_snapshot) } + pub fn hunks_in_row_range<'a>( + &'a self, + range: Range, + buffer: &'a text::BufferSnapshot, + cx: &'a App, + ) -> 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, cx) + } + /// Used in cases where the change set isn't derived from git. pub fn set_base_text( &mut self, @@ -547,7 +726,7 @@ impl BufferDiff { let base_buffer = base_buffer.snapshot(); let base_text = Arc::new(base_buffer.text()); - let snapshot = BufferDiffSnapshot::build( + let snapshot = BufferDiff::build( buffer.clone(), Some(base_text), base_buffer.language().cloned(), @@ -562,8 +741,8 @@ impl BufferDiff { let Some(this) = this.upgrade() else { return; }; - this.update(&mut cx, |this, cx| { - this.set_state(snapshot, &buffer, cx); + this.update(&mut cx, |this, _| { + this.set_state(snapshot, &buffer); }) .log_err(); drop(complete_on_drop) @@ -574,14 +753,14 @@ impl BufferDiff { #[cfg(any(test, feature = "test-support"))] pub fn base_text_string(&self) -> Option { - self.snapshot.base_text.as_ref().map(|buffer| buffer.text()) + self.inner.base_text.as_ref().map(|buffer| buffer.text()) } - pub fn new(buffer: &Entity, cx: &mut App) -> Self { + pub fn new(buffer: &text::BufferSnapshot) -> Self { BufferDiff { - buffer_id: buffer.read(cx).remote_id(), - snapshot: BufferDiffSnapshot::new(&buffer.read(cx)), - unstaged_diff: None, + buffer_id: buffer.remote_id(), + inner: BufferDiff::build_empty(buffer), + secondary_diff: None, } } @@ -593,7 +772,7 @@ impl BufferDiff { ) -> Self { let mut base_text = base_text.to_owned(); text::LineEnding::normalize(&mut base_text); - let snapshot = BufferDiffSnapshot::build( + let snapshot = BufferDiff::build( buffer.read(cx).text_snapshot(), Some(base_text.into()), None, @@ -603,26 +782,60 @@ impl BufferDiff { let snapshot = cx.background_executor().block(snapshot); BufferDiff { buffer_id: buffer.read(cx).remote_id(), - snapshot, - unstaged_diff: None, + inner: snapshot, + secondary_diff: None, } } #[cfg(any(test, feature = "test-support"))] pub fn recalculate_diff_sync(&mut self, buffer: text::BufferSnapshot, cx: &mut Context) { let base_text = self - .snapshot + .inner .base_text .as_ref() .map(|base_text| base_text.text()); - let snapshot = BufferDiffSnapshot::build_with_base_buffer( + let snapshot = BufferDiff::build_with_base_buffer( buffer.clone(), base_text.clone().map(Arc::new), - self.snapshot.base_text.clone(), + self.inner.base_text.clone(), cx, ); let snapshot = cx.background_executor().block(snapshot); - self.set_state(snapshot, &buffer, cx); + let changed_range = self.set_state(snapshot, &buffer); + cx.emit(BufferDiffEvent::DiffChanged { changed_range }); + } +} + +impl DiffHunk { + pub fn status(&self) -> DiffHunkStatus { + if self.buffer_range.start == self.buffer_range.end { + DiffHunkStatus::Removed(self.secondary_status) + } else if self.diff_base_byte_range.is_empty() { + DiffHunkStatus::Added(self.secondary_status) + } else { + DiffHunkStatus::Modified(self.secondary_status) + } + } +} + +impl DiffHunkStatus { + pub fn is_removed(&self) -> bool { + matches!(self, DiffHunkStatus::Removed(_)) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn removed() -> Self { + DiffHunkStatus::Removed(DiffHunkSecondaryStatus::None) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn added() -> Self { + DiffHunkStatus::Added(DiffHunkSecondaryStatus::None) + } + + #[cfg(any(test, feature = "test-support"))] + pub fn modified() -> Self { + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None) } } @@ -633,7 +846,7 @@ pub fn assert_hunks( diff_hunks: Iter, buffer: &text::BufferSnapshot, diff_base: &str, - expected_hunks: &[(Range, &str, &str)], + expected_hunks: &[(Range, &str, &str, DiffHunkStatus)], ) where Iter: Iterator, { @@ -641,19 +854,20 @@ pub fn assert_hunks( .map(|hunk| { ( hunk.row_range.clone(), - &diff_base[hunk.diff_base_byte_range], + &diff_base[hunk.diff_base_byte_range.clone()], buffer .text_for_range( Point::new(hunk.row_range.start, 0)..Point::new(hunk.row_range.end, 0), ) .collect::(), + hunk.status(), ) }) .collect::>(); let expected_hunks: Vec<_> = expected_hunks .iter() - .map(|(r, s, h)| (r.clone(), *s, h.to_string())) + .map(|(r, s, h, status)| (r.clone(), *s, h.to_string(), *status)) .collect(); assert_eq!(actual_hunks, expected_hunks); @@ -685,25 +899,115 @@ mod tests { .unindent(); let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); - let mut diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx); + let mut diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx); assert_hunks( - diff.hunks(&buffer), + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), &buffer, &diff_base, - &[(1..2, "two\n", "HELLO\n")], + &[(1..2, "two\n", "HELLO\n", DiffHunkStatus::modified())], ); buffer.edit([(0..0, "point five\n")]); - diff = BufferDiffSnapshot::build_sync(buffer.clone(), diff_base.clone(), cx); + diff = BufferDiff::build_sync(buffer.clone(), diff_base.clone(), cx); assert_hunks( - diff.hunks(&buffer), + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), &buffer, &diff_base, - &[(0..1, "", "point five\n"), (2..3, "two\n", "HELLO\n")], + &[ + (0..1, "", "point five\n", DiffHunkStatus::added()), + (2..3, "two\n", "HELLO\n", DiffHunkStatus::modified()), + ], + ); + + diff = BufferDiff::build_empty(&buffer); + assert_hunks( + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None), + &buffer, + &diff_base, + &[], ); + } + + #[gpui::test] + async fn test_buffer_diff_with_secondary(cx: &mut gpui::TestAppContext) { + let head_text = " + zero + one + two + three + four + five + six + seven + eight + nine + " + .unindent(); - diff.clear(&buffer); - assert_hunks(diff.hunks(&buffer), &buffer, &diff_base, &[]); + let index_text = " + zero + one + TWO + three + FOUR + five + six + seven + eight + NINE + " + .unindent(); + + let buffer_text = " + zero + one + TWO + three + FOUR + FIVE + six + SEVEN + eight + nine + " + .unindent(); + + let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); + let unstaged_diff = BufferDiff::build_sync(buffer.clone(), index_text.clone(), cx); + + let uncommitted_diff = BufferDiff::build_sync(buffer.clone(), head_text.clone(), cx); + + let expected_hunks = vec![ + ( + 2..3, + "two\n", + "TWO\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::None), + ), + ( + 4..6, + "four\nfive\n", + "FOUR\nFIVE\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), + ), + ( + 7..8, + "seven\n", + "SEVEN\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk), + ), + ]; + + assert_hunks( + uncommitted_diff.hunks_intersecting_range( + Anchor::MIN..Anchor::MAX, + &buffer, + Some(&unstaged_diff), + ), + &buffer, + &head_text, + &expected_hunks, + ); } #[gpui::test] @@ -748,25 +1052,27 @@ mod tests { let buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text); let diff = cx .update(|cx| { - BufferDiffSnapshot::build( - buffer.snapshot(), - Some(diff_base.clone()), - None, - None, - cx, - ) + BufferDiff::build(buffer.snapshot(), Some(diff_base.clone()), None, None, cx) }) .await; - assert_eq!(diff.hunks(&buffer).count(), 8); + assert_eq!( + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, None) + .count(), + 8 + ); assert_hunks( - diff.hunks_in_row_range(7..12, &buffer), + diff.hunks_intersecting_range( + buffer.anchor_before(Point::new(7, 0))..buffer.anchor_before(Point::new(12, 0)), + &buffer, + None, + ), &buffer, &diff_base, &[ - (6..7, "", "HELLO\n"), - (9..10, "six\n", "SIXTEEN\n"), - (12..13, "", "WORLD\n"), + (6..7, "", "HELLO\n", DiffHunkStatus::added()), + (9..10, "six\n", "SIXTEEN\n", DiffHunkStatus::modified()), + (12..13, "", "WORLD\n", DiffHunkStatus::added()), ], ); } @@ -801,8 +1107,8 @@ mod tests { let mut buffer = Buffer::new(0, BufferId::new(1).unwrap(), buffer_text_1); - let empty_diff = BufferDiffSnapshot::new(&buffer); - let diff_1 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let empty_diff = BufferDiff::build_empty(&buffer); + let diff_1 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_1.compare(&empty_diff, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(0, 0)..Point::new(8, 0)); @@ -820,7 +1126,7 @@ mod tests { " .unindent(), ); - let diff_2 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_2 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); assert_eq!(None, diff_2.compare(&diff_1, &buffer)); // Edit turns a deletion hunk into a modification. @@ -837,7 +1143,7 @@ mod tests { " .unindent(), ); - let diff_3 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_3 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_3.compare(&diff_2, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(1, 0)..Point::new(2, 0)); @@ -854,7 +1160,7 @@ mod tests { " .unindent(), ); - let diff_4 = BufferDiffSnapshot::build_sync(buffer.clone(), base_text.clone(), cx); + let diff_4 = BufferDiff::build_sync(buffer.clone(), base_text.clone(), cx); let range = diff_4.compare(&diff_3, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 4)..Point::new(4, 0)); @@ -872,7 +1178,7 @@ mod tests { " .unindent(), ); - let diff_5 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text.clone(), cx); + let diff_5 = BufferDiff::build_sync(buffer.snapshot(), base_text.clone(), cx); let range = diff_5.compare(&diff_4, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(3, 0)..Point::new(4, 0)); @@ -890,7 +1196,7 @@ mod tests { " .unindent(), ); - let diff_6 = BufferDiffSnapshot::build_sync(buffer.snapshot(), base_text, cx); + let diff_6 = BufferDiff::build_sync(buffer.snapshot(), base_text, cx); let range = diff_6.compare(&diff_5, &buffer).unwrap(); assert_eq!(range.to_point(&buffer), Point::new(7, 0)..Point::new(8, 0)); } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 8f133affcbd9e1f56e735c235afb08c486b6484d..2dd571677ee49ecf9f7440fae386d5d1f08039a2 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -33,7 +33,7 @@ clock.workspace = true collections.workspace = true dashmap.workspace = true derive_more.workspace = true -diff.workspace = true +buffer_diff.workspace = true envy = "0.4.2" futures.workspace = true google_ai.workspace = true diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index e9c8ab39f1217c373ddc6a2e5184cd9f55caa6c7..be9890dcb84275badc83404b5f17c24cbcfaf0e5 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -8,6 +8,7 @@ use crate::{ use anyhow::{anyhow, Result}; use assistant_context_editor::ContextStore; use assistant_slash_command::SlashCommandWorkingSet; +use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus}; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; use collections::{HashMap, HashSet}; @@ -2613,11 +2614,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2641,11 +2642,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2663,11 +2664,16 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(committed_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "TWO\n", "two\n")], + &[( + 1..2, + "TWO\n", + "two\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::HasSecondaryHunk), + )], ); }); @@ -2689,11 +2695,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2703,11 +2709,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2717,11 +2723,16 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_committed_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "TWO_HUNDRED\n", "two\n")], + &[( + 1..2, + "TWO_HUNDRED\n", + "two\n", + DiffHunkStatus::Modified(DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk), + )], ); }); @@ -2763,11 +2774,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &diff.base_text_string().unwrap(), - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2790,11 +2801,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &staged_text, - &[(1..2, "", "two\n")], + &[(1..2, "", "two\n", DiffHunkStatus::added())], ); }); @@ -2812,11 +2823,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &new_staged_text, - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); @@ -2826,11 +2837,11 @@ async fn test_git_diff_base_change( diff.base_text_string().as_deref(), Some(new_staged_text.as_str()) ); - diff::assert_hunks( - diff.snapshot.hunks_in_row_range(0..4, buffer), + assert_hunks( + diff.hunks_in_row_range(0..4, buffer, cx), buffer, &new_staged_text, - &[(2..3, "", "three\n")], + &[(2..3, "", "three\n", DiffHunkStatus::added())], ); }); } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index d4bfe38d564a26be8d9a914252bb47d7f5628a18..68467ff4b01201cbf1c91821abd1b6995b039a6e 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -198,26 +198,29 @@ impl CommandPaletteDelegate { ) { self.updating_matches.take(); - let mut intercept_result = CommandPaletteInterceptor::try_global(cx) - .and_then(|interceptor| interceptor.intercept(&query, cx)); + let mut intercept_results = CommandPaletteInterceptor::try_global(cx) + .map(|interceptor| interceptor.intercept(&query, cx)) + .unwrap_or_default(); if parse_zed_link(&query, cx).is_some() { - intercept_result = Some(CommandInterceptResult { + intercept_results = vec![CommandInterceptResult { action: OpenZedUrl { url: query.clone() }.boxed_clone(), string: query.clone(), positions: vec![], - }) + }] } - if let Some(CommandInterceptResult { + let mut new_matches = Vec::new(); + + for CommandInterceptResult { action, string, positions, - }) = intercept_result + } in intercept_results { if let Some(idx) = matches .iter() - .position(|m| commands[m.candidate_id].action.type_id() == action.type_id()) + .position(|m| commands[m.candidate_id].action.partial_eq(&*action)) { matches.remove(idx); } @@ -225,18 +228,16 @@ impl CommandPaletteDelegate { name: string.clone(), action, }); - matches.insert( - 0, - StringMatch { - candidate_id: commands.len() - 1, - string, - positions, - score: 0.0, - }, - ) + new_matches.push(StringMatch { + candidate_id: commands.len() - 1, + string, + positions, + score: 0.0, + }) } + new_matches.append(&mut matches); self.commands = commands; - self.matches = matches; + self.matches = new_matches; if self.matches.is_empty() { self.selected_ix = 0; } else { diff --git a/crates/command_palette_hooks/src/command_palette_hooks.rs b/crates/command_palette_hooks/src/command_palette_hooks.rs index 3ea94bd10f9c5e6c53824f0577e16bac27472771..df64d53874b4907b3bf586ee7935302c2e6979ae 100644 --- a/crates/command_palette_hooks/src/command_palette_hooks.rs +++ b/crates/command_palette_hooks/src/command_palette_hooks.rs @@ -108,7 +108,7 @@ pub struct CommandInterceptResult { /// An interceptor for the command palette. #[derive(Default)] pub struct CommandPaletteInterceptor( - Option Option>>, + Option Vec>>, ); #[derive(Default)] @@ -132,10 +132,12 @@ impl CommandPaletteInterceptor { } /// Intercepts the given query from the command palette. - pub fn intercept(&self, query: &str, cx: &App) -> Option { - let handler = self.0.as_ref()?; - - (handler)(query, cx) + pub fn intercept(&self, query: &str, cx: &App) -> Vec { + if let Some(handler) = self.0.as_ref() { + (handler)(query, cx) + } else { + Vec::new() + } } /// Clears the global interceptor. @@ -146,7 +148,7 @@ impl CommandPaletteInterceptor { /// Sets the global interceptor. /// /// This will override the previous interceptor, if it exists. - pub fn set(&mut self, handler: Box Option>) { + pub fn set(&mut self, handler: Box Vec>) { self.0 = Some(handler); } } diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 282c10eb608c7fe64dd01e1bdb944006ce139ff1..52ea66313c33d66f6a480715138076f994827823 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -38,7 +38,7 @@ clock.workspace = true collections.workspace = true convert_case.workspace = true db.workspace = true -diff.workspace = true +buffer_diff.workspace = true emojis.workspace = true file_icons.workspace = true futures.workspace = true diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 233ac8bf7f263a1c31ca7b39dac254956e409310..957648f4b8aa06353df970091e7f81b5472dcf72 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -48,7 +48,7 @@ mod signature_help; pub mod test; pub(crate) use actions::*; -pub use actions::{OpenExcerpts, OpenExcerptsSplit}; +pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; @@ -73,17 +73,16 @@ use code_context_menus::{ AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, CompletionsMenu, ContextMenuOrigin, }; -use diff::DiffHunkStatus; use git::blame::GitBlame; use gpui::{ - div, impl_actions, linear_color_stop, linear_gradient, point, prelude::*, pulsating_between, - px, relative, size, Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, - AvailableSpace, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, - Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, - FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, - MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, - StyledText, Subscription, Task, TextRun, TextStyle, TextStyleRefinement, UTF16Selection, - UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, + div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, Action, Animation, + AnimationExt, AnyElement, App, AsyncWindowContext, AvailableSpace, Background, Bounds, + ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, Entity, EntityInputHandler, + EventEmitter, FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, + HighlightStyle, Hsla, InteractiveText, KeyContext, Modifiers, MouseButton, MouseDownEvent, + PaintQuad, ParentElement, Pixels, Render, SharedString, Size, Styled, StyledText, Subscription, + Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, + WeakEntity, WeakFocusHandle, Window, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -486,7 +485,6 @@ enum InlineCompletion { }, Move { target: Anchor, - range_around_target: Range, snapshot: BufferSnapshot, }, } @@ -522,6 +520,296 @@ pub enum MenuInlineCompletionsPolicy { ByProvider, } +// TODO az do we need this? +#[derive(Clone)] +pub enum EditPredictionPreview { + /// Modifier is not pressed + Inactive, + /// Modifier pressed, animating to active + MovingTo { + animation: Range, + scroll_position_at_start: Option>, + target_point: DisplayPoint, + }, + Arrived { + scroll_position_at_start: Option>, + scroll_position_at_arrival: Option>, + target_point: Option, + }, + /// Modifier released, animating from active + MovingFrom { + animation: Range, + target_point: DisplayPoint, + }, +} + +impl EditPredictionPreview { + fn start( + &mut self, + completion: &InlineCompletion, + snapshot: &EditorSnapshot, + cursor: DisplayPoint, + ) -> bool { + if matches!(self, Self::MovingTo { .. } | Self::Arrived { .. }) { + return false; + } + (*self, _) = Self::start_now(completion, snapshot, cursor); + true + } + + fn restart( + &mut self, + completion: &InlineCompletion, + snapshot: &EditorSnapshot, + cursor: DisplayPoint, + ) -> bool { + match self { + Self::Inactive => false, + Self::MovingTo { target_point, .. } + | Self::Arrived { + target_point: Some(target_point), + .. + } => { + let (new_preview, new_target_point) = Self::start_now(completion, snapshot, cursor); + + if new_target_point != Some(*target_point) { + *self = new_preview; + return true; + } + + false + } + Self::Arrived { + target_point: None, .. + } => { + let (new_preview, _) = Self::start_now(completion, snapshot, cursor); + + *self = new_preview; + true + } + Self::MovingFrom { .. } => false, + } + } + + fn start_now( + completion: &InlineCompletion, + snapshot: &EditorSnapshot, + cursor: DisplayPoint, + ) -> (Self, Option) { + let now = Instant::now(); + match completion { + InlineCompletion::Edit { .. } => ( + Self::Arrived { + target_point: None, + scroll_position_at_start: None, + scroll_position_at_arrival: None, + }, + None, + ), + InlineCompletion::Move { target, .. } => { + let target_point = target.to_display_point(&snapshot.display_snapshot); + let duration = Self::animation_duration(cursor, target_point); + + ( + Self::MovingTo { + animation: now..now + duration, + scroll_position_at_start: Some(snapshot.scroll_position()), + target_point, + }, + Some(target_point), + ) + } + } + } + + fn animation_duration(a: DisplayPoint, b: DisplayPoint) -> Duration { + const SPEED: f32 = 8.0; + + let row_diff = b.row().0.abs_diff(a.row().0); + let column_diff = b.column().abs_diff(a.column()); + let distance = ((row_diff.pow(2) + column_diff.pow(2)) as f32).sqrt(); + Duration::from_millis((distance * SPEED) as u64) + } + + fn end( + &mut self, + cursor: DisplayPoint, + scroll_pixel_position: gpui::Point, + window: &mut Window, + cx: &mut Context, + ) -> bool { + let (scroll_position, target_point) = match self { + Self::MovingTo { + scroll_position_at_start, + target_point, + .. + } + | Self::Arrived { + scroll_position_at_start, + scroll_position_at_arrival: None, + target_point: Some(target_point), + .. + } => (*scroll_position_at_start, target_point), + Self::Arrived { + scroll_position_at_start, + scroll_position_at_arrival: Some(scroll_at_arrival), + target_point: Some(target_point), + } => { + const TOLERANCE: f32 = 4.0; + + let diff = *scroll_at_arrival - scroll_pixel_position.map(|p| p.0); + + if diff.x.abs() < TOLERANCE && diff.y.abs() < TOLERANCE { + (*scroll_position_at_start, target_point) + } else { + (None, target_point) + } + } + Self::Arrived { + target_point: None, .. + } => { + *self = Self::Inactive; + return true; + } + Self::MovingFrom { .. } | Self::Inactive => return false, + }; + + let now = Instant::now(); + let duration = Self::animation_duration(cursor, *target_point); + let target_point = *target_point; + + *self = Self::MovingFrom { + animation: now..now + duration, + target_point, + }; + + if let Some(scroll_position) = scroll_position { + cx.spawn_in(window, |editor, mut cx| async move { + smol::Timer::after(duration).await; + editor + .update_in(&mut cx, |editor, window, cx| { + if let Self::MovingFrom { .. } | Self::Inactive = + editor.edit_prediction_preview + { + editor.set_scroll_position(scroll_position, window, cx) + } + }) + .log_err(); + }) + .detach(); + } + + true + } + + /// Whether the preview is active or we are animating to or from it. + fn is_active(&self) -> bool { + matches!( + self, + Self::MovingTo { .. } | Self::Arrived { .. } | Self::MovingFrom { .. } + ) + } + + /// Returns true if the preview is active, not cancelled, and the animation is settled. + fn is_active_settled(&self) -> bool { + matches!(self, Self::Arrived { .. }) + } + + #[allow(clippy::too_many_arguments)] + fn move_state( + &mut self, + snapshot: &EditorSnapshot, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + scroll_pixel_position: gpui::Point, + line_height: Pixels, + target: Anchor, + cursor: Option, + ) -> Option { + let delta = match self { + Self::Inactive => return None, + Self::Arrived { .. } => 1., + Self::MovingTo { + animation, + scroll_position_at_start: original_scroll_position, + target_point, + } => { + let now = Instant::now(); + if animation.end < now { + *self = Self::Arrived { + scroll_position_at_start: *original_scroll_position, + scroll_position_at_arrival: Some(scroll_pixel_position.map(|p| p.0)), + target_point: Some(*target_point), + }; + 1.0 + } else { + (now - animation.start).as_secs_f32() + / (animation.end - animation.start).as_secs_f32() + } + } + Self::MovingFrom { animation, .. } => { + let now = Instant::now(); + if animation.end < now { + *self = Self::Inactive; + return None; + } else { + let delta = (now - animation.start).as_secs_f32() + / (animation.end - animation.start).as_secs_f32(); + 1.0 - delta + } + } + }; + + let cursor = cursor?; + + if !visible_row_range.contains(&cursor.row()) { + return None; + } + + let target_position = target.to_display_point(&snapshot.display_snapshot); + + if !visible_row_range.contains(&target_position.row()) { + return None; + } + + let target_row_layout = + &line_layouts[target_position.row().minus(visible_row_range.start) as usize]; + let target_column = target_position.column() as usize; + + let target_character_x = target_row_layout.x_for_index(target_column); + + let target_x = target_character_x - scroll_pixel_position.x; + let target_y = + (target_position.row().as_f32() - scroll_pixel_position.y / line_height) * line_height; + + let origin_x = line_layouts[cursor.row().minus(visible_row_range.start) as usize] + .x_for_index(cursor.column() as usize); + let origin_y = + (cursor.row().as_f32() - scroll_pixel_position.y / line_height) * line_height; + + let delta = 1.0 - (-10.0 * delta).exp2(); + + let x = origin_x + (target_x - origin_x) * delta; + let y = origin_y + (target_y - origin_y) * delta; + + Some(EditPredictionMoveState { + delta, + position: point(x, y), + }) + } +} + +pub(crate) struct EditPredictionMoveState { + delta: f32, + position: gpui::Point, +} + +impl EditPredictionMoveState { + pub fn is_animation_completed(&self) -> bool { + self.delta >= 1. + } +} + #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] struct EditorActionId(usize); @@ -705,7 +993,7 @@ pub struct Editor { inline_completions_hidden_for_vim_mode: bool, show_inline_completions_override: Option, menu_inline_completions_policy: MenuInlineCompletionsPolicy, - previewing_inline_completion: bool, + edit_prediction_preview: EditPredictionPreview, inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, @@ -722,6 +1010,7 @@ pub struct Editor { show_git_blame_gutter: bool, show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, + distinguish_unstaged_diff_hunks: bool, git_blame_inline_enabled: bool, serialize_dirty_buffers: bool, show_selection_menu: Option, @@ -1397,7 +1686,7 @@ impl Editor { edit_prediction_provider: None, active_inline_completion: None, stale_inline_completion_in_menu: None, - previewing_inline_completion: false, + edit_prediction_preview: EditPredictionPreview::Inactive, inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, @@ -1418,6 +1707,7 @@ impl Editor { custom_context_menu: None, show_git_blame_gutter: false, show_git_blame_inline: false, + distinguish_unstaged_diff_hunks: false, show_selection_menu: None, show_git_blame_inline_delay_task: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), @@ -1574,6 +1864,7 @@ impl Editor { window .bindings_for_action_in_context(&AcceptEditPrediction, context) .into_iter() + .rev() .next(), ) } @@ -1939,15 +2230,6 @@ impl Editor { self.refresh_inline_completion(false, true, window, cx); } - pub fn inline_completion_start_anchor(&self) -> Option { - let active_completion = self.active_inline_completion.as_ref()?; - let result = match &active_completion.completion { - InlineCompletion::Edit { edits, .. } => edits.first()?.0.start, - InlineCompletion::Move { target, .. } => *target, - }; - Some(result) - } - fn inline_completions_disabled_in_scope( &self, buffer: &Entity, @@ -5119,11 +5401,11 @@ impl Editor { true } - /// Returns true when we're displaying the inline completion popover below the cursor + /// Returns true when we're displaying the edit prediction popover below the cursor /// like we are not previewing and the LSP autocomplete menu is visible /// or we are in `when_holding_modifier` mode. pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool { - if self.previewing_inline_completion + if self.edit_prediction_preview.is_active() || !self.show_edit_predictions_in_menu() || !self.edit_predictions_enabled() { @@ -5145,15 +5427,7 @@ impl Editor { cx: &mut Context, ) { if self.show_edit_predictions_in_menu() { - let accept_binding = self.accept_edit_prediction_keybind(window, cx); - if let Some(accept_keystroke) = accept_binding.keystroke() { - let was_previewing_inline_completion = self.previewing_inline_completion; - self.previewing_inline_completion = modifiers == accept_keystroke.modifiers - && accept_keystroke.modifiers.modified(); - if self.previewing_inline_completion != was_previewing_inline_completion { - self.update_visible_inline_completion(window, cx); - } - } + self.update_edit_prediction_preview(&modifiers, position_map, window, cx); } let mouse_position = window.mouse_position(); @@ -5170,9 +5444,50 @@ impl Editor { ) } + fn update_edit_prediction_preview( + &mut self, + modifiers: &Modifiers, + position_map: &PositionMap, + window: &mut Window, + cx: &mut Context, + ) { + let accept_keybind = self.accept_edit_prediction_keybind(window, cx); + let Some(accept_keystroke) = accept_keybind.keystroke() else { + return; + }; + + if &accept_keystroke.modifiers == modifiers { + if let Some(completion) = self.active_inline_completion.as_ref() { + if self.edit_prediction_preview.start( + &completion.completion, + &position_map.snapshot, + self.selections + .newest_anchor() + .head() + .to_display_point(&position_map.snapshot), + ) { + self.request_autoscroll(Autoscroll::fit(), cx); + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } + } else if self.edit_prediction_preview.end( + self.selections + .newest_anchor() + .head() + .to_display_point(&position_map.snapshot), + position_map.scroll_pixel_position, + window, + cx, + ) { + self.update_visible_inline_completion(window, cx); + cx.notify(); + } + } + fn update_visible_inline_completion( &mut self, - _window: &mut Window, + window: &mut Window, cx: &mut Context, ) -> Option<()> { let selection = self.selections.newest_anchor(); @@ -5259,25 +5574,11 @@ impl Editor { invalidation_row_range = move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); let target = first_edit_start; - let target_point = text::ToPoint::to_point(&target.text_anchor, &snapshot); - // TODO: Base this off of TreeSitter or word boundaries? - let target_excerpt_begin = snapshot.anchor_before(snapshot.clip_point( - Point::new(target_point.row, target_point.column.saturating_sub(20)), - Bias::Left, - )); - let target_excerpt_end = snapshot.anchor_after(snapshot.clip_point( - Point::new(target_point.row, target_point.column + 20), - Bias::Right, - )); - let range_around_target = target_excerpt_begin..target_excerpt_end; - InlineCompletion::Move { - target, - range_around_target, - snapshot, - } + InlineCompletion::Move { target, snapshot } } else { let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) && !self.inline_completions_hidden_for_vim_mode; + if show_completions_in_buffer { if edits .iter() @@ -5336,6 +5637,15 @@ impl Editor { )); self.stale_inline_completion_in_menu = None; + let editor_snapshot = self.snapshot(window, cx); + if self.edit_prediction_preview.restart( + &completion, + &editor_snapshot, + cursor.to_display_point(&editor_snapshot), + ) { + self.request_autoscroll(Autoscroll::fit(), cx); + } + self.active_inline_completion = Some(InlineCompletionState { inlay_ids, completion, @@ -5563,7 +5873,7 @@ impl Editor { } pub fn context_menu_visible(&self) -> bool { - !self.previewing_inline_completion + !self.edit_prediction_preview.is_active() && self .context_menu .borrow() @@ -5598,7 +5908,7 @@ impl Editor { cursor_point: Point, style: &EditorStyle, accept_keystroke: &gpui::Keystroke, - window: &Window, + _window: &Window, cx: &mut Context, ) -> Option { let provider = self.edit_prediction_provider.as_ref()?; @@ -5653,20 +5963,51 @@ impl Editor { } let completion = match &self.active_inline_completion { - Some(completion) => self.render_edit_prediction_cursor_popover_preview( - completion, - cursor_point, - style, - window, - cx, - )?, + Some(completion) => match &completion.completion { + InlineCompletion::Move { + target, snapshot, .. + } if !self.has_visible_completions_menu() => { + use text::ToPoint as _; + + return Some( + h_flex() + .px_2() + .py_1() + .elevation_2(cx) + .border_color(cx.theme().colors().border) + .rounded_tl(px(0.)) + .gap_2() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Hold")) + .children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(Color::Default), + None, + true, + )) + .into_any(), + ); + } + _ => self.render_edit_prediction_cursor_popover_preview( + completion, + cursor_point, + style, + cx, + )?, + }, None if is_refreshing => match &self.stale_inline_completion_in_menu { Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( stale_completion, cursor_point, style, - window, cx, )?, @@ -5678,9 +6019,6 @@ impl Editor { None => pending_completion_container().child(Label::new("No Prediction")), }; - let buffer_font = theme::ThemeSettings::get_global(cx).buffer_font.clone(); - let completion = completion.font(buffer_font.clone()); - let completion = if is_refreshing { completion .with_animation( @@ -5705,6 +6043,7 @@ impl Editor { .px_2() .py_1() .elevation_2(cx) + .border_color(cx.theme().colors().border) .child(completion) .child(ui::Divider::vertical()) .child( @@ -5712,19 +6051,22 @@ impl Editor { .h_full() .gap_1() .pl_2() - .child(h_flex().font(buffer_font.clone()).gap_1().children( - ui::render_modifiers( - &accept_keystroke.modifiers, - PlatformStyle::platform(), - Some(if !has_completion { - Color::Muted - } else { - Color::Default - }), - None, - true, - ), - )) + .child( + h_flex() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) + .gap_1() + .children(ui::render_modifiers( + &accept_keystroke.modifiers, + PlatformStyle::platform(), + Some(if !has_completion { + Color::Muted + } else { + Color::Default + }), + None, + true, + )), + ) .child(Label::new("Preview").into_any_element()) .opacity(if has_completion { 1.0 } else { 0.4 }), ) @@ -5737,7 +6079,6 @@ impl Editor { completion: &InlineCompletionState, cursor_point: Point, style: &EditorStyle, - window: &Window, cx: &mut Context, ) -> Option
{ use text::ToPoint as _; @@ -5763,6 +6104,23 @@ impl Editor { } match &completion.completion { + InlineCompletion::Move { + target, snapshot, .. + } => Some( + h_flex() + .px_2() + .gap_2() + .flex_1() + .child( + if target.text_anchor.to_point(&snapshot).row > cursor_point.row { + Icon::new(IconName::ZedPredictDown) + } else { + Icon::new(IconName::ZedPredictUp) + }, + ) + .child(Label::new("Jump to Edit")), + ), + InlineCompletion::Edit { edits, edit_preview, @@ -5832,103 +6190,11 @@ impl Editor { .gap_2() .pr_1() .overflow_x_hidden() + .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) .child(left) .child(preview), ) } - - InlineCompletion::Move { - target, - range_around_target, - snapshot, - } => { - let highlighted_text = snapshot.highlighted_text_for_range( - range_around_target.clone(), - None, - &style.syntax, - ); - let base = h_flex().gap_3().flex_1().child(render_relative_row_jump( - "Jump ", - cursor_point.row, - target.text_anchor.to_point(&snapshot).row, - )); - - if highlighted_text.text.is_empty() { - return Some(base); - } - - let cursor_color = self.current_user_player_color(cx).cursor; - - let start_point = range_around_target.start.to_point(&snapshot); - let end_point = range_around_target.end.to_point(&snapshot); - let target_point = target.text_anchor.to_point(&snapshot); - - let styled_text = highlighted_text.to_styled_text(&style.text); - let text_len = highlighted_text.text.len(); - - let cursor_relative_position = window - .text_system() - .layout_line( - highlighted_text.text, - style.text.font_size.to_pixels(window.rem_size()), - // We don't need to include highlights - // because we are only using this for the cursor position - &[TextRun { - len: text_len, - font: style.text.font(), - color: style.text.color, - background_color: None, - underline: None, - strikethrough: None, - }], - ) - .log_err() - .map(|line| { - line.x_for_index( - target_point.column.saturating_sub(start_point.column) as usize - ) - }); - - let fade_before = start_point.column > 0; - let fade_after = end_point.column < snapshot.line_len(end_point.row); - - let background = cx.theme().colors().elevated_surface_background; - - let preview = h_flex() - .relative() - .child(styled_text) - .when(fade_before, |parent| { - parent.child(div().absolute().top_0().left_0().w_4().h_full().bg( - linear_gradient( - 90., - linear_color_stop(background, 0.), - linear_color_stop(background.opacity(0.), 1.), - ), - )) - }) - .when(fade_after, |parent| { - parent.child(div().absolute().top_0().right_0().w_4().h_full().bg( - linear_gradient( - -90., - linear_color_stop(background, 0.), - linear_color_stop(background.opacity(0.), 1.), - ), - )) - }) - .when_some(cursor_relative_position, |parent, position| { - parent.child( - div() - .w(px(2.)) - .h_full() - .bg(cursor_color) - .absolute() - .top_0() - .left(position), - ) - }); - - Some(base.child(preview)) - } } } @@ -6886,8 +7152,7 @@ impl Editor { let buffer = buffer.read(cx); let original_text = diff .read(cx) - .snapshot - .base_text + .base_text() .as_ref()? .as_rope() .slice(hunk.diff_base_byte_range.clone()); @@ -12298,6 +12563,10 @@ impl Editor { }); } + pub fn set_distinguish_unstaged_diff_hunks(&mut self) { + self.distinguish_unstaged_diff_hunks = true; + } + pub fn expand_all_diff_hunks( &mut self, _: &ExpandAllHunkDiffs, @@ -13340,14 +13609,14 @@ impl Editor { &self, window: &mut Window, cx: &mut App, - ) -> BTreeMap { + ) -> BTreeMap { let snapshot = self.snapshot(window, cx); let mut used_highlight_orders = HashMap::default(); self.highlighted_rows .iter() .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) .fold( - BTreeMap::::new(), + BTreeMap::::new(), |mut unique_rows, highlight| { let start = highlight.range.start.to_display_point(&snapshot); let end = highlight.range.end.to_display_point(&snapshot); @@ -13364,7 +13633,7 @@ impl Editor { used_highlight_orders.entry(row).or_insert(highlight.index); if highlight.index >= *used_index { *used_index = highlight.index; - unique_rows.insert(DisplayRow(row), highlight.color); + unique_rows.insert(DisplayRow(row), highlight.color.into()); } } unique_rows @@ -13744,6 +14013,23 @@ impl Editor { } } + pub fn previewing_edit_prediction_move( + &mut self, + ) -> Option<(Anchor, &mut EditPredictionPreview)> { + if !self.edit_prediction_preview.is_active() { + return None; + }; + + self.active_inline_completion + .as_ref() + .and_then(|completion| match completion.completion { + InlineCompletion::Move { target, .. } => { + Some((target, &mut self.edit_prediction_preview)) + } + _ => None, + }) + } + pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { (self.read_only(cx) || self.blink_manager.read(cx).visible()) && self.focus_handle.is_focused(window) @@ -14576,7 +14862,7 @@ impl Editor { } pub fn has_visible_completions_menu(&self) -> bool { - !self.previewing_inline_completion + !self.edit_prediction_preview.is_active() && self.context_menu.borrow().as_ref().map_or(false, |menu| { menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) }) @@ -15526,7 +15812,7 @@ impl EditorSnapshot { ) { // Deleted hunk is an empty row range, no caret can be placed there and Zed allows to revert it // when the caret is just above or just below the deleted hunk. - let allow_adjacent = hunk.status() == DiffHunkStatus::Removed; + let allow_adjacent = hunk.status().is_removed(); let related_to_selection = if allow_adjacent { hunk.row_range.overlaps(&query_rows) || hunk.row_range.start == query_rows.end diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6cf4526f3c22a673019dc2d24deda1ad79132195..03388a19d0d8e04b71cb3cad2466bd69dd4b4e6d 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -7,7 +7,7 @@ use crate::{ }, JoinLines, }; -use diff::{BufferDiff, DiffHunkStatus}; +use buffer_diff::{BufferDiff, DiffHunkStatus}; use futures::StreamExt; use gpui::{ div, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, @@ -11989,7 +11989,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { struct Row9.2; struct Row9.3; struct Row10;"#}, - vec![DiffHunkStatus::Added, DiffHunkStatus::Added], + vec![DiffHunkStatus::added(), DiffHunkStatus::added()], indoc! {r#"struct Row; struct Row1; struct Row1.1; @@ -12027,7 +12027,7 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { struct Row8; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Added, DiffHunkStatus::Added], + vec![DiffHunkStatus::added(), DiffHunkStatus::added()], indoc! {r#"struct Row; struct Row1; struct Row2; @@ -12074,11 +12074,11 @@ async fn test_addition_reverts(cx: &mut gpui::TestAppContext) { «ˇ// something on bottom» struct Row10;"#}, vec![ - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, - DiffHunkStatus::Added, + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), + DiffHunkStatus::added(), ], indoc! {r#"struct Row; ˇstruct Row1; @@ -12126,7 +12126,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row99; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified], + vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()], indoc! {r#"struct Row; struct Row1; struct Row33; @@ -12153,7 +12153,7 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row99; struct Row9; struct Row10;"#}, - vec![DiffHunkStatus::Modified, DiffHunkStatus::Modified], + vec![DiffHunkStatus::modified(), DiffHunkStatus::modified()], indoc! {r#"struct Row; struct Row1; struct Row33; @@ -12182,12 +12182,12 @@ async fn test_modification_reverts(cx: &mut gpui::TestAppContext) { struct Row9; struct Row1011;ˇ"#}, vec![ - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, - DiffHunkStatus::Modified, + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), + DiffHunkStatus::modified(), ], indoc! {r#"struct Row; ˇstruct Row1; @@ -12265,7 +12265,7 @@ struct Row10;"#}; ˇ struct Row8; struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row2; @@ -12288,7 +12288,7 @@ struct Row10;"#}; ˇ» struct Row8; struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row2; @@ -12313,7 +12313,7 @@ struct Row10;"#}; struct Row8;ˇ struct Row10;"#}, - vec![DiffHunkStatus::Removed, DiffHunkStatus::Removed], + vec![DiffHunkStatus::removed(), DiffHunkStatus::removed()], indoc! {r#"struct Row; struct Row1; ˇstruct Row2; @@ -12338,9 +12338,9 @@ struct Row10;"#}; struct Row8;ˇ» struct Row10;"#}, vec![ - DiffHunkStatus::Removed, - DiffHunkStatus::Removed, - DiffHunkStatus::Removed, + DiffHunkStatus::removed(), + DiffHunkStatus::removed(), + DiffHunkStatus::removed(), ], indoc! {r#"struct Row; struct Row1; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c6fd163a46e646ce6abf1c0f26c8fbfadf32f6be..377a620594230d810992c36b502a90b1b7290cfd 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,30 +16,30 @@ use crate::{ mouse_context_menu::{self, MenuPosition, MouseContextMenu}, scroll::{axis_pair, scroll_amount::ScrollAmount, AxisPair}, AcceptEditPrediction, BlockId, ChunkReplacement, CursorShape, CustomBlockId, DisplayPoint, - DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, Editor, EditorMode, - EditorSettings, EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GoToHunk, - GoToPrevHunk, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, - InlineCompletion, JumpData, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, - RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, - StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, + DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode, + EditPredictionPreview, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, + ExpandExcerpts, FocusedBlock, GoToHunk, GoToPrevHunk, GutterDimensions, HalfPageDown, + HalfPageUp, HandleInput, HoveredCursor, InlineCompletion, JumpData, LineDown, LineUp, + OpenExcerpts, PageDown, PageUp, Point, RevertSelectedHunks, RowExt, RowRangeExt, SelectPhase, + Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, CURSORS_VISIBLE_FOR, EDIT_PREDICTION_REQUIRES_MODIFIER_KEY_CONTEXT, FILE_HEADER_HEIGHT, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED, MAX_LINE_LEN, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, }; +use buffer_diff::{DiffHunkSecondaryStatus, DiffHunkStatus}; use client::ParticipantIndex; use collections::{BTreeMap, HashMap, HashSet}; -use diff::DiffHunkStatus; use file_icons::FileIcons; use git::{blame::BlameEntry, Oid}; use gpui::{ - anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, point, px, quad, - relative, size, svg, transparent_black, Action, AnyElement, App, AvailableSpace, Axis, Bounds, - ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, - Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, - Hsla, InteractiveElement, IntoElement, KeyBindingContextPredicate, Keystroke, Length, - ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, - ParentElement, Pixels, ScrollDelta, ScrollWheelEvent, ShapedLine, SharedString, Size, - StatefulInteractiveElement, Style, Styled, Subscription, TextRun, TextStyleRefinement, - WeakEntity, Window, + anchored, deferred, div, fill, linear_color_stop, linear_gradient, outline, pattern_slash, + point, px, quad, relative, size, svg, transparent_black, Action, AnyElement, App, + AvailableSpace, Axis, Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, + CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, + FontId, GlobalElementId, Hitbox, Hsla, InteractiveElement, IntoElement, + KeyBindingContextPredicate, Keystroke, Length, ModifiersChangedEvent, MouseButton, + MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, + ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, + Subscription, TextRun, TextStyleRefinement, WeakEntity, Window, }; use itertools::Itertools; use language::{ @@ -85,7 +85,6 @@ enum DisplayDiffHunk { Folded { display_row: DisplayRow, }, - Unfolded { diff_base_byte_range: Range, display_row_range: Range, @@ -1115,18 +1114,44 @@ impl EditorElement { em_width: Pixels, em_advance: Pixels, autoscroll_containing_element: bool, + newest_selection_head: Option, window: &mut Window, cx: &mut App, ) -> Vec { let mut autoscroll_bounds = None; let cursor_layouts = self.editor.update(cx, |editor, cx| { let mut cursors = Vec::new(); + + let previewing_move = + if let Some((target, preview)) = editor.previewing_edit_prediction_move() { + cursors.extend(self.layout_edit_prediction_preview_cursor( + snapshot, + visible_display_row_range.clone(), + line_layouts, + content_origin, + scroll_pixel_position, + line_height, + em_advance, + preview, + target, + newest_selection_head, + window, + cx, + )); + + true + } else { + false + }; + + let show_local_cursors = !previewing_move && editor.show_local_cursors(window, cx); + for (player_color, selections) in selections { for selection in selections { let cursor_position = selection.head; let in_range = visible_display_row_range.contains(&cursor_position.row()); - if (selection.is_local && !editor.show_local_cursors(window, cx)) + if (selection.is_local && !show_local_cursors) || !in_range || block_start_rows.contains(&cursor_position.row()) { @@ -1250,6 +1275,7 @@ impl EditorElement { cursors.push(cursor); } } + cursors }); @@ -1260,6 +1286,50 @@ impl EditorElement { cursor_layouts } + #[allow(clippy::too_many_arguments)] + fn layout_edit_prediction_preview_cursor( + &self, + snapshot: &EditorSnapshot, + visible_row_range: Range, + line_layouts: &[LineWithInvisibles], + content_origin: gpui::Point, + scroll_pixel_position: gpui::Point, + line_height: Pixels, + em_advance: Pixels, + preview: &mut EditPredictionPreview, + target: Anchor, + cursor: Option, + window: &mut Window, + cx: &mut App, + ) -> Option { + let state = preview.move_state( + snapshot, + visible_row_range, + line_layouts, + scroll_pixel_position, + line_height, + target, + cursor, + )?; + + if !state.is_animation_completed() { + window.request_animation_frame(); + } + + let mut cursor = CursorLayout { + color: self.style.local_player.cursor, + block_width: em_advance, + origin: state.position, + line_height, + shape: CursorShape::Bar, + block_text: None, + cursor_name: None, + }; + + cursor.layout(content_origin, None, window, cx); + Some(cursor) + } + fn layout_scrollbars( &self, snapshot: &EditorSnapshot, @@ -2116,7 +2186,7 @@ impl EditorElement { .get(&display_row) .unwrap_or(&non_relative_number); write!(&mut line_number, "{number}").unwrap(); - if row_info.diff_status == Some(DiffHunkStatus::Removed) { + if matches!(row_info.diff_status, Some(DiffHunkStatus::Removed(_))) { return None; } @@ -3532,7 +3602,7 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn layout_inline_completion_popover( + fn layout_edit_prediction_popover( &self, text_bounds: &Bounds, editor_snapshot: &EditorSnapshot, @@ -3560,6 +3630,49 @@ impl EditorElement { match &active_inline_completion.completion { InlineCompletion::Move { target, .. } => { + if editor.edit_prediction_requires_modifier() { + let cursor_position = + target.to_display_point(&editor_snapshot.display_snapshot); + + if !editor.edit_prediction_preview.is_active_settled() + || !visible_row_range.contains(&cursor_position.row()) + { + return None; + } + + let accept_keybind = editor.accept_edit_prediction_keybind(window, cx); + let accept_keystroke = accept_keybind.keystroke()?; + + let mut element = div() + .px_2() + .py_1() + .elevation_2(cx) + .border_color(cx.theme().colors().border) + .rounded_br(px(0.)) + .child(Label::new(accept_keystroke.key.clone()).buffer_font(cx)) + .into_any(); + + let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); + + let cursor_row_layout = &line_layouts + [cursor_position.row().minus(visible_row_range.start) as usize]; + let cursor_column = cursor_position.column() as usize; + + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + let target_y = (cursor_position.row().as_f32() + - scroll_pixel_position.y / line_height) + * line_height; + + let offset = point( + cursor_character_x - size.width, + target_y - size.height - PADDING_Y, + ); + + element.prepaint_at(text_bounds.origin + offset, window, cx); + + return Some(element); + } + let target_display_point = target.to_display_point(editor_snapshot); if target_display_point.row().as_f32() < scroll_top { let mut element = inline_completion_accept_indicator( @@ -4007,8 +4120,10 @@ impl EditorElement { if row_infos[row_ix].diff_status.is_none() { continue; } - if row_infos[row_ix].diff_status == Some(DiffHunkStatus::Added) - && *status != DiffHunkStatus::Added + if matches!( + row_infos[row_ix].diff_status, + Some(DiffHunkStatus::Added(_)) + ) && !matches!(*status, DiffHunkStatus::Added(_)) { continue; } @@ -4191,26 +4306,26 @@ impl EditorElement { window.paint_quad(fill(Bounds { origin, size }, color)); }; - let mut current_paint: Option<(Hsla, Range)> = None; - for (&new_row, &new_color) in &layout.highlighted_rows { + let mut current_paint: Option<(gpui::Background, Range)> = None; + for (&new_row, &new_background) in &layout.highlighted_rows { match &mut current_paint { - Some((current_color, current_range)) => { - let current_color = *current_color; - let new_range_started = current_color != new_color + Some((current_background, current_range)) => { + let current_background = *current_background; + let new_range_started = current_background != new_background || current_range.end.next_row() != new_row; if new_range_started { paint_highlight( current_range.start, current_range.end, - current_color, + current_background, ); - current_paint = Some((new_color, new_row..new_row)); + current_paint = Some((new_background, new_row..new_row)); continue; } else { current_range.end = current_range.end.next_row(); } } - None => current_paint = Some((new_color, new_row..new_row)), + None => current_paint = Some((new_background, new_row..new_row)), }; } if let Some((color, range)) = current_paint { @@ -4409,6 +4524,7 @@ impl EditorElement { hunk_bounds, cx.theme().status().modified, Corners::all(px(0.)), + &DiffHunkSecondaryStatus::None, )) } DisplayDiffHunk::Unfolded { @@ -4416,22 +4532,29 @@ impl EditorElement { display_row_range, .. } => hitbox.as_ref().map(|hunk_hitbox| match status { - DiffHunkStatus::Added => ( + DiffHunkStatus::Added(secondary_status) => ( hunk_hitbox.bounds, cx.theme().status().created, Corners::all(px(0.)), + secondary_status, ), - DiffHunkStatus::Modified => ( + DiffHunkStatus::Modified(secondary_status) => ( hunk_hitbox.bounds, cx.theme().status().modified, Corners::all(px(0.)), + secondary_status, ), - DiffHunkStatus::Removed if !display_row_range.is_empty() => ( - hunk_hitbox.bounds, - cx.theme().status().deleted, - Corners::all(px(0.)), - ), - DiffHunkStatus::Removed => ( + DiffHunkStatus::Removed(secondary_status) + if !display_row_range.is_empty() => + { + ( + hunk_hitbox.bounds, + cx.theme().status().deleted, + Corners::all(px(0.)), + secondary_status, + ) + } + DiffHunkStatus::Removed(secondary_status) => ( Bounds::new( point( hunk_hitbox.origin.x - hunk_hitbox.size.width, @@ -4441,11 +4564,17 @@ impl EditorElement { ), cx.theme().status().deleted, Corners::all(1. * line_height), + secondary_status, ), }), }; - if let Some((hunk_bounds, background_color, corner_radii)) = hunk_to_paint { + if let Some((hunk_bounds, mut background_color, corner_radii, secondary_status)) = + hunk_to_paint + { + if *secondary_status != DiffHunkSecondaryStatus::None { + background_color.a *= 0.6; + } window.paint_quad(quad( hunk_bounds, corner_radii, @@ -4481,7 +4610,7 @@ impl EditorElement { status, .. } => { - if *status == DiffHunkStatus::Removed && display_row_range.is_empty() { + if status.is_removed() && display_row_range.is_empty() { let row = display_row_range.start; let offset = line_height / 2.; @@ -5128,9 +5257,9 @@ impl EditorElement { end_display_row.0 -= 1; } let color = match &hunk.status() { - DiffHunkStatus::Added => theme.status().created, - DiffHunkStatus::Modified => theme.status().modified, - DiffHunkStatus::Removed => theme.status().deleted, + DiffHunkStatus::Added(_) => theme.status().created, + DiffHunkStatus::Modified(_) => theme.status().modified, + DiffHunkStatus::Removed(_) => theme.status().deleted, }; ColoredRange { start: start_display_row, @@ -5673,7 +5802,7 @@ fn inline_completion_accept_indicator( .text_size(TextSize::XSmall.rems(cx)) .text_color(cx.theme().colors().text) .gap_1() - .when(!editor.previewing_inline_completion, |parent| { + .when(!editor.edit_prediction_preview.is_active(), |parent| { parent.children(ui::render_modifiers( &accept_keystroke.modifiers, PlatformStyle::platform(), @@ -6798,19 +6927,46 @@ impl Element for EditorElement { ) }; - let mut highlighted_rows = self - .editor - .update(cx, |editor, cx| editor.highlighted_display_rows(window, cx)); + let (mut highlighted_rows, distinguish_unstaged_hunks) = + self.editor.update(cx, |editor, cx| { + ( + editor.highlighted_display_rows(window, cx), + editor.distinguish_unstaged_diff_hunks, + ) + }); for (ix, row_info) in row_infos.iter().enumerate() { - let color = match row_info.diff_status { - Some(DiffHunkStatus::Added) => style.status.created_background, - Some(DiffHunkStatus::Removed) => style.status.deleted_background, + let background = match row_info.diff_status { + Some(DiffHunkStatus::Added(secondary_status)) => { + let color = style.status.created_background; + match secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk + | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk + if distinguish_unstaged_hunks => + { + pattern_slash(color, line_height.0 / 4.0) + } + _ => color.into(), + } + } + Some(DiffHunkStatus::Removed(secondary_status)) => { + let color = style.status.deleted_background; + match secondary_status { + DiffHunkSecondaryStatus::HasSecondaryHunk + | DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk + if distinguish_unstaged_hunks => + { + pattern_slash(color, line_height.0 / 4.0) + } + _ => color.into(), + } + } _ => continue, }; + highlighted_rows .entry(start_row + DisplayRow(ix as u32)) - .or_insert(color); + .or_insert(background); } let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( @@ -7204,6 +7360,7 @@ impl Element for EditorElement { em_width, em_advance, autoscroll_containing_element, + newest_selection_head, window, cx, ); @@ -7355,7 +7512,7 @@ impl Element for EditorElement { ); } - let inline_completion_popover = self.layout_inline_completion_popover( + let inline_completion_popover = self.layout_edit_prediction_popover( &text_hitbox.bounds, &snapshot, start_row..end_row, @@ -7643,7 +7800,7 @@ pub struct EditorLayout { indent_guides: Option>, visible_display_row_range: Range, active_rows: BTreeMap, - highlighted_rows: BTreeMap, + highlighted_rows: BTreeMap, line_elements: SmallVec<[AnyElement; 1]>, line_numbers: Arc>, display_hunks: Vec<(DisplayDiffHunk, Option)>, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index 2c7903296b13cc4372f48cb624512323b30bb228..1627d471c0f80cd590bfe316c4abc83a5542e724 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -1,6 +1,6 @@ use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider}; +use buffer_diff::BufferDiff; use collections::HashSet; -use diff::BufferDiff; use futures::{channel::mpsc, future::join_all}; use gpui::{App, Entity, EventEmitter, Focusable, Render, Subscription, Task}; use language::{Buffer, BufferEvent, Capability}; @@ -185,7 +185,7 @@ impl ProposedChangesEditor { } else { branch_buffer = location.buffer.update(cx, |buffer, cx| buffer.branch(cx)); new_diffs.push(cx.new(|cx| { - let mut diff = BufferDiff::new(&branch_buffer, cx); + let mut diff = BufferDiff::new(branch_buffer.read(cx)); let _ = diff.set_base_text( location.buffer.clone(), branch_buffer.read(cx).text_snapshot(), diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 7b6a9fc96234fd577529878fb758131b5ca15abf..d4c57fd11394b9b9a5fd1f8eb31e2246266b2dc6 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -113,6 +113,7 @@ impl Editor { target_bottom = target_top + 1.; } else { let selections = self.selections.all::(cx); + target_top = selections .first() .unwrap() @@ -144,6 +145,29 @@ impl Editor { target_top = newest_selection_top; target_bottom = newest_selection_top + 1.; } + + if self.edit_prediction_preview.is_active() { + if let Some(completion) = self.active_inline_completion.as_ref() { + match completion.completion { + crate::InlineCompletion::Edit { .. } => {} + crate::InlineCompletion::Move { target, .. } => { + let target_row = target.to_display_point(&display_map).row().as_f32(); + + if target_row < target_top { + target_top = target_row; + } else if target_row >= target_bottom { + target_bottom = target_row + 1.; + } + + let selections_fit = target_bottom - target_top <= visible_lines; + if !selections_fit { + target_top = target_row; + target_bottom = target_row + 1.; + } + } + } + } + } } let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 7cfaf5622404000da500b90ea73a02a61e804f08..a632d9fa1ec760a75eea71f36ceed9670956034a 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -2,8 +2,8 @@ use crate::{ display_map::ToDisplayPoint, AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt, }; +use buffer_diff::DiffHunkStatus; use collections::BTreeMap; -use diff::DiffHunkStatus; use futures::Future; use gpui::{ @@ -459,9 +459,9 @@ pub fn assert_state_with_diff( .zip(line_infos) .map(|(line, info)| { let mut marker = match info.diff_status { - Some(DiffHunkStatus::Added) => "+ ", - Some(DiffHunkStatus::Removed) => "- ", - Some(DiffHunkStatus::Modified) => unreachable!(), + Some(DiffHunkStatus::Added(_)) => "+ ", + Some(DiffHunkStatus::Removed(_)) => "- ", + Some(DiffHunkStatus::Modified(_)) => unreachable!(), None => { if has_diff { " " diff --git a/crates/file_finder/src/open_path_prompt.rs b/crates/file_finder/src/open_path_prompt.rs index c28eeb470d4ddbdd006e882385b68d749d2a80d7..71203874f69494b2d34ed380cea0d402a1b6cf86 100644 --- a/crates/file_finder/src/open_path_prompt.rs +++ b/crates/file_finder/src/open_path_prompt.rs @@ -93,6 +93,8 @@ impl PickerDelegate for OpenPathDelegate { cx.notify(); } + // todo(windows) + // Is this method woring correctly on Windows? This method uses `/` for path separator. fn update_matches( &mut self, query: String, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 39f244f12535ecbc58dd260630ca6d526b58455a..e1bf864e503ef0a88091a4626d5de3f8341d90ac 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -571,10 +571,6 @@ impl RepoPath { RepoPath(path.into()) } - - pub fn to_proto(&self) -> String { - self.0.to_string_lossy().to_string() - } } impl std::fmt::Display for RepoPath { diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index a30792fe1051b2a16f9de3d04297756eefe00cfd..4f10e067b8d986c99529d5a59cd74beafd812739 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -16,7 +16,7 @@ path = "src/git_ui.rs" anyhow.workspace = true collections.workspace = true db.workspace = true -diff.workspace = true +buffer_diff.workspace = true editor.workspace = true feature_flags.workspace = true futures.workspace = true diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index ea54fb1bfdf140098e26dbeea150e75cad12b6df..7d4a8c7a46a4bedb8c1e696f64f1dfacb5a63c5c 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -1,8 +1,8 @@ use std::any::{Any, TypeId}; use anyhow::Result; +use buffer_diff::BufferDiff; use collections::HashSet; -use diff::BufferDiff; use editor::{scroll::Autoscroll, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use futures::StreamExt; @@ -126,6 +126,7 @@ impl ProjectDiff { window, cx, ); + diff_display_editor.set_distinguish_unstaged_diff_hunks(); diff_display_editor.set_expand_all_diff_hunks(cx); diff_display_editor.register_addon(GitPanelAddon { git_panel: git_panel.clone(), @@ -317,10 +318,10 @@ impl ProjectDiff { let snapshot = buffer.read(cx).snapshot(); let diff = diff.read(cx); - let diff_hunk_ranges = if diff.snapshot.base_text.is_none() { + let diff_hunk_ranges = if diff.base_text().is_none() { vec![Point::zero()..snapshot.max_point()] } else { - diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot) + diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx) .map(|diff_hunk| diff_hunk.buffer_range.to_point(&snapshot)) .collect::>() }; diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 5c692860c96bbcec002074b2d4bf2054f128dc57..5795f68c30bdedf0885ffe50b3fb9ccbd7dda59b 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -202,11 +202,12 @@ windows-core = "0.58" backtrace = "0.3" collections = { workspace = true, features = ["test-support"] } env_logger.workspace = true -rand.workspace = true -util = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } -unicode-segmentation.workspace = true lyon = { version = "1.0", features = ["extra"] } +rand.workspace = true +unicode-segmentation.workspace = true +reqwest_client = { workspace = true, features = ["test-support"] } +util = { workspace = true, features = ["test-support"] } [target.'cfg(target_os = "windows")'.build-dependencies] embed-resource = "3.0" diff --git a/crates/gpui/examples/gif_viewer.rs b/crates/gpui/examples/gif_viewer.rs index bdec02d26bbcdba77dc7df10c9f2cff120a4d32c..79e618fa558a8585c458d24024d6518a77f5126f 100644 --- a/crates/gpui/examples/gif_viewer.rs +++ b/crates/gpui/examples/gif_viewer.rs @@ -25,15 +25,8 @@ impl Render for GifViewer { fn main() { env_logger::init(); Application::new().run(|cx: &mut App| { - let cwd = std::env::current_dir().expect("Failed to get current working directory"); - let gif_path = cwd.join("crates/gpui/examples/image/black-cat-typing.gif"); - - if !gif_path.exists() { - eprintln!("Image file not found at {:?}", gif_path); - eprintln!("Make sure you're running this example from the root of the gpui crate"); - cx.quit(); - return; - } + let gif_path = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples/image/black-cat-typing.gif"); cx.open_window( WindowOptions { diff --git a/crates/gpui/examples/image/image.rs b/crates/gpui/examples/image/image.rs index 56a188832b1eabb215f25e2796c1659b37bfdd66..28ef60655618d2316ee4470546d77ef2cb65c804 100644 --- a/crates/gpui/examples/image/image.rs +++ b/crates/gpui/examples/image/image.rs @@ -1,6 +1,5 @@ use std::fs; use std::path::PathBuf; -use std::str::FromStr; use std::sync::Arc; use anyhow::Result; @@ -9,6 +8,7 @@ use gpui::{ Bounds, Context, ImageSource, KeyBinding, Menu, MenuItem, Point, SharedString, SharedUri, TitlebarOptions, Window, WindowBounds, WindowOptions, }; +use reqwest_client::ReqwestClient; struct Assets { base: PathBuf, @@ -127,11 +127,16 @@ actions!(image, [Quit]); fn main() { env_logger::init(); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + Application::new() .with_assets(Assets { - base: PathBuf::from("crates/gpui/examples"), + base: manifest_dir.join("examples"), }) - .run(|cx: &mut App| { + .run(move |cx: &mut App| { + let http_client = ReqwestClient::user_agent("gpui example").unwrap(); + cx.set_http_client(Arc::new(http_client)); + cx.activate(true); cx.on_action(|_: &Quit, cx| cx.quit()); cx.bind_keys([KeyBinding::new("cmd-q", Quit, None)]); @@ -158,9 +163,7 @@ fn main() { cx.open_window(window_options, |_, cx| { cx.new(|_| ImageShowcase { // Relative path to your root project path - local_resource: PathBuf::from_str("crates/gpui/examples/image/app-icon.png") - .unwrap() - .into(), + local_resource: manifest_dir.join("examples/image/app-icon.png").into(), remote_resource: "https://picsum.photos/512/512".into(), diff --git a/crates/gpui/examples/image_loading.rs b/crates/gpui/examples/image_loading.rs index f211c10c5e2b8f0f918ff4da282fca169b072c50..5aeed16342de0e1b820d1c7e68d07e8aa4a6d6e8 100644 --- a/crates/gpui/examples/image_loading.rs +++ b/crates/gpui/examples/image_loading.rs @@ -29,7 +29,7 @@ impl AssetSource for Assets { } } -const IMAGE: &str = "examples/image/app-icon.png"; +const IMAGE: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/examples/image/app-icon.png"); #[derive(Copy, Clone, Hash)] struct LoadImageParameters { diff --git a/crates/gpui/examples/opacity.rs b/crates/gpui/examples/opacity.rs index 4f8764497fc61fa53e419431ffdb8a1c7769b168..43c8950f3750bf4224315193d4e870abb4a592c8 100644 --- a/crates/gpui/examples/opacity.rs +++ b/crates/gpui/examples/opacity.rs @@ -159,7 +159,7 @@ impl Render for HelloWorld { fn main() { Application::new() .with_assets(Assets { - base: PathBuf::from("crates/gpui/examples"), + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), }) .run(|cx: &mut App| { let bounds = Bounds::centered(None, size(px(500.0), px(500.0)), cx); @@ -171,5 +171,6 @@ fn main() { |window, cx| cx.new(|cx| HelloWorld::new(window, cx)), ) .unwrap(); + cx.activate(true); }); } diff --git a/crates/gpui/examples/pattern.rs b/crates/gpui/examples/pattern.rs index b872d6b6ad7a5aec50e004313b4dbacb46dbf9a1..724e2dc5dbb6af09b0afdd9afe9298f3888a42c7 100644 --- a/crates/gpui/examples/pattern.rs +++ b/crates/gpui/examples/pattern.rs @@ -25,10 +25,30 @@ impl Render for PatternExample { .flex_col() .border_1() .border_color(gpui::blue()) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))) - .child(div().w(px(54.0)).h(px(18.0)).bg(pattern_slash(gpui::red()))), + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ) + .child( + div() + .w(px(54.0)) + .h(px(18.0)) + .bg(pattern_slash(gpui::red(), 18.0 / 2.0)), + ), ) .child( div() @@ -42,25 +62,25 @@ impl Render for PatternExample { div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::red())), + .bg(pattern_slash(gpui::red(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::green())), + .bg(pattern_slash(gpui::green(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(56.0)) - .bg(pattern_slash(gpui::blue())), + .bg(pattern_slash(gpui::blue(), 56.0 / 3.0)), ) .child( div() .w(px(256.0)) .h(px(26.0)) - .bg(pattern_slash(gpui::yellow())), + .bg(pattern_slash(gpui::yellow(), 56.0 / 3.0)), ), ) .child( diff --git a/crates/gpui/examples/svg/svg.rs b/crates/gpui/examples/svg/svg.rs index 5c583717e3f870fffdb20dd10c379937ca34cc7b..62ce04e1ce94f632f121bc85a2bf531870ccb4bf 100644 --- a/crates/gpui/examples/svg/svg.rs +++ b/crates/gpui/examples/svg/svg.rs @@ -70,7 +70,7 @@ impl Render for SvgExample { fn main() { Application::new() .with_assets(Assets { - base: PathBuf::from("crates/gpui/examples"), + base: PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("examples"), }) .run(|cx: &mut App| { let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); @@ -82,5 +82,6 @@ fn main() { |_, cx| cx.new(|_| SvgExample), ) .unwrap(); + cx.activate(true); }); } diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 089caf158506ac3ae86ff772f38137125ccefcbb..daec9440a4eba3f0a7189cb9eb652e73800812d0 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -587,7 +587,7 @@ pub struct Background { pub(crate) tag: BackgroundTag, pub(crate) color_space: ColorSpace, pub(crate) solid: Hsla, - pub(crate) angle: f32, + pub(crate) gradient_angle_or_pattern_height: f32, pub(crate) colors: [LinearColorStop; 2], /// Padding for alignment for repr(C) layout. pad: u32, @@ -600,7 +600,7 @@ impl Default for Background { tag: BackgroundTag::Solid, solid: Hsla::default(), color_space: ColorSpace::default(), - angle: 0.0, + gradient_angle_or_pattern_height: 0.0, colors: [LinearColorStop::default(), LinearColorStop::default()], pad: 0, } @@ -608,10 +608,11 @@ impl Default for Background { } /// Creates a hash pattern background -pub fn pattern_slash(color: Hsla) -> Background { +pub fn pattern_slash(color: Hsla, thickness: f32) -> Background { Background { tag: BackgroundTag::PatternSlash, solid: color, + gradient_angle_or_pattern_height: thickness, ..Default::default() } } @@ -630,7 +631,7 @@ pub fn linear_gradient( ) -> Background { Background { tag: BackgroundTag::LinearGradient, - angle, + gradient_angle_or_pattern_height: angle, colors: [from.into(), to.into()], ..Default::default() } diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index b41ffb26ef45cca3c9bf47acbeb1da6770bacbc1..e7fe99a5157a2319db49ca16ce9f4ed82aaf86b8 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -51,7 +51,7 @@ struct Background { // 1u is Oklab color color_space: u32, solid: Hsla, - angle: f32, + gradient_angle_or_pattern_height: f32, colors: array, pad: u32, } @@ -310,17 +310,18 @@ fn prepare_gradient_color(tag: u32, color_space: u32, } fn gradient_color(background: Background, position: vec2, bounds: Bounds, - sold_color: vec4, color0: vec4, color1: vec4) -> vec4 { + solid_color: vec4, color0: vec4, color1: vec4) -> vec4 { var background_color = vec4(0.0); switch (background.tag) { default: { - return sold_color; + return solid_color; } case 1u: { // Linear gradient background. // -90 degrees to match the CSS gradient angle. - let radians = (background.angle % 360.0 - 90.0) * M_PI_F / 180.0; + let angle = background.gradient_angle_or_pattern_height; + let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0; var direction = vec2(cos(radians), sin(radians)); let stop0_percentage = background.colors[0].percentage; let stop1_percentage = background.colors[1].percentage; @@ -359,19 +360,18 @@ fn gradient_color(background: Background, position: vec2, bounds: Bounds, } } case 2u: { - let base_pattern_size = bounds.size.y / 5.0; - let width = base_pattern_size * 0.5; - let slash_spacing = 0.89; - let radians = M_PI_F / 4.0; + let pattern_height = background.gradient_angle_or_pattern_height; + let stripe_angle = M_PI_F / 4.0; + let pattern_period = pattern_height * sin(stripe_angle); let rotation = mat2x2( - cos(radians), -sin(radians), - sin(radians), cos(radians) + cos(stripe_angle), -sin(stripe_angle), + sin(stripe_angle), cos(stripe_angle) ); let relative_position = position - bounds.origin; let rotated_point = rotation * relative_position; - let pattern = (rotated_point.x / slash_spacing) % (base_pattern_size * 2.0); - let distance = min(pattern, base_pattern_size * 2.0 - pattern) - width; - background_color = sold_color; + let pattern = rotated_point.x % pattern_period; + let distance = min(pattern, pattern_period - pattern) - pattern_period / 4; + background_color = solid_color; background_color.a *= saturate(0.5 - distance); } } diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 941c20c33dc170d9d5683f16854fd3c071f7f752..2ff64189fc0a680796be9b1e72f7318f50f7ea02 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -833,7 +833,8 @@ float4 fill_color(Background background, break; case 1: { // -90 degrees to match the CSS gradient angle. - float radians = (fmod(background.angle, 360.0) - 90.0) * (M_PI_F / 180.0); + float gradient_angle = background.gradient_angle_or_pattern_height; + float radians = (fmod(gradient_angle, 360.0) - 90.0) * (M_PI_F / 180.0); float2 direction = float2(cos(radians), sin(radians)); // Expand the short side to be the same as the long side @@ -874,19 +875,14 @@ float4 fill_color(Background background, break; } case 2: { - // This pattern is full of magic numbers to make it line up perfectly - // when vertically stacked. Make sure you know what you are doing - // if you change this! - - float base_pattern_size = bounds.size.height / 5; - float width = base_pattern_size * 0.5; - float slash_spacing = .89; - float radians = M_PI_F / 4.0; - float2x2 rotation = rotate2d(radians); + float pattern_height = background.gradient_angle_or_pattern_height; + float stripe_angle = M_PI_F / 4.0; + float pattern_period = pattern_height * sin(stripe_angle); + float2x2 rotation = rotate2d(stripe_angle); float2 relative_position = position - float2(bounds.origin.x, bounds.origin.y); float2 rotated_point = rotation * relative_position; - float pattern = fmod(rotated_point.x / slash_spacing, base_pattern_size * 2.0); - float distance = min(pattern, base_pattern_size * 2.0 - pattern) - width; + float pattern = fmod(rotated_point.x, pattern_period); + float distance = min(pattern, pattern_period - pattern) - pattern_period / 4.0; color = solid_color; color.a *= saturate(0.5 - distance); break; diff --git a/crates/inline_completion/src/inline_completion.rs b/crates/inline_completion/src/inline_completion.rs index cea21472ca3a95597df61bd9ec87cb4e31233aa2..80077cc169860c74cdb33325cb442f37d6252f70 100644 --- a/crates/inline_completion/src/inline_completion.rs +++ b/crates/inline_completion/src/inline_completion.rs @@ -25,18 +25,30 @@ pub enum DataCollectionState { /// The provider doesn't support data collection. Unsupported, /// Data collection is enabled. - Enabled, + Enabled { is_project_open_source: bool }, /// Data collection is disabled or unanswered. - Disabled, + Disabled { is_project_open_source: bool }, } impl DataCollectionState { pub fn is_supported(&self) -> bool { - !matches!(self, DataCollectionState::Unsupported) + !matches!(self, DataCollectionState::Unsupported { .. }) } pub fn is_enabled(&self) -> bool { - matches!(self, DataCollectionState::Enabled) + matches!(self, DataCollectionState::Enabled { .. }) + } + + pub fn is_project_open_source(&self) -> bool { + match self { + Self::Enabled { + is_project_open_source, + } + | Self::Disabled { + is_project_open_source, + } => *is_project_open_source, + _ => false, + } } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 244fa8324bbc8b1d2f1a5a0e407c6d3a12fdf6c4..8961512bd28718b6576346e85a03f7ecad4a48dc 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -36,9 +36,8 @@ use workspace::{ Toast, Workspace, }; use zed_actions::OpenBrowser; -use zeta::RateCompletionModal; +use zeta::RateCompletions; -actions!(zeta, [RateCompletions]); actions!(edit_prediction, [ToggleMenu]); const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; @@ -54,7 +53,6 @@ pub struct InlineCompletionButton { file: Option>, edit_prediction_provider: Option>, fs: Arc, - workspace: WeakEntity, user_store: Entity, popover_menu_handle: PopoverMenuHandle, } @@ -354,7 +352,6 @@ impl Render for InlineCompletionButton { impl InlineCompletionButton { pub fn new( - workspace: WeakEntity, fs: Arc, user_store: Entity, popover_menu_handle: PopoverMenuHandle, @@ -376,7 +373,6 @@ impl InlineCompletionButton { file: None, edit_prediction_provider: None, popover_menu_handle, - workspace, fs, user_store, } @@ -456,17 +452,56 @@ impl InlineCompletionButton { if data_collection.is_supported() { let provider = provider.clone(); let enabled = data_collection.is_enabled(); + let is_open_source = data_collection.is_project_open_source(); + let is_collecting = data_collection.is_enabled(); menu = menu.item( - // TODO: We want to add something later that communicates whether - // the current project is open-source. ContextMenuEntry::new("Share Training Data") .toggleable(IconPosition::Start, data_collection.is_enabled()) - .documentation_aside(|_| { - Label::new(indoc!{" - Help us improve our open model by sharing data from open source repositories. \ - Zed must detect a license file in your repo for this setting to take effect.\ - "}).into_any_element() + .icon_color(if is_open_source && is_collecting { + Color::Success + } else { + Color::Accent + }) + .documentation_aside(move |cx| { + let (msg, label_color, icon_name, icon_color) = match (is_open_source, is_collecting) { + (true, true) => ( + "Project identified as open-source, and you're sharing data.", + Color::Default, + IconName::Check, + Color::Success, + ), + (true, false) => ( + "Project identified as open-source, but you're not sharing data.", + Color::Muted, + IconName::XCircle, + Color::Muted, + ), + (false, _) => ( + "Project not identified as open-source. No data captured.", + Color::Muted, + IconName::XCircle, + Color::Muted, + ), + }; + v_flex() + .gap_2() + .child( + Label::new(indoc!{ + "Help us improve our open model by sharing data from open source repositories. \ + Zed must detect a license file in your repo for this setting to take effect." + }) + ) + .child( + h_flex() + .pt_2() + .gap_1p5() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + .child(Icon::new(icon_name).size(IconSize::XSmall).color(icon_color)) + .child(div().child(Label::new(msg).size(LabelSize::Small).color(label_color))) + ) + .into_any_element() }) .handler(move |_, cx| { provider.toggle_data_collection(cx); @@ -483,7 +518,7 @@ impl InlineCompletionButton { ); } }) - ) + ); } } @@ -574,23 +609,10 @@ impl InlineCompletionButton { window: &mut Window, cx: &mut Context, ) -> Entity { - let workspace = self.workspace.clone(); ContextMenu::build(window, cx, |menu, _window, cx| { self.build_language_settings_menu(menu, cx).when( cx.has_flag::(), - |this| { - this.entry( - "Rate Completions", - Some(RateCompletions.boxed_clone()), - move |window, cx| { - workspace - .update(cx, |workspace, cx| { - RateCompletionModal::toggle(workspace, window, cx) - }) - .ok(); - }, - ) - }, + |this| this.action("Rate Completions", RateCompletions.boxed_clone()), ) }) } diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml index c9e1be241ea619b37e0c7fc9fa912df34b658e49..e2e9d55402aca39130418b2a5aa69a69788eb19a 100644 --- a/crates/multi_buffer/Cargo.toml +++ b/crates/multi_buffer/Cargo.toml @@ -14,7 +14,7 @@ doctest = false [features] test-support = [ - "diff/test-support", + "buffer_diff/test-support", "gpui/test-support", "language/test-support", "text/test-support", @@ -26,7 +26,7 @@ anyhow.workspace = true clock.workspace = true collections.workspace = true ctor.workspace = true -diff.workspace = true +buffer_diff.workspace = true env_logger.workspace = true futures.workspace = true gpui.workspace = true @@ -47,7 +47,7 @@ tree-sitter.workspace = true util.workspace = true [dev-dependencies] -diff = { workspace = true, features = ["test-support"] } +buffer_diff = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } indoc.workspace = true language = { workspace = true, features = ["test-support"] } diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs index ca6bc8cbf65c46cf5dba9f836fe6d74dd4a6fd97..4357dbc7ac79eb2a2357d1dd22a812fb3ab1442d 100644 --- a/crates/multi_buffer/src/anchor.rs +++ b/crates/multi_buffer/src/anchor.rs @@ -73,7 +73,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); @@ -110,7 +110,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { return a.bias_left(base_text); @@ -135,7 +135,7 @@ impl Anchor { if let Some(base_text) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) { if a.buffer_id == Some(base_text.remote_id()) { return a.bias_right(&base_text); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 0e8359412a8d0a4eeb047ad3fcefe112e035f85f..2adbb6a81c76b80b2122e277952277a3a0e2136e 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -7,9 +7,11 @@ pub use anchor::{Anchor, AnchorRangeExt, Offset}; pub use position::{TypedOffset, TypedPoint, TypedRow}; use anyhow::{anyhow, Result}; +use buffer_diff::{ + BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkSecondaryStatus, DiffHunkStatus, +}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; -use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkStatus}; use futures::{channel::mpsc, SinkExt}; use gpui::{App, Context, Entity, EntityId, EventEmitter, Task}; use itertools::Itertools; @@ -129,16 +131,18 @@ pub struct MultiBufferDiffHunk { pub excerpt_id: ExcerptId, /// The range within the buffer's diff base that this hunk corresponds to. pub diff_base_byte_range: Range, + /// Whether or not this hunk also appears in the 'secondary diff'. + pub secondary_status: DiffHunkSecondaryStatus, } impl MultiBufferDiffHunk { pub fn status(&self) -> DiffHunkStatus { if self.buffer_range.start == self.buffer_range.end { - DiffHunkStatus::Removed + DiffHunkStatus::Removed(self.secondary_status) } else if self.diff_base_byte_range.is_empty() { - DiffHunkStatus::Added + DiffHunkStatus::Added(self.secondary_status) } else { - DiffHunkStatus::Modified + DiffHunkStatus::Modified(self.secondary_status) } } } @@ -225,7 +229,14 @@ impl DiffState { DiffState { _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event { BufferDiffEvent::DiffChanged { changed_range } => { - this.buffer_diff_changed(diff, changed_range.clone(), cx) + let changed_range = if let Some(changed_range) = changed_range { + changed_range.clone() + } else if diff.read(cx).base_text().is_none() && this.all_diff_hunks_expanded { + text::Anchor::MIN..text::Anchor::MAX + } else { + return; + }; + this.buffer_diff_changed(diff, changed_range, cx) } BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), }), @@ -241,7 +252,7 @@ pub struct MultiBufferSnapshot { excerpts: SumTree, excerpt_ids: SumTree, diffs: TreeMap, - pub diff_transforms: SumTree, + diff_transforms: SumTree, trailing_excerpt_update_count: usize, non_text_state_update_count: usize, edit_count: usize, @@ -252,20 +263,27 @@ pub struct MultiBufferSnapshot { } #[derive(Debug, Clone)] -pub enum DiffTransform { +enum DiffTransform { BufferContent { summary: TextSummary, - inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + inserted_hunk_info: Option, }, DeletedHunk { summary: TextSummary, buffer_id: BufferId, - hunk_anchor: (ExcerptId, text::Anchor), + hunk_info: DiffTransformHunkInfo, base_text_byte_range: Range, has_trailing_newline: bool, }, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +struct DiffTransformHunkInfo { + excerpt_id: ExcerptId, + hunk_start_anchor: text::Anchor, + hunk_secondary_status: DiffHunkSecondaryStatus, +} + #[derive(Clone)] pub struct ExcerptInfo { pub id: ExcerptId, @@ -310,7 +328,7 @@ pub struct RowInfo { pub buffer_id: Option, pub buffer_row: Option, pub multibuffer_row: Option, - pub diff_status: Option, + pub diff_status: Option, } /// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. @@ -431,7 +449,7 @@ struct MultiBufferCursor<'a, D: TextDimension> { struct MultiBufferRegion<'a, D: TextDimension> { buffer: &'a BufferSnapshot, is_main_buffer: bool, - is_inserted_hunk: bool, + diff_hunk_status: Option, excerpt: &'a Excerpt, buffer_range: Range, range: Range, @@ -2146,7 +2164,7 @@ impl MultiBuffer { let mut snapshot = self.snapshot.borrow_mut(); let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let diff = diff.snapshot.clone(); + let diff = diff.snapshot(cx); snapshot.diffs.insert(buffer_id, diff); } @@ -2160,36 +2178,29 @@ impl MultiBuffer { let diff = diff.read(cx); let buffer_id = diff.buffer_id; - let mut diff = diff.snapshot.clone(); - if diff.base_text.is_none() && self.all_diff_hunks_expanded { - diff = BufferDiffSnapshot::new_with_single_insertion(cx); - } - - let mut snapshot = self.snapshot.borrow_mut(); - let base_text_changed = - snapshot - .diffs - .get(&buffer_id) - .map_or(true, |diff_snapshot| { - match (&diff_snapshot.base_text, &diff.base_text) { - (None, None) => false, - (None, Some(_)) => true, - (Some(_), None) => true, - (Some(old), Some(new)) => { - let (old_id, old_empty) = (old.remote_id(), old.is_empty()); - let (new_id, new_empty) = (new.remote_id(), new.is_empty()); - new_id != old_id && (!new_empty || !old_empty) - } - } - }); - snapshot.diffs.insert(buffer_id, diff); - let buffers = self.buffers.borrow(); let Some(buffer_state) = buffers.get(&buffer_id) else { return; }; - let diff_change_range = range.to_offset(buffer_state.buffer.read(cx)); + let buffer = buffer_state.buffer.read(cx); + let diff_change_range = range.to_offset(buffer); + + let mut new_diff = diff.snapshot(cx); + if new_diff.base_text().is_none() && self.all_diff_hunks_expanded { + let secondary_diff_insertion = new_diff + .secondary_diff() + .map_or(true, |secondary_diff| secondary_diff.base_text().is_none()); + new_diff = BufferDiff::build_with_single_insertion(secondary_diff_insertion, cx); + } + + let mut snapshot = self.snapshot.borrow_mut(); + let base_text_changed = snapshot + .diffs + .get(&buffer_id) + .map_or(true, |old_diff| !new_diff.base_texts_eq(old_diff)); + + snapshot.diffs.insert(buffer_id, new_diff); let mut excerpt_edits = Vec::new(); for locator in &buffer_state.excerpts { @@ -2367,7 +2378,7 @@ impl MultiBuffer { if *cursor.start() >= end { break; } - if item.hunk_anchor().is_some() { + if item.hunk_info().is_some() { return true; } cursor.next(&()); @@ -2820,11 +2831,11 @@ impl MultiBuffer { let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) && match old_diff_transforms.item() { Some(DiffTransform::BufferContent { - inserted_hunk_anchor: Some(hunk_anchor), + inserted_hunk_info: Some(hunk), .. - }) => excerpts - .item() - .is_some_and(|excerpt| hunk_anchor.1.is_valid(&excerpt.buffer)), + }) => excerpts.item().is_some_and(|excerpt| { + hunk.hunk_start_anchor.is_valid(&excerpt.buffer) + }), _ => true, }; @@ -2853,7 +2864,7 @@ impl MultiBuffer { new_diff_transforms.push( DiffTransform::BufferContent { summary: Default::default(), - inserted_hunk_anchor: None, + inserted_hunk_info: None, }, &(), ); @@ -2876,8 +2887,8 @@ impl MultiBuffer { excerpts: &mut Cursor>, old_diff_transforms: &mut Cursor, usize)>, new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option<(TypedOffset, ExcerptId, text::Anchor)>, - old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>, + end_of_current_insert: &mut Option<(TypedOffset, DiffTransformHunkInfo)>, + old_expanded_hunks: &mut HashSet, snapshot: &MultiBufferSnapshot, change_kind: DiffChangeKind, ) -> bool { @@ -2889,12 +2900,12 @@ impl MultiBuffer { // Record which hunks were previously expanded. while let Some(item) = old_diff_transforms.item() { - if let Some(hunk_anchor) = item.hunk_anchor() { + if let Some(hunk_info) = item.hunk_info() { log::trace!( "previously expanded hunk at {}", old_diff_transforms.start().0 ); - old_expanded_hunks.insert(hunk_anchor); + old_expanded_hunks.insert(hunk_info); } if old_diff_transforms.end(&()).0 > edit.old.end { break; @@ -2918,7 +2929,7 @@ impl MultiBuffer { if let Some((diff, base_text)) = snapshot .diffs .get(&excerpt.buffer_id) - .and_then(|diff| Some((diff, diff.base_text.as_ref()?))) + .and_then(|diff| Some((diff, diff.base_text()?))) { let buffer = &excerpt.buffer; let excerpt_start = *excerpts.start(); @@ -2936,7 +2947,11 @@ impl MultiBuffer { for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); - let hunk_anchor = (excerpt.id, hunk.buffer_range.start); + let hunk_info = DiffTransformHunkInfo { + excerpt_id: excerpt.id, + hunk_start_anchor: hunk.buffer_range.start, + hunk_secondary_status: hunk.secondary_status, + }; if hunk_buffer_range.start < excerpt_buffer_start { log::trace!("skipping hunk that starts before excerpt"); continue; @@ -2960,7 +2975,7 @@ impl MultiBuffer { // For every existing hunk, determine if it was previously expanded // and if it should currently be expanded. - let was_previously_expanded = old_expanded_hunks.contains(&hunk_anchor); + let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); let should_expand_hunk = match &change_kind { DiffChangeKind::DiffUpdated { base_changed: true } => { self.all_diff_hunks_expanded @@ -3008,7 +3023,7 @@ impl MultiBuffer { base_text_byte_range: hunk.diff_base_byte_range.clone(), summary: base_text_summary, buffer_id: excerpt.buffer_id, - hunk_anchor, + hunk_info, has_trailing_newline, }, &(), @@ -3016,11 +3031,8 @@ impl MultiBuffer { } if !hunk_buffer_range.is_empty() { - *end_of_current_insert = Some(( - hunk_excerpt_end.min(excerpt_end), - hunk_anchor.0, - hunk_anchor.1, - )); + *end_of_current_insert = + Some((hunk_excerpt_end.min(excerpt_end), hunk_info)); } } } @@ -3042,13 +3054,13 @@ impl MultiBuffer { subtree: SumTree, ) { if let Some(DiffTransform::BufferContent { - inserted_hunk_anchor, + inserted_hunk_info, summary, }) = subtree.first() { if self.extend_last_buffer_content_transform( new_transforms, - *inserted_hunk_anchor, + *inserted_hunk_info, *summary, ) { let mut cursor = subtree.cursor::<()>(&()); @@ -3067,7 +3079,7 @@ impl MultiBuffer { transform: DiffTransform, ) { if let DiffTransform::BufferContent { - inserted_hunk_anchor, + inserted_hunk_info: inserted_hunk_anchor, summary, } = transform { @@ -3087,19 +3099,14 @@ impl MultiBuffer { old_snapshot: &MultiBufferSnapshot, new_transforms: &mut SumTree, end_offset: ExcerptOffset, - current_inserted_hunk: Option<(ExcerptOffset, ExcerptId, text::Anchor)>, + current_inserted_hunk: Option<(ExcerptOffset, DiffTransformHunkInfo)>, ) { - let inserted_region = - current_inserted_hunk.map(|(insertion_end_offset, excerpt_id, anchor)| { - ( - end_offset.min(insertion_end_offset), - Some((excerpt_id, anchor)), - ) - }); + let inserted_region = current_inserted_hunk.map(|(insertion_end_offset, hunk_info)| { + (end_offset.min(insertion_end_offset), Some(hunk_info)) + }); let unchanged_region = [(end_offset, None)]; - for (end_offset, inserted_hunk_anchor) in - inserted_region.into_iter().chain(unchanged_region) + for (end_offset, inserted_hunk_info) in inserted_region.into_iter().chain(unchanged_region) { let start_offset = new_transforms.summary().excerpt_len(); if end_offset <= start_offset { @@ -3110,13 +3117,13 @@ impl MultiBuffer { if !self.extend_last_buffer_content_transform( new_transforms, - inserted_hunk_anchor, + inserted_hunk_info, summary_to_add, ) { new_transforms.push( DiffTransform::BufferContent { summary: summary_to_add, - inserted_hunk_anchor, + inserted_hunk_info, }, &(), ) @@ -3127,7 +3134,7 @@ impl MultiBuffer { fn extend_last_buffer_content_transform( &self, new_transforms: &mut SumTree, - new_inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>, + new_inserted_hunk_info: Option, summary_to_add: TextSummary, ) -> bool { let mut did_extend = false; @@ -3135,10 +3142,10 @@ impl MultiBuffer { |last_transform| { if let DiffTransform::BufferContent { summary, - inserted_hunk_anchor, + inserted_hunk_info: inserted_hunk_anchor, } = last_transform { - if *inserted_hunk_anchor == new_inserted_hunk_anchor { + if *inserted_hunk_anchor == new_inserted_hunk_info { *summary += summary_to_add; did_extend = true; } @@ -3469,6 +3476,7 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id, buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), + secondary_status: hunk.secondary_status, }) }) } @@ -3837,6 +3845,7 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id, buffer_range: hunk.buffer_range.clone(), diff_base_byte_range: hunk.diff_base_byte_range.clone(), + secondary_status: hunk.secondary_status, }); } } @@ -4309,10 +4318,7 @@ impl MultiBufferSnapshot { } => { let buffer_start = base_text_byte_range.start + start_overshoot; let mut buffer_end = base_text_byte_range.start + end_overshoot; - let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + let Some(base_text) = self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.start) }; @@ -4361,10 +4367,7 @@ impl MultiBufferSnapshot { .. } => { let buffer_end = base_text_byte_range.start + overshoot; - let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + let Some(base_text) = self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) else { panic!("{:?} is in non-existent deleted hunk", range.end) }; @@ -4469,10 +4472,8 @@ impl MultiBufferSnapshot { }) => { let mut in_deleted_hunk = false; if let Some(diff_base_anchor) = &anchor.diff_base_anchor { - if let Some(base_text) = self - .diffs - .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + if let Some(base_text) = + self.diffs.get(buffer_id).and_then(|diff| diff.base_text()) { if base_text.can_resolve(&diff_base_anchor) { let base_text_offset = diff_base_anchor.to_offset(&base_text); @@ -4809,7 +4810,7 @@ impl MultiBufferSnapshot { let base_text = self .diffs .get(buffer_id) - .and_then(|diff| diff.base_text.as_ref()) + .and_then(|diff| diff.base_text()) .expect("missing diff base"); if offset_in_transform > base_text_byte_range.len() { debug_assert!(*has_trailing_newline); @@ -5969,17 +5970,17 @@ impl MultiBufferSnapshot { for item in self.diff_transforms.iter() { if let DiffTransform::BufferContent { summary, - inserted_hunk_anchor, + inserted_hunk_info, } = item { if let Some(DiffTransform::BufferContent { - inserted_hunk_anchor: prev_inserted_hunk_anchor, + inserted_hunk_info: prev_inserted_hunk_info, .. }) = prev_transform { - if *inserted_hunk_anchor == *prev_inserted_hunk_anchor { + if *inserted_hunk_info == *prev_inserted_hunk_info { panic!( - "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_anchor:?}. transforms: {:+?}", + "multiple adjacent buffer content transforms with is_inserted_hunk = {inserted_hunk_info:?}. transforms: {:+?}", self.diff_transforms.items(&())); } } @@ -6149,10 +6150,11 @@ where buffer_id, base_text_byte_range, has_trailing_newline, + hunk_info, .. } => { let diff = self.diffs.get(&buffer_id)?; - let buffer = diff.base_text.as_ref()?; + let buffer = diff.base_text()?; let mut rope_cursor = buffer.as_rope().cursor(0); let buffer_start = rope_cursor.summary::(base_text_byte_range.start); let buffer_range_len = rope_cursor.summary::(base_text_byte_range.end); @@ -6165,14 +6167,15 @@ where excerpt, has_trailing_newline: *has_trailing_newline, is_main_buffer: false, - is_inserted_hunk: false, + diff_hunk_status: Some(DiffHunkStatus::Removed( + hunk_info.hunk_secondary_status, + )), buffer_range: buffer_start..buffer_end, range: start..end, }); } DiffTransform::BufferContent { - inserted_hunk_anchor, - .. + inserted_hunk_info, .. } => { let buffer = &excerpt.buffer; let buffer_context_start = excerpt.range.context.start.summary::(buffer); @@ -6209,7 +6212,8 @@ where excerpt, has_trailing_newline, is_main_buffer: true, - is_inserted_hunk: inserted_hunk_anchor.is_some(), + diff_hunk_status: inserted_hunk_info + .map(|info| DiffHunkStatus::Added(info.hunk_secondary_status)), buffer_range: buffer_start..buffer_end, range: start..end, }) @@ -6717,13 +6721,12 @@ impl sum_tree::KeyedItem for ExcerptIdMapping { } impl DiffTransform { - fn hunk_anchor(&self) -> Option<(ExcerptId, text::Anchor)> { + fn hunk_info(&self) -> Option { match self { - DiffTransform::DeletedHunk { hunk_anchor, .. } => Some(*hunk_anchor), + DiffTransform::DeletedHunk { hunk_info, .. } => Some(*hunk_info), DiffTransform::BufferContent { - inserted_hunk_anchor, - .. - } => *inserted_hunk_anchor, + inserted_hunk_info, .. + } => *inserted_hunk_info, } } } @@ -7020,13 +7023,9 @@ impl<'a> Iterator for MultiBufferRows<'a> { buffer_id: Some(region.buffer.remote_id()), buffer_row: Some(buffer_point.row), multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status: if region.is_inserted_hunk && self.point < region.range.end { - Some(DiffHunkStatus::Added) - } else if !region.is_main_buffer { - Some(DiffHunkStatus::Removed) - } else { - None - }, + diff_status: region + .diff_hunk_status + .filter(|_| self.point < region.range.end), }); self.point += Point::new(1, 0); result @@ -7194,7 +7193,7 @@ impl<'a> Iterator for MultiBufferChunks<'a> { } chunks } else { - let base_buffer = &self.diffs.get(&buffer_id)?.base_text.as_ref()?; + let base_buffer = &self.diffs.get(&buffer_id)?.base_text()?; base_buffer.chunks(base_text_start..base_text_end, self.language_aware) }; diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs index d98a9db30a5272b3e9498b86fb6345692fe96928..3b8d0e2a1658336d14f67c55e6801aaa9ade70b9 100644 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ b/crates/multi_buffer/src/multi_buffer_tests.rs @@ -1,5 +1,5 @@ use super::*; -use diff::DiffHunkStatus; +use buffer_diff::DiffHunkStatus; use gpui::{App, TestAppContext}; use indoc::indoc; use language::{Buffer, Rope}; @@ -979,8 +979,6 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) { let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff.clone(), cx); multibuffer.push_excerpts( buffer.clone(), [ExcerptRange { @@ -989,6 +987,8 @@ fn test_empty_diff_excerpt(cx: &mut TestAppContext) { }], cx, ); + multibuffer.set_all_diff_hunks_expanded(cx); + multibuffer.add_diff(diff.clone(), cx); }); cx.run_until_parked(); @@ -1325,13 +1325,13 @@ fn test_basic_diff_hunks(cx: &mut TestAppContext) { .map(|info| (info.buffer_row, info.diff_status)) .collect::>(), vec![ - (Some(0), Some(DiffHunkStatus::Added)), + (Some(0), Some(DiffHunkStatus::added())), (Some(1), None), - (Some(1), Some(DiffHunkStatus::Removed)), - (Some(2), Some(DiffHunkStatus::Added)), + (Some(1), Some(DiffHunkStatus::removed())), + (Some(2), Some(DiffHunkStatus::added())), (Some(3), None), - (Some(3), Some(DiffHunkStatus::Removed)), - (Some(4), Some(DiffHunkStatus::Removed)), + (Some(3), Some(DiffHunkStatus::removed())), + (Some(4), Some(DiffHunkStatus::removed())), (Some(4), None), (Some(5), None) ] @@ -1999,12 +1999,8 @@ fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); - let base_id_1 = diff_1.read_with(cx, |diff, _| { - diff.snapshot.base_text.as_ref().unwrap().remote_id() - }); - let base_id_2 = diff_2.read_with(cx, |diff, _| { - diff.snapshot.base_text.as_ref().unwrap().remote_id() - }); + let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().as_ref().unwrap().remote_id()); + let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().as_ref().unwrap().remote_id()); let buffer_lines = (0..=snapshot.max_row().0) .map(|row| { @@ -2191,9 +2187,8 @@ impl ReferenceMultibuffer { let Some(diff) = self.diffs.get(&buffer_id) else { return; }; - let diff = diff.read(cx).snapshot.clone(); let excerpt_range = excerpt.range.to_offset(&buffer); - for hunk in diff.hunks_intersecting_range(range, &buffer) { + for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) { let hunk_range = hunk.buffer_range.to_offset(&buffer); if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end { continue; @@ -2226,12 +2221,12 @@ impl ReferenceMultibuffer { let buffer = excerpt.buffer.read(cx); let buffer_range = excerpt.range.to_offset(buffer); let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx); - let diff = diff.snapshot.clone(); - let base_buffer = diff.base_text.as_ref().unwrap(); + // let diff = diff.snapshot.clone(); + let base_buffer = diff.base_text().unwrap(); let mut offset = buffer_range.start; let mut hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer) + .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) .peekable(); while let Some(hunk) = hunks.next() { @@ -2284,7 +2279,7 @@ impl ReferenceMultibuffer { buffer_start: Some( base_buffer.offset_to_point(hunk.diff_base_byte_range.start), ), - status: Some(DiffHunkStatus::Removed), + status: Some(DiffHunkStatus::Removed(hunk.secondary_status)), }); } @@ -2299,7 +2294,7 @@ impl ReferenceMultibuffer { buffer_id: Some(buffer.remote_id()), range: len..text.len(), buffer_start: Some(buffer.offset_to_point(offset)), - status: Some(DiffHunkStatus::Added), + status: Some(DiffHunkStatus::Added(hunk.secondary_status)), }); offset = hunk_range.end; } @@ -2365,8 +2360,8 @@ impl ReferenceMultibuffer { let buffer = excerpt.buffer.read(cx).snapshot(); let excerpt_range = excerpt.range.to_offset(&buffer); let buffer_id = buffer.remote_id(); - let diff = &self.diffs.get(&buffer_id).unwrap().read(cx).snapshot; - let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer).peekable(); + let diff = self.diffs.get(&buffer_id).unwrap().read(cx); + let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable(); excerpt.expanded_diff_hunks.retain(|hunk_anchor| { if !hunk_anchor.is_valid(&buffer) { return false; @@ -2670,7 +2665,7 @@ async fn test_random_multibuffer(cx: &mut TestAppContext, mut rng: StdRng) { expected_row_infos .into_iter() .filter_map( - |info| if info.diff_status == Some(DiffHunkStatus::Removed) { + |info| if matches!(info.diff_status, Some(DiffHunkStatus::Removed(_))) { None } else { info.buffer_row @@ -3027,9 +3022,9 @@ fn format_diff( .zip(row_infos) .map(|((ix, line), info)| { let marker = match info.diff_status { - Some(DiffHunkStatus::Added) => "+ ", - Some(DiffHunkStatus::Removed) => "- ", - Some(DiffHunkStatus::Modified) => unreachable!(), + Some(DiffHunkStatus::Added(_)) => "+ ", + Some(DiffHunkStatus::Removed(_)) => "- ", + Some(DiffHunkStatus::Modified(_)) => unreachable!(), None => { if has_diff && !line.is_empty() { " " diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index bb96d1b518cc6821068533a2f45022207770fd3e..23ab43c46b7c03d393e2d405d93c238da963c112 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -30,7 +30,7 @@ async-trait.workspace = true client.workspace = true clock.workspace = true collections.workspace = true -diff.workspace = true +buffer_diff.workspace = true fs.workspace = true futures.workspace = true fuzzy.workspace = true @@ -78,7 +78,7 @@ fancy-regex.workspace = true [dev-dependencies] client = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] } -diff = { workspace = true, features = ["test-support"] } +buffer_diff = { workspace = true, features = ["test-support"] } env_logger.workspace = true fs = { workspace = true, features = ["test-support"] } git2.workspace = true diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs index eed41f1f6b0e0167cfc246dfd617c4f533c58a85..0a8721b4b05a4ef22912f1bbee8d91396a3f337b 100644 --- a/crates/project/src/buffer_store.rs +++ b/crates/project/src/buffer_store.rs @@ -6,9 +6,9 @@ use crate::{ }; use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry}; use anyhow::{anyhow, bail, Context as _, Result}; +use buffer_diff::{BufferDiff, BufferDiffEvent}; use client::Client; use collections::{hash_map, HashMap, HashSet}; -use diff::{BufferDiff, BufferDiffEvent, BufferDiffSnapshot}; use fs::Fs; use futures::{channel::oneshot, future::Shared, Future, FutureExt as _, StreamExt}; use git::{blame::Blame, repository::RepoPath}; @@ -23,7 +23,10 @@ use language::{ }, Buffer, BufferEvent, Capability, DiskState, File as _, Language, LanguageRegistry, Operation, }; -use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope}; +use rpc::{ + proto::{self, ToProto}, + AnyProtoClient, ErrorExt as _, TypedEnvelope, +}; use serde::Deserialize; use smol::channel::Receiver; use std::{ @@ -204,72 +207,74 @@ impl BufferDiffState { _ => false, }; self.recalculate_diff_task = Some(cx.spawn(|this, mut cx| async move { + let mut unstaged_changed_range = None; if let Some(unstaged_diff) = &unstaged_diff { - let snapshot = if index_changed || language_changed { - cx.update(|cx| { - BufferDiffSnapshot::build( - buffer.clone(), - index, - language.clone(), - language_registry.clone(), - cx, - ) - })? - .await - } else { - unstaged_diff - .read_with(&cx, |changes, cx| { - BufferDiffSnapshot::build_with_base_buffer( - buffer.clone(), - index, - changes.snapshot.base_text.clone(), - cx, - ) - })? - .await - }; + unstaged_changed_range = BufferDiff::update_diff( + unstaged_diff.clone(), + buffer.clone(), + index, + index_changed, + language_changed, + language.clone(), + language_registry.clone(), + &mut cx, + ) + .await?; - unstaged_diff.update(&mut cx, |unstaged_diff, cx| { - unstaged_diff.set_state(snapshot, &buffer, cx); + unstaged_diff.update(&mut cx, |_, cx| { if language_changed { cx.emit(BufferDiffEvent::LanguageChanged); } + if let Some(changed_range) = unstaged_changed_range.clone() { + cx.emit(BufferDiffEvent::DiffChanged { + changed_range: Some(changed_range), + }) + } })?; } if let Some(uncommitted_diff) = &uncommitted_diff { - let snapshot = + let uncommitted_changed_range = if let (Some(unstaged_diff), true) = (&unstaged_diff, index_matches_head) { - unstaged_diff.read_with(&cx, |diff, _| diff.snapshot.clone())? - } else if head_changed || language_changed { - cx.update(|cx| { - BufferDiffSnapshot::build( - buffer.clone(), - head, - language.clone(), - language_registry.clone(), - cx, - ) + uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| { + uncommitted_diff.update_diff_from(&buffer, unstaged_diff, cx) })? - .await } else { - uncommitted_diff - .read_with(&cx, |changes, cx| { - BufferDiffSnapshot::build_with_base_buffer( - buffer.clone(), - head, - changes.snapshot.base_text.clone(), - cx, - ) - })? - .await + BufferDiff::update_diff( + uncommitted_diff.clone(), + buffer.clone(), + head, + head_changed, + language_changed, + language.clone(), + language_registry.clone(), + &mut cx, + ) + .await? }; - uncommitted_diff.update(&mut cx, |diff, cx| { - diff.set_state(snapshot, &buffer, cx); + uncommitted_diff.update(&mut cx, |uncommitted_diff, cx| { if language_changed { cx.emit(BufferDiffEvent::LanguageChanged); } + let changed_range = match (unstaged_changed_range, uncommitted_changed_range) { + (None, None) => None, + (Some(unstaged_range), None) => { + uncommitted_diff.range_to_hunk_range(unstaged_range, &buffer, cx) + } + (None, Some(uncommitted_range)) => Some(uncommitted_range), + (Some(unstaged_range), Some(uncommitted_range)) => maybe!({ + let expanded_range = uncommitted_diff.range_to_hunk_range( + unstaged_range, + &buffer, + cx, + )?; + let start = expanded_range.start.min(&uncommitted_range.start, &buffer); + let end = expanded_range.end.max(&uncommitted_range.end, &buffer); + Some(start..end) + }), + }; + cx.emit(BufferDiffEvent::DiffChanged { changed_range }); })?; } @@ -277,6 +282,7 @@ impl BufferDiffState { this.update(&mut cx, |this, _| { this.index_changed = false; this.head_changed = false; + this.language_changed = false; for tx in this.diff_updated_futures.drain(..) { tx.send(()).ok(); } @@ -580,13 +586,12 @@ impl RemoteBufferStore { let worktree_id = worktree.read(cx).id().to_proto(); let project_id = self.project_id; let client = self.upstream_client.clone(); - let path_string = path.clone().to_string_lossy().to_string(); cx.spawn(move |this, mut cx| async move { let response = client .request(proto::OpenBufferByPath { project_id, worktree_id, - path: path_string, + path: path.to_proto(), }) .await?; let buffer_id = BufferId::new(response.buffer_id)?; @@ -1476,29 +1481,19 @@ impl BufferStore { diff_state.language = language; diff_state.language_registry = language_registry; - let diff = cx.new(|_| BufferDiff { - buffer_id, - snapshot: BufferDiffSnapshot::new(&text_snapshot), - unstaged_diff: None, - }); + let diff = cx.new(|_| BufferDiff::new(&text_snapshot)); match kind { DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()), DiffKind::Uncommitted => { let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() { diff } else { - let unstaged_diff = cx.new(|_| BufferDiff { - buffer_id, - snapshot: BufferDiffSnapshot::new(&text_snapshot), - unstaged_diff: None, - }); + let unstaged_diff = cx.new(|_| BufferDiff::new(&text_snapshot)); diff_state.unstaged_diff = Some(unstaged_diff.downgrade()); unstaged_diff }; - diff.update(cx, |diff, _| { - diff.unstaged_diff = Some(unstaged_diff); - }); + diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff)); diff_state.uncommitted_diff = Some(diff.downgrade()) } }; @@ -2395,9 +2390,8 @@ impl BufferStore { shared.diff = Some(diff.clone()); } })?; - let staged_text = diff.read_with(&cx, |diff, _| { - diff.snapshot.base_text.as_ref().map(|buffer| buffer.text()) - })?; + let staged_text = + diff.read_with(&cx, |diff, _| diff.base_text().map(|buffer| buffer.text()))?; Ok(proto::OpenUnstagedDiffResponse { staged_text }) } @@ -2428,14 +2422,13 @@ impl BufferStore { use proto::open_uncommitted_diff_response::Mode; let staged_buffer = diff - .unstaged_diff - .as_ref() - .and_then(|diff| diff.read(cx).snapshot.base_text.as_ref()); + .secondary_diff() + .and_then(|diff| diff.read(cx).base_text()); let mode; let staged_text; let committed_text; - if let Some(committed_buffer) = &diff.snapshot.base_text { + if let Some(committed_buffer) = diff.base_text() { committed_text = Some(committed_buffer.text()); if let Some(staged_buffer) = staged_buffer { if staged_buffer.remote_id() == committed_buffer.remote_id() { diff --git a/crates/project/src/git.rs b/crates/project/src/git.rs index debc89b3210e731ad8f94394a8bf2de49c4b2ab2..6385025ff5c72a1e099ffa6522a171cbfd2c47ba 100644 --- a/crates/project/src/git.rs +++ b/crates/project/src/git.rs @@ -13,6 +13,7 @@ use gpui::{ App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity, }; use language::{Buffer, LanguageRegistry}; +use rpc::proto::ToProto; use rpc::{proto, AnyProtoClient}; use settings::WorktreeId; use std::path::{Path, PathBuf}; @@ -222,7 +223,7 @@ impl GitState { work_directory_id: work_directory_id.to_proto(), paths: paths .into_iter() - .map(|repo_path| repo_path.to_proto()) + .map(|repo_path| repo_path.as_ref().to_proto()) .collect(), }) .await @@ -247,7 +248,7 @@ impl GitState { work_directory_id: work_directory_id.to_proto(), paths: paths .into_iter() - .map(|repo_path| repo_path.to_proto()) + .map(|repo_path| repo_path.as_ref().to_proto()) .collect(), }) .await diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 7fbb781f0f8a12791c8e324c3f1667bee12368d7..b831b0e6810e7087cae4e37f9c0d7c5806390587 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -55,7 +55,10 @@ use parking_lot::Mutex; use postage::watch; use rand::prelude::*; -use rpc::AnyProtoClient; +use rpc::{ + proto::{FromProto, ToProto}, + AnyProtoClient, +}; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; use sha2::{Digest, Sha256}; @@ -5360,7 +5363,7 @@ impl LspStore { project_id: *project_id, worktree_id: worktree_id.to_proto(), summary: Some(proto::DiagnosticSummary { - path: worktree_path.to_string_lossy().to_string(), + path: worktree_path.to_proto(), language_server_id: server_id.0 as u64, error_count: new_summary.error_count as u32, warning_count: new_summary.warning_count as u32, @@ -5848,10 +5851,8 @@ impl LspStore { .ok_or_else(|| anyhow!("worktree not found"))?; let (old_abs_path, new_abs_path) = { let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?; - ( - root_path.join(&old_path), - root_path.join(&envelope.payload.new_path), - ) + let new_path = PathBuf::from_proto(envelope.payload.new_path.clone()); + (root_path.join(&old_path), root_path.join(&new_path)) }; Self::will_rename_entry( @@ -5881,7 +5882,7 @@ impl LspStore { if let Some(message) = envelope.payload.summary { let project_path = ProjectPath { worktree_id, - path: Path::new(&message.path).into(), + path: Arc::::from_proto(message.path), }; let path = project_path.path.clone(); let server_id = LanguageServerId(message.language_server_id as usize); @@ -5915,7 +5916,7 @@ impl LspStore { project_id: *project_id, worktree_id: worktree_id.to_proto(), summary: Some(proto::DiagnosticSummary { - path: project_path.path.to_string_lossy().to_string(), + path: project_path.path.as_ref().to_proto(), language_server_id: server_id.0 as u64, error_count: summary.error_count as u32, warning_count: summary.warning_count as u32, @@ -7114,7 +7115,7 @@ impl LspStore { project_id, worktree_id: worktree_id.to_proto(), summary: Some(proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), + path: path.as_ref().to_proto(), language_server_id: server_id.0 as u64, error_count: 0, warning_count: 0, @@ -7768,7 +7769,7 @@ impl LspStore { language_server_name: symbol.language_server_name.0.to_string(), source_worktree_id: symbol.source_worktree_id.to_proto(), worktree_id: symbol.path.worktree_id.to_proto(), - path: symbol.path.path.to_string_lossy().to_string(), + path: symbol.path.path.as_ref().to_proto(), name: symbol.name.clone(), kind: unsafe { mem::transmute::(symbol.kind) }, start: Some(proto::PointUtf16 { @@ -7789,7 +7790,7 @@ impl LspStore { let kind = unsafe { mem::transmute::(serialized_symbol.kind) }; let path = ProjectPath { worktree_id, - path: PathBuf::from(serialized_symbol.path).into(), + path: Arc::::from_proto(serialized_symbol.path), }; let start = serialized_symbol @@ -8263,7 +8264,7 @@ impl DiagnosticSummary { path: &Path, ) -> proto::DiagnosticSummary { proto::DiagnosticSummary { - path: path.to_string_lossy().to_string(), + path: path.to_proto(), language_server_id: language_server_id.0 as u64, error_count: self.error_count as u32, warning_count: self.warning_count as u32, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index da2eeb857834dd644407bdd15b240549cafaa072..5446471b90f4d51b0db2b1000dc2bfbf94b09969 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -21,7 +21,7 @@ mod project_tests; mod direnv; mod environment; -use diff::BufferDiff; +use buffer_diff::BufferDiff; pub use environment::EnvironmentErrorMessage; use git::Repository; pub mod search_history; @@ -73,7 +73,7 @@ pub use prettier_store::PrettierStore; use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent}; use remote::{SshConnectionOptions, SshRemoteClient}; use rpc::{ - proto::{LanguageServerPromptResponse, SSH_PROJECT_ID}, + proto::{FromProto, LanguageServerPromptResponse, ToProto, SSH_PROJECT_ID}, AnyProtoClient, ErrorCode, }; use search::{SearchInputKind, SearchQuery, SearchResult}; @@ -297,14 +297,14 @@ impl ProjectPath { pub fn from_proto(p: proto::ProjectPath) -> Self { Self { worktree_id: WorktreeId::from_proto(p.worktree_id), - path: Arc::from(PathBuf::from(p.path)), + path: Arc::::from_proto(p.path), } } pub fn to_proto(&self) -> proto::ProjectPath { proto::ProjectPath { worktree_id: self.worktree_id.to_proto(), - path: self.path.to_string_lossy().to_string(), + path: self.path.as_ref().to_proto(), } } @@ -3360,18 +3360,19 @@ impl Project { }) }) } else if let Some(ssh_client) = self.ssh_client.as_ref() { + let request_path = Path::new(path); let request = ssh_client .read(cx) .proto_client() .request(proto::GetPathMetadata { project_id: SSH_PROJECT_ID, - path: path.to_string(), + path: request_path.to_proto(), }); cx.background_executor().spawn(async move { let response = request.await.log_err()?; if response.exists { Some(ResolvedPath::AbsPath { - path: PathBuf::from(response.path), + path: PathBuf::from_proto(response.path), is_dir: response.is_dir, }) } else { @@ -3441,9 +3442,10 @@ impl Project { if self.is_local() { DirectoryLister::Local(self.fs.clone()).list_directory(query, cx) } else if let Some(session) = self.ssh_client.as_ref() { + let path_buf = PathBuf::from(query); let request = proto::ListRemoteDirectory { dev_server_id: SSH_PROJECT_ID, - path: query, + path: path_buf.to_proto(), }; let response = session.read(cx).proto_client().request(request); @@ -3994,7 +3996,7 @@ impl Project { this.open_buffer( ProjectPath { worktree_id, - path: PathBuf::from(envelope.payload.path).into(), + path: Arc::::from_proto(envelope.payload.path), }, cx, ) diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 642418e3f156d9da84e9dbba2a6a1a4d6f527a29..360d8a952953caffffac30697d5346ac5f7b7388 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -7,18 +7,17 @@ use paths::{ local_settings_file_relative_path, local_tasks_file_relative_path, local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME, }; -use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use rpc::{ + proto::{self, FromProto, ToProto}, + AnyProtoClient, TypedEnvelope, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources, SettingsStore, }; -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; +use std::{path::Path, sync::Arc, time::Duration}; use task::{TaskTemplates, VsCodeTaskFile}; use util::ResultExt; use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId}; @@ -292,7 +291,7 @@ impl SettingsObserver { .send(proto::UpdateWorktreeSettings { project_id, worktree_id, - path: path.to_string_lossy().into(), + path: path.to_proto(), content: Some(content), kind: Some( local_settings_kind_to_proto(LocalSettingsKind::Settings).into(), @@ -305,7 +304,7 @@ impl SettingsObserver { .send(proto::UpdateWorktreeSettings { project_id, worktree_id, - path: path.to_string_lossy().into(), + path: path.to_proto(), content: Some(content), kind: Some( local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(), @@ -343,7 +342,7 @@ impl SettingsObserver { this.update_settings( worktree, [( - PathBuf::from(&envelope.payload.path).into(), + Arc::::from_proto(envelope.payload.path.clone()), local_settings_kind_from_proto(kind), envelope.payload.content, )], @@ -551,7 +550,7 @@ impl SettingsObserver { .send(proto::UpdateWorktreeSettings { project_id: self.project_id, worktree_id: remote_worktree_id.to_proto(), - path: directory.to_string_lossy().into_owned(), + path: directory.to_proto(), content: file_content, kind: Some(local_settings_kind_to_proto(kind).into()), }) diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 32fff6f1aa1520a9bcd0fa43687fafb6eb63be99..a002431253c210a367891a090f870e504068b935 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,5 +1,5 @@ use crate::{Event, *}; -use diff::assert_hunks; +use buffer_diff::{assert_hunks, DiffHunkSecondaryStatus, DiffHunkStatus}; use fs::FakeFs; use futures::{future, StreamExt}; use gpui::{App, SemanticVersion, UpdateGlobal}; @@ -5692,15 +5692,16 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, &unstaged_diff.base_text_string().unwrap(), &[ - (0..1, "", "// print goodbye\n"), + (0..1, "", "// print goodbye\n", DiffHunkStatus::added()), ( 2..3, " println!(\"hello world\");\n", " println!(\"goodbye world\");\n", + DiffHunkStatus::modified(), ), ], ); @@ -5722,10 +5723,15 @@ async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) { unstaged_diff.update(cx, |unstaged_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - unstaged_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &unstaged_diff.snapshot.base_text.as_ref().unwrap().text(), - &[(2..3, "", " println!(\"goodbye world\");\n")], + &unstaged_diff.base_text().unwrap().text(), + &[( + 2..3, + "", + " println!(\"goodbye world\");\n", + DiffHunkStatus::added(), + )], ); }); } @@ -5795,10 +5801,7 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.read_with(cx, |diff, _| { assert_eq!( - diff.snapshot - .base_text - .as_ref() - .and_then(|base| base.language().cloned()), + diff.base_text().and_then(|base| base.language().cloned()), Some(language) ) }); @@ -5807,15 +5810,21 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, &uncommitted_diff.base_text_string().unwrap(), &[ - (0..1, "", "// print goodbye\n"), + ( + 0..1, + "", + "// print goodbye\n", + DiffHunkStatus::Added(DiffHunkSecondaryStatus::HasSecondaryHunk), + ), ( 2..3, " println!(\"hello world\");\n", " println!(\"goodbye world\");\n", + DiffHunkStatus::modified(), ), ], ); @@ -5837,10 +5846,15 @@ async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(), - &[(2..3, "", " println!(\"goodbye world\");\n")], + &uncommitted_diff.base_text().unwrap().text(), + &[( + 2..3, + "", + " println!(\"goodbye world\");\n", + DiffHunkStatus::added(), + )], ); }); } @@ -5898,13 +5912,14 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) { uncommitted_diff.update(cx, |uncommitted_diff, cx| { let snapshot = buffer.read(cx).snapshot(); assert_hunks( - uncommitted_diff.diff_hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot), + uncommitted_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx), &snapshot, - &uncommitted_diff.snapshot.base_text.as_ref().unwrap().text(), + &uncommitted_diff.base_text_string().unwrap(), &[( 1..2, " println!(\"hello from HEAD\");\n", " println!(\"hello from the working copy\");\n", + DiffHunkStatus::modified(), )], ); }); diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs index 4fc1134272d336b85e667c8817941855c1fbebff..d4af70e41f84578d3c2744ffb1e16ce3d8870c17 100644 --- a/crates/project/src/toolchain_store.rs +++ b/crates/project/src/toolchain_store.rs @@ -1,4 +1,4 @@ -use std::{str::FromStr, sync::Arc}; +use std::{path::PathBuf, str::FromStr, sync::Arc}; use anyhow::{bail, Result}; @@ -8,7 +8,10 @@ use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList}; -use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use rpc::{ + proto::{self, FromProto, ToProto}, + AnyProtoClient, TypedEnvelope, +}; use settings::WorktreeId; use util::ResultExt as _; @@ -120,7 +123,9 @@ impl ToolchainStore { }; let toolchain = Toolchain { name: toolchain.name.into(), - path: toolchain.path.into(), + // todo(windows) + // Do we need to convert path to native string? + path: PathBuf::from(toolchain.path).to_proto().into(), as_json: serde_json::Value::from_str(&toolchain.raw_json)?, language_name, }; @@ -144,10 +149,13 @@ impl ToolchainStore { .await; Ok(proto::ActiveToolchainResponse { - toolchain: toolchain.map(|toolchain| proto::Toolchain { - name: toolchain.name.into(), - path: toolchain.path.into(), - raw_json: toolchain.as_json.to_string(), + toolchain: toolchain.map(|toolchain| { + let path = PathBuf::from(toolchain.path.to_string()); + proto::Toolchain { + name: toolchain.name.into(), + path: path.to_proto(), + raw_json: toolchain.as_json.to_string(), + } }), }) } @@ -183,10 +191,13 @@ impl ToolchainStore { toolchains .toolchains .into_iter() - .map(|toolchain| proto::Toolchain { - name: toolchain.name.to_string(), - path: toolchain.path.to_string(), - raw_json: toolchain.as_json.to_string(), + .map(|toolchain| { + let path = PathBuf::from(toolchain.path.to_string()); + proto::Toolchain { + name: toolchain.name.to_string(), + path: path.to_proto(), + raw_json: toolchain.as_json.to_string(), + } }) .collect::>() } else { @@ -354,6 +365,7 @@ impl RemoteToolchainStore { let project_id = self.project_id; let client = self.client.clone(); cx.spawn(move |_| async move { + let path = PathBuf::from(toolchain.path.to_string()); let _ = client .request(proto::ActivateToolchain { project_id, @@ -361,7 +373,7 @@ impl RemoteToolchainStore { language_name: toolchain.language_name.into(), toolchain: Some(proto::Toolchain { name: toolchain.name.into(), - path: toolchain.path.into(), + path: path.to_proto(), raw_json: toolchain.as_json.to_string(), }), }) @@ -398,7 +410,12 @@ impl RemoteToolchainStore { Some(Toolchain { language_name: language_name.clone(), name: toolchain.name.into(), - path: toolchain.path.into(), + // todo(windows) + // Do we need to convert path to native string? + path: PathBuf::from_proto(toolchain.path) + .to_string_lossy() + .to_string() + .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, }) }) @@ -439,7 +456,12 @@ impl RemoteToolchainStore { Some(Toolchain { language_name: language_name.clone(), name: toolchain.name.into(), - path: toolchain.path.into(), + // todo(windows) + // Do we need to convert path to native string? + path: PathBuf::from_proto(toolchain.path) + .to_string_lossy() + .to_string() + .into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, }) }) diff --git a/crates/project/src/worktree_store.rs b/crates/project/src/worktree_store.rs index 95d88384fef905a0f772cc7611388ad6c6c14385..6461d97723b9f3fe1ba5d4aa5ebc8d758c6b9443 100644 --- a/crates/project/src/worktree_store.rs +++ b/crates/project/src/worktree_store.rs @@ -15,7 +15,7 @@ use futures::{ use gpui::{App, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity}; use postage::oneshot; use rpc::{ - proto::{self, SSH_PROJECT_ID}, + proto::{self, FromProto, ToProto, SSH_PROJECT_ID}, AnyProtoClient, ErrorExt, TypedEnvelope, }; use smol::{ @@ -268,10 +268,11 @@ impl WorktreeStore { cx.spawn(|this, mut cx| async move { let this = this.upgrade().context("Dropped worktree store")?; + let path = Path::new(abs_path.as_str()); let response = client .request(proto::AddWorktree { project_id: SSH_PROJECT_ID, - path: abs_path.clone(), + path: path.to_proto(), visible, }) .await?; @@ -282,10 +283,11 @@ impl WorktreeStore { return Ok(existing_worktree); } - let root_name = PathBuf::from(&response.canonicalized_path) + let root_path_buf = PathBuf::from_proto(response.canonicalized_path.clone()); + let root_name = root_path_buf .file_name() .map(|n| n.to_string_lossy().to_string()) - .unwrap_or(response.canonicalized_path.to_string()); + .unwrap_or(root_path_buf.to_string_lossy().to_string()); let worktree = cx.update(|cx| { Worktree::remote( @@ -596,7 +598,7 @@ impl WorktreeStore { id: worktree.id().to_proto(), root_name: worktree.root_name().into(), visible: worktree.is_visible(), - abs_path: worktree.abs_path().to_string_lossy().into(), + abs_path: worktree.abs_path().to_proto(), } }) .collect() @@ -923,7 +925,7 @@ impl WorktreeStore { project_id: remote_worktree.project_id(), repository: Some(proto::ProjectPath { worktree_id: project_path.worktree_id.to_proto(), - path: project_path.path.to_string_lossy().to_string(), // Root path + path: project_path.path.to_proto(), // Root path }), }); @@ -994,7 +996,7 @@ impl WorktreeStore { project_id: remote_worktree.project_id(), repository: Some(proto::ProjectPath { worktree_id: repository.worktree_id.to_proto(), - path: repository.path.to_string_lossy().to_string(), // Root path + path: repository.path.to_proto(), // Root path }), branch_name: new_branch, }); @@ -1116,7 +1118,7 @@ impl WorktreeStore { .context("Invalid GitBranches call")?; let project_path = ProjectPath { worktree_id: WorktreeId::from_proto(project_path.worktree_id), - path: Path::new(&project_path.path).into(), + path: Arc::::from_proto(project_path.path), }; let branches = this @@ -1147,7 +1149,7 @@ impl WorktreeStore { .context("Invalid GitBranches call")?; let project_path = ProjectPath { worktree_id: WorktreeId::from_proto(project_path.worktree_id), - path: Path::new(&project_path.path).into(), + path: Arc::::from_proto(project_path.path), }; let new_branch = update_branch.payload.branch_name; diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index e94bd1479df3960ff8ab97454a999c205d8f9ba3..7d33dd1a3e842d100c4e8496f294ba6006a4a307 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -2489,8 +2489,8 @@ message RefreshLlmToken {} // Remote FS message AddWorktree { - uint64 project_id = 2; string path = 1; + uint64 project_id = 2; bool visible = 3; } @@ -2625,6 +2625,7 @@ message UpdateGitBranch { string branch_name = 2; ProjectPath repository = 3; } + message GetPanicFiles { } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index 0ba9b6ef19a8e71315e596bfc263bde8073d6ec9..d45cc0936cf399c86ff59eed9f7db728213ffa0a 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -15,6 +15,8 @@ use std::{ cmp, fmt::{self, Debug}, iter, mem, + path::{Path, PathBuf}, + sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -137,6 +139,62 @@ impl fmt::Display for PeerId { } } +pub trait FromProto { + fn from_proto(proto: String) -> Self; +} + +pub trait ToProto { + fn to_proto(self) -> String; +} + +impl FromProto for PathBuf { + #[cfg(target_os = "windows")] + fn from_proto(proto: String) -> Self { + proto.split("/").collect() + } + + #[cfg(not(target_os = "windows"))] + fn from_proto(proto: String) -> Self { + PathBuf::from(proto) + } +} + +impl FromProto for Arc { + fn from_proto(proto: String) -> Self { + PathBuf::from_proto(proto).into() + } +} + +impl ToProto for PathBuf { + #[cfg(target_os = "windows")] + fn to_proto(self) -> String { + self.components() + .map(|comp| comp.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/") + } + + #[cfg(not(target_os = "windows"))] + fn to_proto(self) -> String { + self.to_string_lossy().to_string() + } +} + +impl ToProto for &Path { + #[cfg(target_os = "windows")] + fn to_proto(self) -> String { + self.components() + .map(|comp| comp.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/") + } + + #[cfg(not(target_os = "windows"))] + fn to_proto(self) -> String { + self.to_string_lossy().to_string() + } +} + messages!( (AcceptTermsOfService, Foreground), (AcceptTermsOfServiceResponse, Foreground), @@ -757,4 +815,22 @@ mod tests { }; assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); } + + #[test] + #[cfg(target_os = "windows")] + fn test_proto() { + fn generate_proto_path(path: PathBuf) -> PathBuf { + let proto = path.to_proto(); + PathBuf::from_proto(proto) + } + + let path = PathBuf::from("C:\\foo\\bar"); + assert_eq!(path, generate_proto_path(path.clone())); + + let path = PathBuf::from("C:/foo/bar/"); + assert_eq!(path, generate_proto_path(path.clone())); + + let path = PathBuf::from("C:/foo\\bar\\"); + assert_eq!(path, generate_proto_path(path.clone())); + } } diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index be22a52fa77b71c178a908b729fa652316f6435a..eb4122a321dac05d6fa38cf26074bc047c2ec346 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -1,3 +1,4 @@ +use ::proto::{FromProto, ToProto}; use anyhow::{anyhow, Context as _, Result}; use extension::ExtensionHostProxy; use extension_host::headless_host::HeadlessExtensionStore; @@ -325,10 +326,8 @@ impl HeadlessProject { mut cx: AsyncApp, ) -> Result { use client::ErrorCodeExt; - let path = shellexpand::tilde(&message.payload.path).to_string(); - let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?; - let path = PathBuf::from(path); + let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string()); let canonicalized = match fs.canonicalize(&path).await { Ok(path) => path, @@ -363,7 +362,7 @@ impl HeadlessProject { let response = this.update(&mut cx, |_, cx| { worktree.update(cx, |worktree, _| proto::AddWorktreeResponse { worktree_id: worktree.id().to_proto(), - canonicalized_path: canonicalized.to_string_lossy().to_string(), + canonicalized_path: canonicalized.to_proto(), }) })?; @@ -418,7 +417,7 @@ impl HeadlessProject { buffer_store.open_buffer( ProjectPath { worktree_id, - path: PathBuf::from(message.payload.path).into(), + path: Arc::::from_proto(message.payload.path), }, cx, ) @@ -559,11 +558,11 @@ impl HeadlessProject { envelope: TypedEnvelope, cx: AsyncApp, ) -> Result { - let expanded = shellexpand::tilde(&envelope.payload.path).to_string(); let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; + let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string()); let mut entries = Vec::new(); - let mut response = fs.read_dir(Path::new(&expanded)).await?; + let mut response = fs.read_dir(&expanded).await?; while let Some(path) = response.next().await { if let Some(file_name) = path?.file_name() { entries.push(file_name.to_string_lossy().to_string()); @@ -578,15 +577,15 @@ impl HeadlessProject { cx: AsyncApp, ) -> Result { let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; - let expanded = shellexpand::tilde(&envelope.payload.path).to_string(); + let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string()); - let metadata = fs.metadata(&PathBuf::from(expanded.clone())).await?; + let metadata = fs.metadata(&expanded).await?; let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false); Ok(proto::GetPathMetadataResponse { exists: metadata.is_some(), is_dir, - path: expanded, + path: expanded.to_proto(), }) } diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index 7552e950aaca64a7a5a6ba78f70a5725671543aa..c1a22b2c8a8b29fe362796bf38c29d94956c67c4 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -859,7 +859,7 @@ async fn test_remote_resolve_path_in_buffer( async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -876,7 +876,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T let path = project .update(cx, |project, cx| { - project.resolve_abs_path("/code/project1/README.md", cx) + project.resolve_abs_path(path!("/code/project1/README.md"), cx) }) .await .unwrap(); @@ -884,12 +884,12 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T assert!(path.is_file()); assert_eq!( path.abs_path().unwrap().to_string_lossy(), - "/code/project1/README.md" + path!("/code/project1/README.md") ); let path = project .update(cx, |project, cx| { - project.resolve_abs_path("/code/project1/src", cx) + project.resolve_abs_path(path!("/code/project1/src"), cx) }) .await .unwrap(); @@ -897,12 +897,12 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T assert!(path.is_dir()); assert_eq!( path.abs_path().unwrap().to_string_lossy(), - "/code/project1/src" + path!("/code/project1/src") ); let path = project .update(cx, |project, cx| { - project.resolve_abs_path("/code/project1/DOESNOTEXIST", cx) + project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx) }) .await; assert!(path.is_none()); @@ -958,7 +958,7 @@ async fn test_adding_then_removing_then_adding_worktrees( ) { let fs = FakeFs::new(server_cx.executor()); fs.insert_tree( - "/code", + path!("/code"), json!({ "project1": { ".git": {}, @@ -977,14 +977,14 @@ async fn test_adding_then_removing_then_adding_worktrees( let (project, _headless) = init_test(&fs, cx, server_cx).await; let (_worktree, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) + project.find_or_create_worktree(path!("/code/project1"), true, cx) }) .await .unwrap(); let (worktree_2, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project2", true, cx) + project.find_or_create_worktree(path!("/code/project2"), true, cx) }) .await .unwrap(); @@ -994,7 +994,7 @@ async fn test_adding_then_removing_then_adding_worktrees( let (worktree_2, _) = project .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project2", true, cx) + project.find_or_create_worktree(path!("/code/project2"), true, cx) }) .await .unwrap(); @@ -1246,8 +1246,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_1); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string() @@ -1266,8 +1265,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_1); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string() @@ -1286,8 +1284,7 @@ async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppC diff.read_with(cx, |diff, cx| { assert_eq!(diff.base_text_string().unwrap(), text_2); assert_eq!( - diff.unstaged_diff - .as_ref() + diff.secondary_diff() .unwrap() .read(cx) .base_text_string() diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index a851b431f7c0eb259bc13d51164abf7c11e3bbb3..0bbfbcb00190295e1233c0d80d4a0a18358b85d8 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -37,7 +37,6 @@ streaming-iterator.workspace = true tree-sitter-json.workspace = true tree-sitter.workspace = true util.workspace = true -migrator.workspace = true [dev-dependencies] fs = { workspace = true, features = ["test-support"] } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index f4de45270a14aac9485cd768798bd098a9e7f563..c264b56666358f067245c3efe90ffbb5f23cf588 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1,11 +1,10 @@ -use anyhow::{anyhow, Context as _, Result}; +use anyhow::{anyhow, Result}; use collections::{BTreeMap, HashMap, IndexMap}; use fs::Fs; use gpui::{ Action, ActionBuildError, App, InvalidKeystrokeError, KeyBinding, KeyBindingContextPredicate, NoAction, SharedString, KEYSTROKE_PARSE_EXPECTED_MESSAGE, }; -use migrator::migrate_keymap; use schemars::{ gen::{SchemaGenerator, SchemaSettings}, schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation}, @@ -598,7 +597,7 @@ impl KeymapFile { self.0.iter() } - async fn load_keymap_file(fs: &Arc) -> Result { + pub async fn load_keymap_file(fs: &Arc) -> Result { match fs.load(paths::keymap_file()).await { result @ Ok(_) => result, Err(err) => { @@ -611,41 +610,6 @@ impl KeymapFile { } } } - - pub fn should_migrate_keymap(keymap_file: Self) -> bool { - let Ok(old_text) = serde_json::to_string(&keymap_file) else { - return false; - }; - migrate_keymap(&old_text).is_some() - } - - pub async fn migrate_keymap(fs: Arc) -> Result<()> { - let old_text = Self::load_keymap_file(&fs).await?; - let Some(new_text) = migrate_keymap(&old_text) else { - return Ok(()); - }; - let keymap_path = paths::keymap_file().as_path(); - if fs.is_file(keymap_path).await { - fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text) - .await - .with_context(|| { - "Failed to create settings backup in home directory".to_string() - })?; - let resolved_path = fs - .canonicalize(keymap_path) - .await - .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?; - } else { - fs.atomic_write(keymap_path.to_path_buf(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; - } - - Ok(()) - } } // Double quotes a string and wraps it in backticks for markdown inline code.. diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index de51986916b922b83296a6bd5ecb7fa90bf939ad..924b25fabe0a074cd25c57a83a733b7bf3388148 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -4,7 +4,7 @@ use ec4rs::{ConfigParser, PropertiesSource, Section}; use fs::Fs; use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal}; -use migrator::migrate_settings; + use paths::{local_settings_file_relative_path, EDITORCONFIG_NAME}; use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -390,7 +390,7 @@ impl SettingsStore { self.set_user_settings(&new_text, cx).unwrap(); } - async fn load_settings(fs: &Arc) -> Result { + pub async fn load_settings(fs: &Arc) -> Result { match fs.load(paths::settings_file()).await { result @ Ok(_) => result, Err(err) => { @@ -996,51 +996,6 @@ impl SettingsStore { properties.use_fallbacks(); Some(properties) } - - pub fn should_migrate_settings(settings: &serde_json::Value) -> bool { - let Ok(old_text) = serde_json::to_string(settings) else { - return false; - }; - migrate_settings(&old_text).is_some() - } - - pub fn migrate_settings(&self, fs: Arc) { - self.setting_file_updates_tx - .unbounded_send(Box::new(move |_: AsyncApp| { - async move { - let old_text = Self::load_settings(&fs).await?; - let Some(new_text) = migrate_settings(&old_text) else { - return anyhow::Ok(()); - }; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) - .await - .with_context(|| { - "Failed to create settings backup in home directory".to_string() - })?; - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; - } - anyhow::Ok(()) - } - .boxed_local() - })) - .ok(); - } } #[derive(Debug, Clone, PartialEq)] diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 465391b26c8bb595dde6b6ae28ada5d55529e4fd..cd6e5e5e58df8db8c8b607e19712e79c58142af8 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -94,6 +94,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("lock", "icons/file_icons/lock.svg"), ("log", "icons/file_icons/info.svg"), ("lua", "icons/file_icons/lua.svg"), + ("markdown", "icons/file_icons/book.svg"), ("metal", "icons/file_icons/metal.svg"), ("nim", "icons/file_icons/nim.svg"), ("nix", "icons/file_icons/nix.svg"), @@ -112,6 +113,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("scala", "icons/file_icons/scala.svg"), ("settings", "icons/file_icons/settings.svg"), ("storage", "icons/file_icons/database.svg"), + ("svelte", "icons/file_icons/html.svg"), ("swift", "icons/file_icons/swift.svg"), ("tcl", "icons/file_icons/tcl.svg"), ("template", "icons/file_icons/html.svg"), diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 2ebaa551052ffecdea235d179d94f47c742c2a67..2349754293d08e7478f261d5aebfeea46b81f2a0 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -524,7 +524,7 @@ impl Render for ContextMenu { .occlude() .elevation_2(cx) .p_2() - .max_w_80() + .max_w_96() .child(aside(cx)), ) }) @@ -600,6 +600,8 @@ impl Render for ContextMenu { let menu = cx.entity().downgrade(); let icon_color = if *disabled { Color::Muted + } else if toggle.is_some() { + icon_color.unwrap_or(Color::Accent) } else { icon_color.unwrap_or(Color::Default) }; @@ -674,7 +676,7 @@ impl Render for ContextMenu { let contents = div().flex_none().child( Icon::new(IconName::Check) - .color(Color::Accent) + .color(icon_color) .size(*icon_size) ) .when(!toggled, |contents| diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 3b1f225d3f081efc1be2899c532a81f249823e58..d8043f74001a483e0ced274e6f08e00291542931 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -221,7 +221,6 @@ pub enum IconName { Hash, HistoryRerun, Indicator, - IndicatorX, Info, InlayHint, Keyboard, @@ -325,6 +324,8 @@ pub enum IconName { ZedAssistant2, ZedAssistantFilled, ZedPredict, + ZedPredictUp, + ZedPredictDown, ZedPredictDisabled, ZedXCopilot, } diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index bbd579218a16e10e235ddea2039dc95b195fff77..76c7b464e08da8a36b06b7dd4e147136e1ec0ab1 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -8,6 +8,7 @@ use editor::{ Bias, Editor, ToPoint, }; use gpui::{actions, impl_internal_actions, Action, App, Context, Global, Window}; +use itertools::Itertools; use language::Point; use multi_buffer::MultiBufferRow; use regex::Regex; @@ -64,6 +65,95 @@ pub struct WithCount { action: WrappedAction, } +#[derive(Clone, Deserialize, JsonSchema, PartialEq)] +pub enum VimOption { + Wrap(bool), + Number(bool), + RelativeNumber(bool), +} + +impl VimOption { + fn possible_commands(query: &str) -> Vec { + let mut prefix_of_options = Vec::new(); + let mut options = query.split(" ").collect::>(); + let prefix = options.pop().unwrap_or_default(); + for option in options { + if let Some(opt) = Self::from(option) { + prefix_of_options.push(opt) + } else { + return vec![]; + } + } + + Self::possibilities(&prefix) + .map(|possible| { + let mut options = prefix_of_options.clone(); + options.push(possible); + + CommandInterceptResult { + string: format!( + "set {}", + options.iter().map(|opt| opt.to_string()).join(" ") + ), + action: VimSet { options }.boxed_clone(), + positions: vec![], + } + }) + .collect() + } + + fn possibilities(query: &str) -> impl Iterator + '_ { + [ + (None, VimOption::Wrap(true)), + (None, VimOption::Wrap(false)), + (None, VimOption::Number(true)), + (None, VimOption::Number(false)), + (None, VimOption::RelativeNumber(true)), + (None, VimOption::RelativeNumber(false)), + (Some("rnu"), VimOption::RelativeNumber(true)), + (Some("nornu"), VimOption::RelativeNumber(false)), + ] + .into_iter() + .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query)) + .map(|(_, option)| option) + } + + fn from(option: &str) -> Option { + match option { + "wrap" => Some(Self::Wrap(true)), + "nowrap" => Some(Self::Wrap(false)), + + "number" => Some(Self::Number(true)), + "nu" => Some(Self::Number(true)), + "nonumber" => Some(Self::Number(false)), + "nonu" => Some(Self::Number(false)), + + "relativenumber" => Some(Self::RelativeNumber(true)), + "rnu" => Some(Self::RelativeNumber(true)), + "norelativenumber" => Some(Self::RelativeNumber(false)), + "nornu" => Some(Self::RelativeNumber(false)), + + _ => None, + } + } + + fn to_string(&self) -> &'static str { + match self { + VimOption::Wrap(true) => "wrap", + VimOption::Wrap(false) => "nowrap", + VimOption::Number(true) => "number", + VimOption::Number(false) => "nonumber", + VimOption::RelativeNumber(true) => "relativenumber", + VimOption::RelativeNumber(false) => "norelativenumber", + } + } +} + +#[derive(Clone, Deserialize, JsonSchema, PartialEq)] +pub struct VimSet { + options: Vec, +} + #[derive(Debug)] struct WrappedAction(Box); @@ -76,7 +166,8 @@ impl_internal_actions!( WithRange, WithCount, OnMatchingLines, - ShellExec + ShellExec, + VimSet, ] ); @@ -100,6 +191,26 @@ impl Deref for WrappedAction { } pub fn register(editor: &mut Editor, cx: &mut Context) { + // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { + Vim::action(editor, cx, |vim, action: &VimSet, window, cx| { + for option in action.options.iter() { + vim.update_editor(window, cx, |_, editor, _, cx| match option { + VimOption::Wrap(true) => { + editor + .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); + } + VimOption::Wrap(false) => { + editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); + } + VimOption::Number(enabled) => { + editor.set_show_line_numbers(*enabled, cx); + } + VimOption::RelativeNumber(enabled) => { + editor.set_relative_line_number(Some(*enabled), cx); + } + }); + } + }); Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { let Some(workspace) = vim.workspace(window) else { return; @@ -808,7 +919,7 @@ fn wrap_count(action: Box, range: &CommandRange) -> Option Option { +pub fn command_interceptor(mut input: &str, cx: &App) -> Vec { // NOTE: We also need to support passing arguments to commands like :w // (ideally with filename autocompletion). while input.starts_with(':') { @@ -834,6 +945,8 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option Option Option Vec { @@ -982,7 +1095,12 @@ impl OnMatchingLines { let command: String = chars.collect(); - let action = WrappedAction(command_interceptor(&command, cx)?.action); + let action = WrappedAction( + command_interceptor(&command, cx) + .first()? + .action + .boxed_clone(), + ); Some(Self { range, diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index ed5e3c21bf2fa59bcced32222c6795ec89cad6a5..7b920c252f3b76e58c200bc93bbd742dbef2cdcf 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -422,7 +422,7 @@ impl Object { /// If the selection spans multiple lines and is preceded by an opening brace (`{`), /// this function will trim the selection to exclude the final newline /// in order to preserve a properly indented line. -fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection) { +pub fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection) { let (start_point, end_point) = (selection.start.to_point(map), selection.end.to_point(map)); if start_point.row == end_point.row { @@ -446,6 +446,7 @@ fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection { selection.end = offset.to_display_point(map); + selection.reversed = true; break; } ch if !ch.is_whitespace() => break, @@ -1759,6 +1760,17 @@ mod test { Mode::Normal, ); cx.simulate_keystrokes("v i {"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + «ˇif a == \"\" { + return true + } + return false» + }" + }, + Mode::Visual, + ); cx.set_state( indoc! { @@ -1772,6 +1784,17 @@ mod test { Mode::Normal, ); cx.simulate_keystrokes("v i {"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + «ˇreturn true» + } + return false + }" + }, + Mode::Visual, + ); cx.set_state( indoc! { @@ -1785,6 +1808,41 @@ mod test { Mode::Normal, ); cx.simulate_keystrokes("v i {"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + «ˇreturn true» + } + return false + }" + }, + Mode::Visual, + ); + + cx.set_state( + indoc! { + "func empty(a string) bool { + if a == \"\" { + return true + } + return false + ˇ}" + }, + Mode::Normal, + ); + cx.simulate_keystrokes("v i {"); + cx.assert_state( + indoc! { + "func empty(a string) bool { + «ˇif a == \"\" { + return true + } + return false» + }" + }, + Mode::Visual, + ); } #[gpui::test] diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a3a3afc4438a1b28bb2ce6893a678c21f725354c..05cfc00bfd158a8f71a44c680df502edcdaa6cb9 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -23,7 +23,6 @@ use anyhow::Result; use collections::HashMap; use editor::{ movement::{self, FindRange}, - scroll::Autoscroll, Anchor, Bias, Editor, EditorEvent, EditorMode, ToPoint, }; use gpui::{ @@ -649,20 +648,24 @@ impl Vim { vim.push_count_digit(n.0, window, cx); }); Vim::action(editor, cx, |vim, _: &Tab, window, cx| { - let Some(anchor) = vim - .editor() - .and_then(|editor| editor.read(cx).inline_completion_start_anchor()) - else { - return; - }; - - vim.update_editor(window, cx, |_, editor, window, cx| { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges([anchor..anchor]) - }); - }); - vim.switch_mode(Mode::Insert, true, window, cx); + vim.input_ignored(" ".into(), window, cx) }); + Vim::action( + editor, + cx, + |vim, action: &editor::AcceptEditPrediction, window, cx| { + vim.update_editor(window, cx, |_, editor, window, cx| { + editor.accept_edit_prediction(action, window, cx); + }); + // In non-insertion modes, predictions will be hidden and instead a jump will be + // displayed (and performed by `accept_edit_prediction`). This switches to + // insert mode so that the prediction is displayed after the jump. + match vim.mode { + Mode::Replace => {} + _ => vim.switch_mode(Mode::Insert, true, window, cx), + }; + }, + ); Vim::action(editor, cx, |vim, _: &Enter, window, cx| { vim.input_ignored("\n".into(), window, cx) }); diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index a9e4dd9767fc48ec706d132f782f4ea82b12675a..1a4cda5e2241b73747c903aae18e91d4aa0cbaec 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -16,7 +16,7 @@ use workspace::searchable::Direction; use crate::{ motion::{first_non_whitespace, next_line_end, start_of_line, Motion}, - object::Object, + object::{self, Object}, state::{Mode, Operator}, Vim, }; @@ -375,6 +375,9 @@ impl Vim { } else { selection.end = range.end; } + if !around && object.is_multiline() { + object::preserve_indented_newline(map, selection); + } } // In the visual selection result of a paragraph object, the cursor is diff --git a/crates/vim/test_data/test_multiline_surrounding_character_objects.json b/crates/vim/test_data/test_multiline_surrounding_character_objects.json index 973df647a2f4d42226f3ef8a31672f5fc66ec1fa..c61b7b9145b94ef2767f9de2241e1be1f2232408 100644 --- a/crates/vim/test_data/test_multiline_surrounding_character_objects.json +++ b/crates/vim/test_data/test_multiline_surrounding_character_objects.json @@ -1,15 +1,20 @@ -{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n ˇreturn false\n}"}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n ˇreturn false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n« if a == \"\" {\n return true\n }\n return false\nˇ»}","mode":"Visual"}} +{"Get":{"state":"func empty(a string) bool {\n «ˇif a == \"\" {\n return true\n }\n return false»\n}","mode":"Visual"}} {"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n ˇreturn true\n }\n return false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n «ˇreturn true»\n }\n return false\n}","mode":"Visual"}} {"Put":{"state":"func empty(a string) bool {\n if a == \"\" ˇ{\n return true\n }\n return false\n}"}} {"Key":"v"} {"Key":"i"} {"Key":"{"} -{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n« return true\nˇ» }\n return false\n}","mode":"Visual"}} +{"Get":{"state":"func empty(a string) bool {\n if a == \"\" {\n «ˇreturn true»\n }\n return false\n}","mode":"Visual"}} +{"Put":{"state":"func empty(a string) bool {\n if a == \"\" {\n return true\n }\n return false\nˇ}"}} +{"Key":"v"} +{"Key":"i"} +{"Key":"{"} +{"Get":{"state":"func empty(a string) bool {\n «ˇif a == \"\" {\n return true\n }\n return false»\n}","mode":"Visual"}} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 08d55e0540e6df6e931a36a874336ad0e4ffa73b..07f77283db070d1a1bb43f7899888b9de97eaec7 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -39,7 +39,7 @@ use postage::{ watch, }; use rpc::{ - proto::{self, split_worktree_update}, + proto::{self, split_worktree_update, FromProto, ToProto}, AnyProtoClient, }; pub use settings::WorktreeId; @@ -283,13 +283,13 @@ impl RepositoryEntry { current_new_entry = new_statuses.next(); } Ordering::Greater => { - removed_statuses.push(old_entry.repo_path.to_proto()); + removed_statuses.push(old_entry.repo_path.as_ref().to_proto()); current_old_entry = old_statuses.next(); } } } (None, Some(old_entry)) => { - removed_statuses.push(old_entry.repo_path.to_proto()); + removed_statuses.push(old_entry.repo_path.as_ref().to_proto()); current_old_entry = old_statuses.next(); } (Some(new_entry), None) => { @@ -308,7 +308,7 @@ impl RepositoryEntry { current_merge_conflicts: self .current_merge_conflicts .iter() - .map(RepoPath::to_proto) + .map(|path| path.as_ref().to_proto()) .collect(), } } @@ -700,7 +700,7 @@ impl Worktree { let snapshot = Snapshot::new( worktree.id, worktree.root_name, - Arc::from(PathBuf::from(worktree.abs_path)), + Arc::::from_proto(worktree.abs_path), ); let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new()))); @@ -849,7 +849,7 @@ impl Worktree { id: self.id().to_proto(), root_name: self.root_name().to_string(), visible: self.is_visible(), - abs_path: self.abs_path().as_os_str().to_string_lossy().into(), + abs_path: self.abs_path().to_proto(), } } @@ -1007,7 +1007,7 @@ impl Worktree { is_directory: bool, cx: &Context, ) -> Task> { - let path = path.into(); + let path: Arc = path.into(); let worktree_id = self.id(); match self { Worktree::Local(this) => this.create_entry(path, is_directory, cx), @@ -1016,7 +1016,7 @@ impl Worktree { let request = this.client.request(proto::CreateProjectEntry { worktree_id: worktree_id.to_proto(), project_id, - path: path.to_string_lossy().into(), + path: path.as_ref().to_proto(), is_directory, }); cx.spawn(move |this, mut cx| async move { @@ -1101,21 +1101,19 @@ impl Worktree { new_path: impl Into>, cx: &Context, ) -> Task>> { - let new_path = new_path.into(); + let new_path: Arc = new_path.into(); match self { Worktree::Local(this) => { this.copy_entry(entry_id, relative_worktree_source_path, new_path, cx) } Worktree::Remote(this) => { - let relative_worktree_source_path = - relative_worktree_source_path.map(|relative_worktree_source_path| { - relative_worktree_source_path.to_string_lossy().into() - }); + let relative_worktree_source_path = relative_worktree_source_path + .map(|relative_worktree_source_path| relative_worktree_source_path.to_proto()); let response = this.client.request(proto::CopyProjectEntry { project_id: this.project_id, entry_id: entry_id.to_proto(), relative_worktree_source_path, - new_path: new_path.to_string_lossy().into(), + new_path: new_path.to_proto(), }); cx.spawn(move |this, mut cx| async move { let response = response.await?; @@ -1214,7 +1212,11 @@ impl Worktree { let (scan_id, entry) = this.update(&mut cx, |this, cx| { ( this.scan_id(), - this.create_entry(PathBuf::from(request.path), request.is_directory, cx), + this.create_entry( + Arc::::from_proto(request.path), + request.is_directory, + cx, + ), ) })?; Ok(proto::ProjectEntryResponse { @@ -1288,7 +1290,7 @@ impl Worktree { this.scan_id(), this.rename_entry( ProjectEntryId::from_proto(request.entry_id), - PathBuf::from(request.new_path), + Arc::::from_proto(request.new_path), cx, ), ) @@ -1308,14 +1310,15 @@ impl Worktree { mut cx: AsyncApp, ) -> Result { let (scan_id, task) = this.update(&mut cx, |this, cx| { - let relative_worktree_source_path = - request.relative_worktree_source_path.map(PathBuf::from); + let relative_worktree_source_path = request + .relative_worktree_source_path + .map(PathBuf::from_proto); ( this.scan_id(), this.copy_entry( ProjectEntryId::from_proto(request.entry_id), relative_worktree_source_path, - PathBuf::from(request.new_path), + PathBuf::from_proto(request.new_path), cx, ), ) @@ -2368,11 +2371,11 @@ impl RemoteWorktree { new_path: impl Into>, cx: &Context, ) -> Task> { - let new_path = new_path.into(); + let new_path: Arc = new_path.into(); let response = self.client.request(proto::RenameProjectEntry { project_id: self.project_id, entry_id: entry_id.to_proto(), - new_path: new_path.to_string_lossy().into(), + new_path: new_path.as_ref().to_proto(), }); cx.spawn(move |this, mut cx| async move { let response = response.await?; @@ -2454,7 +2457,7 @@ impl Snapshot { proto::UpdateWorktree { project_id, worktree_id, - abs_path: self.abs_path().to_string_lossy().into(), + abs_path: self.abs_path().to_proto(), root_name: self.root_name().to_string(), updated_entries, removed_entries: Vec::new(), @@ -2555,7 +2558,7 @@ impl Snapshot { update.removed_entries.len() ); self.update_abs_path( - SanitizedPath::from(PathBuf::from(update.abs_path)), + SanitizedPath::from(PathBuf::from_proto(update.abs_path)), update.root_name, ); @@ -2617,7 +2620,7 @@ impl Snapshot { let edits = repository .removed_statuses .into_iter() - .map(|path| Edit::Remove(PathKey(Path::new(&path).into()))) + .map(|path| Edit::Remove(PathKey(FromProto::from_proto(path)))) .chain(repository.updated_statuses.into_iter().filter_map( |updated_status| { Some(Edit::Insert(updated_status.try_into().log_err()?)) @@ -2952,7 +2955,7 @@ impl LocalSnapshot { proto::UpdateWorktree { project_id, worktree_id, - abs_path: self.abs_path().to_string_lossy().into(), + abs_path: self.abs_path().to_proto(), root_name: self.root_name().to_string(), updated_entries, removed_entries, @@ -3635,7 +3638,7 @@ impl language::File for File { rpc::proto::File { worktree_id: self.worktree.read(cx).id().to_proto(), entry_id: self.entry_id.map(|id| id.to_proto()), - path: self.path.to_string_lossy().into(), + path: self.path.as_ref().to_proto(), mtime: self.disk_state.mtime().map(|time| time.into()), is_deleted: self.disk_state == DiskState::Deleted, } @@ -3716,7 +3719,7 @@ impl File { Ok(Self { worktree, - path: Path::new(&proto.path).into(), + path: Arc::::from_proto(proto.path), disk_state, entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, @@ -3835,8 +3838,9 @@ impl StatusEntry { index_status }), }; + proto::StatusEntry { - repo_path: self.repo_path.to_proto(), + repo_path: self.repo_path.as_ref().to_proto(), simple_status, status: Some(status_to_proto(self.status)), } @@ -3847,7 +3851,7 @@ impl TryFrom for StatusEntry { type Error = anyhow::Error; fn try_from(value: proto::StatusEntry) -> Result { - let repo_path = RepoPath(Path::new(&value.repo_path).into()); + let repo_path = RepoPath(Arc::::from_proto(value.repo_path)); let status = status_from_proto(value.simple_status, value.status)?; Ok(Self { repo_path, status }) } @@ -6231,7 +6235,7 @@ impl<'a> From<&'a Entry> for proto::Entry { Self { id: entry.id.to_proto(), is_dir: entry.is_dir(), - path: entry.path.to_string_lossy().into(), + path: entry.path.as_ref().to_proto(), inode: entry.inode, mtime: entry.mtime.map(|time| time.into()), is_ignored: entry.is_ignored, @@ -6241,7 +6245,7 @@ impl<'a> From<&'a Entry> for proto::Entry { canonical_path: entry .canonical_path .as_ref() - .map(|path| path.to_string_lossy().to_string()), + .map(|path| path.as_ref().to_proto()), } } } @@ -6257,20 +6261,22 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry { } else { EntryKind::File }; - let path: Arc = PathBuf::from(entry.path).into(); + + let path = Arc::::from_proto(entry.path); let char_bag = char_bag_for_path(*root_char_bag, &path); + let is_always_included = always_included.is_match(path.as_ref()); Ok(Entry { id: ProjectEntryId::from_proto(entry.id), kind, - path: path.clone(), + path, inode: entry.inode, mtime: entry.mtime.map(|time| time.into()), size: entry.size.unwrap_or(0), canonical_path: entry .canonical_path - .map(|path_string| Box::from(Path::new(&path_string))), + .map(|path_string| Box::from(PathBuf::from_proto(path_string))), is_ignored: entry.is_ignored, - is_always_included: always_included.is_match(path.as_ref()), + is_always_included, is_external: entry.is_external, is_private: false, char_bag, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 7ef63b3d88733865edf6e394444e0c077b6586b9..12ae59a6a64d039a7c8ab589cd70c03f837477d0 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -77,6 +77,7 @@ log.workspace = true markdown.workspace = true markdown_preview.workspace = true menu.workspace = true +migrator.workspace = true mimalloc = { version = "0.1", optional = true } nix = { workspace = true, features = ["pthread", "signal"] } node_runtime.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9b66e92b9f5ad2d76637504e3baf1da0966cea69..e0e0cbaecfdaba0027c259e039b32468b440499b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4,6 +4,7 @@ pub mod inline_completion_registry; pub(crate) mod linux_prompts; #[cfg(target_os = "macos")] pub(crate) mod mac_only_instance; +mod migrate; mod open_listener; mod quick_action_bar; #[cfg(target_os = "windows")] @@ -176,7 +177,6 @@ pub fn initialize_workspace( let inline_completion_button = cx.new(|cx| { inline_completion_button::InlineCompletionButton::new( - workspace.weak_handle(), app_state.fs.clone(), app_state.user_store.clone(), popover_menu_handle.clone(), @@ -1214,7 +1214,7 @@ fn show_keymap_migration_notification_if_needed( notification_id: NotificationId, cx: &mut App, ) -> bool { - if !KeymapFile::should_migrate_keymap(keymap_file) { + if !migrate::should_migrate_keymap(keymap_file) { return false; } let message = MarkdownString(format!( @@ -1229,7 +1229,7 @@ fn show_keymap_migration_notification_if_needed( move |_, cx| { let fs = ::global(cx); cx.spawn(move |weak_notification, mut cx| async move { - KeymapFile::migrate_keymap(fs).await.ok(); + migrate::migrate_keymap(fs).await.ok(); weak_notification .update(&mut cx, |_, cx| { cx.emit(DismissEvent); @@ -1248,7 +1248,7 @@ fn show_settings_migration_notification_if_needed( settings: serde_json::Value, cx: &mut App, ) { - if !SettingsStore::should_migrate_settings(&settings) { + if !migrate::should_migrate_settings(&settings) { return; } let message = MarkdownString(format!( @@ -1262,7 +1262,7 @@ fn show_settings_migration_notification_if_needed( "Backup and Migrate Settings".into(), move |_, cx| { let fs = ::global(cx); - cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs)); + migrate::migrate_settings(fs, cx); cx.emit(DismissEvent); }, cx, diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs new file mode 100644 index 0000000000000000000000000000000000000000..e9a333aad585039ff36ac93c1fcc4a50bb6da5c9 --- /dev/null +++ b/crates/zed/src/zed/migrate.rs @@ -0,0 +1,79 @@ +use std::sync::Arc; + +use anyhow::Context; +use fs::Fs; +use settings::{KeymapFile, SettingsStore}; + +pub fn should_migrate_settings(settings: &serde_json::Value) -> bool { + let Ok(old_text) = serde_json::to_string(settings) else { + return false; + }; + migrator::migrate_settings(&old_text).is_some() +} + +pub fn migrate_settings(fs: Arc, cx: &mut gpui::App) { + cx.background_executor() + .spawn(async move { + let old_text = SettingsStore::load_settings(&fs).await?; + let Some(new_text) = migrator::migrate_settings(&old_text) else { + return anyhow::Ok(()); + }; + let settings_path = paths::settings_file().as_path(); + if fs.is_file(settings_path).await { + fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) + .await + .with_context(|| { + "Failed to create settings backup in home directory".to_string() + })?; + let resolved_path = fs.canonicalize(settings_path).await.with_context(|| { + format!("Failed to canonicalize settings path {:?}", settings_path) + })?; + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", resolved_path) + })?; + } else { + fs.atomic_write(settings_path.to_path_buf(), new_text) + .await + .with_context(|| { + format!("Failed to write settings to file {:?}", settings_path) + })?; + } + Ok(()) + }) + .detach_and_log_err(cx); +} + +pub fn should_migrate_keymap(keymap_file: KeymapFile) -> bool { + let Ok(old_text) = serde_json::to_string(&keymap_file) else { + return false; + }; + migrator::migrate_keymap(&old_text).is_some() +} + +pub async fn migrate_keymap(fs: Arc) -> anyhow::Result<()> { + let old_text = KeymapFile::load_keymap_file(&fs).await?; + let Some(new_text) = migrator::migrate_keymap(&old_text) else { + return Ok(()); + }; + let keymap_path = paths::keymap_file().as_path(); + if fs.is_file(keymap_path).await { + fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text) + .await + .with_context(|| "Failed to create settings backup in home directory".to_string())?; + let resolved_path = fs + .canonicalize(keymap_path) + .await + .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?; + fs.atomic_write(resolved_path.clone(), new_text) + .await + .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?; + } else { + fs.atomic_write(keymap_path.to_path_buf(), new_text) + .await + .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; + } + + Ok(()) +} diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index ee0d810e5b10ca7f55413791b0a676b70fb19463..229d4d287bdf4177472588bf933b7997333096e0 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -10,9 +10,9 @@ use settings::update_settings_file; use ui::App; use workspace::Workspace; -use crate::{onboarding_modal::ZedPredictModal, RateCompletionModal, RateCompletions}; +use crate::{onboarding_modal::ZedPredictModal, RateCompletionModal}; -actions!(edit_predictions, [ResetOnboarding]); +actions!(edit_prediction, [ResetOnboarding, RateCompletions]); pub fn init(cx: &mut App) { cx.observe_new(move |workspace: &mut Workspace, _, _cx| { diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index ed79f41fe8f3b1943770107a714dd6cd235a6563..3c5562532368389e2e762d3ea2e597b099599a29 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -9,7 +9,6 @@ use workspace::{ModalView, Workspace}; actions!( zeta, [ - RateCompletions, ThumbsUpActiveCompletion, ThumbsDownActiveCompletion, NextEdit, diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 14cd32b300f76508044cd00bab1f6fe8427ba75c..9365f1945d4520f3e1d1c7c5080d4b44dc4db011 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -1024,7 +1024,7 @@ impl LicenseDetectionWatcher { } /// Answers false until we find out it's open source - pub fn is_open_source(&self) -> bool { + pub fn is_project_open_source(&self) -> bool { *self.is_open_source_rx.borrow() } } @@ -1227,7 +1227,6 @@ impl ProviderDataCollection { let zeta = zeta.read(cx); let choice = zeta.data_collection_choice.clone(); - // Unwrap safety: there should be a watcher for each worktree let license_detection_watcher = zeta .license_detection_watchers .get(&file.worktree_id(cx)) @@ -1249,20 +1248,20 @@ impl ProviderDataCollection { } } - pub fn user_data_collection_choice(&self, cx: &App) -> bool { - self.choice - .as_ref() - .map_or(false, |choice| choice.read(cx).is_enabled()) + pub fn can_collect_data(&self, cx: &App) -> bool { + self.is_data_collection_enabled(cx) && self.is_project_open_source() } - pub fn can_collect_data(&self, cx: &App) -> bool { + pub fn is_data_collection_enabled(&self, cx: &App) -> bool { self.choice .as_ref() .is_some_and(|choice| choice.read(cx).is_enabled()) - && self - .license_detection_watcher - .as_ref() - .is_some_and(|watcher| watcher.is_open_source()) + } + + fn is_project_open_source(&self) -> bool { + self.license_detection_watcher + .as_ref() + .is_some_and(|watcher| watcher.is_project_open_source()) } pub fn toggle(&mut self, cx: &mut App) { @@ -1326,13 +1325,16 @@ impl inline_completion::EditPredictionProvider for ZetaInlineCompletionProvider } fn data_collection_state(&self, cx: &App) -> DataCollectionState { - if self - .provider_data_collection - .user_data_collection_choice(cx) - { - DataCollectionState::Enabled + let is_project_open_source = self.provider_data_collection.is_project_open_source(); + + if self.provider_data_collection.is_data_collection_enabled(cx) { + DataCollectionState::Enabled { + is_project_open_source, + } } else { - DataCollectionState::Disabled + DataCollectionState::Disabled { + is_project_open_source, + } } } diff --git a/docs/src/languages/lua.md b/docs/src/languages/lua.md index dbd2b8c665df206c75618b1a653ba4e757b4879b..864757192e492760acbb4531b6061eb5018fb049 100644 --- a/docs/src/languages/lua.md +++ b/docs/src/languages/lua.md @@ -59,7 +59,13 @@ Alternative you can use [StyLua](https://github.com/JohnnyMorganz/StyLua): "formatter": { "external": { "command": "stylua", - "arguments": ["--syntax=Lua54", "-"] + "arguments": [ + "--syntax=Lua54", + "--respect-ignores", + "--stdin-filepath", + "{buffer_path}", + "-" + ] } } } diff --git a/docs/src/vim.md b/docs/src/vim.md index 0555cd817e84c134c07ed158a8a9630800ed08f9..711d69488fb6806f48077ae1e9d0f02f1912da66 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -380,17 +380,15 @@ But you cannot use the same shortcuts to move between all the editor docks (the Subword motion, which allows you to navigate and select individual words in camelCase or snake_case, is not enabled by default. To enable it, add these bindings to your keymap. ```json -[ - { - "context": "VimControl && !menu && vim_mode != operator", - "bindings": { - "w": "vim::NextSubwordStart", - "b": "vim::PreviousSubwordStart", - "e": "vim::NextSubwordEnd", - "g e": "vim::PreviousSubwordEnd" - } +{ + "context": "VimControl && !menu && vim_mode != operator", + "bindings": { + "w": "vim::NextSubwordStart", + "b": "vim::PreviousSubwordStart", + "e": "vim::NextSubwordEnd", + "g e": "vim::PreviousSubwordEnd" } -] +} ``` Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), but it doesn't have a shortcut to add surrounds in visual mode. By default, `shift-s` substitutes the selection (erases the text and enters insert mode). To use `shift-s` to add surrounds in visual mode, you can add the following object to your keymap. @@ -407,15 +405,13 @@ Vim mode comes with shortcuts to surround the selection in normal mode (`ys`), b The [Sneak motion](https://github.com/justinmk/vim-sneak) feature allows for quick navigation to any two-character sequence in your text. You can enable it by adding the following keybindings to your keymap. By default, the `s` key is mapped to `vim::Substitute`. Adding these bindings will override that behavior, so ensure this change aligns with your workflow preferences. ```json -[ - { - "context": "vim_mode == normal || vim_mode == visual", - "bindings": { - "s": ["vim::PushSneak", {}], - "S": ["vim::PushSneakBackward", {}] - } +{ + "context": "vim_mode == normal || vim_mode == visual", + "bindings": { + "s": ["vim::PushSneak", {}], + "S": ["vim::PushSneakBackward", {}] } -] +} ``` ### Restoring common text editing keybindings diff --git a/extensions/EXTRACTION.md b/extensions/EXTRACTION.md index 09fed7970d87f2eaf2aad178614d6578a9deaeba..35b33514cc2fd92a9a79b3aff78ab24b8da71d85 100644 --- a/extensions/EXTRACTION.md +++ b/extensions/EXTRACTION.md @@ -112,11 +112,19 @@ OLD_VERSION=$(grep '^version = ' extension.toml | cut -d'"' -f2) NEW_VERSION=$(echo "$OLD_VERSION" | awk -F. '{$NF = $NF + 1;} 1' OFS=.) echo $OLD_VERSION $NEW_VERSION perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" extension.toml +perl -i -pe "s#https://github.com/zed-industries/zed#https://github.com/zed-extensions/${LANGNAME}#g" extension.toml # if there's rust code, update this too. -test -f Cargo.toml && perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" cargo.toml +test -f Cargo.toml && perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" Cargo.toml +# remove workspace Cargo.toml lines +test -f Cargo.toml && perl -ni -e 'print unless /^.*(workspace\s*=\s*true|\[lints\])\s*$/' Cargo.toml test -f Cargo.toml && cargo check +# add a .gitignore +echo "target/ +grammars/ +*.wasm" > .gitignore + # commit and push git add -u git checkout -b "bump_${NEW_VERSION}"