Saving changes
This commit is contained in:
parent
82f12f0af3
commit
024dd01d41
48 changed files with 2440 additions and 901 deletions
1038
Cargo.lock
generated
1038
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -44,6 +44,14 @@ Run (opens the repo passed as the first arg, or falls back to the current direct
|
|||
cargo run -p gitgpui-app --features ui-gpui,gix -- /path/to/repo
|
||||
```
|
||||
|
||||
### Profiling (Callgrind)
|
||||
|
||||
To profile the app with Valgrind Callgrind (interactive on/off instrumentation):
|
||||
|
||||
```bash
|
||||
bash scripts/profile-callgrind.sh --open -- /path/to/repo
|
||||
```
|
||||
|
||||
### Crash logs
|
||||
|
||||
If the app crashes due to a Rust panic, GitGpui writes a crash log to:
|
||||
|
|
|
|||
43
assets/gitgpui_logo.svg
Normal file
43
assets/gitgpui_logo.svg
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
|
||||
<title>gitgpui</title>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="64" y1="48" x2="464" y2="480" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6" />
|
||||
<stop offset="0.52" stop-color="#6D28D9" />
|
||||
<stop offset="1" stop-color="#3B0764" />
|
||||
</linearGradient>
|
||||
<radialGradient id="hl" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(160 120) rotate(55) scale(420)">
|
||||
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.35" />
|
||||
<stop offset="0.45" stop-color="#FFFFFF" stop-opacity="0.12" />
|
||||
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id="shade" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(420 420) rotate(225) scale(460)">
|
||||
<stop offset="0" stop-color="#000000" stop-opacity="0.35" />
|
||||
<stop offset="0.55" stop-color="#000000" stop-opacity="0.10" />
|
||||
<stop offset="1" stop-color="#000000" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<filter id="markShadow" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
|
||||
<feDropShadow dx="0" dy="14" stdDeviation="14" flood-color="#000000" flood-opacity="0.35" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="512" height="512" rx="112" fill="url(#bg)" />
|
||||
<rect width="512" height="512" rx="112" fill="url(#hl)" />
|
||||
<rect width="512" height="512" rx="112" fill="url(#shade)" />
|
||||
<rect x="18" y="18" width="476" height="476" rx="104" stroke="#FFFFFF" stroke-opacity="0.18" stroke-width="6" />
|
||||
|
||||
<g filter="url(#markShadow)">
|
||||
<path
|
||||
d="M196 188V324M196 236C250 236 256 176 352 176"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="44"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="196" cy="144" r="46" fill="#FFFFFF" />
|
||||
<circle cx="352" cy="176" r="46" fill="#FFFFFF" />
|
||||
<circle cx="196" cy="368" r="46" fill="#FFFFFF" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
13
assets/gitgpui_logo_window.svg
Normal file
13
assets/gitgpui_logo_window.svg
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<rect width="16" height="16" rx="3.5" fill="#6D28D9" />
|
||||
<path
|
||||
d="M6 6v4M6 8c1.6 0 1.8-1.8 4.2-1.8"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="1.4"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="6" cy="4.5" r="1.4" fill="#FFFFFF" />
|
||||
<circle cx="10.2" cy="6.2" r="1.4" fill="#FFFFFF" />
|
||||
<circle cx="6" cy="11.5" r="1.4" fill="#FFFFFF" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 451 B |
9
assets/linux/gitgpui.desktop
Normal file
9
assets/linux/gitgpui.desktop
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=GitGpui
|
||||
Comment=Git UI built with GPUI
|
||||
Exec=gitgpui-app
|
||||
Icon=gitgpui
|
||||
StartupWMClass=gitgpui
|
||||
Terminal=false
|
||||
Categories=Development;VersionControl;
|
||||
43
assets/linux/hicolor/scalable/apps/gitgpui.svg
Normal file
43
assets/linux/hicolor/scalable/apps/gitgpui.svg
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
|
||||
<title>gitgpui</title>
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="64" y1="48" x2="464" y2="480" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#8B5CF6" />
|
||||
<stop offset="0.52" stop-color="#6D28D9" />
|
||||
<stop offset="1" stop-color="#3B0764" />
|
||||
</linearGradient>
|
||||
<radialGradient id="hl" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(160 120) rotate(55) scale(420)">
|
||||
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.35" />
|
||||
<stop offset="0.45" stop-color="#FFFFFF" stop-opacity="0.12" />
|
||||
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<radialGradient id="shade" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(420 420) rotate(225) scale(460)">
|
||||
<stop offset="0" stop-color="#000000" stop-opacity="0.35" />
|
||||
<stop offset="0.55" stop-color="#000000" stop-opacity="0.10" />
|
||||
<stop offset="1" stop-color="#000000" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<filter id="markShadow" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
|
||||
<feDropShadow dx="0" dy="14" stdDeviation="14" flood-color="#000000" flood-opacity="0.35" />
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<rect width="512" height="512" rx="112" fill="url(#bg)" />
|
||||
<rect width="512" height="512" rx="112" fill="url(#hl)" />
|
||||
<rect width="512" height="512" rx="112" fill="url(#shade)" />
|
||||
<rect x="18" y="18" width="476" height="476" rx="104" stroke="#FFFFFF" stroke-opacity="0.18" stroke-width="6" />
|
||||
|
||||
<g filter="url(#markShadow)">
|
||||
<path
|
||||
d="M196 188V324M196 236C250 236 256 176 352 176"
|
||||
stroke="#FFFFFF"
|
||||
stroke-width="44"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="196" cy="144" r="46" fill="#FFFFFF" />
|
||||
<circle cx="352" cy="176" r="46" fill="#FFFFFF" />
|
||||
<circle cx="196" cy="368" r="46" fill="#FFFFFF" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -86,6 +86,11 @@ pub trait GitRepository: Send + Sync {
|
|||
fn create_branch(&self, name: &str, target: &CommitId) -> Result<()>;
|
||||
fn delete_branch(&self, name: &str) -> Result<()>;
|
||||
fn checkout_branch(&self, name: &str) -> Result<()>;
|
||||
fn checkout_remote_branch(&self, _remote: &str, _branch: &str) -> Result<()> {
|
||||
Err(Error::new(ErrorKind::Unsupported(
|
||||
"remote branch checkout is not implemented for this backend",
|
||||
)))
|
||||
}
|
||||
fn checkout_commit(&self, id: &CommitId) -> Result<()>;
|
||||
fn cherry_pick(&self, id: &CommitId) -> Result<()>;
|
||||
fn revert(&self, id: &CommitId) -> Result<()>;
|
||||
|
|
@ -101,6 +106,11 @@ pub trait GitRepository: Send + Sync {
|
|||
fn fetch_all(&self) -> Result<()>;
|
||||
fn pull(&self, mode: PullMode) -> Result<()>;
|
||||
fn push(&self) -> Result<()>;
|
||||
fn push_set_upstream(&self, _remote: &str, _branch: &str) -> Result<()> {
|
||||
Err(Error::new(ErrorKind::Unsupported(
|
||||
"pushing with --set-upstream is not implemented for this backend",
|
||||
)))
|
||||
}
|
||||
|
||||
fn fetch_all_with_output(&self) -> Result<CommandOutput> {
|
||||
self.fetch_all()?;
|
||||
|
|
@ -117,6 +127,13 @@ pub trait GitRepository: Send + Sync {
|
|||
Ok(CommandOutput::empty_success("git push"))
|
||||
}
|
||||
|
||||
fn push_set_upstream_with_output(&self, remote: &str, branch: &str) -> Result<CommandOutput> {
|
||||
self.push_set_upstream(remote, branch)?;
|
||||
Ok(CommandOutput::empty_success(format!(
|
||||
"git push --set-upstream {remote} HEAD:refs/heads/{branch}"
|
||||
)))
|
||||
}
|
||||
|
||||
fn pull_branch_with_output(&self, _remote: &str, _branch: &str) -> Result<CommandOutput> {
|
||||
Err(Error::new(ErrorKind::Unsupported(
|
||||
"pulling a specific remote branch is not implemented for this backend",
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ workspace = true
|
|||
|
||||
[dependencies]
|
||||
gitgpui-core = { path = "../gitgpui-core" }
|
||||
gix = "0.77"
|
||||
gix-diff = "0.57"
|
||||
gix = "0.78"
|
||||
gix-diff = "0.58"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.24.0"
|
||||
|
|
|
|||
|
|
@ -165,6 +165,21 @@ impl GitRepository for GixRepo {
|
|||
}
|
||||
}
|
||||
|
||||
// Remote tracking branches (often where "other branches" live)
|
||||
let branches = refs
|
||||
.remote_branches()
|
||||
.map_err(|e| Error::new(ErrorKind::Backend(format!("gix remote_branches: {e}"))))?
|
||||
.peeled()
|
||||
.map_err(|e| Error::new(ErrorKind::Backend(format!("gix peel refs: {e}"))))?;
|
||||
for reference in branches {
|
||||
let reference = reference
|
||||
.map_err(|e| Error::new(ErrorKind::Backend(format!("gix ref iter: {e}"))))?;
|
||||
let id = reference.id().detach();
|
||||
if id != head_id && !tips.iter().any(|t| *t == id) {
|
||||
tips.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
let mut walk = repo
|
||||
.rev_walk(tips)
|
||||
.sorting(gix::revision::walk::Sorting::ByCommitTime(
|
||||
|
|
@ -855,6 +870,54 @@ impl GitRepository for GixRepo {
|
|||
run_git_simple(cmd, "git checkout")
|
||||
}
|
||||
|
||||
fn checkout_remote_branch(&self, remote: &str, branch: &str) -> Result<()> {
|
||||
let upstream = format!("{remote}/{branch}");
|
||||
|
||||
let output = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&self.spec.workdir)
|
||||
.arg("checkout")
|
||||
.arg("--track")
|
||||
.arg("-b")
|
||||
.arg(branch)
|
||||
.arg(&upstream)
|
||||
.output()
|
||||
.map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
|
||||
|
||||
if output.status.success() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let already_exists =
|
||||
stderr.contains("already exists") || stderr.contains("fatal: a branch named");
|
||||
|
||||
if !already_exists {
|
||||
return Err(Error::new(ErrorKind::Backend(format!(
|
||||
"git checkout --track failed: {}",
|
||||
stderr.trim()
|
||||
))));
|
||||
}
|
||||
|
||||
// If the local branch already exists, check it out and update its upstream.
|
||||
let mut checkout = Command::new("git");
|
||||
checkout
|
||||
.arg("-C")
|
||||
.arg(&self.spec.workdir)
|
||||
.arg("checkout")
|
||||
.arg(branch);
|
||||
run_git_simple(checkout, "git checkout")?;
|
||||
|
||||
let mut set_upstream = Command::new("git");
|
||||
set_upstream
|
||||
.arg("-C")
|
||||
.arg(&self.spec.workdir)
|
||||
.arg("branch")
|
||||
.arg(format!("--set-upstream-to={upstream}"))
|
||||
.arg(branch);
|
||||
run_git_simple(set_upstream, "git branch --set-upstream-to")
|
||||
}
|
||||
|
||||
fn checkout_commit(&self, id: &CommitId) -> Result<()> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("-C")
|
||||
|
|
@ -1095,6 +1158,34 @@ impl GitRepository for GixRepo {
|
|||
run_git_with_output(cmd, "git push")
|
||||
}
|
||||
|
||||
fn push_set_upstream(&self, remote: &str, branch: &str) -> Result<()> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("-C")
|
||||
.arg(&self.spec.workdir)
|
||||
.arg("push")
|
||||
.arg("--set-upstream")
|
||||
.arg(remote)
|
||||
.arg(format!("HEAD:refs/heads/{branch}"));
|
||||
run_git_simple(
|
||||
cmd,
|
||||
&format!("git push --set-upstream {remote} HEAD:refs/heads/{branch}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn push_set_upstream_with_output(&self, remote: &str, branch: &str) -> Result<CommandOutput> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("-C")
|
||||
.arg(&self.spec.workdir)
|
||||
.arg("push")
|
||||
.arg("--set-upstream")
|
||||
.arg(remote)
|
||||
.arg(format!("HEAD:refs/heads/{branch}"));
|
||||
run_git_with_output(
|
||||
cmd,
|
||||
&format!("git push --set-upstream {remote} HEAD:refs/heads/{branch}"),
|
||||
)
|
||||
}
|
||||
|
||||
fn blame_file(&self, path: &Path, rev: Option<&str>) -> Result<Vec<BlameLine>> {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.arg("-C")
|
||||
|
|
@ -1229,8 +1320,7 @@ fn run_git_simple(mut cmd: Command, label: &str) -> Result<()> {
|
|||
.output()
|
||||
.map_err(|e| Error::new(ErrorKind::Io(e.kind())))?;
|
||||
|
||||
let ok_exit = output.status.success() || output.status.code() == Some(1);
|
||||
if !ok_exit {
|
||||
if !output.status.success() {
|
||||
let stderr = str::from_utf8(&output.stderr).unwrap_or("<non-utf8 stderr>");
|
||||
return Err(Error::new(ErrorKind::Backend(format!(
|
||||
"{label} failed: {stderr}"
|
||||
|
|
@ -1249,8 +1339,7 @@ fn run_git_with_output(mut cmd: Command, label: &str) -> Result<CommandOutput> {
|
|||
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
|
||||
|
||||
let ok_exit = output.status.success() || output.status.code() == Some(1);
|
||||
if !ok_exit {
|
||||
if !output.status.success() {
|
||||
return Err(Error::new(ErrorKind::Backend(format!(
|
||||
"{label} failed: {}",
|
||||
stderr.trim()
|
||||
|
|
|
|||
72
crates/gitgpui-git-gix/tests/log_integration.rs
Normal file
72
crates/gitgpui-git-gix/tests/log_integration.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use gitgpui_core::services::GitBackend;
|
||||
use gitgpui_git_gix::GixBackend;
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
fn run_git(repo: &Path, args: &[&str]) {
|
||||
let status = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(repo)
|
||||
.args(args)
|
||||
.status()
|
||||
.expect("git command to run");
|
||||
assert!(status.success(), "git {:?} failed", args);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn log_all_branches_includes_remote_tracking_branches() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let repo = dir.path().join("repo");
|
||||
let origin = dir.path().join("origin.git");
|
||||
|
||||
std::fs::create_dir_all(&repo).unwrap();
|
||||
run_git(&repo, &["init", "-b", "main"]);
|
||||
run_git(&repo, &["config", "user.email", "you@example.com"]);
|
||||
run_git(&repo, &["config", "user.name", "You"]);
|
||||
run_git(&repo, &["config", "commit.gpgsign", "false"]);
|
||||
|
||||
std::fs::write(repo.join("a.txt"), "one\n").unwrap();
|
||||
run_git(&repo, &["add", "a.txt"]);
|
||||
run_git(&repo, &["-c", "commit.gpgsign=false", "commit", "-m", "A"]);
|
||||
|
||||
run_git(&repo, &["checkout", "-b", "feature"]);
|
||||
std::fs::write(repo.join("b.txt"), "two\n").unwrap();
|
||||
run_git(&repo, &["add", "b.txt"]);
|
||||
run_git(&repo, &["-c", "commit.gpgsign=false", "commit", "-m", "C"]);
|
||||
let feature_tip = {
|
||||
let out = Command::new("git")
|
||||
.arg("-C")
|
||||
.arg(&repo)
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("git rev-parse to run");
|
||||
assert!(out.status.success());
|
||||
String::from_utf8(out.stdout).unwrap().trim().to_string()
|
||||
};
|
||||
|
||||
run_git(
|
||||
dir.path(),
|
||||
&["init", "--bare", "-b", "main", origin.to_str().unwrap()],
|
||||
);
|
||||
run_git(&repo, &["remote", "add", "origin", origin.to_str().unwrap()]);
|
||||
run_git(&repo, &["push", "-u", "origin", "feature"]);
|
||||
|
||||
run_git(&repo, &["checkout", "main"]);
|
||||
run_git(&repo, &["branch", "-D", "feature"]);
|
||||
run_git(&repo, &["fetch", "origin"]);
|
||||
|
||||
let backend = GixBackend::default();
|
||||
let opened = backend.open(&repo).unwrap();
|
||||
|
||||
let head = opened.log_head_page(200, None).unwrap();
|
||||
assert!(
|
||||
!head.commits.iter().any(|c| c.id.0 == feature_tip),
|
||||
"head log unexpectedly contains feature commit"
|
||||
);
|
||||
|
||||
let all = opened.log_all_branches_page(200, None).unwrap();
|
||||
assert!(
|
||||
all.commits.iter().any(|c| c.id.0 == feature_tip),
|
||||
"all-branches log should include remote-tracking branch commit"
|
||||
);
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ pub enum RepoCommandKind {
|
|||
PullBranch { remote: String, branch: String },
|
||||
MergeRef { reference: String },
|
||||
Push,
|
||||
PushSetUpstream { remote: String, branch: String },
|
||||
CheckoutConflict { path: PathBuf, side: ConflictSide },
|
||||
}
|
||||
|
||||
|
|
@ -63,6 +64,11 @@ pub enum Msg {
|
|||
repo_id: RepoId,
|
||||
name: String,
|
||||
},
|
||||
CheckoutRemoteBranch {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
name: String,
|
||||
},
|
||||
CheckoutCommit {
|
||||
repo_id: RepoId,
|
||||
commit_id: CommitId,
|
||||
|
|
@ -118,6 +124,11 @@ pub enum Msg {
|
|||
Push {
|
||||
repo_id: RepoId,
|
||||
},
|
||||
PushSetUpstream {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
branch: String,
|
||||
},
|
||||
CheckoutConflictSide {
|
||||
repo_id: RepoId,
|
||||
path: PathBuf,
|
||||
|
|
@ -287,6 +298,16 @@ impl std::fmt::Debug for Msg {
|
|||
.field("repo_id", repo_id)
|
||||
.field("name", name)
|
||||
.finish(),
|
||||
Msg::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
} => f
|
||||
.debug_struct("CheckoutRemoteBranch")
|
||||
.field("repo_id", repo_id)
|
||||
.field("remote", remote)
|
||||
.field("name", name)
|
||||
.finish(),
|
||||
Msg::CheckoutCommit { repo_id, commit_id } => f
|
||||
.debug_struct("CheckoutCommit")
|
||||
.field("repo_id", repo_id)
|
||||
|
|
@ -357,6 +378,16 @@ impl std::fmt::Debug for Msg {
|
|||
.field("reference", reference)
|
||||
.finish(),
|
||||
Msg::Push { repo_id } => f.debug_struct("Push").field("repo_id", repo_id).finish(),
|
||||
Msg::PushSetUpstream {
|
||||
repo_id,
|
||||
remote,
|
||||
branch,
|
||||
} => f
|
||||
.debug_struct("PushSetUpstream")
|
||||
.field("repo_id", repo_id)
|
||||
.field("remote", remote)
|
||||
.field("branch", branch)
|
||||
.finish(),
|
||||
Msg::CheckoutConflictSide {
|
||||
repo_id,
|
||||
path,
|
||||
|
|
@ -570,6 +601,11 @@ pub enum Effect {
|
|||
repo_id: RepoId,
|
||||
name: String,
|
||||
},
|
||||
CheckoutRemoteBranch {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
name: String,
|
||||
},
|
||||
CheckoutCommit {
|
||||
repo_id: RepoId,
|
||||
commit_id: CommitId,
|
||||
|
|
@ -625,6 +661,11 @@ pub enum Effect {
|
|||
Push {
|
||||
repo_id: RepoId,
|
||||
},
|
||||
PushSetUpstream {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
branch: String,
|
||||
},
|
||||
CheckoutConflictSide {
|
||||
repo_id: RepoId,
|
||||
path: PathBuf,
|
||||
|
|
|
|||
|
|
@ -218,6 +218,21 @@ pub(super) fn schedule_effect(
|
|||
}
|
||||
}
|
||||
|
||||
Effect::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
} => {
|
||||
if let Some(repo) = repos.get(&repo_id).cloned() {
|
||||
executor.spawn(move || {
|
||||
let _ = msg_tx.send(Msg::RepoActionFinished {
|
||||
repo_id,
|
||||
result: repo.checkout_remote_branch(&remote, &name),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Effect::CheckoutCommit { repo_id, commit_id } => {
|
||||
if let Some(repo) = repos.get(&repo_id).cloned() {
|
||||
executor.spawn(move || {
|
||||
|
|
@ -397,6 +412,25 @@ pub(super) fn schedule_effect(
|
|||
}
|
||||
}
|
||||
|
||||
Effect::PushSetUpstream {
|
||||
repo_id,
|
||||
remote,
|
||||
branch,
|
||||
} => {
|
||||
if let Some(repo) = repos.get(&repo_id).cloned() {
|
||||
executor.spawn(move || {
|
||||
let _ = msg_tx.send(Msg::RepoCommandFinished {
|
||||
repo_id,
|
||||
command: crate::msg::RepoCommandKind::PushSetUpstream {
|
||||
remote: remote.clone(),
|
||||
branch: branch.clone(),
|
||||
},
|
||||
result: repo.push_set_upstream_with_output(&remote, &branch),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Effect::CheckoutConflictSide {
|
||||
repo_id,
|
||||
path,
|
||||
|
|
|
|||
|
|
@ -289,6 +289,15 @@ pub(super) fn reduce(
|
|||
}
|
||||
|
||||
Msg::CheckoutBranch { repo_id, name } => vec![Effect::CheckoutBranch { repo_id, name }],
|
||||
Msg::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
} => vec![Effect::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
}],
|
||||
Msg::CheckoutCommit { repo_id, commit_id } => {
|
||||
vec![Effect::CheckoutCommit { repo_id, commit_id }]
|
||||
}
|
||||
|
|
@ -317,6 +326,15 @@ pub(super) fn reduce(
|
|||
}],
|
||||
Msg::MergeRef { repo_id, reference } => vec![Effect::MergeRef { repo_id, reference }],
|
||||
Msg::Push { repo_id } => vec![Effect::Push { repo_id }],
|
||||
Msg::PushSetUpstream {
|
||||
repo_id,
|
||||
remote,
|
||||
branch,
|
||||
} => vec![Effect::PushSetUpstream {
|
||||
repo_id,
|
||||
remote,
|
||||
branch,
|
||||
}],
|
||||
Msg::CheckoutConflictSide {
|
||||
repo_id,
|
||||
path,
|
||||
|
|
@ -484,9 +502,7 @@ pub(super) fn reduce(
|
|||
|
||||
match result {
|
||||
Ok(mut page) => {
|
||||
if loading_more
|
||||
&& let Loadable::Ready(existing) = &mut repo_state.log
|
||||
{
|
||||
if loading_more && let Loadable::Ready(existing) = &mut repo_state.log {
|
||||
existing.commits.extend(page.commits.drain(..));
|
||||
existing.next_cursor = page.next_cursor;
|
||||
} else {
|
||||
|
|
@ -823,6 +839,7 @@ fn summarize_command(
|
|||
RepoCommandKind::PullBranch { .. } => "Pull",
|
||||
RepoCommandKind::MergeRef { .. } => "Merge",
|
||||
RepoCommandKind::Push => "Push",
|
||||
RepoCommandKind::PushSetUpstream { .. } => "Push",
|
||||
RepoCommandKind::CheckoutConflict { side, .. } => match side {
|
||||
ConflictSide::Ours => "Checkout ours",
|
||||
ConflictSide::Theirs => "Checkout theirs",
|
||||
|
|
@ -890,6 +907,14 @@ fn summarize_command(
|
|||
"Push: Completed".to_string()
|
||||
}
|
||||
}
|
||||
RepoCommandKind::PushSetUpstream { remote, branch } => {
|
||||
let base = if output.stderr.contains("Everything up-to-date") {
|
||||
"Everything up-to-date"
|
||||
} else {
|
||||
"Completed"
|
||||
};
|
||||
format!("Push -u {remote}/{branch}: {base}")
|
||||
}
|
||||
RepoCommandKind::CheckoutConflict { side, .. } => match side {
|
||||
ConflictSide::Ours => "Resolved using ours".to_string(),
|
||||
ConflictSide::Theirs => "Resolved using theirs".to_string(),
|
||||
|
|
|
|||
|
|
@ -889,6 +889,24 @@ fn repo_operations_emit_effects() {
|
|||
[Effect::Push { repo_id: RepoId(1) }]
|
||||
));
|
||||
|
||||
let push_set_upstream = reduce(
|
||||
&mut repos,
|
||||
&id_alloc,
|
||||
&mut state,
|
||||
Msg::PushSetUpstream {
|
||||
repo_id: RepoId(1),
|
||||
remote: "origin".to_string(),
|
||||
branch: "feature/foo".to_string(),
|
||||
},
|
||||
);
|
||||
assert!(matches!(
|
||||
push_set_upstream.as_slice(),
|
||||
[Effect::PushSetUpstream {
|
||||
repo_id: RepoId(1),
|
||||
..
|
||||
}]
|
||||
));
|
||||
|
||||
let stash = reduce(
|
||||
&mut repos,
|
||||
&id_alloc,
|
||||
|
|
|
|||
|
|
@ -15,24 +15,20 @@ gitgpui-ui = { path = "../gitgpui-ui" }
|
|||
gpui = "0.2.2"
|
||||
tree-sitter = "0.26"
|
||||
tree-sitter-bash = "0.25.1"
|
||||
tree-sitter-css = "0.23.2"
|
||||
tree-sitter-go = "0.23"
|
||||
tree-sitter-css = "0.25.0"
|
||||
tree-sitter-go = "0.25"
|
||||
tree-sitter-html = "0.23.2"
|
||||
tree-sitter-json = "0.24"
|
||||
tree-sitter-python = "0.25"
|
||||
tree-sitter-rust = "0.24"
|
||||
tree-sitter-typescript = "0.23.2"
|
||||
tree-sitter-yaml = "0.6.1"
|
||||
tree-sitter-yaml = "0.7.2"
|
||||
unicode-segmentation = "1.12.0"
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { version = "0.2.2", features = ["test-support"] }
|
||||
criterion = "0.5.1"
|
||||
|
||||
[features]
|
||||
bench = []
|
||||
criterion = "0.8.1"
|
||||
|
||||
[[bench]]
|
||||
name = "performance"
|
||||
harness = false
|
||||
required-features = ["bench"]
|
||||
|
|
|
|||
12
crates/gitgpui-ui-gpui/assets/icons/gitgpui_mark.svg
Normal file
12
crates/gitgpui-ui-gpui/assets/icons/gitgpui_mark.svg
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
|
||||
<path
|
||||
d="M196 188V324M196 236C250 236 256 176 352 176"
|
||||
stroke="currentColor"
|
||||
stroke-width="44"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="196" cy="144" r="46" fill="currentColor" />
|
||||
<circle cx="352" cy="176" r="46" fill="currentColor" />
|
||||
<circle cx="196" cy="368" r="46" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 450 B |
5
crates/gitgpui-ui-gpui/assets/icons/menu.svg
Normal file
5
crates/gitgpui-ui-gpui/assets/icons/menu.svg
Normal file
|
|
@ -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="M3 4.5H13" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
<path d="M3 8H13" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
<path d="M3 11.5H13" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 369 B |
|
|
@ -1,6 +1,9 @@
|
|||
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||
use gitgpui_ui_gpui::benchmarks::{CommitDetailsFixture, LargeFileDiffScrollFixture, OpenRepoFixture};
|
||||
use gitgpui_ui_gpui::benchmarks::{
|
||||
CommitDetailsFixture, LargeFileDiffScrollFixture, OpenRepoFixture,
|
||||
};
|
||||
use std::env;
|
||||
use std::time::Duration;
|
||||
|
||||
fn env_usize(key: &str, default: usize) -> usize {
|
||||
env::var(key)
|
||||
|
|
@ -10,14 +13,18 @@ fn env_usize(key: &str, default: usize) -> usize {
|
|||
}
|
||||
|
||||
fn bench_open_repo(c: &mut Criterion) {
|
||||
let commits = env_usize("GITGPUI_BENCH_COMMITS", 100_000);
|
||||
let local_branches = env_usize("GITGPUI_BENCH_LOCAL_BRANCHES", 5_000);
|
||||
let remote_branches = env_usize("GITGPUI_BENCH_REMOTE_BRANCHES", 20_000);
|
||||
let remotes = env_usize("GITGPUI_BENCH_REMOTES", 3);
|
||||
// Note: Criterion's "Warming up for Xs" can look "stuck" if a single iteration takes longer
|
||||
// than the warm-up duration. Keep defaults moderate; scale up via env vars for stress runs.
|
||||
let commits = env_usize("GITGPUI_BENCH_COMMITS", 5_000);
|
||||
let local_branches = env_usize("GITGPUI_BENCH_LOCAL_BRANCHES", 200);
|
||||
let remote_branches = env_usize("GITGPUI_BENCH_REMOTE_BRANCHES", 800);
|
||||
let remotes = env_usize("GITGPUI_BENCH_REMOTES", 2);
|
||||
|
||||
let fixture = OpenRepoFixture::new(commits, local_branches, remote_branches, remotes);
|
||||
|
||||
let mut group = c.benchmark_group("open_repo");
|
||||
group.sample_size(10);
|
||||
group.warm_up_time(Duration::from_secs(1));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("long_history_and_branches", commits),
|
||||
&commits,
|
||||
|
|
@ -27,25 +34,27 @@ fn bench_open_repo(c: &mut Criterion) {
|
|||
}
|
||||
|
||||
fn bench_commit_details(c: &mut Criterion) {
|
||||
let files = env_usize("GITGPUI_BENCH_COMMIT_FILES", 50_000);
|
||||
let files = env_usize("GITGPUI_BENCH_COMMIT_FILES", 5_000);
|
||||
let depth = env_usize("GITGPUI_BENCH_COMMIT_PATH_DEPTH", 4);
|
||||
let fixture = CommitDetailsFixture::new(files, depth);
|
||||
|
||||
let mut group = c.benchmark_group("commit_details");
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("many_files", files),
|
||||
&files,
|
||||
|b, _| b.iter(|| fixture.run()),
|
||||
);
|
||||
group.sample_size(10);
|
||||
group.warm_up_time(Duration::from_secs(1));
|
||||
group.bench_with_input(BenchmarkId::new("many_files", files), &files, |b, _| {
|
||||
b.iter(|| fixture.run())
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_large_file_diff_scroll(c: &mut Criterion) {
|
||||
let lines = env_usize("GITGPUI_BENCH_DIFF_LINES", 100_000);
|
||||
let lines = env_usize("GITGPUI_BENCH_DIFF_LINES", 10_000);
|
||||
let window = env_usize("GITGPUI_BENCH_DIFF_WINDOW", 200);
|
||||
let fixture = LargeFileDiffScrollFixture::new(lines);
|
||||
|
||||
let mut group = c.benchmark_group("diff_scroll");
|
||||
group.sample_size(10);
|
||||
group.warm_up_time(Duration::from_secs(1));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::new("style_window", window),
|
||||
&window,
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ pub fn run(backend: Arc<dyn GitBackend>) {
|
|||
appears_transparent: true,
|
||||
traffic_light_position: Some(point(px(9.0), px(9.0))),
|
||||
}),
|
||||
app_id: Some("gitgpui".to_string()),
|
||||
window_decorations: Some(WindowDecorations::Client),
|
||||
is_movable: true,
|
||||
is_resizable: true,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,12 @@ pub struct GitGpuiAssets;
|
|||
impl GitGpuiAssets {
|
||||
fn load_static(path: &str) -> Option<Cow<'static, [u8]>> {
|
||||
match path {
|
||||
"gitgpui_logo.svg" => Some(Cow::Borrowed(include_bytes!(
|
||||
"../../../assets/gitgpui_logo.svg"
|
||||
))),
|
||||
"gitgpui_logo_window.svg" => Some(Cow::Borrowed(include_bytes!(
|
||||
"../../../assets/gitgpui_logo_window.svg"
|
||||
))),
|
||||
"icons/arrow_down.svg" => Some(Cow::Borrowed(include_bytes!(
|
||||
"../assets/icons/arrow_down.svg"
|
||||
))),
|
||||
|
|
@ -36,13 +42,21 @@ impl GitGpuiAssets {
|
|||
"icons/git_branch.svg" => Some(Cow::Borrowed(include_bytes!(
|
||||
"../assets/icons/git_branch.svg"
|
||||
))),
|
||||
"icons/gitgpui_mark.svg" => Some(Cow::Borrowed(include_bytes!(
|
||||
"../assets/icons/gitgpui_mark.svg"
|
||||
))),
|
||||
"icons/menu.svg" => Some(Cow::Borrowed(include_bytes!("../assets/icons/menu.svg"))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_static(dir: &str) -> Vec<SharedString> {
|
||||
match dir.trim_end_matches('/') {
|
||||
"" => vec!["icons".into()],
|
||||
"" => vec![
|
||||
"gitgpui_logo.svg".into(),
|
||||
"gitgpui_logo_window.svg".into(),
|
||||
"icons".into(),
|
||||
],
|
||||
"icons" => vec![
|
||||
"icons/arrow_down.svg".into(),
|
||||
"icons/arrow_up.svg".into(),
|
||||
|
|
@ -56,6 +70,8 @@ impl GitGpuiAssets {
|
|||
"icons/generic_restore.svg".into(),
|
||||
"icons/generic_close.svg".into(),
|
||||
"icons/git_branch.svg".into(),
|
||||
"icons/gitgpui_mark.svg".into(),
|
||||
"icons/menu.svg".into(),
|
||||
],
|
||||
_ => vec![],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,29 +32,63 @@ actions!(
|
|||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct TextInputStyle {
|
||||
is_dark: bool,
|
||||
background: Rgba,
|
||||
border: Rgba,
|
||||
hover_border: Rgba,
|
||||
focus_border: Rgba,
|
||||
radius: f32,
|
||||
text: gpui::Hsla,
|
||||
placeholder: gpui::Hsla,
|
||||
cursor: Rgba,
|
||||
selection: Rgba,
|
||||
}
|
||||
|
||||
impl TextInputStyle {
|
||||
fn from_theme(theme: AppTheme) -> Self {
|
||||
fn mix(mut a: Rgba, b: Rgba, t: f32) -> Rgba {
|
||||
let t = t.clamp(0.0, 1.0);
|
||||
a.r = a.r + (b.r - a.r) * t;
|
||||
a.g = a.g + (b.g - a.g) * t;
|
||||
a.b = a.b + (b.b - a.b) * t;
|
||||
a.a = a.a + (b.a - a.a) * t;
|
||||
a
|
||||
}
|
||||
|
||||
// Ensure inputs look like inputs even in themes where `surface_bg` and `surface_bg_elevated`
|
||||
// are equal (Ayu/One).
|
||||
let background = if theme.is_dark {
|
||||
mix(
|
||||
theme.colors.surface_bg_elevated,
|
||||
gpui::rgba(0xFFFFFFFF),
|
||||
0.03,
|
||||
)
|
||||
} else {
|
||||
mix(
|
||||
theme.colors.surface_bg_elevated,
|
||||
gpui::rgba(0x000000FF),
|
||||
0.03,
|
||||
)
|
||||
};
|
||||
|
||||
let base_border = theme.colors.border;
|
||||
let hover_border = with_alpha(
|
||||
theme.colors.text_muted,
|
||||
if theme.is_dark { 0.55 } else { 0.40 },
|
||||
);
|
||||
let focus_border = with_alpha(theme.colors.accent, if theme.is_dark { 0.98 } else { 0.92 });
|
||||
let placeholder = if theme.is_dark {
|
||||
hsla(0., 0., 1., 0.35)
|
||||
} else {
|
||||
hsla(0., 0., 0., 0.2)
|
||||
};
|
||||
Self {
|
||||
is_dark: theme.is_dark,
|
||||
background: theme.colors.surface_bg_elevated,
|
||||
border: theme.colors.border,
|
||||
background,
|
||||
border: base_border,
|
||||
hover_border,
|
||||
focus_border: theme.colors.focus_ring,
|
||||
focus_border,
|
||||
radius: theme.radii.row,
|
||||
text: theme.colors.text.into(),
|
||||
placeholder,
|
||||
cursor: with_alpha(theme.colors.text, if theme.is_dark { 0.78 } else { 0.62 }),
|
||||
selection: with_alpha(theme.colors.accent, if theme.is_dark { 0.28 } else { 0.18 }),
|
||||
}
|
||||
|
|
@ -780,16 +814,10 @@ impl Element for TextElement {
|
|||
let soft_wrap = input.soft_wrap && input.multiline;
|
||||
let style = window.text_style();
|
||||
|
||||
let placeholder_color = if style_colors.is_dark {
|
||||
hsla(0., 0., 1., 0.35)
|
||||
} else {
|
||||
hsla(0., 0., 0., 0.2)
|
||||
};
|
||||
|
||||
let (display_text, text_color) = if content.is_empty() {
|
||||
(input.placeholder.clone(), placeholder_color)
|
||||
(input.placeholder.clone(), style_colors.placeholder)
|
||||
} else {
|
||||
(content, style.color)
|
||||
(content, style_colors.text)
|
||||
};
|
||||
|
||||
let font_size = style.font_size.to_pixels(window.rem_size());
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ mod zed_port;
|
|||
|
||||
pub use app::run;
|
||||
|
||||
#[cfg(feature = "bench")]
|
||||
#[doc(hidden)]
|
||||
pub mod benchmarks {
|
||||
pub use crate::view::rows::benchmarks::*;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ impl SmokeView {
|
|||
let input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Enter…".into(),
|
||||
placeholder: "Enter".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -191,7 +191,7 @@ fn text_input_constructs_without_panicking(cx: &mut gpui::TestAppContext) {
|
|||
cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Commit message…".into(),
|
||||
placeholder: "Commit message".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use super::*;
|
||||
use gpui::ObjectFit;
|
||||
|
||||
pub(super) const CLIENT_SIDE_DECORATION_INSET: Pixels = px(10.0);
|
||||
|
||||
|
|
@ -10,6 +11,38 @@ fn titlebar_control_icon(theme: AppTheme, path: &'static str) -> gpui::Svg {
|
|||
.text_color(theme.colors.text)
|
||||
}
|
||||
|
||||
fn titlebar_app_icon(theme: AppTheme) -> AnyElement {
|
||||
gpui::image_cache(gpui::retain_all("titlebar_icon_cache"))
|
||||
.child(
|
||||
div()
|
||||
.id("titlebar_app_icon")
|
||||
.size(px(16.0))
|
||||
.rounded(px(4.0))
|
||||
.bg(with_alpha(
|
||||
theme.colors.text,
|
||||
if theme.is_dark { 0.12 } else { 0.08 },
|
||||
))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
gpui::img("gitgpui_logo_window.svg")
|
||||
.size(px(16.0))
|
||||
.object_fit(ObjectFit::Contain)
|
||||
.with_fallback({
|
||||
let theme = theme;
|
||||
move || {
|
||||
gpui::svg()
|
||||
.path("icons/gitgpui_mark.svg")
|
||||
.w(px(16.0))
|
||||
.h(px(16.0))
|
||||
.text_color(theme.colors.text)
|
||||
.into_any_element()
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn titlebar_control_button(
|
||||
theme: AppTheme,
|
||||
id: &'static str,
|
||||
|
|
@ -138,17 +171,36 @@ impl GitGpuiView {
|
|||
with_alpha(theme.colors.border, 0.7)
|
||||
};
|
||||
|
||||
let app_icon = div()
|
||||
.id("app_icon")
|
||||
.h_full()
|
||||
.pl_2()
|
||||
.pr_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.child(titlebar_app_icon(theme));
|
||||
|
||||
let hamburger = div()
|
||||
.id("app_menu")
|
||||
.debug_selector(|| "app_menu".to_string())
|
||||
.h_full()
|
||||
.px_2()
|
||||
.w(px(44.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("≡")
|
||||
.child(
|
||||
div()
|
||||
.id("app_menu_btn")
|
||||
.size(px(26.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.rounded(px(theme.radii.pill))
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child(titlebar_control_icon(theme, "icons/menu.svg")),
|
||||
)
|
||||
.on_click(cx.listener(|this, e: &ClickEvent, _w, cx| {
|
||||
this.popover = Some(PopoverKind::AppMenu);
|
||||
this.popover_anchor = Some(e.position());
|
||||
|
|
@ -166,6 +218,10 @@ impl GitGpuiView {
|
|||
.id("title_drag")
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.min_w(px(0.0))
|
||||
.px_2()
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|
|
@ -196,8 +252,12 @@ impl GitGpuiView {
|
|||
}))
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.whitespace_nowrap()
|
||||
.child("GitGpui"),
|
||||
);
|
||||
|
||||
|
|
@ -263,7 +323,15 @@ impl GitGpuiView {
|
|||
.bg(bar_bg)
|
||||
.border_b_1()
|
||||
.border_color(bar_border)
|
||||
.child(hamburger)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.h_full()
|
||||
.gap_1()
|
||||
.child(app_icon)
|
||||
.child(hamburger),
|
||||
)
|
||||
.child(drag_region)
|
||||
.child(
|
||||
div()
|
||||
|
|
|
|||
|
|
@ -188,16 +188,16 @@ impl Element for DiffTextSelectionOverlay {
|
|||
underline: None,
|
||||
strikethrough: None,
|
||||
};
|
||||
let layout = window
|
||||
.text_system()
|
||||
.shape_line(self.text.clone(), font_size, &[run], None);
|
||||
let layout =
|
||||
window
|
||||
.text_system()
|
||||
.shape_line(self.text.clone(), font_size, &[run], None);
|
||||
let x0 = selection
|
||||
.as_ref()
|
||||
.map(|r| layout.x_for_index(r.start.min(self.text.len())));
|
||||
let x1 =
|
||||
selection
|
||||
.as_ref()
|
||||
.map(|r| layout.x_for_index(r.end.min(self.text.len())));
|
||||
let x1 = selection
|
||||
.as_ref()
|
||||
.map(|r| layout.x_for_index(r.end.min(self.text.len())));
|
||||
(x0, x1, Some(layout))
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -205,6 +205,10 @@ enum PopoverKind {
|
|||
BranchPicker,
|
||||
CreateBranch,
|
||||
StashPrompt,
|
||||
PushSetUpstreamPrompt {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
},
|
||||
PullPicker,
|
||||
AppMenu,
|
||||
DiffHunks,
|
||||
|
|
@ -414,6 +418,7 @@ pub struct GitGpuiView {
|
|||
commit_message_input: Entity<zed::TextInput>,
|
||||
create_branch_input: Entity<zed::TextInput>,
|
||||
stash_message_input: Entity<zed::TextInput>,
|
||||
push_upstream_branch_input: Entity<zed::TextInput>,
|
||||
|
||||
popover: Option<PopoverKind>,
|
||||
popover_anchor: Option<Point<Pixels>>,
|
||||
|
|
@ -463,7 +468,8 @@ struct DiffTextLayoutCacheEntry {
|
|||
|
||||
impl GitGpuiView {
|
||||
fn is_file_preview_active(&self) -> bool {
|
||||
self.untracked_worktree_preview_path().is_some() || self.added_file_preview_abs_path().is_some()
|
||||
self.untracked_worktree_preview_path().is_some()
|
||||
|| self.added_file_preview_abs_path().is_some()
|
||||
}
|
||||
|
||||
fn worktree_preview_line_count(&self) -> Option<usize> {
|
||||
|
|
@ -658,7 +664,7 @@ impl GitGpuiView {
|
|||
let commit_message_input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Enter commit message…".into(),
|
||||
placeholder: "Enter commit message".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -686,7 +692,21 @@ impl GitGpuiView {
|
|||
let stash_message_input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Stash message…".into(),
|
||||
placeholder: "Stash message".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
soft_wrap: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let push_upstream_branch_input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Remote branch name".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -700,7 +720,7 @@ impl GitGpuiView {
|
|||
let history_search_input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Search commits…".into(),
|
||||
placeholder: "Search commits".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -714,7 +734,7 @@ impl GitGpuiView {
|
|||
let diff_search_input = cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Search diff…".into(),
|
||||
placeholder: "Search diff".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -853,6 +873,7 @@ impl GitGpuiView {
|
|||
commit_message_input,
|
||||
create_branch_input,
|
||||
stash_message_input,
|
||||
push_upstream_branch_input,
|
||||
popover: None,
|
||||
popover_anchor: None,
|
||||
context_menu_focus_handle,
|
||||
|
|
@ -894,6 +915,10 @@ impl GitGpuiView {
|
|||
|
||||
view.set_theme(initial_theme, cx);
|
||||
view.rebuild_diff_cache();
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
view.maybe_auto_install_linux_desktop_integration(cx);
|
||||
|
||||
view
|
||||
}
|
||||
|
||||
|
|
@ -911,6 +936,8 @@ impl GitGpuiView {
|
|||
.update(cx, |input, cx| input.set_theme(theme, cx));
|
||||
self.stash_message_input
|
||||
.update(cx, |input, cx| input.set_theme(theme, cx));
|
||||
self.push_upstream_branch_input
|
||||
.update(cx, |input, cx| input.set_theme(theme, cx));
|
||||
self.history_search_input
|
||||
.update(cx, |input, cx| input.set_theme(theme, cx));
|
||||
self.diff_search_input
|
||||
|
|
@ -1292,7 +1319,10 @@ impl GitGpuiView {
|
|||
let Loadable::Ready(lines) = &self.worktree_preview else {
|
||||
return fallback;
|
||||
};
|
||||
return lines.get(visible_ix).map(|l| expand_tabs(l)).unwrap_or(fallback);
|
||||
return lines
|
||||
.get(visible_ix)
|
||||
.map(|l| expand_tabs(l))
|
||||
.unwrap_or(fallback);
|
||||
}
|
||||
|
||||
let Some(&mapped_ix) = self.diff_visible_indices.get(visible_ix) else {
|
||||
|
|
@ -1731,21 +1761,19 @@ impl GitGpuiView {
|
|||
return (false, false);
|
||||
}
|
||||
|
||||
let handle_w = px(HISTORY_COL_HANDLE_PX);
|
||||
let min_message = px(220.0);
|
||||
|
||||
// Always show Branch + Graph; Message is flex.
|
||||
let fixed_base = self.history_col_branch + handle_w + self.history_col_graph + handle_w;
|
||||
let fixed_base = self.history_col_branch + self.history_col_graph;
|
||||
|
||||
// Show both by default.
|
||||
let mut show_date = true;
|
||||
let mut show_sha = true;
|
||||
let mut fixed =
|
||||
fixed_base + handle_w + self.history_col_date + handle_w + self.history_col_sha;
|
||||
let mut fixed = fixed_base + self.history_col_date + self.history_col_sha;
|
||||
|
||||
if available - fixed < min_message {
|
||||
show_sha = false;
|
||||
fixed -= handle_w + self.history_col_sha;
|
||||
fixed -= self.history_col_sha;
|
||||
}
|
||||
if available - fixed < min_message {
|
||||
show_date = false;
|
||||
|
|
@ -1765,7 +1793,7 @@ impl GitGpuiView {
|
|||
cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Filter repositories…".into(),
|
||||
placeholder: "Filter repositories".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -1795,7 +1823,7 @@ impl GitGpuiView {
|
|||
cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Filter branches…".into(),
|
||||
placeholder: "Filter branches".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -1825,7 +1853,7 @@ impl GitGpuiView {
|
|||
cx.new(|cx| {
|
||||
zed::TextInput::new(
|
||||
zed::TextInputOptions {
|
||||
placeholder: "Filter hunks…".into(),
|
||||
placeholder: "Filter hunks".into(),
|
||||
multiline: false,
|
||||
read_only: false,
|
||||
chromeless: false,
|
||||
|
|
@ -1938,7 +1966,7 @@ impl GitGpuiView {
|
|||
}
|
||||
Loadable::Loading => rows.push(BranchSidebarRow::Placeholder {
|
||||
section: BranchSection::Local,
|
||||
message: "Loading…".into(),
|
||||
message: "Loading".into(),
|
||||
}),
|
||||
Loadable::NotLoaded => rows.push(BranchSidebarRow::Placeholder {
|
||||
section: BranchSection::Local,
|
||||
|
|
@ -1969,7 +1997,7 @@ impl GitGpuiView {
|
|||
Loadable::Loading => {
|
||||
rows.push(BranchSidebarRow::Placeholder {
|
||||
section: BranchSection::Remote,
|
||||
message: "Loading…".into(),
|
||||
message: "Loading".into(),
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
|
@ -2042,7 +2070,7 @@ impl GitGpuiView {
|
|||
}
|
||||
}
|
||||
Loadable::Loading => rows.push(BranchSidebarRow::StashPlaceholder {
|
||||
message: "Loading…".into(),
|
||||
message: "Loading".into(),
|
||||
}),
|
||||
Loadable::NotLoaded => rows.push(BranchSidebarRow::StashPlaceholder {
|
||||
message: "Not loaded".into(),
|
||||
|
|
@ -2906,7 +2934,7 @@ impl GitGpuiView {
|
|||
multiline: true,
|
||||
read_only: true,
|
||||
chromeless: true,
|
||||
soft_wrap: false,
|
||||
soft_wrap: true,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
|
@ -2919,9 +2947,14 @@ impl GitGpuiView {
|
|||
|
||||
self.toasts.push(ToastState { id, kind, input });
|
||||
|
||||
let ttl = match kind {
|
||||
zed::ToastKind::Error => Duration::from_secs(15),
|
||||
zed::ToastKind::Success => Duration::from_secs(6),
|
||||
zed::ToastKind::Info => Duration::from_secs(6),
|
||||
};
|
||||
cx.spawn(
|
||||
async move |view: WeakEntity<GitGpuiView>, cx: &mut gpui::AsyncApp| {
|
||||
Timer::after(Duration::from_secs(5)).await;
|
||||
Timer::after(ttl).await;
|
||||
let _ = view.update(cx, |this, cx| {
|
||||
this.toasts.retain(|t| t.id != id);
|
||||
cx.notify();
|
||||
|
|
@ -2931,6 +2964,130 @@ impl GitGpuiView {
|
|||
.detach();
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn maybe_auto_install_linux_desktop_integration(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
use std::path::PathBuf;
|
||||
|
||||
if std::env::var_os("GITGPUI_NO_DESKTOP_INSTALL").is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
|
||||
if !desktop.to_ascii_lowercase().contains("gnome") {
|
||||
return;
|
||||
}
|
||||
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from);
|
||||
let data_home = std::env::var_os("XDG_DATA_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| home.as_ref().map(|h| h.join(".local/share")));
|
||||
let Some(data_home) = data_home else {
|
||||
return;
|
||||
};
|
||||
|
||||
let desktop_path = data_home.join("applications/gitgpui.desktop");
|
||||
let icon_path = data_home.join("icons/hicolor/scalable/apps/gitgpui.svg");
|
||||
if desktop_path.exists() && icon_path.exists() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.install_linux_desktop_integration(cx);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
fn install_linux_desktop_integration(&mut self, cx: &mut gpui::Context<Self>) {
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
const DESKTOP_TEMPLATE: &str = include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../assets/linux/gitgpui.desktop"
|
||||
));
|
||||
const ICON_SVG: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../../assets/gitgpui_logo.svg"
|
||||
));
|
||||
|
||||
let Ok(exe) = std::env::current_exe() else {
|
||||
self.push_toast(
|
||||
zed::ToastKind::Error,
|
||||
"Desktop install failed: could not resolve executable path".to_string(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let home = std::env::var_os("HOME").map(PathBuf::from);
|
||||
let data_home = std::env::var_os("XDG_DATA_HOME")
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| home.as_ref().map(|h| h.join(".local/share")));
|
||||
let Some(data_home) = data_home else {
|
||||
self.push_toast(
|
||||
zed::ToastKind::Error,
|
||||
"Desktop install failed: HOME/XDG_DATA_HOME not set".to_string(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let applications_dir = data_home.join("applications");
|
||||
let icons_dir = data_home.join("icons/hicolor/scalable/apps");
|
||||
let desktop_path = applications_dir.join("gitgpui.desktop");
|
||||
let icon_path = icons_dir.join("gitgpui.svg");
|
||||
|
||||
if let Err(e) =
|
||||
fs::create_dir_all(&applications_dir).and_then(|_| fs::create_dir_all(&icons_dir))
|
||||
{
|
||||
self.push_toast(
|
||||
zed::ToastKind::Error,
|
||||
format!("Desktop install failed: {e}"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut desktop_out = String::new();
|
||||
for line in DESKTOP_TEMPLATE.lines() {
|
||||
if line.starts_with("Exec=") {
|
||||
desktop_out.push_str("Exec=");
|
||||
desktop_out.push_str(&exe.display().to_string());
|
||||
desktop_out.push('\n');
|
||||
} else {
|
||||
desktop_out.push_str(line);
|
||||
desktop_out.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = fs::write(&desktop_path, desktop_out.as_bytes())
|
||||
.and_then(|_| fs::write(&icon_path, ICON_SVG))
|
||||
{
|
||||
self.push_toast(
|
||||
zed::ToastKind::Error,
|
||||
format!("Desktop install failed: {e}"),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let _ = Command::new("update-desktop-database")
|
||||
.arg(&applications_dir)
|
||||
.output();
|
||||
let _ = Command::new("gtk-update-icon-cache")
|
||||
.arg(data_home.join("icons/hicolor"))
|
||||
.output();
|
||||
|
||||
self.push_toast(
|
||||
zed::ToastKind::Success,
|
||||
format!(
|
||||
"Installed desktop entry + icon to:\n{}\n{}\n\nIf GNOME still shows a generic icon, log out/in (or restart GNOME Shell).",
|
||||
desktop_path.display(),
|
||||
icon_path.display()
|
||||
),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn rebuild_diff_word_highlights(&mut self) {
|
||||
self.diff_word_highlights.clear();
|
||||
self.diff_word_highlights
|
||||
|
|
|
|||
|
|
@ -216,7 +216,7 @@ impl GitGpuiView {
|
|||
.active_repo()
|
||||
.map(|r| match &r.head_branch {
|
||||
Loadable::Ready(name) => name.clone().into(),
|
||||
Loadable::Loading => "…".into(),
|
||||
Loadable::Loading => "".into(),
|
||||
Loadable::Error(_) => "error".into(),
|
||||
Loadable::NotLoaded => "—".into(),
|
||||
})
|
||||
|
|
@ -373,10 +373,67 @@ impl GitGpuiView {
|
|||
push = push.end_slot(count_badge(push_count, push_color));
|
||||
}
|
||||
let push = push
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
if let Some(repo_id) = this.active_repo_id() {
|
||||
this.store.dispatch(Msg::Push { repo_id });
|
||||
.on_click(theme, cx, |this, e, window, cx| {
|
||||
let Some(repo) = this.active_repo() else {
|
||||
return;
|
||||
};
|
||||
let repo_id = repo.id;
|
||||
let head = match &repo.head_branch {
|
||||
Loadable::Ready(head) => head.clone(),
|
||||
_ => {
|
||||
this.store.dispatch(Msg::Push { repo_id });
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let upstream_missing = match &repo.branches {
|
||||
Loadable::Ready(branches) => branches
|
||||
.iter()
|
||||
.find(|b| b.name == head)
|
||||
.is_some_and(|b| b.upstream.is_none()),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if upstream_missing {
|
||||
let remote = match &repo.remotes {
|
||||
Loadable::Ready(remotes) => {
|
||||
if remotes.is_empty() {
|
||||
None
|
||||
} else if remotes.iter().any(|r| r.name == "origin") {
|
||||
Some("origin".to_string())
|
||||
} else {
|
||||
Some(remotes[0].name.clone())
|
||||
}
|
||||
}
|
||||
_ => Some("origin".to_string()),
|
||||
};
|
||||
|
||||
if let Some(remote) = remote {
|
||||
this.push_upstream_branch_input
|
||||
.update(cx, |i, cx| i.set_text(head, cx));
|
||||
let focus = this
|
||||
.push_upstream_branch_input
|
||||
.read_with(cx, |i, _| i.focus_handle());
|
||||
window.focus(&focus);
|
||||
this.open_popover_at(
|
||||
PopoverKind::PushSetUpstreamPrompt { repo_id, remote },
|
||||
e.position(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.push_toast(
|
||||
zed::ToastKind::Error,
|
||||
"Cannot push: no remotes configured".to_string(),
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.store.dispatch(Msg::Push { repo_id });
|
||||
cx.notify();
|
||||
})
|
||||
.on_hover(cx.listener(move |this, hovering: &bool, _w, cx| {
|
||||
|
|
@ -404,7 +461,7 @@ impl GitGpuiView {
|
|||
})
|
||||
.on_hover(cx.listener(move |this, hovering: &bool, _w, cx| {
|
||||
let text: SharedString = if can_stash {
|
||||
"Create stash…".into()
|
||||
"Create stash".into()
|
||||
} else {
|
||||
"No changes to stash".into()
|
||||
};
|
||||
|
|
@ -429,7 +486,7 @@ impl GitGpuiView {
|
|||
this.open_popover_at(PopoverKind::CreateBranch, e.position(), window, cx);
|
||||
})
|
||||
.on_hover(cx.listener(|this, hovering: &bool, _w, cx| {
|
||||
let text: SharedString = "Create branch…".into();
|
||||
let text: SharedString = "Create branch".into();
|
||||
if *hovering {
|
||||
this.tooltip_text = Some(text);
|
||||
} else if this.tooltip_text.as_ref() == Some(&text) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ impl GitGpuiView {
|
|||
.min_h(px(0.0))
|
||||
.track_scroll(self.branches_scroll.clone());
|
||||
let scroll_handle = self.branches_scroll.0.borrow().base_handle.clone();
|
||||
let list = div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.flex_1()
|
||||
.min_h(px(0.0))
|
||||
.px(px(2.0))
|
||||
.child(list)
|
||||
.into_any_element();
|
||||
let panel_body: AnyElement = div()
|
||||
.id("branch_sidebar_scroll_container")
|
||||
.relative()
|
||||
|
|
@ -59,8 +67,7 @@ impl GitGpuiView {
|
|||
.and_then(|r| r.selected_commit.clone())
|
||||
});
|
||||
|
||||
if let (Some(repo_id), Some(selected_id)) = (active_repo_id, selected_id)
|
||||
{
|
||||
if let (Some(repo_id), Some(selected_id)) = (active_repo_id, selected_id) {
|
||||
let show_delayed_loading = self.commit_details_delay.as_ref().is_some_and(|s| {
|
||||
s.repo_id == repo_id && s.commit_id == selected_id && s.show_loading
|
||||
});
|
||||
|
|
@ -116,7 +123,7 @@ impl GitGpuiView {
|
|||
None => zed::empty_state(theme, "Commit", "No repository.").into_any_element(),
|
||||
Some(Loadable::Loading) => {
|
||||
if show_delayed_loading {
|
||||
zed::empty_state(theme, "Commit", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "Commit", "Loading").into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
|
|
@ -126,7 +133,7 @@ impl GitGpuiView {
|
|||
}
|
||||
Some(Loadable::NotLoaded) => {
|
||||
if show_delayed_loading {
|
||||
zed::empty_state(theme, "Commit", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "Commit", "Loading").into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
|
|
@ -134,7 +141,7 @@ impl GitGpuiView {
|
|||
Some(Loadable::Ready(details)) => {
|
||||
if &details.id != &selected_id {
|
||||
if show_delayed_loading {
|
||||
zed::empty_state(theme, "Commit", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "Commit", "Loading").into_any_element()
|
||||
} else {
|
||||
let parent = details
|
||||
.parent_ids
|
||||
|
|
@ -150,13 +157,16 @@ impl GitGpuiView {
|
|||
.into_any_element()
|
||||
} else {
|
||||
let total_files = details.files.len();
|
||||
let list_h = px((total_files as f32 * 24.0)
|
||||
.min(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX)
|
||||
.max(24.0));
|
||||
let list = uniform_list(
|
||||
("commit_details_files_list", repo_id.0),
|
||||
total_files,
|
||||
cx.processor(Self::render_commit_file_rows),
|
||||
)
|
||||
.w_full()
|
||||
.max_h(px(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX))
|
||||
.h(list_h)
|
||||
.track_scroll(self.commit_files_scroll.clone());
|
||||
let scroll_handle =
|
||||
self.commit_files_scroll.0.borrow().base_handle.clone();
|
||||
|
|
@ -165,7 +175,7 @@ impl GitGpuiView {
|
|||
.id(("commit_details_files_container", repo_id.0))
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_h(px(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX))
|
||||
.h(list_h)
|
||||
.child(list)
|
||||
.child(
|
||||
zed::Scrollbar::new(
|
||||
|
|
@ -254,21 +264,25 @@ impl GitGpuiView {
|
|||
.into_any_element()
|
||||
} else {
|
||||
let total_files = details.files.len();
|
||||
let list_h = px((total_files as f32 * 24.0)
|
||||
.min(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX)
|
||||
.max(24.0));
|
||||
let list = uniform_list(
|
||||
("commit_details_files_list", repo_id.0),
|
||||
total_files,
|
||||
cx.processor(Self::render_commit_file_rows),
|
||||
)
|
||||
.w_full()
|
||||
.max_h(px(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX))
|
||||
.h(list_h)
|
||||
.track_scroll(self.commit_files_scroll.clone());
|
||||
let scroll_handle = self.commit_files_scroll.0.borrow().base_handle.clone();
|
||||
let scroll_handle =
|
||||
self.commit_files_scroll.0.borrow().base_handle.clone();
|
||||
|
||||
div()
|
||||
.id(("commit_details_files_container", repo_id.0))
|
||||
.relative()
|
||||
.w_full()
|
||||
.max_h(px(COMMIT_DETAILS_FILES_MAX_HEIGHT_PX))
|
||||
.h(list_h)
|
||||
.child(list)
|
||||
.child(
|
||||
zed::Scrollbar::new(
|
||||
|
|
@ -531,7 +545,7 @@ impl GitGpuiView {
|
|||
.bg(theme.colors.surface_bg)
|
||||
.px_2()
|
||||
.py_2()
|
||||
.child(self.commit_box(cx)),
|
||||
.child(self.commit_box(staged_count > 0, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
|
|
@ -591,7 +605,11 @@ impl GitGpuiView {
|
|||
}
|
||||
}
|
||||
|
||||
pub(in super::super) fn commit_box(&mut self, cx: &mut gpui::Context<Self>) -> gpui::Div {
|
||||
pub(in super::super) fn commit_box(
|
||||
&mut self,
|
||||
can_commit: bool,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) -> gpui::Div {
|
||||
let theme = self.theme;
|
||||
div()
|
||||
.flex()
|
||||
|
|
@ -612,6 +630,7 @@ impl GitGpuiView {
|
|||
.child(
|
||||
zed::Button::new("commit", "Commit")
|
||||
.style(zed::ButtonStyle::Filled)
|
||||
.disabled(!can_commit)
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
let Some(repo_id) = this.active_repo_id() else {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ impl GitGpuiView {
|
|||
match repo.map(|r| &r.log) {
|
||||
None => zed::empty_state(theme, "History", "No repository.").into_any_element(),
|
||||
Some(Loadable::Loading) => {
|
||||
zed::empty_state(theme, "History", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "History", "Loading").into_any_element()
|
||||
}
|
||||
Some(Loadable::Error(e)) => {
|
||||
zed::empty_state(theme, "History", e.clone()).into_any_element()
|
||||
|
|
@ -50,17 +50,13 @@ impl GitGpuiView {
|
|||
} else {
|
||||
self.history_search_debounced.is_empty()
|
||||
};
|
||||
let should_load_more = state.last_item_size.is_some()
|
||||
&& repo.is_some_and(|repo| {
|
||||
!repo.log_loading_more
|
||||
&& matches!(&repo.log, Loadable::Ready(page) if page.next_cursor.is_some())
|
||||
})
|
||||
&& should_load_by_scroll;
|
||||
let should_load_more = state.last_item_size.is_some() && repo.is_some_and(|repo| {
|
||||
!repo.log_loading_more
|
||||
&& matches!(&repo.log, Loadable::Ready(page) if page.next_cursor.is_some())
|
||||
}) && should_load_by_scroll;
|
||||
(scroll_handle, should_load_more)
|
||||
};
|
||||
if should_load_more
|
||||
&& let Some(repo_id) = self.active_repo_id()
|
||||
{
|
||||
if should_load_more && let Some(repo_id) = self.active_repo_id() {
|
||||
self.store.dispatch(Msg::LoadMoreHistory { repo_id });
|
||||
}
|
||||
div()
|
||||
|
|
@ -209,6 +205,14 @@ impl GitGpuiView {
|
|||
|
||||
let repo = self.active_repo();
|
||||
|
||||
let diff_nav_hotkey_hint = |label: &'static str| {
|
||||
div()
|
||||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child(label)
|
||||
};
|
||||
|
||||
let mut controls = div().flex().items_center().gap_1();
|
||||
if !is_file_preview {
|
||||
let nav_entries = self.diff_nav_entries();
|
||||
|
|
@ -265,6 +269,7 @@ impl GitGpuiView {
|
|||
)
|
||||
.child(
|
||||
zed::Button::new("diff_prev_hunk", "Prev")
|
||||
.end_slot(diff_nav_hotkey_hint("F2"))
|
||||
.style(zed::ButtonStyle::Outlined)
|
||||
.disabled(!can_nav_prev)
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
|
|
@ -272,7 +277,8 @@ impl GitGpuiView {
|
|||
cx.notify();
|
||||
})
|
||||
.on_hover(cx.listener(|this, hovering: &bool, _w, cx| {
|
||||
let text: SharedString = "Previous change (Shift+F7 / Alt+Up)".into();
|
||||
let text: SharedString =
|
||||
"Previous change (F2 / Shift+F7 / Alt+Up)".into();
|
||||
if *hovering {
|
||||
this.tooltip_text = Some(text);
|
||||
} else if this.tooltip_text.as_ref() == Some(&text) {
|
||||
|
|
@ -283,6 +289,7 @@ impl GitGpuiView {
|
|||
)
|
||||
.child(
|
||||
zed::Button::new("diff_next_hunk", "Next")
|
||||
.end_slot(diff_nav_hotkey_hint("F3"))
|
||||
.style(zed::ButtonStyle::Outlined)
|
||||
.disabled(!can_nav_next)
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
|
|
@ -290,7 +297,7 @@ impl GitGpuiView {
|
|||
cx.notify();
|
||||
})
|
||||
.on_hover(cx.listener(|this, hovering: &bool, _w, cx| {
|
||||
let text: SharedString = "Next change (F7 / Alt+Down)".into();
|
||||
let text: SharedString = "Next change (F3 / F7 / Alt+Down)".into();
|
||||
if *hovering {
|
||||
this.tooltip_text = Some(text);
|
||||
} else if this.tooltip_text.as_ref() == Some(&text) {
|
||||
|
|
@ -301,7 +308,7 @@ impl GitGpuiView {
|
|||
)
|
||||
.when(!wants_file_diff, |controls| {
|
||||
controls.child(
|
||||
zed::Button::new("diff_hunks", "Hunks…")
|
||||
zed::Button::new("diff_hunks", "Hunks")
|
||||
.style(zed::ButtonStyle::Outlined)
|
||||
.on_click(theme, cx, |this, e, window, cx| {
|
||||
let _ = this.ensure_diff_hunk_picker_search_input(window, cx);
|
||||
|
|
@ -310,7 +317,7 @@ impl GitGpuiView {
|
|||
cx.notify();
|
||||
})
|
||||
.on_hover(cx.listener(|this, hovering: &bool, _w, cx| {
|
||||
let text: SharedString = "Jump to hunk… (Alt+H)".into();
|
||||
let text: SharedString = "Jump to hunk (Alt+H)".into();
|
||||
if *hovering {
|
||||
this.tooltip_text = Some(text);
|
||||
} else if this.tooltip_text.as_ref() == Some(&text) {
|
||||
|
|
@ -363,7 +370,7 @@ impl GitGpuiView {
|
|||
}
|
||||
match &self.worktree_preview {
|
||||
Loadable::NotLoaded | Loadable::Loading => {
|
||||
zed::empty_state(theme, "File", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "File", "Loading").into_any_element()
|
||||
}
|
||||
Loadable::Error(e) => {
|
||||
self.diff_raw_input.update(cx, |input, cx| {
|
||||
|
|
@ -395,7 +402,8 @@ impl GitGpuiView {
|
|||
.min_h(px(0.0))
|
||||
.track_scroll(self.worktree_preview_scroll.clone());
|
||||
|
||||
let scroll_handle = self.worktree_preview_scroll.0.borrow().base_handle.clone();
|
||||
let scroll_handle =
|
||||
self.worktree_preview_scroll.0.borrow().base_handle.clone();
|
||||
div()
|
||||
.id("worktree_preview_scroll_container")
|
||||
.debug_selector(|| "worktree_preview_scroll_container".to_string())
|
||||
|
|
@ -419,7 +427,7 @@ impl GitGpuiView {
|
|||
zed::empty_state(theme, "Diff", "Select a file.").into_any_element()
|
||||
}
|
||||
Loadable::Loading => {
|
||||
zed::empty_state(theme, "Diff", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "Diff", "Loading").into_any_element()
|
||||
}
|
||||
Loadable::Error(e) => {
|
||||
self.diff_raw_input.update(cx, |input, cx| {
|
||||
|
|
@ -463,7 +471,7 @@ impl GitGpuiView {
|
|||
.into_any_element()
|
||||
}
|
||||
DiffFileState::Loading => {
|
||||
zed::empty_state(theme, "Diff", "Loading…").into_any_element()
|
||||
zed::empty_state(theme, "Diff", "Loading").into_any_element()
|
||||
}
|
||||
DiffFileState::Error(e) => {
|
||||
self.diff_raw_input.update(cx, |input, cx| {
|
||||
|
|
@ -760,7 +768,7 @@ impl GitGpuiView {
|
|||
.w_full()
|
||||
.h_full()
|
||||
.min_h(px(0.0))
|
||||
.bg(theme.colors.surface_bg)
|
||||
.bg(theme.colors.surface_bg_elevated)
|
||||
.track_focus(&self.diff_panel_focus_handle)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
|
|
|
|||
|
|
@ -24,6 +24,11 @@ enum ContextMenuAction {
|
|||
repo_id: RepoId,
|
||||
name: String,
|
||||
},
|
||||
CheckoutRemoteBranch {
|
||||
repo_id: RepoId,
|
||||
remote: String,
|
||||
name: String,
|
||||
},
|
||||
SetHistoryScope {
|
||||
repo_id: RepoId,
|
||||
scope: gitgpui_core::domain::LogScope,
|
||||
|
|
|
|||
|
|
@ -247,9 +247,25 @@ impl GitGpuiView {
|
|||
icon: Some("⎇".into()),
|
||||
shortcut: Some("Enter".into()),
|
||||
disabled: false,
|
||||
action: ContextMenuAction::CheckoutBranch {
|
||||
repo_id: *repo_id,
|
||||
name: name.clone(),
|
||||
action: match section {
|
||||
BranchSection::Local => ContextMenuAction::CheckoutBranch {
|
||||
repo_id: *repo_id,
|
||||
name: name.clone(),
|
||||
},
|
||||
BranchSection::Remote => {
|
||||
if let Some((remote, branch)) = name.split_once('/') {
|
||||
ContextMenuAction::CheckoutRemoteBranch {
|
||||
repo_id: *repo_id,
|
||||
remote: remote.to_string(),
|
||||
name: branch.to_string(),
|
||||
}
|
||||
} else {
|
||||
ContextMenuAction::CheckoutBranch {
|
||||
repo_id: *repo_id,
|
||||
name: name.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
items.push(ContextMenuItem::Entry {
|
||||
|
|
@ -305,7 +321,7 @@ impl GitGpuiView {
|
|||
let mut items = vec![ContextMenuItem::Header(header)];
|
||||
items.push(ContextMenuItem::Separator);
|
||||
items.push(ContextMenuItem::Entry {
|
||||
label: "Switch branch…".into(),
|
||||
label: "Switch branch".into(),
|
||||
icon: Some("⎇".into()),
|
||||
shortcut: Some("Enter".into()),
|
||||
disabled: false,
|
||||
|
|
@ -418,6 +434,18 @@ impl GitGpuiView {
|
|||
self.store.dispatch(Msg::CheckoutBranch { repo_id, name });
|
||||
self.rebuild_diff_cache();
|
||||
}
|
||||
ContextMenuAction::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
} => {
|
||||
self.store.dispatch(Msg::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote,
|
||||
name,
|
||||
});
|
||||
self.rebuild_diff_cache();
|
||||
}
|
||||
ContextMenuAction::SetHistoryScope { repo_id, scope } => {
|
||||
self.store.dispatch(Msg::SetHistoryScope { repo_id, scope });
|
||||
}
|
||||
|
|
@ -636,6 +664,8 @@ impl GitGpuiView {
|
|||
let (show_date, show_sha) = self.history_visible_columns();
|
||||
let col_date = self.history_col_date;
|
||||
let col_sha = self.history_col_sha;
|
||||
let handle_w = px(HISTORY_COL_HANDLE_PX);
|
||||
let handle_half = px(HISTORY_COL_HANDLE_PX / 2.0);
|
||||
let scope_label: SharedString = self
|
||||
.active_repo()
|
||||
.map(|r| match r.history_scope {
|
||||
|
|
@ -649,8 +679,10 @@ impl GitGpuiView {
|
|||
let resize_handle = |id: &'static str, handle: HistoryColResizeHandle| {
|
||||
div()
|
||||
.id(id)
|
||||
.w(px(HISTORY_COL_HANDLE_PX))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.w(handle_w)
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
|
|
@ -732,6 +764,7 @@ impl GitGpuiView {
|
|||
};
|
||||
|
||||
let mut header = div()
|
||||
.relative()
|
||||
.flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
|
|
@ -799,10 +832,6 @@ impl GitGpuiView {
|
|||
})),
|
||||
),
|
||||
)
|
||||
.child(resize_handle(
|
||||
"history_col_resize_branch",
|
||||
HistoryColResizeHandle::Branch,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.w(self.history_col_graph)
|
||||
|
|
@ -811,10 +840,6 @@ impl GitGpuiView {
|
|||
.whitespace_nowrap()
|
||||
.child("GRAPH"),
|
||||
)
|
||||
.child(resize_handle(
|
||||
"history_col_resize_graph",
|
||||
HistoryColResizeHandle::Graph,
|
||||
))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
|
|
@ -824,10 +849,6 @@ impl GitGpuiView {
|
|||
);
|
||||
|
||||
if show_date {
|
||||
header = header.child(resize_handle(
|
||||
"history_col_resize_message",
|
||||
HistoryColResizeHandle::Message,
|
||||
));
|
||||
header = header.child(
|
||||
div()
|
||||
.w(col_date)
|
||||
|
|
@ -839,10 +860,6 @@ impl GitGpuiView {
|
|||
}
|
||||
|
||||
if show_sha {
|
||||
header = header.child(resize_handle(
|
||||
"history_col_resize_date",
|
||||
HistoryColResizeHandle::Date,
|
||||
));
|
||||
header = header.child(
|
||||
div()
|
||||
.w(col_sha)
|
||||
|
|
@ -853,7 +870,36 @@ impl GitGpuiView {
|
|||
);
|
||||
}
|
||||
|
||||
header
|
||||
let mut header_with_handles = header
|
||||
.child(
|
||||
resize_handle("history_col_resize_branch", HistoryColResizeHandle::Branch)
|
||||
.left((self.history_col_branch - handle_half).max(px(0.0))),
|
||||
)
|
||||
.child(
|
||||
resize_handle("history_col_resize_graph", HistoryColResizeHandle::Graph).left(
|
||||
(self.history_col_branch + self.history_col_graph - handle_half).max(px(0.0)),
|
||||
),
|
||||
);
|
||||
|
||||
if show_date {
|
||||
let right_fixed = col_date + if show_sha { col_sha } else { px(0.0) };
|
||||
header_with_handles = header_with_handles.child(
|
||||
resize_handle(
|
||||
"history_col_resize_message",
|
||||
HistoryColResizeHandle::Message,
|
||||
)
|
||||
.right((right_fixed - handle_half).max(px(0.0))),
|
||||
);
|
||||
}
|
||||
|
||||
if show_sha {
|
||||
header_with_handles = header_with_handles.child(
|
||||
resize_handle("history_col_resize_date", HistoryColResizeHandle::Date)
|
||||
.right((col_sha - handle_half).max(px(0.0))),
|
||||
);
|
||||
}
|
||||
|
||||
header_with_handles
|
||||
}
|
||||
|
||||
pub(in super::super) fn popover_view(
|
||||
|
|
@ -867,10 +913,17 @@ impl GitGpuiView {
|
|||
.unwrap_or_else(|| point(px(64.0), px(64.0)));
|
||||
|
||||
let is_app_menu = matches!(&kind, PopoverKind::AppMenu);
|
||||
let popover_use_surface_bg = matches!(
|
||||
&kind,
|
||||
PopoverKind::CreateBranch
|
||||
| PopoverKind::StashPrompt
|
||||
| PopoverKind::PushSetUpstreamPrompt { .. }
|
||||
);
|
||||
let anchor_corner = match &kind {
|
||||
PopoverKind::PullPicker
|
||||
| PopoverKind::CreateBranch
|
||||
| PopoverKind::StashPrompt
|
||||
| PopoverKind::PushSetUpstreamPrompt { .. }
|
||||
| PopoverKind::HistoryBranchFilter { .. } => Corner::TopRight,
|
||||
_ => Corner::TopLeft,
|
||||
};
|
||||
|
|
@ -1000,7 +1053,7 @@ impl GitGpuiView {
|
|||
}
|
||||
}
|
||||
Loadable::Loading => {
|
||||
menu = menu.child(zed::context_menu_label(theme, "Loading…"));
|
||||
menu = menu.child(zed::context_menu_label(theme, "Loading"));
|
||||
}
|
||||
Loadable::Error(e) => {
|
||||
menu = menu.child(zed::context_menu_label(theme, e.clone()));
|
||||
|
|
@ -1011,40 +1064,9 @@ impl GitGpuiView {
|
|||
}
|
||||
}
|
||||
|
||||
zed::context_menu(
|
||||
theme,
|
||||
menu.child(zed::context_menu_separator(theme))
|
||||
.child(zed::context_menu_header(theme, "Create branch"))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.w_full()
|
||||
.min_w(px(0.0))
|
||||
.child(self.create_branch_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div().px_2().child(
|
||||
zed::Button::new("create_branch_go", "Create")
|
||||
.style(zed::ButtonStyle::Filled)
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
let name = this
|
||||
.create_branch_input
|
||||
.read_with(cx, |i, _| i.text().trim().to_string());
|
||||
if let Some(repo_id) = this.active_repo_id()
|
||||
&& !name.is_empty()
|
||||
{
|
||||
this.store
|
||||
.dispatch(Msg::CreateBranch { repo_id, name });
|
||||
}
|
||||
this.popover = None;
|
||||
this.popover_anchor = None;
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.min_w(px(240.0))
|
||||
.max_w(px(420.0))
|
||||
zed::context_menu(theme, menu)
|
||||
.min_w(px(240.0))
|
||||
.max_w(px(420.0))
|
||||
}
|
||||
PopoverKind::CreateBranch => div()
|
||||
.flex()
|
||||
|
|
@ -1163,6 +1185,75 @@ impl GitGpuiView {
|
|||
}),
|
||||
),
|
||||
),
|
||||
PopoverKind::PushSetUpstreamPrompt { repo_id, remote } => {
|
||||
let remote = remote.clone();
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.min_w(px(320.0))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.child("Set upstream and push"),
|
||||
)
|
||||
.child(div().border_t_1().border_color(theme.colors.border))
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child(format!("Remote: {remote}")),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.w_full()
|
||||
.min_w(px(0.0))
|
||||
.child(self.push_upstream_branch_input.clone()),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
zed::Button::new("push_upstream_cancel", "Cancel")
|
||||
.style(zed::ButtonStyle::Outlined)
|
||||
.on_click(theme, cx, |this, _e, _w, cx| {
|
||||
this.popover = None;
|
||||
this.popover_anchor = None;
|
||||
cx.notify();
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
zed::Button::new("push_upstream_go", "Push")
|
||||
.style(zed::ButtonStyle::Filled)
|
||||
.on_click(theme, cx, move |this, _e, _w, cx| {
|
||||
let branch = this
|
||||
.push_upstream_branch_input
|
||||
.read_with(cx, |i, _| i.text().trim().to_string());
|
||||
if branch.is_empty() {
|
||||
return;
|
||||
}
|
||||
this.store.dispatch(Msg::PushSetUpstream {
|
||||
repo_id,
|
||||
remote: remote.clone(),
|
||||
branch,
|
||||
});
|
||||
this.popover = None;
|
||||
this.popover_anchor = None;
|
||||
cx.notify();
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
PopoverKind::HistoryBranchFilter { repo_id } => self
|
||||
.context_menu_view(PopoverKind::HistoryBranchFilter { repo_id }, cx)
|
||||
.min_w(px(160.0))
|
||||
|
|
@ -1336,34 +1427,62 @@ impl GitGpuiView {
|
|||
},
|
||||
cx,
|
||||
),
|
||||
PopoverKind::AppMenu => div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.min_w(px(200.0))
|
||||
.child(
|
||||
div()
|
||||
.id("app_menu_quit")
|
||||
.debug_selector(|| "app_menu_quit".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("Quit")
|
||||
.on_click(cx.listener(|_this, _e: &ClickEvent, _w, cx| {
|
||||
cx.quit();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("app_menu_close")
|
||||
.debug_selector(|| "app_menu_close".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("Close")
|
||||
.on_click(close),
|
||||
),
|
||||
PopoverKind::AppMenu => {
|
||||
let mut install_desktop = div()
|
||||
.id("app_menu_install_desktop")
|
||||
.debug_selector(|| "app_menu_install_desktop".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("Install desktop integration");
|
||||
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
install_desktop =
|
||||
install_desktop.on_click(cx.listener(|this, _e: &ClickEvent, _w, cx| {
|
||||
this.install_linux_desktop_integration(cx);
|
||||
this.popover = None;
|
||||
this.popover_anchor = None;
|
||||
cx.notify();
|
||||
}));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||
{
|
||||
install_desktop = install_desktop.text_color(theme.colors.text_muted);
|
||||
}
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.min_w(px(200.0))
|
||||
.child(install_desktop)
|
||||
.child(
|
||||
div()
|
||||
.id("app_menu_quit")
|
||||
.debug_selector(|| "app_menu_quit".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("Quit")
|
||||
.on_click(cx.listener(|_this, _e: &ClickEvent, _w, cx| {
|
||||
cx.quit();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("app_menu_close")
|
||||
.debug_selector(|| "app_menu_close".to_string())
|
||||
.px_2()
|
||||
.py_1()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
.child("Close")
|
||||
.on_click(close),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let offset_y = if is_app_menu {
|
||||
|
|
@ -1384,7 +1503,11 @@ impl GitGpuiView {
|
|||
.debug_selector(|| "app_popover".to_string())
|
||||
.on_any_mouse_down(|_e, _w, cx| cx.stop_propagation())
|
||||
.occlude()
|
||||
.bg(theme.colors.surface_bg_elevated)
|
||||
.bg(if popover_use_surface_bg {
|
||||
theme.colors.surface_bg
|
||||
} else {
|
||||
theme.colors.surface_bg_elevated
|
||||
})
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
.rounded(px(theme.radii.panel))
|
||||
|
|
|
|||
|
|
@ -30,10 +30,7 @@ fn file_preview_renders_scrollable_syntax_highlighted_rows(cx: &mut gpui::TestAp
|
|||
});
|
||||
|
||||
let repo_id = gitgpui_state::model::RepoId(1);
|
||||
let workdir = std::env::temp_dir().join(format!(
|
||||
"gitgpui_ui_test_{}",
|
||||
std::process::id()
|
||||
));
|
||||
let workdir = std::env::temp_dir().join(format!("gitgpui_ui_test_{}", std::process::id()));
|
||||
let file_rel = std::path::PathBuf::from("preview.rs");
|
||||
let lines: Arc<Vec<String>> = Arc::new(
|
||||
(0..300)
|
||||
|
|
@ -45,7 +42,9 @@ fn file_preview_renders_scrollable_syntax_highlighted_rows(cx: &mut gpui::TestAp
|
|||
view.update(app, |this, cx| {
|
||||
let mut repo = gitgpui_state::model::RepoState::new_opening(
|
||||
repo_id,
|
||||
gitgpui_core::domain::RepoSpec { workdir: workdir.clone() },
|
||||
gitgpui_core::domain::RepoSpec {
|
||||
workdir: workdir.clone(),
|
||||
},
|
||||
);
|
||||
repo.status = gitgpui_state::model::Loadable::Ready(gitgpui_core::domain::RepoStatus {
|
||||
staged: vec![],
|
||||
|
|
@ -108,10 +107,8 @@ fn patch_view_applies_syntax_highlighting_to_context_lines(cx: &mut gpui::TestAp
|
|||
});
|
||||
|
||||
let repo_id = gitgpui_state::model::RepoId(2);
|
||||
let workdir = std::env::temp_dir().join(format!(
|
||||
"gitgpui_ui_test_{}_patch",
|
||||
std::process::id()
|
||||
));
|
||||
let workdir =
|
||||
std::env::temp_dir().join(format!("gitgpui_ui_test_{}_patch", std::process::id()));
|
||||
|
||||
cx.update(|_window, app| {
|
||||
view.update(app, |this, cx| {
|
||||
|
|
@ -140,7 +137,9 @@ fn patch_view_applies_syntax_highlighting_to_context_lines(cx: &mut gpui::TestAp
|
|||
|
||||
let mut repo = gitgpui_state::model::RepoState::new_opening(
|
||||
repo_id,
|
||||
gitgpui_core::domain::RepoSpec { workdir: workdir.clone() },
|
||||
gitgpui_core::domain::RepoSpec {
|
||||
workdir: workdir.clone(),
|
||||
},
|
||||
);
|
||||
repo.status = gitgpui_state::model::Loadable::Ready(Default::default());
|
||||
repo.diff_target = Some(target);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ use super::*;
|
|||
use crate::theme::AppTheme;
|
||||
use crate::view::history_graph;
|
||||
use gitgpui_core::domain::{
|
||||
Branch, Commit, CommitDetails, CommitFileChange, CommitId, FileStatusKind, Remote, RemoteBranch,
|
||||
RepoSpec, Upstream, UpstreamDivergence,
|
||||
Branch, Commit, CommitDetails, CommitFileChange, CommitId, FileStatusKind, Remote,
|
||||
RemoteBranch, RepoSpec, Upstream, UpstreamDivergence,
|
||||
};
|
||||
use gitgpui_state::model::{Loadable, RepoId, RepoState};
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
|
|
@ -18,10 +18,16 @@ pub struct OpenRepoFixture {
|
|||
}
|
||||
|
||||
impl OpenRepoFixture {
|
||||
pub fn new(commits: usize, local_branches: usize, remote_branches: usize, remotes: usize) -> Self {
|
||||
pub fn new(
|
||||
commits: usize,
|
||||
local_branches: usize,
|
||||
remote_branches: usize,
|
||||
remotes: usize,
|
||||
) -> Self {
|
||||
let theme = AppTheme::zed_ayu_dark();
|
||||
let commits_vec = build_synthetic_commits(commits);
|
||||
let repo = build_synthetic_repo_state(local_branches, remote_branches, remotes, &commits_vec);
|
||||
let repo =
|
||||
build_synthetic_repo_state(local_branches, remote_branches, remotes, &commits_vec);
|
||||
Self {
|
||||
repo,
|
||||
commits: commits_vec,
|
||||
|
|
@ -166,7 +172,10 @@ fn build_synthetic_repo_state(
|
|||
remote: "origin".to_string(),
|
||||
branch: head.clone(),
|
||||
}),
|
||||
divergence: Some(UpstreamDivergence { ahead: 1, behind: 2 }),
|
||||
divergence: Some(UpstreamDivergence {
|
||||
ahead: 1,
|
||||
behind: 2,
|
||||
}),
|
||||
});
|
||||
for ix in 0..local_branches.saturating_sub(1) {
|
||||
branches.push(Branch {
|
||||
|
|
@ -286,10 +295,15 @@ fn build_synthetic_source_lines(count: usize) -> Vec<String> {
|
|||
1 => format!("{indent}let value_{ix} = \"string {ix}\";"),
|
||||
2 => format!("{indent}// comment {ix} with some extra words and tokens"),
|
||||
3 => format!("{indent}if value_{ix} > 10 {{ return value_{ix}; }}"),
|
||||
4 => format!("{indent}for i in 0..{r} {{ sum += i; }}", r = (ix % 100) + 1),
|
||||
4 => format!(
|
||||
"{indent}for i in 0..{r} {{ sum += i; }}",
|
||||
r = (ix % 100) + 1
|
||||
),
|
||||
5 => format!("{indent}match tag_{ix} {{ Some(v) => v, None => 0 }}"),
|
||||
6 => format!("{indent}struct S{ix} {{ a: i32, b: String }}"),
|
||||
7 => format!("{indent}impl S{ix} {{ fn new() -> Self {{ Self {{ a: 0, b: String::new() }} }} }}"),
|
||||
7 => format!(
|
||||
"{indent}impl S{ix} {{ fn new() -> Self {{ Self {{ a: 0, b: String::new() }} }} }}"
|
||||
),
|
||||
8 => format!("{indent}const CONST_{ix}: u64 = {v};", v = ix as u64 * 31),
|
||||
_ => format!("{indent}println!(\"{ix} {{}}\", value_{ix});"),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,13 +16,16 @@ impl GitGpuiView {
|
|||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let empty_ranges: &[Range<usize>] = &[];
|
||||
let language = (this.file_diff_inline_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING)
|
||||
.then(|| {
|
||||
this.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()))
|
||||
})
|
||||
.flatten();
|
||||
let syntax_mode =
|
||||
if this.file_diff_inline_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let language = this
|
||||
.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()));
|
||||
|
||||
return range
|
||||
.map(|visible_ix| {
|
||||
|
|
@ -38,7 +41,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -57,7 +60,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -82,7 +85,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(inline_ix, computed);
|
||||
|
|
@ -96,7 +99,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let styled = this
|
||||
|
|
@ -124,7 +127,11 @@ impl GitGpuiView {
|
|||
this.diff_text_segments_cache.clear();
|
||||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let syntax_enabled = this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING;
|
||||
let syntax_mode = if this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
range
|
||||
.map(|visible_ix| {
|
||||
let selected = this
|
||||
|
|
@ -139,7 +146,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let click_kind = {
|
||||
|
|
@ -151,7 +158,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -174,14 +181,11 @@ impl GitGpuiView {
|
|||
|
||||
let file_stat = this.diff_file_stats.get(src_ix).and_then(|s| *s);
|
||||
|
||||
let language = if syntax_enabled {
|
||||
this.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language = this
|
||||
.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path);
|
||||
|
||||
if matches!(click_kind, DiffClickKind::Line)
|
||||
&& this.diff_text_segments_cache_get(src_ix).is_none()
|
||||
|
|
@ -194,7 +198,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -219,7 +223,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(src_ix, computed);
|
||||
|
|
@ -238,7 +242,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -271,13 +275,16 @@ impl GitGpuiView {
|
|||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let empty_ranges: &[Range<usize>] = &[];
|
||||
let language = (this.file_diff_cache_rows.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING)
|
||||
.then(|| {
|
||||
this.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()))
|
||||
})
|
||||
.flatten();
|
||||
let syntax_mode =
|
||||
if this.file_diff_cache_rows.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let language = this
|
||||
.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()));
|
||||
|
||||
return range
|
||||
.map(|visible_ix| {
|
||||
|
|
@ -293,7 +300,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let key = row_ix * 2;
|
||||
|
|
@ -306,7 +313,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -330,7 +337,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(key, computed);
|
||||
|
|
@ -345,7 +352,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let styled: Option<&CachedDiffStyledText> = row
|
||||
|
|
@ -373,7 +380,11 @@ impl GitGpuiView {
|
|||
this.diff_text_segments_cache.clear();
|
||||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let syntax_enabled = this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING;
|
||||
let syntax_mode = if this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let empty_ranges: &[Range<usize>] = &[];
|
||||
range
|
||||
.map(|visible_ix| {
|
||||
|
|
@ -389,7 +400,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let Some(row) = this.diff_split_cache.get(row_ix) else {
|
||||
|
|
@ -400,7 +411,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -419,19 +430,16 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
let text = row.old.as_deref().unwrap_or("");
|
||||
let language = if syntax_enabled {
|
||||
this.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language = this
|
||||
.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path);
|
||||
let language = this
|
||||
.diff_cache
|
||||
.get(src_ix)
|
||||
|
|
@ -469,7 +477,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(src_ix, computed);
|
||||
|
|
@ -486,7 +494,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -512,7 +520,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let file_stat = this.diff_file_stats.get(*src_ix).and_then(|s| *s);
|
||||
|
|
@ -546,13 +554,16 @@ impl GitGpuiView {
|
|||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let empty_ranges: &[Range<usize>] = &[];
|
||||
let language = (this.file_diff_cache_rows.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING)
|
||||
.then(|| {
|
||||
this.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()))
|
||||
})
|
||||
.flatten();
|
||||
let syntax_mode =
|
||||
if this.file_diff_cache_rows.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let language = this
|
||||
.file_diff_cache_path
|
||||
.as_ref()
|
||||
.and_then(|p| diff_syntax_language_for_path(p.to_string_lossy().as_ref()));
|
||||
|
||||
return range
|
||||
.map(|visible_ix| {
|
||||
|
|
@ -568,7 +579,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let key = row_ix * 2 + 1;
|
||||
|
|
@ -581,7 +592,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -605,7 +616,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(key, computed);
|
||||
|
|
@ -620,7 +631,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let styled: Option<&CachedDiffStyledText> = row
|
||||
|
|
@ -648,7 +659,11 @@ impl GitGpuiView {
|
|||
this.diff_text_segments_cache.clear();
|
||||
}
|
||||
let query = this.diff_visible_query.clone();
|
||||
let syntax_enabled = this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING;
|
||||
let syntax_mode = if this.diff_cache.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let empty_ranges: &[Range<usize>] = &[];
|
||||
range
|
||||
.map(|visible_ix| {
|
||||
|
|
@ -664,7 +679,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let Some(row) = this.diff_split_cache.get(row_ix) else {
|
||||
|
|
@ -675,7 +690,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -694,19 +709,16 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
let text = row.new.as_deref().unwrap_or("");
|
||||
let language = if syntax_enabled {
|
||||
this.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let language = this
|
||||
.diff_file_for_src_ix
|
||||
.get(src_ix)
|
||||
.and_then(|p| p.as_deref())
|
||||
.and_then(diff_syntax_language_for_path);
|
||||
let language = this
|
||||
.diff_cache
|
||||
.get(src_ix)
|
||||
|
|
@ -744,7 +756,7 @@ impl GitGpuiView {
|
|||
word_ranges,
|
||||
query.as_str(),
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
word_color,
|
||||
);
|
||||
this.diff_text_segments_cache_set(src_ix, computed);
|
||||
|
|
@ -761,7 +773,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
|
||||
|
|
@ -787,7 +799,7 @@ impl GitGpuiView {
|
|||
.font_family("monospace")
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.child("…")
|
||||
.child("")
|
||||
.into_any_element();
|
||||
};
|
||||
let file_stat = this.diff_file_stats.get(*src_ix).and_then(|s| *s);
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ pub(in super::super) enum DiffSyntaxLanguage {
|
|||
Json,
|
||||
Toml,
|
||||
Yaml,
|
||||
Sql,
|
||||
Bash,
|
||||
}
|
||||
|
||||
|
|
@ -94,6 +95,7 @@ pub(in super::super) fn diff_syntax_language_for_path(path: &str) -> Option<Diff
|
|||
"json" => DiffSyntaxLanguage::Json,
|
||||
"toml" => DiffSyntaxLanguage::Toml,
|
||||
"yaml" | "yml" => DiffSyntaxLanguage::Yaml,
|
||||
"sql" => DiffSyntaxLanguage::Sql,
|
||||
"sh" | "bash" | "zsh" => DiffSyntaxLanguage::Bash,
|
||||
_ => {
|
||||
if file_name == "makefile" || file_name == "gnumakefile" {
|
||||
|
|
@ -244,11 +246,12 @@ fn tree_sitter_language(language: DiffSyntaxLanguage) -> Option<tree_sitter::Lan
|
|||
DiffSyntaxLanguage::Php => return None,
|
||||
DiffSyntaxLanguage::Ruby => return None,
|
||||
DiffSyntaxLanguage::Json => tree_sitter_json::LANGUAGE.into(),
|
||||
DiffSyntaxLanguage::Yaml => tree_sitter_yaml::language(),
|
||||
DiffSyntaxLanguage::Yaml => tree_sitter_yaml::LANGUAGE.into(),
|
||||
DiffSyntaxLanguage::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
|
||||
DiffSyntaxLanguage::Tsx | DiffSyntaxLanguage::JavaScript => {
|
||||
tree_sitter_typescript::LANGUAGE_TSX.into()
|
||||
}
|
||||
DiffSyntaxLanguage::Sql => return None,
|
||||
DiffSyntaxLanguage::Bash => tree_sitter_bash::LANGUAGE.into(),
|
||||
DiffSyntaxLanguage::Toml => return None,
|
||||
})
|
||||
|
|
@ -337,7 +340,7 @@ fn tree_sitter_highlight_spec(
|
|||
}),
|
||||
DiffSyntaxLanguage::Yaml => YAML.get_or_init(|| {
|
||||
init(
|
||||
tree_sitter_yaml::language(),
|
||||
tree_sitter_yaml::LANGUAGE.into(),
|
||||
include_str!("../../../../../../zed/crates/languages/src/yaml/highlights.scm"),
|
||||
)
|
||||
}),
|
||||
|
|
@ -369,6 +372,7 @@ fn tree_sitter_highlight_spec(
|
|||
include_str!("../../../../../../zed/crates/languages/src/bash/highlights.scm"),
|
||||
)
|
||||
}),
|
||||
DiffSyntaxLanguage::Sql => return None,
|
||||
DiffSyntaxLanguage::Toml => return None,
|
||||
})
|
||||
}
|
||||
|
|
@ -450,6 +454,7 @@ fn syntax_tokens_for_line_heuristic(text: &str, language: DiffSyntaxLanguage) ->
|
|||
}
|
||||
DiffSyntaxLanguage::Bash => (None, Some('#'), false),
|
||||
DiffSyntaxLanguage::Makefile => (None, Some('#'), false),
|
||||
DiffSyntaxLanguage::Sql => (Some("--"), None, true),
|
||||
DiffSyntaxLanguage::Rust
|
||||
| DiffSyntaxLanguage::JavaScript
|
||||
| DiffSyntaxLanguage::TypeScript
|
||||
|
|
@ -536,6 +541,7 @@ fn syntax_tokens_for_line_heuristic(text: &str, language: DiffSyntaxLanguage) ->
|
|||
| DiffSyntaxLanguage::Tsx
|
||||
| DiffSyntaxLanguage::Go
|
||||
| DiffSyntaxLanguage::Bash
|
||||
| DiffSyntaxLanguage::Sql
|
||||
| DiffSyntaxLanguage::Plain
|
||||
))
|
||||
{
|
||||
|
|
@ -1087,6 +1093,80 @@ fn is_keyword(language: DiffSyntaxLanguage, ident: &str) -> bool {
|
|||
DiffSyntaxLanguage::Json => matches!(ident, "true" | "false" | "null"),
|
||||
DiffSyntaxLanguage::Toml => matches!(ident, "true" | "false"),
|
||||
DiffSyntaxLanguage::Yaml => matches!(ident, "true" | "false" | "null"),
|
||||
DiffSyntaxLanguage::Sql => matches!(
|
||||
ident.to_ascii_lowercase().as_str(),
|
||||
"add"
|
||||
| "all"
|
||||
| "alter"
|
||||
| "and"
|
||||
| "as"
|
||||
| "asc"
|
||||
| "begin"
|
||||
| "between"
|
||||
| "by"
|
||||
| "case"
|
||||
| "check"
|
||||
| "column"
|
||||
| "commit"
|
||||
| "constraint"
|
||||
| "create"
|
||||
| "cross"
|
||||
| "database"
|
||||
| "default"
|
||||
| "delete"
|
||||
| "desc"
|
||||
| "distinct"
|
||||
| "drop"
|
||||
| "else"
|
||||
| "end"
|
||||
| "exists"
|
||||
| "false"
|
||||
| "foreign"
|
||||
| "from"
|
||||
| "full"
|
||||
| "group"
|
||||
| "having"
|
||||
| "if"
|
||||
| "in"
|
||||
| "index"
|
||||
| "inner"
|
||||
| "insert"
|
||||
| "intersect"
|
||||
| "into"
|
||||
| "is"
|
||||
| "join"
|
||||
| "key"
|
||||
| "left"
|
||||
| "like"
|
||||
| "limit"
|
||||
| "materialized"
|
||||
| "not"
|
||||
| "null"
|
||||
| "offset"
|
||||
| "on"
|
||||
| "or"
|
||||
| "order"
|
||||
| "outer"
|
||||
| "primary"
|
||||
| "references"
|
||||
| "returning"
|
||||
| "right"
|
||||
| "rollback"
|
||||
| "select"
|
||||
| "set"
|
||||
| "table"
|
||||
| "then"
|
||||
| "transaction"
|
||||
| "true"
|
||||
| "union"
|
||||
| "unique"
|
||||
| "update"
|
||||
| "values"
|
||||
| "view"
|
||||
| "when"
|
||||
| "where"
|
||||
| "with"
|
||||
),
|
||||
DiffSyntaxLanguage::Bash => matches!(
|
||||
ident,
|
||||
"if" | "then"
|
||||
|
|
@ -1129,6 +1209,14 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sql_extension_is_supported() {
|
||||
assert_eq!(
|
||||
diff_syntax_language_for_path("query.sql"),
|
||||
Some(DiffSyntaxLanguage::Sql)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn treesitter_variable_capture_is_not_colored() {
|
||||
assert_eq!(super::syntax_kind_from_capture_name("variable"), None);
|
||||
|
|
|
|||
|
|
@ -25,9 +25,12 @@ impl GitGpuiView {
|
|||
this.worktree_preview_segments_cache.clear();
|
||||
}
|
||||
|
||||
let language = (lines.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING)
|
||||
.then(|| diff_syntax_language_for_path(path.to_string_lossy().as_ref()))
|
||||
.flatten();
|
||||
let syntax_mode = if lines.len() <= MAX_LINES_FOR_SYNTAX_HIGHLIGHTING {
|
||||
DiffSyntaxMode::Auto
|
||||
} else {
|
||||
DiffSyntaxMode::HeuristicOnly
|
||||
};
|
||||
let language = diff_syntax_language_for_path(path.to_string_lossy().as_ref());
|
||||
|
||||
let highlight_new_file = this.untracked_worktree_preview_path().is_some()
|
||||
|| this.added_file_preview_abs_path().is_some()
|
||||
|
|
@ -47,7 +50,7 @@ impl GitGpuiView {
|
|||
&[],
|
||||
"",
|
||||
language,
|
||||
DiffSyntaxMode::Auto,
|
||||
syntax_mode,
|
||||
None,
|
||||
)
|
||||
});
|
||||
|
|
@ -114,7 +117,7 @@ impl GitGpuiView {
|
|||
let col_sha = this.history_col_sha;
|
||||
let (show_date, show_sha) = this.history_visible_columns();
|
||||
|
||||
let (show_working_tree_summary_row, unstaged_counts, staged_counts) = match &repo.status {
|
||||
let (show_working_tree_summary_row, worktree_counts) = match &repo.status {
|
||||
Loadable::Ready(status) => {
|
||||
let count_for = |entries: &[FileStatus]| {
|
||||
let mut added = 0usize;
|
||||
|
|
@ -131,13 +134,18 @@ impl GitGpuiView {
|
|||
}
|
||||
(added, modified, deleted)
|
||||
};
|
||||
let unstaged_counts = count_for(&status.unstaged);
|
||||
let staged_counts = count_for(&status.staged);
|
||||
(
|
||||
!status.unstaged.is_empty(),
|
||||
count_for(&status.unstaged),
|
||||
count_for(&status.staged),
|
||||
!status.unstaged.is_empty() || !status.staged.is_empty(),
|
||||
(
|
||||
unstaged_counts.0 + staged_counts.0,
|
||||
unstaged_counts.1 + staged_counts.1,
|
||||
unstaged_counts.2 + staged_counts.2,
|
||||
),
|
||||
)
|
||||
}
|
||||
_ => (false, (0, 0, 0), (0, 0, 0)),
|
||||
_ => (false, (0, 0, 0)),
|
||||
};
|
||||
|
||||
let page = match &repo.log {
|
||||
|
|
@ -165,8 +173,7 @@ impl GitGpuiView {
|
|||
worktree_node_color,
|
||||
repo.id,
|
||||
selected,
|
||||
unstaged_counts,
|
||||
staged_counts,
|
||||
worktree_counts,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
|
|
@ -180,6 +187,21 @@ impl GitGpuiView {
|
|||
let commit_ix = cache.visible_indices.get(visible_ix).copied()?;
|
||||
let commit = page.commits.get(commit_ix)?;
|
||||
let graph_row = cache.graph_rows.get(visible_ix)?;
|
||||
let mut graph_row_with_incoming;
|
||||
let graph_row = if show_working_tree_summary_row && visible_ix == 0 {
|
||||
graph_row_with_incoming = graph_row.clone();
|
||||
if !graph_row_with_incoming
|
||||
.incoming_ids
|
||||
.contains(&graph_row_with_incoming.node_id)
|
||||
{
|
||||
graph_row_with_incoming
|
||||
.incoming_ids
|
||||
.push(graph_row_with_incoming.node_id);
|
||||
}
|
||||
&graph_row_with_incoming
|
||||
} else {
|
||||
graph_row
|
||||
};
|
||||
let refs = commit_refs(repo, commit);
|
||||
let when = format_relative_time(commit.time);
|
||||
let selected = repo.selected_commit.as_ref() == Some(&commit.id);
|
||||
|
|
@ -600,11 +622,9 @@ fn working_tree_summary_history_row(
|
|||
node_color: gpui::Rgba,
|
||||
repo_id: RepoId,
|
||||
selected: bool,
|
||||
unstaged: (usize, usize, usize),
|
||||
staged: (usize, usize, usize),
|
||||
counts: (usize, usize, usize),
|
||||
cx: &mut gpui::Context<GitGpuiView>,
|
||||
) -> AnyElement {
|
||||
let staged_total = staged.0 + staged.1 + staged.2;
|
||||
let icon_count = |icon: &'static str, color: gpui::Rgba, count: usize| {
|
||||
div()
|
||||
.flex()
|
||||
|
|
@ -626,36 +646,23 @@ fn working_tree_summary_history_row(
|
|||
.into_any_element()
|
||||
};
|
||||
|
||||
let group = |label: &'static str, (added, modified, deleted): (usize, usize, usize)| {
|
||||
let mut parts: Vec<AnyElement> = Vec::new();
|
||||
if modified > 0 {
|
||||
parts.push(icon_count("✎", theme.colors.warning, modified));
|
||||
}
|
||||
if added > 0 {
|
||||
parts.push(icon_count("+", theme.colors.success, added));
|
||||
}
|
||||
if deleted > 0 {
|
||||
parts.push(icon_count("−", theme.colors.danger, deleted));
|
||||
}
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.whitespace_nowrap()
|
||||
.child(label),
|
||||
)
|
||||
.children(parts)
|
||||
.into_any_element()
|
||||
};
|
||||
let (added, modified, deleted) = counts;
|
||||
let mut parts: Vec<AnyElement> = Vec::new();
|
||||
if modified > 0 {
|
||||
parts.push(icon_count("✎", theme.colors.warning, modified));
|
||||
}
|
||||
if added > 0 {
|
||||
parts.push(icon_count("+", theme.colors.success, added));
|
||||
}
|
||||
if deleted > 0 {
|
||||
parts.push(icon_count("−", theme.colors.danger, deleted));
|
||||
}
|
||||
|
||||
let black = gpui::rgba(0x000000ff);
|
||||
let circle = gpui::canvas(
|
||||
|_, _, _| (),
|
||||
move |bounds, _, window, _cx| {
|
||||
use gpui::{PathBuilder, fill, point, px, size};
|
||||
let r = px(3.0);
|
||||
let border = px(1.0);
|
||||
let outer = r + border;
|
||||
|
|
@ -666,6 +673,16 @@ fn working_tree_summary_history_row(
|
|||
bounds.left() + node_x,
|
||||
bounds.top() + bounds.size.height / 2.0,
|
||||
);
|
||||
|
||||
// Connect the working tree node into the history graph below.
|
||||
let stroke_width = px(1.6);
|
||||
let mut path = PathBuilder::stroke(stroke_width);
|
||||
path.move_to(point(center.x, center.y));
|
||||
path.line_to(point(center.x, bounds.bottom()));
|
||||
if let Ok(p) = path.build() {
|
||||
window.paint_path(p, node_color);
|
||||
}
|
||||
|
||||
window.paint_quad(
|
||||
fill(
|
||||
gpui::Bounds::new(
|
||||
|
|
@ -690,11 +707,10 @@ fn working_tree_summary_history_row(
|
|||
|
||||
let mut row = div()
|
||||
.id(("history_worktree_summary", repo_id.0))
|
||||
.h(px(28.0))
|
||||
.h(px(24.0))
|
||||
.flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.px_2()
|
||||
.hover(move |s| s.bg(theme.colors.hover))
|
||||
.active(move |s| s.bg(theme.colors.active))
|
||||
|
|
@ -703,34 +719,35 @@ fn working_tree_summary_history_row(
|
|||
.w(col_branch)
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
.line_clamp(1)
|
||||
.whitespace_nowrap()
|
||||
.child("Working tree"),
|
||||
.child(div()),
|
||||
)
|
||||
.child(div().w(col_graph).h_full().child(circle))
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w(px(0.0))
|
||||
.w(col_graph)
|
||||
.h_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.min_w(px(0.0))
|
||||
.child(
|
||||
div()
|
||||
.text_sm()
|
||||
.line_clamp(1)
|
||||
.whitespace_nowrap()
|
||||
.child("Uncommitted changes"),
|
||||
)
|
||||
.child(group("Unstaged", unstaged))
|
||||
.when(staged_total > 0, |this| this.child(group("Staged", staged))),
|
||||
),
|
||||
.justify_center()
|
||||
.overflow_hidden()
|
||||
.child(circle),
|
||||
)
|
||||
.child(div().flex_1().min_w(px(0.0)).flex().items_center().child({
|
||||
let mut summary = div().flex_1().min_w(px(0.0)).flex().items_center().gap_2();
|
||||
summary = summary.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w(px(0.0))
|
||||
.text_sm()
|
||||
.line_clamp(1)
|
||||
.whitespace_nowrap()
|
||||
.child("Uncommitted changes"),
|
||||
);
|
||||
if !parts.is_empty() {
|
||||
summary = summary.child(div().flex().items_center().gap_2().children(parts));
|
||||
}
|
||||
summary
|
||||
}))
|
||||
.when(show_date, |row| {
|
||||
row.child(
|
||||
div()
|
||||
|
|
@ -748,8 +765,7 @@ fn working_tree_summary_history_row(
|
|||
this.store.dispatch(Msg::ClearCommitSelection { repo_id });
|
||||
this.store.dispatch(Msg::ClearDiffSelection { repo_id });
|
||||
cx.notify();
|
||||
}))
|
||||
;
|
||||
}));
|
||||
|
||||
if selected {
|
||||
row = row.bg(with_alpha(theme.colors.accent, 0.15));
|
||||
|
|
|
|||
|
|
@ -33,5 +33,4 @@ mod history;
|
|||
mod sidebar;
|
||||
mod status;
|
||||
|
||||
#[cfg(feature = "bench")]
|
||||
pub(crate) mod benchmarks;
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ impl GitGpuiView {
|
|||
is_upstream,
|
||||
} => {
|
||||
let name_for_tooltip: SharedString = name.clone();
|
||||
let full_name_for_checkout = name.to_string();
|
||||
let branch_icon_color = if muted {
|
||||
theme.colors.text_muted
|
||||
} else {
|
||||
|
|
@ -342,8 +343,8 @@ impl GitGpuiView {
|
|||
has_right = true;
|
||||
right = right.child(
|
||||
div()
|
||||
.px_1()
|
||||
.py(px(1.0))
|
||||
.px(px(3.0))
|
||||
.py(px(0.0))
|
||||
.rounded(px(999.0))
|
||||
.text_xs()
|
||||
.text_color(theme.colors.text_muted)
|
||||
|
|
@ -399,6 +400,34 @@ impl GitGpuiView {
|
|||
}
|
||||
|
||||
row = row
|
||||
.on_click(cx.listener(move |this, e: &ClickEvent, _w, cx| {
|
||||
if !e.standard_click() || e.click_count() < 2 {
|
||||
return;
|
||||
}
|
||||
match section {
|
||||
BranchSection::Local => {
|
||||
this.store.dispatch(Msg::CheckoutBranch {
|
||||
repo_id,
|
||||
name: full_name_for_checkout.clone(),
|
||||
});
|
||||
this.rebuild_diff_cache();
|
||||
cx.notify();
|
||||
}
|
||||
BranchSection::Remote => {
|
||||
if let Some((remote, branch)) =
|
||||
full_name_for_checkout.split_once('/')
|
||||
{
|
||||
this.store.dispatch(Msg::CheckoutRemoteBranch {
|
||||
repo_id,
|
||||
remote: remote.to_string(),
|
||||
name: branch.to_string(),
|
||||
});
|
||||
this.rebuild_diff_cache();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.on_mouse_down(
|
||||
MouseButton::Right,
|
||||
cx.listener(move |this, e: &MouseDownEvent, window, cx| {
|
||||
|
|
@ -460,7 +489,7 @@ impl GitGpuiView {
|
|||
let (icon, color) = match f.kind {
|
||||
FileStatusKind::Added => (Some("+"), theme.colors.success),
|
||||
FileStatusKind::Modified => (Some("✎"), theme.colors.warning),
|
||||
FileStatusKind::Deleted => (None, theme.colors.text_muted),
|
||||
FileStatusKind::Deleted => (Some("−"), theme.colors.danger),
|
||||
FileStatusKind::Renamed => (Some("→"), theme.colors.accent),
|
||||
FileStatusKind::Untracked => (Some("?"), theme.colors.warning),
|
||||
FileStatusKind::Conflicted => (Some("!"), theme.colors.danger),
|
||||
|
|
|
|||
|
|
@ -83,13 +83,13 @@ impl Button {
|
|||
let (bg, hover_bg, active_bg, border, hover_border, active_border, text) = match self.style
|
||||
{
|
||||
ButtonStyle::Filled => (
|
||||
transparent,
|
||||
hover_overlay,
|
||||
active_overlay,
|
||||
with_alpha(theme.colors.accent, 0.90),
|
||||
with_alpha(theme.colors.accent, 1.00),
|
||||
with_alpha(theme.colors.accent, 1.00),
|
||||
theme.colors.accent,
|
||||
with_alpha(theme.colors.accent, 0.85),
|
||||
with_alpha(theme.colors.accent, 0.78),
|
||||
with_alpha(theme.colors.accent, 0.9),
|
||||
with_alpha(theme.colors.accent, 0.9),
|
||||
with_alpha(theme.colors.accent, 0.9),
|
||||
theme.colors.window_bg,
|
||||
),
|
||||
ButtonStyle::Outlined => (
|
||||
transparent,
|
||||
|
|
@ -200,7 +200,7 @@ impl Button {
|
|||
}
|
||||
|
||||
fn looks_like_icon_button(label: &str) -> bool {
|
||||
matches!(label.trim(), "✕" | "+" | "▾" | "≡" | "…" | "⋯" | "⟳" | "↻")
|
||||
matches!(label.trim(), "✕" | "+" | "▾" | "≡" | "" | "⋯" | "⟳" | "↻")
|
||||
|| (label.chars().count() <= 2 && !label.chars().any(|c| c.is_alphanumeric()))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ pub fn toast(theme: AppTheme, kind: ToastKind, message: impl IntoElement) -> Div
|
|||
let accent = with_alpha(accent, if theme.is_dark { 0.85 } else { 0.75 });
|
||||
|
||||
div()
|
||||
.min_w(px(260.0))
|
||||
.max_w(px(520.0))
|
||||
.min_w(px(300.0))
|
||||
.max_w(px(760.0))
|
||||
.flex()
|
||||
.items_center()
|
||||
.items_start()
|
||||
.gap_2()
|
||||
.bg(bg)
|
||||
.border_1()
|
||||
|
|
@ -52,7 +52,14 @@ pub fn toast(theme: AppTheme, kind: ToastKind, message: impl IntoElement) -> Div
|
|||
.shadow_sm()
|
||||
.text_sm()
|
||||
.text_color(theme.colors.text)
|
||||
.child(div().w(px(3.0)).h(px(18.0)).bg(accent).rounded(px(2.0)))
|
||||
.child(
|
||||
div()
|
||||
.w(px(3.0))
|
||||
.h_full()
|
||||
.min_h(px(18.0))
|
||||
.bg(accent)
|
||||
.rounded(px(2.0)),
|
||||
)
|
||||
.child(div().flex_1().px_2().py_1().child(message))
|
||||
}
|
||||
|
||||
|
|
|
|||
95
crates/vendor/gpui/Cargo.toml
vendored
95
crates/vendor/gpui/Cargo.toml
vendored
|
|
@ -229,7 +229,7 @@ name = "action_macros"
|
|||
path = "tests/action_macros.rs"
|
||||
|
||||
[dependencies.anyhow]
|
||||
version = "1.0.86"
|
||||
version = "1.0.100"
|
||||
|
||||
[dependencies.async-task]
|
||||
version = "4.7"
|
||||
|
|
@ -259,10 +259,11 @@ version = "0.2.2"
|
|||
package = "gpui_collections"
|
||||
|
||||
[dependencies.ctor]
|
||||
version = "0.4.0"
|
||||
version = "0.6.3"
|
||||
|
||||
[dependencies.derive_more]
|
||||
version = "0.99.17"
|
||||
version = "2.1.1"
|
||||
features = ["full"]
|
||||
|
||||
[dependencies.etagere]
|
||||
version = "0.2"
|
||||
|
|
@ -279,10 +280,10 @@ version = "0.2.2"
|
|||
package = "gpui_http_client"
|
||||
|
||||
[dependencies.image]
|
||||
version = "0.25.1"
|
||||
version = "0.25.9"
|
||||
|
||||
[dependencies.inventory]
|
||||
version = "0.3.19"
|
||||
version = "0.3.21"
|
||||
|
||||
[dependencies.itertools]
|
||||
version = "0.14.0"
|
||||
|
|
@ -291,7 +292,7 @@ version = "0.14.0"
|
|||
version = "0.2"
|
||||
|
||||
[dependencies.log]
|
||||
version = "0.4.16"
|
||||
version = "0.4.29"
|
||||
features = [
|
||||
"kv_unstable_serde",
|
||||
"serde",
|
||||
|
|
@ -301,13 +302,13 @@ features = [
|
|||
version = "1.0"
|
||||
|
||||
[dependencies.num_cpus]
|
||||
version = "1.13"
|
||||
version = "1.17"
|
||||
|
||||
[dependencies.parking]
|
||||
version = "2.0.0"
|
||||
version = "2.2.1"
|
||||
|
||||
[dependencies.parking_lot]
|
||||
version = "0.12.1"
|
||||
version = "0.12.5"
|
||||
|
||||
[dependencies.pin-project]
|
||||
version = "1.1.10"
|
||||
|
|
@ -331,7 +332,7 @@ version = "0.2.2"
|
|||
package = "gpui_refineable"
|
||||
|
||||
[dependencies.resvg]
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
features = [
|
||||
"text",
|
||||
"system-fonts",
|
||||
|
|
@ -340,7 +341,7 @@ features = [
|
|||
default-features = false
|
||||
|
||||
[dependencies.schemars]
|
||||
version = "1.0"
|
||||
version = "1.2"
|
||||
features = ["indexmap2"]
|
||||
|
||||
[dependencies.seahash]
|
||||
|
|
@ -351,31 +352,31 @@ version = "0.2.2"
|
|||
package = "gpui_semantic_version"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.221"
|
||||
version = "1.0.228"
|
||||
features = [
|
||||
"derive",
|
||||
"rc",
|
||||
]
|
||||
|
||||
[dependencies.serde_json]
|
||||
version = "1.0.144"
|
||||
version = "1.0.149"
|
||||
features = [
|
||||
"preserve_order",
|
||||
"raw_value",
|
||||
]
|
||||
|
||||
[dependencies.slotmap]
|
||||
version = "1.0.6"
|
||||
version = "1.1.1"
|
||||
|
||||
[dependencies.smallvec]
|
||||
version = "1.6"
|
||||
version = "1.15"
|
||||
features = ["union"]
|
||||
|
||||
[dependencies.smol]
|
||||
version = "2.0"
|
||||
|
||||
[dependencies.stacksafe]
|
||||
version = "0.1"
|
||||
version = "1.0"
|
||||
|
||||
[dependencies.strum]
|
||||
version = "0.27.2"
|
||||
|
|
@ -389,10 +390,10 @@ package = "gpui_sum_tree"
|
|||
version = "=0.9.0"
|
||||
|
||||
[dependencies.thiserror]
|
||||
version = "2.0.12"
|
||||
version = "2.0.18"
|
||||
|
||||
[dependencies.usvg]
|
||||
version = "0.45.0"
|
||||
version = "0.46.0"
|
||||
default-features = false
|
||||
|
||||
[dependencies.util]
|
||||
|
|
@ -404,7 +405,7 @@ version = "0.2.2"
|
|||
package = "gpui_util_macros"
|
||||
|
||||
[dependencies.uuid]
|
||||
version = "1.1.2"
|
||||
version = "1.19.0"
|
||||
features = [
|
||||
"v4",
|
||||
"v5",
|
||||
|
|
@ -436,14 +437,14 @@ version = "1.0"
|
|||
features = ["extra"]
|
||||
|
||||
[dev-dependencies.pretty_assertions]
|
||||
version = "1.3.0"
|
||||
version = "1.4.1"
|
||||
features = ["unstable"]
|
||||
|
||||
[dev-dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[dev-dependencies.unicode-segmentation]
|
||||
version = "1.10"
|
||||
version = "1.12"
|
||||
|
||||
[dev-dependencies.util]
|
||||
version = "0.2.2"
|
||||
|
|
@ -477,22 +478,22 @@ version = "1"
|
|||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop]
|
||||
version = "0.13.0"
|
||||
version = "0.14.3"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.calloop-wayland-source]
|
||||
version = "0.3.0"
|
||||
version = "0.4.1"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.cosmic-text]
|
||||
version = "0.14.0"
|
||||
version = "0.16.0"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.filedescriptor]
|
||||
version = "0.8.2"
|
||||
version = "0.8.3"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.flume]
|
||||
version = "0.11"
|
||||
version = "0.12"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.font-kit]
|
||||
version = "0.14.1-zed"
|
||||
|
|
@ -522,11 +523,11 @@ features = [
|
|||
default-features = false
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.open]
|
||||
version = "5.2.0"
|
||||
version = "5.3.3"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-backend]
|
||||
version = "0.3.3"
|
||||
version = "0.3.12"
|
||||
features = [
|
||||
"client_system",
|
||||
"dlopen",
|
||||
|
|
@ -534,15 +535,15 @@ features = [
|
|||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-client]
|
||||
version = "0.31.2"
|
||||
version = "0.31.12"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-cursor]
|
||||
version = "0.31.1"
|
||||
version = "0.31.12"
|
||||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols]
|
||||
version = "0.31.2"
|
||||
version = "0.32.10"
|
||||
features = [
|
||||
"client",
|
||||
"staging",
|
||||
|
|
@ -551,7 +552,7 @@ features = [
|
|||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.wayland-protocols-plasma]
|
||||
version = "0.2.0"
|
||||
version = "0.3.10"
|
||||
features = ["client"]
|
||||
optional = true
|
||||
|
||||
|
|
@ -560,7 +561,7 @@ version = "0.9.3"
|
|||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.x11rb]
|
||||
version = "0.13.1"
|
||||
version = "0.13.2"
|
||||
features = [
|
||||
"allow-unsafe-code",
|
||||
"xkb",
|
||||
|
|
@ -582,7 +583,7 @@ optional = true
|
|||
package = "zed-xim"
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies.xkbcommon]
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
features = [
|
||||
"wayland",
|
||||
"x11",
|
||||
|
|
@ -590,7 +591,7 @@ features = [
|
|||
optional = true
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies.naga]
|
||||
version = "25.0"
|
||||
version = "28.0"
|
||||
features = ["wgsl-in"]
|
||||
|
||||
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies.pathfinder_geometry]
|
||||
|
|
@ -615,10 +616,10 @@ version = "=0.2.0"
|
|||
version = "=0.10.0"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.core-foundation-sys]
|
||||
version = "0.8.6"
|
||||
version = "0.8.7"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.core-graphics]
|
||||
version = "0.24"
|
||||
version = "0.25"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.core-text]
|
||||
version = "21"
|
||||
|
|
@ -636,7 +637,7 @@ package = "zed-font-kit"
|
|||
version = "0.5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.log]
|
||||
version = "0.4.16"
|
||||
version = "0.4.29"
|
||||
features = [
|
||||
"kv_unstable_serde",
|
||||
"serde",
|
||||
|
|
@ -647,7 +648,7 @@ version = "0.2.2"
|
|||
package = "gpui_media"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.metal]
|
||||
version = "0.29"
|
||||
version = "0.33"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies.objc]
|
||||
version = "0.2"
|
||||
|
|
@ -661,24 +662,24 @@ version = "0.3"
|
|||
optional = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies.bindgen]
|
||||
version = "0.71"
|
||||
version = "0.72"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies.cbindgen]
|
||||
version = "0.28.0"
|
||||
version = "0.29.2"
|
||||
default-features = false
|
||||
|
||||
[target.'cfg(target_os = "macos")'.build-dependencies.naga]
|
||||
version = "25.0"
|
||||
version = "28.0"
|
||||
features = ["wgsl-in"]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.flume]
|
||||
version = "0.11"
|
||||
version = "0.12"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.rand]
|
||||
version = "0.9"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows]
|
||||
version = "0.61"
|
||||
version = "0.62"
|
||||
features = [
|
||||
"Foundation_Numerics",
|
||||
"Storage_Search",
|
||||
|
|
@ -729,13 +730,13 @@ features = [
|
|||
]
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows-core]
|
||||
version = "0.61"
|
||||
version = "0.62"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows-numerics]
|
||||
version = "0.2"
|
||||
version = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies.windows-registry]
|
||||
version = "0.5"
|
||||
version = "0.6"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.build-dependencies.embed-resource]
|
||||
version = "3.0"
|
||||
|
|
|
|||
|
|
@ -225,9 +225,15 @@ impl CosmicTextSystemState {
|
|||
|
||||
let mut loaded_font_ids = SmallVec::new();
|
||||
for (font_id, postscript_name) in families {
|
||||
let weight = self
|
||||
.font_system
|
||||
.db()
|
||||
.face(font_id)
|
||||
.map(|face| face.weight)
|
||||
.unwrap_or_default();
|
||||
let font = self
|
||||
.font_system
|
||||
.get_font(font_id)
|
||||
.get_font(font_id, weight)
|
||||
.context("Could not load font")?;
|
||||
|
||||
// HACK: To let the storybook run and render Windows caption icons. We should actually do better font fallback.
|
||||
|
|
@ -274,6 +280,12 @@ impl CosmicTextSystemState {
|
|||
|
||||
fn raster_bounds(&mut self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let weight = self
|
||||
.font_system
|
||||
.db()
|
||||
.face(font.id())
|
||||
.map(|face| face.weight)
|
||||
.unwrap_or_default();
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
|
|
@ -287,6 +299,7 @@ impl CosmicTextSystemState {
|
|||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
weight,
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
|
|
@ -310,6 +323,12 @@ impl CosmicTextSystemState {
|
|||
} else {
|
||||
let bitmap_size = glyph_bounds.size;
|
||||
let font = &self.loaded_fonts[params.font_id.0].font;
|
||||
let weight = self
|
||||
.font_system
|
||||
.db()
|
||||
.face(font.id())
|
||||
.map(|face| face.weight)
|
||||
.unwrap_or_default();
|
||||
let subpixel_shift = point(
|
||||
params.subpixel_variant.x as f32 / SUBPIXEL_VARIANTS_X as f32 / params.scale_factor,
|
||||
params.subpixel_variant.y as f32 / SUBPIXEL_VARIANTS_Y as f32 / params.scale_factor,
|
||||
|
|
@ -323,6 +342,7 @@ impl CosmicTextSystemState {
|
|||
params.glyph_id.0 as u16,
|
||||
(params.font_size * params.scale_factor).into(),
|
||||
(subpixel_shift.x, subpixel_shift.y.trunc()),
|
||||
weight,
|
||||
cosmic_text::CacheKeyFlags::empty(),
|
||||
)
|
||||
.0,
|
||||
|
|
@ -357,14 +377,17 @@ impl CosmicTextSystemState {
|
|||
{
|
||||
FontId(ix)
|
||||
} else {
|
||||
let font = self.font_system.get_font(id).unwrap();
|
||||
let face = self.font_system.db().face(id).unwrap();
|
||||
let (weight, is_known_emoji_font) = {
|
||||
let face = self.font_system.db().face(id).unwrap();
|
||||
(face.weight, check_is_known_emoji_font(&face.post_script_name))
|
||||
};
|
||||
let font = self.font_system.get_font(id, weight).unwrap();
|
||||
|
||||
let font_id = FontId(self.loaded_fonts.len());
|
||||
self.loaded_fonts.push(LoadedFont {
|
||||
font,
|
||||
features: CosmicFontFeatures::new(),
|
||||
is_known_emoji_font: check_is_known_emoji_font(&face.post_script_name),
|
||||
is_known_emoji_font,
|
||||
});
|
||||
|
||||
font_id
|
||||
|
|
@ -408,6 +431,7 @@ impl CosmicTextSystemState {
|
|||
None,
|
||||
&mut layout_lines,
|
||||
None,
|
||||
cosmic_text::Hinting::Disabled,
|
||||
);
|
||||
let layout = layout_lines.first().unwrap();
|
||||
|
||||
|
|
|
|||
6
crates/vendor/gpui/src/styled.rs
vendored
6
crates/vendor/gpui/src/styled.rs
vendored
|
|
@ -10,7 +10,7 @@ pub use gpui_macros::{
|
|||
visibility_style_methods,
|
||||
};
|
||||
|
||||
const ELLIPSIS: SharedString = SharedString::new_static("…");
|
||||
const ELLIPSIS: SharedString = SharedString::new_static("");
|
||||
|
||||
/// A trait for elements that can be styled.
|
||||
/// Use this to opt-in to a utility CSS-like styling API.
|
||||
|
|
@ -78,7 +78,7 @@ pub trait Styled: Sized {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the truncate overflowing text with an ellipsis (…) if needed.
|
||||
/// Sets the truncate overflowing text with an ellipsis () if needed.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-overflow#ellipsis)
|
||||
fn text_ellipsis(mut self) -> Self {
|
||||
self.text_style()
|
||||
|
|
@ -118,7 +118,7 @@ pub trait Styled: Sized {
|
|||
self.text_align(TextAlign::Right)
|
||||
}
|
||||
|
||||
/// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis (…) if needed.
|
||||
/// Sets the truncate to prevent text from wrapping and truncate overflowing text with an ellipsis () if needed.
|
||||
/// [Docs](https://tailwindcss.com/docs/text-overflow#truncate)
|
||||
fn truncate(mut self) -> Self {
|
||||
self.overflow_hidden().whitespace_nowrap().text_ellipsis()
|
||||
|
|
|
|||
|
|
@ -514,8 +514,8 @@ mod tests {
|
|||
perform_test(
|
||||
&mut wrapper,
|
||||
"aa bbb cccc ddddd eeee ffff gggg",
|
||||
"aa bbb cccc ddddd eee…",
|
||||
"…",
|
||||
"aa bbb cccc ddddd eee",
|
||||
"",
|
||||
);
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
|
|
@ -539,7 +539,7 @@ mod tests {
|
|||
) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
assert_eq!(
|
||||
wrapper.truncate_line(text.into(), line_width, "…", &mut dummy_runs),
|
||||
wrapper.truncate_line(text.into(), line_width, "", &mut dummy_runs),
|
||||
result
|
||||
);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_len) {
|
||||
|
|
@ -550,20 +550,20 @@ mod tests {
|
|||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: abcd… (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd…, len: 7, ... }
|
||||
perform_test(&mut wrapper, "abcdefghijkl", "abcd…", &[12], &[7], px(50.));
|
||||
// Truncate res: abcd (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd, len: 7, ... }
|
||||
perform_test(&mut wrapper, "abcdefghijkl", "abcd", &[12], &[7], px(50.));
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdef… (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
|
||||
// Truncate res: abcdef (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef, len:
|
||||
// 5, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"abcdef…",
|
||||
"abcdef",
|
||||
&[4, 4, 4],
|
||||
&[4, 5],
|
||||
px(70.),
|
||||
|
|
@ -572,13 +572,13 @@ mod tests {
|
|||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 8)
|
||||
// Truncate res: abcdefgh (truncate_at = 8)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: …, len: 3, ... }
|
||||
// 4, ... }, Run2 { string: , len: 3, ... }
|
||||
perform_test(
|
||||
&mut wrapper,
|
||||
"abcdefghijkl",
|
||||
"abcdefgh…",
|
||||
"abcdefgh",
|
||||
&[4, 4, 4],
|
||||
&[4, 4, 3],
|
||||
px(90.),
|
||||
|
|
@ -589,7 +589,7 @@ mod tests {
|
|||
fn test_update_run_after_truncation() {
|
||||
fn perform_test(result: &str, run_lens: &[usize], result_run_lens: &[usize]) {
|
||||
let mut dummy_runs = generate_test_runs(run_lens);
|
||||
update_runs_after_truncation(result, "…", &mut dummy_runs);
|
||||
update_runs_after_truncation(result, "", &mut dummy_runs);
|
||||
for (run, result_len) in dummy_runs.iter().zip(result_run_lens) {
|
||||
assert_eq!(run.len, *result_len);
|
||||
}
|
||||
|
|
@ -598,25 +598,25 @@ mod tests {
|
|||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 12, ... }
|
||||
//
|
||||
// Truncate res: abcd… (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd…, len: 7, ... }
|
||||
perform_test("abcd…", &[12], &[7]);
|
||||
// Truncate res: abcd (truncate_at = 4)
|
||||
// Run res: Run0 { string: abcd, len: 7, ... }
|
||||
perform_test("abcd", &[12], &[7]);
|
||||
// Case 1: Drop some runs
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdef… (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef…, len:
|
||||
// Truncate res: abcdef (truncate_at = 6)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: ef, len:
|
||||
// 5, ... }
|
||||
perform_test("abcdef…", &[4, 4, 4], &[4, 5]);
|
||||
perform_test("abcdef", &[4, 4, 4], &[4, 5]);
|
||||
// Case 2: Truncate at start of some run
|
||||
// Text: abcdefghijkl
|
||||
// Runs: Run0 { len: 4, ... }, Run1 { len: 4, ... }, Run2 { len: 4, ... }
|
||||
//
|
||||
// Truncate res: abcdefgh… (truncate_at = 8)
|
||||
// Truncate res: abcdefgh (truncate_at = 8)
|
||||
// Runs res: Run0 { string: abcd, len: 4, ... }, Run1 { string: efgh, len:
|
||||
// 4, ... }, Run2 { string: …, len: 3, ... }
|
||||
perform_test("abcdefgh…", &[4, 4, 4], &[4, 4, 3]);
|
||||
// 4, ... }, Run2 { string: , len: 3, ... }
|
||||
perform_test("abcdefgh", &[4, 4, 4], &[4, 4, 3]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
24
docs/LINUX_DESKTOP.md
Normal file
24
docs/LINUX_DESKTOP.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Linux desktop integration (GNOME/Wayland)
|
||||
|
||||
GitGpui sets `app_id` to `gitgpui` so GNOME can associate the running window with a desktop entry.
|
||||
|
||||
To make GNOME show the correct app name/icon, run:
|
||||
|
||||
```sh
|
||||
./scripts/install-linux.sh
|
||||
```
|
||||
|
||||
Manual install (what the script does):
|
||||
|
||||
```sh
|
||||
install -Dm644 assets/linux/gitgpui.desktop \
|
||||
~/.local/share/applications/gitgpui.desktop
|
||||
|
||||
install -Dm644 assets/gitgpui_logo.svg \
|
||||
~/.local/share/icons/hicolor/scalable/apps/gitgpui.svg
|
||||
|
||||
update-desktop-database ~/.local/share/applications >/dev/null 2>&1 || true
|
||||
gtk-update-icon-cache ~/.local/share/icons/hicolor >/dev/null 2>&1 || true
|
||||
```
|
||||
|
||||
Then restart GNOME Shell (or log out/in).
|
||||
69
scripts/install-linux.sh
Executable file
69
scripts/install-linux.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: scripts/install-linux.sh [--release|--debug] [--prefix PATH] [--no-build]
|
||||
|
||||
Installs:
|
||||
- binary to <prefix>/bin/gitgpui-app
|
||||
- desktop entry to ~/.local/share/applications/gitgpui.desktop
|
||||
- icon to ~/.local/share/icons/hicolor/scalable/apps/gitgpui.svg
|
||||
|
||||
Defaults:
|
||||
--release, --prefix ~/.local, build if needed
|
||||
EOF
|
||||
}
|
||||
|
||||
mode="release"
|
||||
prefix="${HOME}/.local"
|
||||
build=1
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--release) mode="release"; shift ;;
|
||||
--debug) mode="debug"; shift ;;
|
||||
--prefix) prefix="$2"; shift 2 ;;
|
||||
--no-build) build=0; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
bin_src="${repo_root}/target/${mode}/gitgpui-app"
|
||||
|
||||
if [[ $build -eq 1 && ! -x "$bin_src" ]]; then
|
||||
(cd "$repo_root" && cargo build -p gitgpui-app --${mode})
|
||||
fi
|
||||
|
||||
if [[ ! -x "$bin_src" ]]; then
|
||||
echo "Binary not found or not executable: $bin_src" >&2
|
||||
echo "Build first or omit --no-build." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
bindir="${prefix}/bin"
|
||||
appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
|
||||
icondir="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor/scalable/apps"
|
||||
|
||||
install -Dm755 "$bin_src" "${bindir}/gitgpui-app"
|
||||
|
||||
# Install desktop file with absolute Exec path so it works even if ~/.local/bin isn't on PATH.
|
||||
tmp_desktop="$(mktemp)"
|
||||
trap 'rm -f "$tmp_desktop"' EXIT
|
||||
sed "s|^Exec=.*$|Exec=${bindir}/gitgpui-app|g" \
|
||||
"${repo_root}/assets/linux/gitgpui.desktop" >"$tmp_desktop"
|
||||
install -Dm644 "$tmp_desktop" "${appdir}/gitgpui.desktop"
|
||||
|
||||
install -Dm644 "${repo_root}/assets/gitgpui_logo.svg" \
|
||||
"${icondir}/gitgpui.svg"
|
||||
|
||||
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$appdir" >/dev/null 2>&1 || true
|
||||
command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache "${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor" >/dev/null 2>&1 || true
|
||||
|
||||
echo "Installed GitGpui:"
|
||||
echo " ${bindir}/gitgpui-app"
|
||||
echo " ${appdir}/gitgpui.desktop"
|
||||
echo " ${icondir}/gitgpui.svg"
|
||||
echo "If GNOME still shows a generic icon, log out/in (or restart GNOME Shell)."
|
||||
171
scripts/profile-callgrind.sh
Normal file
171
scripts/profile-callgrind.sh
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
scripts/profile-callgrind.sh [options] [-- <app-args...>]
|
||||
|
||||
Runs GitGpui under Valgrind Callgrind with instrumentation OFF at startup,
|
||||
then lets you toggle collection ON/OFF interactively.
|
||||
|
||||
Options:
|
||||
--bin NAME Binary name to run (default: gitgpui-app)
|
||||
--package NAME Cargo package to build (default: gitgpui-app)
|
||||
--release Shortcut for --profile release
|
||||
--debug Shortcut for --profile dev
|
||||
--profile NAME Cargo profile to build/run (default: release)
|
||||
--features LIST Cargo features to enable (passed to cargo build)
|
||||
--no-default-features Disable default Cargo features
|
||||
--no-build Do not run cargo build (fail if binary missing)
|
||||
--out FILE Callgrind output file pattern (default: callgrind.out.%p)
|
||||
(%p expands to PID; recommended to avoid collisions)
|
||||
--open Open output with kcachegrind after exit (if installed)
|
||||
-h, --help Show help
|
||||
|
||||
Examples:
|
||||
scripts/profile-callgrind.sh
|
||||
scripts/profile-callgrind.sh --profile dev -- --help
|
||||
scripts/profile-callgrind.sh --features ui-gpui,gix -- /path/to/repo
|
||||
scripts/profile-callgrind.sh --out callgrind.out.%p --open
|
||||
EOF
|
||||
}
|
||||
|
||||
bin_name="gitgpui-app"
|
||||
package_name="gitgpui-app"
|
||||
cargo_profile="release"
|
||||
features=""
|
||||
no_default_features=0
|
||||
build=1
|
||||
out_pattern="callgrind.out.%p"
|
||||
open_after=0
|
||||
app_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--bin) bin_name="$2"; shift 2 ;;
|
||||
--package) package_name="$2"; shift 2 ;;
|
||||
--release) cargo_profile="release"; shift ;;
|
||||
--debug) cargo_profile="dev"; shift ;;
|
||||
--profile) cargo_profile="$2"; shift 2 ;;
|
||||
--features) features="$2"; shift 2 ;;
|
||||
--no-default-features) no_default_features=1; shift ;;
|
||||
--no-build) build=0; shift ;;
|
||||
--out) out_pattern="$2"; shift 2 ;;
|
||||
--open) open_after=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; app_args+=("$@"); break ;;
|
||||
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
target_dir="${CARGO_TARGET_DIR:-${repo_root}/target}"
|
||||
|
||||
profile_dir="$cargo_profile"
|
||||
case "$cargo_profile" in
|
||||
dev) profile_dir="debug" ;;
|
||||
release) profile_dir="release" ;;
|
||||
esac
|
||||
|
||||
bin_path="${target_dir}/${profile_dir}/${bin_name}"
|
||||
|
||||
if [[ $build -eq 1 && ! -x "$bin_path" ]]; then
|
||||
(
|
||||
cd "$repo_root"
|
||||
# Ensure line info for attribution in callgrind output (especially for release).
|
||||
profile_env="${cargo_profile^^}"
|
||||
profile_env="${profile_env//-/_}"
|
||||
cargo_args=(build -p "$package_name" --profile "$cargo_profile")
|
||||
[[ -n "$features" ]] && cargo_args+=(--features "$features")
|
||||
[[ $no_default_features -eq 1 ]] && cargo_args+=(--no-default-features)
|
||||
env \
|
||||
"CARGO_PROFILE_${profile_env}_DEBUG=true" \
|
||||
"CARGO_PROFILE_${profile_env}_STRIP=none" \
|
||||
cargo "${cargo_args[@]}"
|
||||
)
|
||||
fi
|
||||
|
||||
if [[ ! -x "$bin_path" ]]; then
|
||||
echo "Binary not found or not executable: $bin_path" >&2
|
||||
echo "Build first or omit --no-build." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v valgrind >/dev/null 2>&1; then
|
||||
echo "valgrind not found on PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v callgrind_control >/dev/null 2>&1; then
|
||||
echo "callgrind_control not found on PATH." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vgdb_dir="$(mktemp -d -t gitgpui-callgrind.XXXXXX)"
|
||||
vgdb_prefix="${vgdb_dir}/vgdb-pipe"
|
||||
cleanup() {
|
||||
rm -rf "$vgdb_dir" >/dev/null 2>&1 || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
cd "$repo_root"
|
||||
|
||||
echo "Starting under Callgrind:"
|
||||
echo " $bin_path ${app_args[*]:-}"
|
||||
echo
|
||||
echo "When the app is ready, press Enter to start collecting."
|
||||
echo "When you want to stop collecting, press Enter again."
|
||||
echo
|
||||
|
||||
valgrind \
|
||||
--tool=callgrind \
|
||||
--callgrind-out-file="$out_pattern" \
|
||||
--dump-instr=yes \
|
||||
--instr-atstart=no \
|
||||
--collect-jumps=yes \
|
||||
--sigill-diagnostics=no \
|
||||
--error-limit=no \
|
||||
--vgdb=yes \
|
||||
--vgdb-error=0 \
|
||||
--vgdb-prefix="$vgdb_prefix" \
|
||||
"$bin_path" "${app_args[@]}" &
|
||||
valgrind_pid=$!
|
||||
|
||||
out_file="${out_pattern//%p/${valgrind_pid}}"
|
||||
out_path="$out_file"
|
||||
[[ "$out_path" != /* ]] && out_path="${repo_root}/${out_path}"
|
||||
|
||||
echo "Valgrind pid: $valgrind_pid"
|
||||
echo "Manual toggles (from another shell):"
|
||||
echo " callgrind_control --vgdb-prefix=\"$vgdb_prefix\" -i on $valgrind_pid"
|
||||
echo " callgrind_control --vgdb-prefix=\"$vgdb_prefix\" -i off $valgrind_pid"
|
||||
echo
|
||||
|
||||
read -r _ || true
|
||||
enabled=0
|
||||
for _try in {1..50}; do
|
||||
if callgrind_control --vgdb-prefix="$vgdb_prefix" -i on "$valgrind_pid" >/dev/null 2>&1; then
|
||||
enabled=1
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
if [[ $enabled -ne 1 ]]; then
|
||||
callgrind_control --vgdb-prefix="$vgdb_prefix" -i on "$valgrind_pid"
|
||||
fi
|
||||
echo "Collecting (pid $valgrind_pid). Press Enter to stop collecting."
|
||||
|
||||
read -r _ || true
|
||||
callgrind_control --vgdb-prefix="$vgdb_prefix" -i off "$valgrind_pid" >/dev/null
|
||||
echo "Instrumentation off. Quit the app to flush final output to:"
|
||||
echo " $out_path"
|
||||
|
||||
wait "$valgrind_pid"
|
||||
|
||||
if [[ $open_after -eq 1 ]]; then
|
||||
if command -v kcachegrind >/dev/null 2>&1; then
|
||||
kcachegrind "$out_path" >/dev/null 2>&1 &
|
||||
else
|
||||
echo "kcachegrind not found on PATH; output is at: $out_path" >&2
|
||||
fi
|
||||
fi
|
||||
28
scripts/uninstall-linux.sh
Executable file
28
scripts/uninstall-linux.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
prefix="${HOME}/.local"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--prefix) prefix="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: scripts/uninstall-linux.sh [--prefix PATH]"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown arg: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
bindir="${prefix}/bin"
|
||||
appdir="${XDG_DATA_HOME:-${HOME}/.local/share}/applications"
|
||||
icondir="${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor/scalable/apps"
|
||||
|
||||
rm -f "${bindir}/gitgpui-app"
|
||||
rm -f "${appdir}/gitgpui.desktop"
|
||||
rm -f "${icondir}/gitgpui.svg"
|
||||
|
||||
command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database "$appdir" >/dev/null 2>&1 || true
|
||||
command -v gtk-update-icon-cache >/dev/null 2>&1 && gtk-update-icon-cache "${XDG_DATA_HOME:-${HOME}/.local/share}/icons/hicolor" >/dev/null 2>&1 || true
|
||||
|
||||
echo "Uninstalled GitGpui desktop integration from ${prefix} and ~/.local/share."
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue