Detailed changes
@@ -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",
@@ -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" }
@@ -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",
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M15 9.33333L14.5 9.66667L12.5 11L10.5 9.66667L10 9.33333" stroke="black" stroke-width="1.5"/>
+<path d="M12.5 11V4.5" stroke="black" stroke-width="1.5"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+</svg>
@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10 6.66667L10.5 6.33333L12.5 5L14.5 6.33333L15 6.66667" stroke="black" stroke-width="1.5"/>
+<path d="M12.5 11V5" stroke="black" stroke-width="1.5"/>
+<path d="M7.5 8.9V11C5.43097 11 4.56903 11 2.5 11V10.4L7.5 5.6V5H2.5V7.1" stroke="black" stroke-width="1.5"/>
+</svg>
@@ -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"
}
},
{
@@ -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"
}
},
@@ -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"
+ }
}
]
@@ -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
@@ -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<Entity<BufferDiff>>,
+}
+
+#[derive(Clone)]
+pub struct BufferDiffSnapshot {
+ inner: BufferDiffInner,
+ secondary_diff: Option<Box<BufferDiffSnapshot>>,
+}
+
+#[derive(Clone)]
+struct BufferDiffInner {
+ hunks: SumTree<InternalDiffHunk>,
+ base_text: Option<language::BufferSnapshot>,
+}
+
+#[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<Anchor>,
/// The range in the buffer's diff base text to which this hunk corresponds.
pub diff_base_byte_range: Range<usize>,
+ 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<InternalDiffHunk>,
- pub base_text: Option<language::BufferSnapshot>,
+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<Anchor>,
+ buffer: &'a text::BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ 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<Arc<String>>,
- language: Option<Arc<Language>>,
- language_registry: Option<Arc<LanguageRegistry>>,
- cx: &mut App,
- ) -> impl Future<Output = Self> {
- 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<Anchor>,
+ buffer: &'a text::BufferSnapshot,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ self.inner.hunks_intersecting_range_rev(range, buffer)
}
- pub fn build_with_base_buffer(
- buffer: text::BufferSnapshot,
- diff_base: Option<Arc<String>>,
- diff_base_buffer: Option<language::BufferSnapshot>,
- cx: &App,
- ) -> impl Future<Output = Self> {
- 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<Arc<String>>,
- buffer: text::BufferSnapshot,
- ) -> SumTree<InternalDiffHunk> {
- 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<u32>,
- buffer: &'a text::BufferSnapshot,
- ) -> impl 'a + Iterator<Item = DiffHunk> {
- 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<Anchor>,
buffer: &'a text::BufferSnapshot,
+ secondary: Option<&'a Self>,
) -> impl 'a + Iterator<Item = DiffHunk> {
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::<DiffHunkSummary>(buffer);
+ cursor.next(buffer);
+ cursor
+ });
+
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(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<Anchor>,
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<Range<Anchor>> {
+ fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option<Range<Anchor>> {
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<Arc<String>>,
+ buffer: text::BufferSnapshot,
+) -> SumTree<InternalDiffHunk> {
+ let mut tree = SumTree::new(&buffer);
- #[cfg(test)]
- fn hunks<'a>(&'a self, text: &'a text::BufferSnapshot) -> impl 'a + Iterator<Item = DiffHunk> {
- 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<GitPatch<'a>> {
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<u32> = None;
- let mut buffer_row_range: Option<Range<u32>> = None;
- let mut diff_base_byte_range: Option<Range<usize>> = 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<u32> = None;
+ let mut buffer_row_range: Option<Range<u32>> = None;
+ let mut diff_base_byte_range: Option<Range<usize>> = 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<Entity<BufferDiff>>,
+ //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<text::Anchor> },
+ DiffChanged {
+ changed_range: Option<Range<text::Anchor>>,
+ },
LanguageChanged,
}
impl EventEmitter<BufferDiffEvent> 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<Arc<String>>,
+ language: Option<Arc<Language>>,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ cx: &mut App,
+ ) -> impl Future<Output = BufferDiffInner> {
+ 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<Arc<String>>,
+ diff_base_buffer: Option<language::BufferSnapshot>,
+ cx: &App,
+ ) -> impl Future<Output = BufferDiffInner> {
+ 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<BufferDiff>) {
+ self.secondary_diff = Some(diff);
+ }
+
+ pub fn secondary_diff(&self) -> Option<Entity<BufferDiff>> {
+ Some(self.secondary_diff.as_ref()?.clone())
+ }
+
+ pub fn range_to_hunk_range(
+ &self,
+ range: Range<Anchor>,
+ buffer: &text::BufferSnapshot,
+ cx: &App,
+ ) -> Option<Range<Anchor>> {
+ 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<BufferDiff>,
+ buffer: text::BufferSnapshot,
+ base_text: Option<Arc<String>>,
+ base_text_changed: bool,
+ language_changed: bool,
+ language: Option<Arc<Language>>,
+ language_registry: Option<Arc<LanguageRegistry>>,
+ cx: &mut AsyncApp,
+ ) -> anyhow::Result<Option<Range<Anchor>>> {
+ 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<Self>,
cx: &mut Context<Self>,
- ) {
- 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<Range<Anchor>> {
+ let other = other.read(cx).inner.clone();
+ self.set_state(other, buffer)
+ }
+
+ fn set_state(
+ &mut self,
+ inner: BufferDiffInner,
+ buffer: &text::BufferSnapshot,
+ ) -> Option<Range<Anchor>> {
+ 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<text::Anchor>,
buffer_snapshot: &'a text::BufferSnapshot,
+ cx: &'a App,
) -> impl 'a + Iterator<Item = DiffHunk> {
- 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<text::Anchor>,
buffer_snapshot: &'a text::BufferSnapshot,
) -> impl 'a + Iterator<Item = DiffHunk> {
- self.snapshot
+ self.inner
.hunks_intersecting_range_rev(range, buffer_snapshot)
}
+ pub fn hunks_in_row_range<'a>(
+ &'a self,
+ range: Range<u32>,
+ buffer: &'a text::BufferSnapshot,
+ cx: &'a App,
+ ) -> impl 'a + Iterator<Item = DiffHunk> {
+ 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<String> {
- 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<language::Buffer>, 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<Self>) {
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<Iter>(
diff_hunks: Iter,
buffer: &text::BufferSnapshot,
diff_base: &str,
- expected_hunks: &[(Range<u32>, &str, &str)],
+ expected_hunks: &[(Range<u32>, &str, &str, DiffHunkStatus)],
) where
Iter: Iterator<Item = DiffHunk>,
{
@@ -641,19 +854,20 @@ pub fn assert_hunks<Iter>(
.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::<String>(),
+ hunk.status(),
)
})
.collect::<Vec<_>>();
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]
@@ -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
@@ -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())],
);
});
}
@@ -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 {
@@ -108,7 +108,7 @@ pub struct CommandInterceptResult {
/// An interceptor for the command palette.
#[derive(Default)]
pub struct CommandPaletteInterceptor(
- Option<Box<dyn Fn(&str, &App) -> Option<CommandInterceptResult>>>,
+ Option<Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>>,
);
#[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<CommandInterceptResult> {
- let handler = self.0.as_ref()?;
-
- (handler)(query, cx)
+ pub fn intercept(&self, query: &str, cx: &App) -> Vec<CommandInterceptResult> {
+ 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<dyn Fn(&str, &App) -> Option<CommandInterceptResult>>) {
+ pub fn set(&mut self, handler: Box<dyn Fn(&str, &App) -> Vec<CommandInterceptResult>>) {
self.0 = Some(handler);
}
}
@@ -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
@@ -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<text::Anchor>,
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<Instant>,
+ scroll_position_at_start: Option<gpui::Point<f32>>,
+ target_point: DisplayPoint,
+ },
+ Arrived {
+ scroll_position_at_start: Option<gpui::Point<f32>>,
+ scroll_position_at_arrival: Option<gpui::Point<f32>>,
+ target_point: Option<DisplayPoint>,
+ },
+ /// Modifier released, animating from active
+ MovingFrom {
+ animation: Range<Instant>,
+ 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<DisplayPoint>) {
+ 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<Pixels>,
+ window: &mut Window,
+ cx: &mut Context<Editor>,
+ ) -> 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<DisplayRow>,
+ line_layouts: &[LineWithInvisibles],
+ scroll_pixel_position: gpui::Point<Pixels>,
+ line_height: Pixels,
+ target: Anchor,
+ cursor: Option<DisplayPoint>,
+ ) -> Option<EditPredictionMoveState> {
+ 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<Pixels>,
+}
+
+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<bool>,
menu_inline_completions_policy: MenuInlineCompletionsPolicy,
- previewing_inline_completion: bool,
+ edit_prediction_preview: EditPredictionPreview,
inlay_hint_cache: InlayHintCache,
next_inlay_id: usize,
_subscriptions: Vec<Subscription>,
@@ -722,6 +1010,7 @@ pub struct Editor {
show_git_blame_gutter: bool,
show_git_blame_inline: bool,
show_git_blame_inline_delay_task: Option<Task<()>>,
+ distinguish_unstaged_diff_hunks: bool,
git_blame_inline_enabled: bool,
serialize_dirty_buffers: bool,
show_selection_menu: Option<bool>,
@@ -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<Anchor> {
- 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<Buffer>,
@@ -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<Self>,
) {
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<Self>,
+ ) {
+ 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<Self>,
) -> 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<Editor>,
) -> Option<AnyElement> {
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<Editor>,
) -> Option<Div> {
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<DisplayRow, Hsla> {
+ ) -> BTreeMap<DisplayRow, Background> {
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::<DisplayRow, Hsla>::new(),
+ BTreeMap::<DisplayRow, Background>::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
@@ -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;
@@ -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<usize>,
display_row_range: Range<DisplayRow>,
@@ -1115,18 +1114,44 @@ impl EditorElement {
em_width: Pixels,
em_advance: Pixels,
autoscroll_containing_element: bool,
+ newest_selection_head: Option<DisplayPoint>,
window: &mut Window,
cx: &mut App,
) -> Vec<CursorLayout> {
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<DisplayRow>,
+ line_layouts: &[LineWithInvisibles],
+ content_origin: gpui::Point<Pixels>,
+ scroll_pixel_position: gpui::Point<Pixels>,
+ line_height: Pixels,
+ em_advance: Pixels,
+ preview: &mut EditPredictionPreview,
+ target: Anchor,
+ cursor: Option<DisplayPoint>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<CursorLayout> {
+ 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<Pixels>,
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<DisplayRow>)> = None;
- for (&new_row, &new_color) in &layout.highlighted_rows {
+ let mut current_paint: Option<(gpui::Background, Range<DisplayRow>)> = 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<Vec<IndentGuideLayout>>,
visible_display_row_range: Range<DisplayRow>,
active_rows: BTreeMap<DisplayRow, bool>,
- highlighted_rows: BTreeMap<DisplayRow, Hsla>,
+ highlighted_rows: BTreeMap<DisplayRow, gpui::Background>,
line_elements: SmallVec<[AnyElement; 1]>,
line_numbers: Arc<HashMap<MultiBufferRow, LineNumberLayout>>,
display_hunks: Vec<(DisplayDiffHunk, Option<Hitbox>)>,
@@ -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(),
@@ -113,6 +113,7 @@ impl Editor {
target_bottom = target_top + 1.;
} else {
let selections = self.selections.all::<Point>(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 { .. }) {
@@ -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 {
" "
@@ -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,
@@ -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 {
@@ -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
@@ -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::<Vec<_>>()
};
@@ -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"
@@ -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 {
@@ -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(),
@@ -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 {
@@ -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);
});
}
@@ -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(
@@ -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);
});
}
@@ -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()
}
@@ -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<LinearColorStop, 2>,
pad: u32,
}
@@ -310,17 +310,18 @@ fn prepare_gradient_color(tag: u32, color_space: u32,
}
fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
- sold_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
+ solid_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
var background_color = vec4<f32>(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<f32>(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<f32>, 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<f32>(
- 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);
}
}
@@ -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;
@@ -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,
+ }
}
}
@@ -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<Arc<dyn File>>,
edit_prediction_provider: Option<Arc<dyn inline_completion::InlineCompletionProviderHandle>>,
fs: Arc<dyn Fs>,
- workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
}
@@ -354,7 +352,6 @@ impl Render for InlineCompletionButton {
impl InlineCompletionButton {
pub fn new(
- workspace: WeakEntity<Workspace>,
fs: Arc<dyn Fs>,
user_store: Entity<UserStore>,
popover_menu_handle: PopoverMenuHandle<ContextMenu>,
@@ -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<Self>,
) -> Entity<ContextMenu> {
- let workspace = self.workspace.clone();
ContextMenu::build(window, cx, |menu, _window, cx| {
self.build_language_settings_menu(menu, cx).when(
cx.has_flag::<PredictEditsRateCompletionsFeatureFlag>(),
- |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()),
)
})
}
@@ -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"] }
@@ -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);
@@ -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<usize>,
+ /// 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>,
excerpt_ids: SumTree<ExcerptIdMapping>,
diffs: TreeMap<BufferId, BufferDiffSnapshot>,
- pub diff_transforms: SumTree<DiffTransform>,
+ diff_transforms: SumTree<DiffTransform>,
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<DiffTransformHunkInfo>,
},
DeletedHunk {
summary: TextSummary,
buffer_id: BufferId,
- hunk_anchor: (ExcerptId, text::Anchor),
+ hunk_info: DiffTransformHunkInfo,
base_text_byte_range: Range<usize>,
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<BufferId>,
pub buffer_row: Option<u32>,
pub multibuffer_row: Option<MultiBufferRow>,
- pub diff_status: Option<diff::DiffHunkStatus>,
+ pub diff_status: Option<buffer_diff::DiffHunkStatus>,
}
/// 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<DiffHunkStatus>,
excerpt: &'a Excerpt,
buffer_range: Range<D>,
range: Range<D>,
@@ -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<Excerpt, TypedOffset<Excerpt>>,
old_diff_transforms: &mut Cursor<DiffTransform, (TypedOffset<Excerpt>, usize)>,
new_diff_transforms: &mut SumTree<DiffTransform>,
- end_of_current_insert: &mut Option<(TypedOffset<Excerpt>, ExcerptId, text::Anchor)>,
- old_expanded_hunks: &mut HashSet<(ExcerptId, text::Anchor)>,
+ end_of_current_insert: &mut Option<(TypedOffset<Excerpt>, DiffTransformHunkInfo)>,
+ old_expanded_hunks: &mut HashSet<DiffTransformHunkInfo>,
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<DiffTransform>,
) {
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<DiffTransform>,
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<DiffTransform>,
- new_inserted_hunk_anchor: Option<(ExcerptId, text::Anchor)>,
+ new_inserted_hunk_info: Option<DiffTransformHunkInfo>,
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::<D>(base_text_byte_range.start);
let buffer_range_len = rope_cursor.summary::<D>(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::<D>(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<DiffTransformHunkInfo> {
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)
};
@@ -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<_>>(),
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() {
" "
@@ -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
@@ -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() {
@@ -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
@@ -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::<Path>::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::<lsp::SymbolKind, i32>(symbol.kind) },
start: Some(proto::PointUtf16 {
@@ -7789,7 +7790,7 @@ impl LspStore {
let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
let path = ProjectPath {
worktree_id,
- path: PathBuf::from(serialized_symbol.path).into(),
+ path: Arc::<Path>::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,
@@ -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::<Path>::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::<Path>::from_proto(envelope.payload.path),
},
cx,
)
@@ -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::<Path>::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()),
})
@@ -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(),
)],
);
});
@@ -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::<Vec<_>>()
} 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()?,
})
})
@@ -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::<Path>::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::<Path>::from_proto(project_path.path),
};
let new_branch = update_branch.payload.branch_name;
@@ -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 {
}
@@ -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<Path> {
+ 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::<Vec<_>>()
+ .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::<Vec<_>>()
+ .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()));
+ }
}
@@ -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<proto::AddWorktreeResponse> {
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::<Path>::from_proto(message.payload.path),
},
cx,
)
@@ -559,11 +558,11 @@ impl HeadlessProject {
envelope: TypedEnvelope<proto::ListRemoteDirectory>,
cx: AsyncApp,
) -> Result<proto::ListRemoteDirectoryResponse> {
- 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<proto::GetPathMetadataResponse> {
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(),
})
}
@@ -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()
@@ -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"] }
@@ -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<dyn Fs>) -> Result<String> {
+ pub async fn load_keymap_file(fs: &Arc<dyn Fs>) -> Result<String> {
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<dyn Fs>) -> 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..
@@ -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<dyn Fs>) -> Result<String> {
+ pub async fn load_settings(fs: &Arc<dyn Fs>) -> Result<String> {
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<dyn Fs>) {
- 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)]
@@ -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"),
@@ -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|
@@ -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,
}
@@ -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<CommandInterceptResult> {
+ let mut prefix_of_options = Vec::new();
+ let mut options = query.split(" ").collect::<Vec<_>>();
+ 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<Item = Self> + '_ {
+ [
+ (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<Self> {
+ 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<VimOption>,
+}
+
#[derive(Debug)]
struct WrappedAction(Box<dyn Action>);
@@ -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>) {
+ // 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<dyn Action>, range: &CommandRange) -> Option<Box<dyn A
})
}
-pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandInterceptResult> {
+pub fn command_interceptor(mut input: &str, cx: &App) -> Vec<CommandInterceptResult> {
// 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<CommandIntercept
}
.boxed_clone(),
)
+ } else if query.starts_with("se ") || query.starts_with("set ") {
+ return VimOption::possible_commands(query.split_once(" ").unwrap().1);
} else if query.starts_with('s') {
let mut substitute = "substitute".chars().peekable();
let mut query = query.chars().peekable();
@@ -886,11 +999,11 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandIntercept
if let Some(action) = action {
let string = input.to_string();
let positions = generate_positions(&string, &(range_prefix + query));
- return Some(CommandInterceptResult {
+ return vec![CommandInterceptResult {
action,
string,
positions,
- });
+ }];
}
for command in commands(cx).iter() {
@@ -901,14 +1014,14 @@ pub fn command_interceptor(mut input: &str, cx: &App) -> Option<CommandIntercept
}
let positions = generate_positions(&string, &(range_prefix + query));
- return Some(CommandInterceptResult {
+ return vec![CommandInterceptResult {
action,
string,
positions,
- });
+ }];
}
}
- None
+ return Vec::default();
}
fn generate_positions(string: &str, query: &str) -> Vec<usize> {
@@ -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,
@@ -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<DisplayPoint>) {
+pub fn preserve_indented_newline(map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>) {
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<Di
match ch {
'\n' => {
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]
@@ -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)
});
@@ -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
@@ -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"}}
@@ -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::<Path>::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<Worktree>,
) -> Task<Result<CreatedEntry>> {
- let path = path.into();
+ let path: Arc<Path> = 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<Arc<Path>>,
cx: &Context<Self>,
) -> Task<Result<Option<Entry>>> {
- let new_path = new_path.into();
+ let new_path: Arc<Path> = 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::<Path>::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::<Path>::from_proto(request.new_path),
cx,
),
)
@@ -1308,14 +1310,15 @@ impl Worktree {
mut cx: AsyncApp,
) -> Result<proto::ProjectEntryResponse> {
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<Arc<Path>>,
cx: &Context<Worktree>,
) -> Task<Result<CreatedEntry>> {
- let new_path = new_path.into();
+ let new_path: Arc<Path> = 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::<Path>::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<proto::StatusEntry> for StatusEntry {
type Error = anyhow::Error;
fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
- let repo_path = RepoPath(Path::new(&value.repo_path).into());
+ let repo_path = RepoPath(Arc::<Path>::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<Path> = PathBuf::from(entry.path).into();
+
+ let path = Arc::<Path>::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,
@@ -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
@@ -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 = <dyn 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 = <dyn Fs>::global(cx);
- cx.update_global(|store: &mut SettingsStore, _| store.migrate_settings(fs));
+ migrate::migrate_settings(fs, cx);
cx.emit(DismissEvent);
},
cx,
@@ -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<dyn Fs>, 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<dyn Fs>) -> 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(())
+}
@@ -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| {
@@ -9,7 +9,6 @@ use workspace::{ModalView, Workspace};
actions!(
zeta,
[
- RateCompletions,
ThumbsUpActiveCompletion,
ThumbsDownActiveCompletion,
NextEdit,
@@ -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,
+ }
}
}
@@ -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}",
+ "-"
+ ]
}
}
}
@@ -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
@@ -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}"