mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
cli: Teach --diff to recurse into directories and add a MultiDiffView (#45131)
This branch: 1. teaches `--diff `command line option to to recurse into folders if provided. 2. Adds a `MultiDiffView` that shows _all_ changed files in a single, scrollable view. This is necessary to provide a smooth user experience for `jj` and Zed users who wish to use Zed as their jj difftool. I'm not fully sure how this change interacts with https://github.com/zed-industries/zed/pull/44936, or what plans y'all have in mind. Here's a screenshot of the resulting behavior: <img width="1090" height="950" alt="Screenshot 2025-12-17 at 9 10 52 AM" src="https://github.com/user-attachments/assets/8efd09b4-974f-4059-9f94-539c484c6d4a" /> I setup zed to handle jj diffs by adding the following to my jj config: ```toml [aliases] zdiff = ["diff", "--tool", "zed"] [merge-tools.zed] program = "/Users/dbarsky/Developer/zed/target/debug/cli" # omit diff-invocation-mode to keep the default (JJ passes two dirs) diff-args = [ "--zed", "/Users/dbarsky/Developer/zed/target/debug/zed", "--wait", "--new", "--diff", "$left", "$right", ] ``` Release Notes: - `--diff`, if provided with folders instead of files, will recurse into those directories. - Added a `MultiDiffView`, which will show all changed files within a single, scrollable view. **AI Disclosure**: Pretty much all of this code was written using Codex with GPT-5.1-Codex. I edited by hand and tested this. --------- Co-authored-by: Lukas Wirth <lukas@zed.dev>
This commit is contained in:
parent
7053561b63
commit
185a80a21b
9 changed files with 550 additions and 7 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -3082,6 +3082,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tempfile",
|
||||
"util",
|
||||
"walkdir",
|
||||
"windows 0.61.3",
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ serde.workspace = true
|
|||
util.workspace = true
|
||||
tempfile.workspace = true
|
||||
rayon.workspace = true
|
||||
walkdir = "2.5"
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ pub enum CliRequest {
|
|||
paths: Vec<String>,
|
||||
urls: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
diff_all: bool,
|
||||
wsl: Option<String>,
|
||||
wait: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use clap::Parser;
|
|||
use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer};
|
||||
use parking_lot::Mutex;
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
env,
|
||||
ffi::OsStr,
|
||||
fs, io,
|
||||
|
|
@ -20,8 +21,9 @@ use std::{
|
|||
sync::Arc,
|
||||
thread::{self, JoinHandle},
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
use tempfile::{NamedTempFile, TempDir};
|
||||
use util::paths::PathWithPosition;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use std::io::IsTerminal;
|
||||
|
|
@ -117,6 +119,7 @@ struct Args {
|
|||
#[arg(long)]
|
||||
system_specs: bool,
|
||||
/// Pairs of file paths to diff. Can be specified multiple times.
|
||||
/// When directories are provided, recurses into them and shows all changed files in a single multi-diff view.
|
||||
#[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
|
||||
diff: Vec<String>,
|
||||
/// Uninstall Zed from user system
|
||||
|
|
@ -180,6 +183,104 @@ fn parse_path_with_position(argument_str: &str) -> anyhow::Result<String> {
|
|||
.map(|path_with_pos| path_with_pos.to_string(|path| path.to_string_lossy().into_owned()))
|
||||
}
|
||||
|
||||
fn expand_directory_diff_pairs(
|
||||
diff_pairs: Vec<[String; 2]>,
|
||||
) -> anyhow::Result<(Vec<[String; 2]>, Vec<TempDir>)> {
|
||||
let mut expanded = Vec::new();
|
||||
let mut temp_dirs = Vec::new();
|
||||
|
||||
for pair in diff_pairs {
|
||||
let left = PathBuf::from(&pair[0]);
|
||||
let right = PathBuf::from(&pair[1]);
|
||||
|
||||
if left.is_dir() && right.is_dir() {
|
||||
let (mut pairs, temp_dir) = expand_directory_pair(&left, &right)?;
|
||||
expanded.append(&mut pairs);
|
||||
if let Some(temp_dir) = temp_dir {
|
||||
temp_dirs.push(temp_dir);
|
||||
}
|
||||
} else {
|
||||
expanded.push(pair);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((expanded, temp_dirs))
|
||||
}
|
||||
|
||||
fn expand_directory_pair(
|
||||
left: &Path,
|
||||
right: &Path,
|
||||
) -> anyhow::Result<(Vec<[String; 2]>, Option<TempDir>)> {
|
||||
let left_files = collect_files(left)?;
|
||||
let right_files = collect_files(right)?;
|
||||
|
||||
let mut rel_paths = BTreeSet::new();
|
||||
rel_paths.extend(left_files.keys().cloned());
|
||||
rel_paths.extend(right_files.keys().cloned());
|
||||
|
||||
let mut temp_dir = TempDir::new()?;
|
||||
let mut temp_dir_used = false;
|
||||
let mut pairs = Vec::new();
|
||||
|
||||
for rel in rel_paths {
|
||||
match (left_files.get(&rel), right_files.get(&rel)) {
|
||||
(Some(left_path), Some(right_path)) => {
|
||||
pairs.push([
|
||||
left_path.to_string_lossy().into_owned(),
|
||||
right_path.to_string_lossy().into_owned(),
|
||||
]);
|
||||
}
|
||||
(Some(left_path), None) => {
|
||||
let stub = create_empty_stub(&mut temp_dir, &rel)?;
|
||||
temp_dir_used = true;
|
||||
pairs.push([
|
||||
left_path.to_string_lossy().into_owned(),
|
||||
stub.to_string_lossy().into_owned(),
|
||||
]);
|
||||
}
|
||||
(None, Some(right_path)) => {
|
||||
let stub = create_empty_stub(&mut temp_dir, &rel)?;
|
||||
temp_dir_used = true;
|
||||
pairs.push([
|
||||
stub.to_string_lossy().into_owned(),
|
||||
right_path.to_string_lossy().into_owned(),
|
||||
]);
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
}
|
||||
|
||||
let temp_dir = if temp_dir_used { Some(temp_dir) } else { None };
|
||||
Ok((pairs, temp_dir))
|
||||
}
|
||||
|
||||
fn collect_files(root: &Path) -> anyhow::Result<BTreeMap<PathBuf, PathBuf>> {
|
||||
let mut files = BTreeMap::new();
|
||||
|
||||
for entry in WalkDir::new(root) {
|
||||
let entry = entry?;
|
||||
if entry.file_type().is_file() {
|
||||
let rel = entry
|
||||
.path()
|
||||
.strip_prefix(root)
|
||||
.context("stripping directory prefix")?
|
||||
.to_path_buf();
|
||||
files.insert(rel, entry.into_path());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn create_empty_stub(temp_dir: &mut TempDir, rel: &Path) -> anyhow::Result<PathBuf> {
|
||||
let stub_path = temp_dir.path().join(rel);
|
||||
if let Some(parent) = stub_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::File::create(&stub_path)?;
|
||||
Ok(stub_path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
@ -476,6 +577,12 @@ fn main() -> Result<()> {
|
|||
let mut stdin_tmp_file: Option<fs::File> = None;
|
||||
let mut anonymous_fd_tmp_files = vec![];
|
||||
|
||||
// Check if any diff paths are directories to determine diff_all mode
|
||||
let diff_all_mode = args
|
||||
.diff
|
||||
.chunks(2)
|
||||
.any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir());
|
||||
|
||||
for path in args.diff.chunks(2) {
|
||||
diff_paths.push([
|
||||
parse_path_with_position(&path[0])?,
|
||||
|
|
@ -483,6 +590,16 @@ fn main() -> Result<()> {
|
|||
]);
|
||||
}
|
||||
|
||||
let (expanded_diff_paths, temp_dirs) = expand_directory_diff_pairs(diff_paths)?;
|
||||
diff_paths = expanded_diff_paths;
|
||||
// Prevent automatic cleanup of temp directories containing empty stub files
|
||||
// for directory diffs. The CLI process may exit before Zed has read these
|
||||
// files (e.g., when RPC-ing into an already-running instance). The files
|
||||
// live in the OS temp directory and will be cleaned up on reboot.
|
||||
for temp_dir in temp_dirs {
|
||||
let _ = temp_dir.keep();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let wsl = args.wsl.as_ref();
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
|
@ -508,6 +625,14 @@ fn main() -> Result<()> {
|
|||
}
|
||||
}
|
||||
|
||||
// When only diff paths are provided (no regular paths), add the current
|
||||
// working directory so the workspace opens with the right context.
|
||||
if paths.is_empty() && urls.is_empty() && !diff_paths.is_empty() {
|
||||
if let Ok(cwd) = env::current_dir() {
|
||||
paths.push(cwd.to_string_lossy().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::ensure!(
|
||||
args.dev_server_token.is_none(),
|
||||
"Dev servers were removed in v0.157.x please upgrade to SSH remoting: https://zed.dev/docs/remote-development"
|
||||
|
|
@ -538,6 +663,7 @@ fn main() -> Result<()> {
|
|||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
diff_all: diff_all_mode,
|
||||
wsl,
|
||||
wait: args.wait,
|
||||
open_new_workspace,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ pub mod file_history_view;
|
|||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod git_picker;
|
||||
pub mod multi_diff_view;
|
||||
pub mod onboarding;
|
||||
pub mod picker_prompt;
|
||||
pub mod project_diff;
|
||||
|
|
|
|||
377
crates/git_ui/src/multi_diff_view.rs
Normal file
377
crates/git_ui/src/multi_diff_view.rs
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use editor::{Editor, EditorEvent, MultiBuffer, multibuffer_context_lines};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, Render, SharedString, Task, Window,
|
||||
};
|
||||
use language::{Buffer, Capability, OffsetRangeExt};
|
||||
use multi_buffer::PathKey;
|
||||
use project::Project;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use theme;
|
||||
use ui::{Color, Icon, IconName, Label, LabelCommon as _};
|
||||
use util::paths::PathStyle;
|
||||
use util::rel_path::RelPath;
|
||||
use workspace::{
|
||||
Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams},
|
||||
searchable::SearchableItemHandle,
|
||||
};
|
||||
|
||||
pub struct MultiDiffView {
|
||||
editor: Entity<Editor>,
|
||||
file_count: usize,
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
index: usize,
|
||||
new_path: PathBuf,
|
||||
new_buffer: Entity<Buffer>,
|
||||
diff: Entity<BufferDiff>,
|
||||
}
|
||||
|
||||
async fn load_entries(
|
||||
diff_pairs: Vec<[String; 2]>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<(Vec<Entry>, Option<PathBuf>)> {
|
||||
let mut entries = Vec::with_capacity(diff_pairs.len());
|
||||
let mut all_paths = Vec::with_capacity(diff_pairs.len());
|
||||
|
||||
for (ix, pair) in diff_pairs.into_iter().enumerate() {
|
||||
let old_path = PathBuf::from(&pair[0]);
|
||||
let new_path = PathBuf::from(&pair[1]);
|
||||
|
||||
let old_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer(&old_path, cx))
|
||||
.await?;
|
||||
let new_buffer = project
|
||||
.update(cx, |project, cx| project.open_local_buffer(&new_path, cx))
|
||||
.await?;
|
||||
|
||||
let diff = build_buffer_diff(&old_buffer, &new_buffer, cx).await?;
|
||||
|
||||
all_paths.push(new_path.clone());
|
||||
entries.push(Entry {
|
||||
index: ix,
|
||||
new_path,
|
||||
new_buffer: new_buffer.clone(),
|
||||
diff,
|
||||
});
|
||||
}
|
||||
|
||||
let common_root = common_prefix(&all_paths);
|
||||
Ok((entries, common_root))
|
||||
}
|
||||
|
||||
fn register_entry(
|
||||
multibuffer: &Entity<MultiBuffer>,
|
||||
entry: Entry,
|
||||
common_root: &Option<PathBuf>,
|
||||
context_lines: u32,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
let snapshot = entry.new_buffer.read(cx).snapshot();
|
||||
let diff_snapshot = entry.diff.read(cx).snapshot(cx);
|
||||
|
||||
let ranges: Vec<std::ops::Range<language::Point>> = diff_snapshot
|
||||
.hunks(&snapshot)
|
||||
.map(|hunk| hunk.buffer_range.to_point(&snapshot))
|
||||
.collect();
|
||||
|
||||
let display_rel = common_root
|
||||
.as_ref()
|
||||
.and_then(|root| entry.new_path.strip_prefix(root).ok())
|
||||
.map(|rel| {
|
||||
RelPath::new(rel, PathStyle::local())
|
||||
.map(|r| r.into_owned().into())
|
||||
.unwrap_or_else(|_| {
|
||||
RelPath::new(Path::new("untitled"), PathStyle::Posix)
|
||||
.unwrap()
|
||||
.into_owned()
|
||||
.into()
|
||||
})
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
entry
|
||||
.new_path
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.and_then(|s| RelPath::new(Path::new(s), PathStyle::Posix).ok())
|
||||
.map(|r| r.into_owned().into())
|
||||
.unwrap_or_else(|| {
|
||||
RelPath::new(Path::new("untitled"), PathStyle::Posix)
|
||||
.unwrap()
|
||||
.into_owned()
|
||||
.into()
|
||||
})
|
||||
});
|
||||
|
||||
let path_key = PathKey::with_sort_prefix(entry.index as u64, display_rel);
|
||||
|
||||
multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpts_for_path(
|
||||
path_key,
|
||||
entry.new_buffer.clone(),
|
||||
ranges,
|
||||
context_lines,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(entry.diff.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn common_prefix(paths: &[PathBuf]) -> Option<PathBuf> {
|
||||
let mut iter = paths.iter();
|
||||
let mut prefix = iter.next()?.clone();
|
||||
|
||||
for path in iter {
|
||||
while !path.starts_with(&prefix) {
|
||||
if !prefix.pop() {
|
||||
return Some(PathBuf::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(prefix)
|
||||
}
|
||||
|
||||
async fn build_buffer_diff(
|
||||
old_buffer: &Entity<Buffer>,
|
||||
new_buffer: &Entity<Buffer>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Entity<BufferDiff>> {
|
||||
let old_buffer_snapshot = old_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
let new_buffer_snapshot = new_buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
||||
|
||||
let diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot.text, cx));
|
||||
|
||||
let update = diff
|
||||
.update(cx, |diff, cx| {
|
||||
diff.update_diff(
|
||||
new_buffer_snapshot.text.clone(),
|
||||
Some(old_buffer_snapshot.text().into()),
|
||||
Some(true),
|
||||
new_buffer_snapshot.language().cloned(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
diff.update(cx, |diff, cx| {
|
||||
diff.set_snapshot(update, &new_buffer_snapshot.text, cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(diff)
|
||||
}
|
||||
|
||||
impl MultiDiffView {
|
||||
pub fn open(
|
||||
diff_pairs: Vec<[String; 2]>,
|
||||
workspace: &Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<Self>>> {
|
||||
let project = workspace.project().clone();
|
||||
let workspace = workspace.weak_handle();
|
||||
let context_lines = multibuffer_context_lines(cx);
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let (entries, common_root) = load_entries(diff_pairs, &project, cx).await?;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let multibuffer = cx.new(|cx| {
|
||||
let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
|
||||
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||
multibuffer
|
||||
});
|
||||
|
||||
let file_count = entries.len();
|
||||
for entry in entries {
|
||||
register_entry(&multibuffer, entry, &common_root, context_lines, cx);
|
||||
}
|
||||
|
||||
let diff_view = cx.new(|cx| {
|
||||
Self::new(multibuffer.clone(), project.clone(), file_count, window, cx)
|
||||
});
|
||||
|
||||
let pane = workspace.active_pane();
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.add_item(Box::new(diff_view.clone()), true, true, None, window, cx);
|
||||
});
|
||||
|
||||
// Hide the left dock (file explorer) for a cleaner diff view
|
||||
workspace.left_dock().update(cx, |dock, cx| {
|
||||
dock.set_open(false, window, cx);
|
||||
});
|
||||
|
||||
diff_view
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
project: Entity<Project>,
|
||||
file_count: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer, Some(project.clone()), window, cx);
|
||||
editor.start_temporary_diff_override();
|
||||
editor.disable_diagnostics(cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_render_diff_hunk_controls(
|
||||
Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()),
|
||||
cx,
|
||||
);
|
||||
editor
|
||||
});
|
||||
|
||||
Self { editor, file_count }
|
||||
}
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
let suffix = if self.file_count == 1 {
|
||||
"1 file".to_string()
|
||||
} else {
|
||||
format!("{} files", self.file_count)
|
||||
};
|
||||
format!("Diff ({suffix})").into()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for MultiDiffView {}
|
||||
|
||||
impl Focusable for MultiDiffView {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for MultiDiffView {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::Diff).color(Color::Muted))
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
|
||||
Label::new(self.title())
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _cx: &App) -> Option<ui::SharedString> {
|
||||
Some(self.title())
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
self.title()
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Diff View Opened")
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<gpui::AnyEntity> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.clone().into())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.clone().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
||||
Some(Box::new(self.editor.clone()))
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
});
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Arc<dyn Any + Send>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &App) -> bool {
|
||||
self.editor.read(cx).can_save(cx)
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
options: SaveOptions,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> gpui::Task<Result<()>> {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.save(options, project, window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for MultiDiffView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
self.editor.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -765,6 +765,12 @@ fn main() {
|
|||
.map(|arg| parse_url_arg(arg, cx))
|
||||
.collect();
|
||||
|
||||
// Check if any diff paths are directories to determine diff_all mode
|
||||
let diff_all_mode = args
|
||||
.diff
|
||||
.chunks(2)
|
||||
.any(|pair| Path::new(&pair[0]).is_dir() || Path::new(&pair[1]).is_dir());
|
||||
|
||||
let diff_paths: Vec<[String; 2]> = args
|
||||
.diff
|
||||
.chunks(2)
|
||||
|
|
@ -781,6 +787,7 @@ fn main() {
|
|||
urls,
|
||||
diff_paths,
|
||||
wsl,
|
||||
diff_all: diff_all_mode,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -1052,6 +1059,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||
let (workspace, _results) = open_paths_with_positions(
|
||||
&paths_with_position,
|
||||
&[],
|
||||
false,
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
|
|
@ -1113,6 +1121,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
|||
let (_window, results) = open_paths_with_positions(
|
||||
&paths_with_position,
|
||||
&request.diff_paths,
|
||||
request.diff_all,
|
||||
app_state,
|
||||
workspace::OpenOptions::default(),
|
||||
cx,
|
||||
|
|
@ -1476,6 +1485,7 @@ struct Args {
|
|||
paths_or_urls: Vec<String>,
|
||||
|
||||
/// Pairs of file paths to diff. Can be specified multiple times.
|
||||
/// When directories are provided, recurses into them and shows all changed files in a single multi-diff view.
|
||||
#[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])]
|
||||
diff: Vec<String>,
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use futures::channel::{mpsc, oneshot};
|
|||
use futures::future;
|
||||
use futures::future::join_all;
|
||||
use futures::{FutureExt, SinkExt, StreamExt};
|
||||
use git_ui::file_diff_view::FileDiffView;
|
||||
use git_ui::{file_diff_view::FileDiffView, multi_diff_view::MultiDiffView};
|
||||
use gpui::{App, AsyncApp, Global, WindowHandle};
|
||||
use language::Point;
|
||||
use onboarding::FIRST_OPEN;
|
||||
|
|
@ -37,6 +37,7 @@ pub struct OpenRequest {
|
|||
pub kind: Option<OpenRequestKind>,
|
||||
pub open_paths: Vec<String>,
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub diff_all: bool,
|
||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||
pub join_channel: Option<u64>,
|
||||
pub remote_connection: Option<RemoteConnectionOptions>,
|
||||
|
|
@ -77,6 +78,7 @@ impl OpenRequest {
|
|||
let mut this = Self::default();
|
||||
|
||||
this.diff_paths = request.diff_paths;
|
||||
this.diff_all = request.diff_all;
|
||||
if let Some(wsl) = request.wsl {
|
||||
let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') {
|
||||
if user.is_empty() {
|
||||
|
|
@ -253,6 +255,7 @@ pub struct OpenListener(UnboundedSender<RawOpenRequest>);
|
|||
pub struct RawOpenRequest {
|
||||
pub urls: Vec<String>,
|
||||
pub diff_paths: Vec<[String; 2]>,
|
||||
pub diff_all: bool,
|
||||
pub wsl: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -329,6 +332,7 @@ fn connect_to_cli(
|
|||
pub async fn open_paths_with_positions(
|
||||
path_positions: &[PathWithPosition],
|
||||
diff_paths: &[[String; 2]],
|
||||
diff_all: bool,
|
||||
app_state: Arc<AppState>,
|
||||
open_options: workspace::OpenOptions,
|
||||
cx: &mut AsyncApp,
|
||||
|
|
@ -357,14 +361,24 @@ pub async fn open_paths_with_positions(
|
|||
.update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))
|
||||
.await?;
|
||||
|
||||
for diff_pair in diff_paths {
|
||||
let old_path = Path::new(&diff_pair[0]).canonicalize()?;
|
||||
let new_path = Path::new(&diff_pair[1]).canonicalize()?;
|
||||
if diff_all && !diff_paths.is_empty() {
|
||||
if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| {
|
||||
FileDiffView::open(old_path, new_path, workspace, window, cx)
|
||||
MultiDiffView::open(diff_paths.to_vec(), workspace, window, cx)
|
||||
}) {
|
||||
if let Some(diff_view) = diff_view.await.log_err() {
|
||||
items.push(Some(Ok(Box::new(diff_view))))
|
||||
items.push(Some(Ok(Box::new(diff_view))));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for diff_pair in diff_paths {
|
||||
let old_path = Path::new(&diff_pair[0]).canonicalize()?;
|
||||
let new_path = Path::new(&diff_pair[1]).canonicalize()?;
|
||||
if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| {
|
||||
FileDiffView::open(old_path, new_path, workspace, window, cx)
|
||||
}) {
|
||||
if let Some(diff_view) = diff_view.await.log_err() {
|
||||
items.push(Some(Ok(Box::new(diff_view))))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -405,6 +419,7 @@ pub async fn handle_cli_connection(
|
|||
urls,
|
||||
paths,
|
||||
diff_paths,
|
||||
diff_all,
|
||||
wait,
|
||||
wsl,
|
||||
open_new_workspace,
|
||||
|
|
@ -418,6 +433,7 @@ pub async fn handle_cli_connection(
|
|||
RawOpenRequest {
|
||||
urls,
|
||||
diff_paths,
|
||||
diff_all,
|
||||
wsl,
|
||||
},
|
||||
cx,
|
||||
|
|
@ -442,6 +458,7 @@ pub async fn handle_cli_connection(
|
|||
let open_workspace_result = open_workspaces(
|
||||
paths,
|
||||
diff_paths,
|
||||
diff_all,
|
||||
open_new_workspace,
|
||||
reuse,
|
||||
&responses,
|
||||
|
|
@ -462,6 +479,7 @@ pub async fn handle_cli_connection(
|
|||
async fn open_workspaces(
|
||||
paths: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
diff_all: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
reuse: bool,
|
||||
responses: &IpcSender<CliResponse>,
|
||||
|
|
@ -525,6 +543,7 @@ async fn open_workspaces(
|
|||
let workspace_failed_to_open = open_local_workspace(
|
||||
workspace_paths,
|
||||
diff_paths.clone(),
|
||||
diff_all,
|
||||
open_new_workspace,
|
||||
reuse,
|
||||
wait,
|
||||
|
|
@ -572,6 +591,7 @@ async fn open_workspaces(
|
|||
async fn open_local_workspace(
|
||||
workspace_paths: Vec<String>,
|
||||
diff_paths: Vec<[String; 2]>,
|
||||
diff_all: bool,
|
||||
open_new_workspace: Option<bool>,
|
||||
reuse: bool,
|
||||
wait: bool,
|
||||
|
|
@ -596,6 +616,7 @@ async fn open_local_workspace(
|
|||
let (workspace, items) = match open_paths_with_positions(
|
||||
&paths_with_position,
|
||||
&diff_paths,
|
||||
diff_all,
|
||||
app_state.clone(),
|
||||
workspace::OpenOptions {
|
||||
open_new_workspace,
|
||||
|
|
@ -935,6 +956,7 @@ mod tests {
|
|||
let errored = open_local_workspace(
|
||||
workspace_paths,
|
||||
vec![],
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
|
|
@ -1026,6 +1048,7 @@ mod tests {
|
|||
open_local_workspace(
|
||||
workspace_paths,
|
||||
vec![],
|
||||
false,
|
||||
open_new_workspace,
|
||||
false,
|
||||
false,
|
||||
|
|
@ -1099,6 +1122,7 @@ mod tests {
|
|||
open_local_workspace(
|
||||
workspace_paths,
|
||||
vec![],
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
|
|
@ -1123,6 +1147,7 @@ mod tests {
|
|||
open_local_workspace(
|
||||
workspace_paths_reuse,
|
||||
vec![],
|
||||
false,
|
||||
None, // open_new_workspace will be overridden by reuse logic
|
||||
true, // reuse = true
|
||||
false,
|
||||
|
|
|
|||
|
|
@ -155,6 +155,7 @@ fn send_args_to_instance(args: &Args) -> anyhow::Result<()> {
|
|||
paths,
|
||||
urls,
|
||||
diff_paths,
|
||||
diff_all: false,
|
||||
wait: false,
|
||||
wsl: args.wsl.clone(),
|
||||
open_new_workspace: None,
|
||||
|
|
|
|||
Loading…
Reference in a new issue