Persist the default Git diff layout

This commit is contained in:
Georg Bauer
2026-07-28 19:07:59 +02:00
parent 07c89094b4
commit 191ff0b2ce
7 changed files with 64 additions and 21 deletions

View File

@@ -8,14 +8,15 @@ mod view;
pub(crate) use view::app_theme; pub(crate) use view::app_theme;
use generation::ChatMessage; use generation::ChatMessage;
use git::{ActiveGitOperation, GitDiff, GitDiffMode, GitWorktree}; use git::{ActiveGitOperation, GitDiff, GitWorktree};
use model_manager::{ActiveDownload, ModelDownload, ModelOperation}; use model_manager::{ActiveDownload, ModelDownload, ModelOperation};
use preferences::PreferenceDraft; use preferences::PreferenceDraft;
#[cfg(test)] #[cfg(test)]
use preferences::{parse_optional_gib, parse_streaming_cache}; use preferences::{parse_optional_gib, parse_streaming_cache};
use crate::config::{ use crate::config::{
Config, DevBrainConfig, EndpointConfig, GitConfig, GitDiffAlgorithm, GitDiffWhitespace, Config, DevBrainConfig, EndpointConfig, GitConfig, GitDiffAlgorithm, GitDiffLayout,
GitDiffWhitespace,
}; };
use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage}; use crate::database::{Database, ProjectWithSessions, SessionState, StoredMessage};
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -77,7 +78,7 @@ pub(crate) struct App {
git_selected_files: HashSet<PathBuf>, git_selected_files: HashSet<PathBuf>,
git_commit_message: String, git_commit_message: String,
git_diff: Option<GitDiff>, git_diff: Option<GitDiff>,
git_diff_mode: GitDiffMode, git_diff_layout: GitDiffLayout,
git_commit_all_confirmation: bool, git_commit_all_confirmation: bool,
git_operation: Option<ActiveGitOperation>, git_operation: Option<ActiveGitOperation>,
last_git_scan: Instant, last_git_scan: Instant,
@@ -350,6 +351,7 @@ pub(crate) enum Message {
PreferenceEndpointCorsChanged(bool), PreferenceEndpointCorsChanged(bool),
PreferenceDevBrainEnabledChanged(bool), PreferenceDevBrainEnabledChanged(bool),
PreferenceDevBrainVaultChanged(String), PreferenceDevBrainVaultChanged(String),
PreferenceGitDiffLayoutChanged(GitDiffLayout),
PreferenceGitDiffAlgorithmChanged(GitDiffAlgorithm), PreferenceGitDiffAlgorithmChanged(GitDiffAlgorithm),
PreferenceGitContextLinesChanged(String), PreferenceGitContextLinesChanged(String),
PreferenceGitInterhunkLinesChanged(String), PreferenceGitInterhunkLinesChanged(String),
@@ -437,7 +439,7 @@ pub(crate) enum Message {
SwitchGitBranch(String), SwitchGitBranch(String),
ToggleGitFile(PathBuf), ToggleGitFile(PathBuf),
OpenGitDiff(PathBuf), OpenGitDiff(PathBuf),
SetGitDiffMode(GitDiffMode), SetGitDiffLayout(GitDiffLayout),
GitDiffScrolled(scrollable::Viewport), GitDiffScrolled(scrollable::Viewport),
GitCommitMessageChanged(String), GitCommitMessageChanged(String),
GitStageSelected, GitStageSelected,
@@ -503,6 +505,7 @@ impl App {
let metrics = let metrics =
Arc::new(Metrics::new(&application_support_path().join("kv-cache"))); Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot(); let metrics_snapshot = metrics.snapshot();
let git_diff_layout = config.git.diff_layout;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let (runtime_config, generation_service, endpoint, service_error) = let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics)); spawn_services(&config, Arc::clone(&metrics));
@@ -532,7 +535,7 @@ impl App {
git_selected_files: HashSet::new(), git_selected_files: HashSet::new(),
git_commit_message: String::new(), git_commit_message: String::new(),
git_diff: None, git_diff: None,
git_diff_mode: GitDiffMode::default(), git_diff_layout,
git_commit_all_confirmation: false, git_commit_all_confirmation: false,
git_operation: None, git_operation: None,
last_git_scan: Instant::now() - GIT_SCAN_INTERVAL, last_git_scan: Instant::now() - GIT_SCAN_INTERVAL,
@@ -641,6 +644,7 @@ impl App {
let context_limit = config.generation.context_tokens.max(0) as u32; let context_limit = config.generation.context_tokens.max(0) as u32;
let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache"))); let metrics = Arc::new(Metrics::new(&application_support_path().join("kv-cache")));
let metrics_snapshot = metrics.snapshot(); let metrics_snapshot = metrics.snapshot();
let git_diff_layout = config.git.diff_layout;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
let (runtime_config, generation_service, endpoint, service_error) = let (runtime_config, generation_service, endpoint, service_error) =
spawn_services(&config, Arc::clone(&metrics)); spawn_services(&config, Arc::clone(&metrics));
@@ -674,7 +678,7 @@ impl App {
git_selected_files: HashSet::new(), git_selected_files: HashSet::new(),
git_commit_message: String::new(), git_commit_message: String::new(),
git_diff: None, git_diff: None,
git_diff_mode: GitDiffMode::default(), git_diff_layout,
git_commit_all_confirmation: false, git_commit_all_confirmation: false,
git_operation: None, git_operation: None,
last_git_scan: Instant::now() - GIT_SCAN_INTERVAL, last_git_scan: Instant::now() - GIT_SCAN_INTERVAL,
@@ -1187,6 +1191,10 @@ impl App {
self.preference_draft.dev_brain_vault_path = value; self.preference_draft.dev_brain_vault_path = value;
self.preference_error = None; self.preference_error = None;
} }
Message::PreferenceGitDiffLayoutChanged(layout) => {
self.preference_draft.git_diff_layout = layout;
self.preference_error = None;
}
Message::PreferenceGitDiffAlgorithmChanged(algorithm) => { Message::PreferenceGitDiffAlgorithmChanged(algorithm) => {
self.preference_draft.git_diff_algorithm = algorithm; self.preference_draft.git_diff_algorithm = algorithm;
self.preference_error = None; self.preference_error = None;
@@ -1798,7 +1806,7 @@ impl App {
Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch), Message::SwitchGitBranch(branch) => self.switch_git_branch(&branch),
Message::ToggleGitFile(path) => self.toggle_git_file(path), Message::ToggleGitFile(path) => self.toggle_git_file(path),
Message::OpenGitDiff(path) => self.open_git_diff(&path), Message::OpenGitDiff(path) => self.open_git_diff(&path),
Message::SetGitDiffMode(mode) => self.git_diff_mode = mode, Message::SetGitDiffLayout(layout) => self.git_diff_layout = layout,
Message::GitDiffScrolled(viewport) => { Message::GitDiffScrolled(viewport) => {
let offset = scrollable::AbsoluteOffset { let offset = scrollable::AbsoluteOffset {
x: viewport.absolute_offset().x, x: viewport.absolute_offset().x,

View File

@@ -40,13 +40,6 @@ pub(super) struct GitWorktree {
pub(super) files: Vec<GitFile>, pub(super) files: Vec<GitFile>,
} }
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) enum GitDiffMode {
#[default]
Unified,
Split,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)] #[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(super) enum GitDiffLineKind { pub(super) enum GitDiffLineKind {
Section, Section,
@@ -186,6 +179,7 @@ impl App {
match load_diff(&worktree.root, file, &self.config.git) { match load_diff(&worktree.root, file, &self.config.git) {
Ok(diff) => { Ok(diff) => {
self.git_diff = Some(diff); self.git_diff = Some(diff);
self.git_diff_layout = self.config.git.diff_layout;
self.error = None; self.error = None;
} }
Err(error) => self.error = Some(error), Err(error) => self.error = Some(error),
@@ -1113,6 +1107,7 @@ mod tests {
.find(|file| file.display_path == "changed.txt") .find(|file| file.display_path == "changed.txt")
.unwrap(); .unwrap();
let settings = GitConfig { let settings = GitConfig {
diff_layout: GitDiffLayout::Unified,
diff_algorithm: GitDiffAlgorithm::Patience, diff_algorithm: GitDiffAlgorithm::Patience,
context_lines: 0, context_lines: 0,
interhunk_lines: 0, interhunk_lines: 0,

View File

@@ -12,6 +12,7 @@ pub(super) struct PreferenceDraft {
pub(super) endpoint_cors: bool, pub(super) endpoint_cors: bool,
pub(super) dev_brain_enabled: bool, pub(super) dev_brain_enabled: bool,
pub(super) dev_brain_vault_path: String, pub(super) dev_brain_vault_path: String,
pub(super) git_diff_layout: GitDiffLayout,
pub(super) git_diff_algorithm: GitDiffAlgorithm, pub(super) git_diff_algorithm: GitDiffAlgorithm,
pub(super) git_context_lines: String, pub(super) git_context_lines: String,
pub(super) git_interhunk_lines: String, pub(super) git_interhunk_lines: String,
@@ -70,6 +71,7 @@ impl PreferenceDraft {
endpoint_cors: config.endpoint.cors, endpoint_cors: config.endpoint.cors,
dev_brain_enabled: config.dev_brain.enabled, dev_brain_enabled: config.dev_brain.enabled,
dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(), dev_brain_vault_path: config.dev_brain.vault_path.clone().unwrap_or_default(),
git_diff_layout: config.git.diff_layout,
git_diff_algorithm: config.git.diff_algorithm, git_diff_algorithm: config.git.diff_algorithm,
git_context_lines: config.git.context_lines.to_string(), git_context_lines: config.git.context_lines.to_string(),
git_interhunk_lines: config.git.interhunk_lines.to_string(), git_interhunk_lines: config.git.interhunk_lines.to_string(),
@@ -140,6 +142,7 @@ impl PreferenceDraft {
pub(super) fn git(&self) -> Result<GitConfig, String> { pub(super) fn git(&self) -> Result<GitConfig, String> {
Ok(GitConfig { Ok(GitConfig {
diff_layout: self.git_diff_layout,
diff_algorithm: self.git_diff_algorithm, diff_algorithm: self.git_diff_algorithm,
context_lines: parse_u32("Git diff context lines", &self.git_context_lines)?, context_lines: parse_u32("Git diff context lines", &self.git_context_lines)?,
interhunk_lines: parse_u32("Git diff interhunk lines", &self.git_interhunk_lines)?, interhunk_lines: parse_u32("Git diff interhunk lines", &self.git_interhunk_lines)?,

View File

@@ -12,7 +12,7 @@ use super::{
ModelDownload, ModelOperation, PreferenceSection, ProjectChoice, chat_scroll_id, composer_id, ModelDownload, ModelOperation, PreferenceSection, ProjectChoice, chat_scroll_id, composer_id,
models_path, preferences_scroll_id, models_path, preferences_scroll_id,
}; };
use crate::config::{GIT_DIFF_ALGORITHMS, GIT_DIFF_WHITESPACE_MODES}; use crate::config::{GIT_DIFF_ALGORITHMS, GIT_DIFF_LAYOUTS, GIT_DIFF_WHITESPACE_MODES};
use crate::database::{ProjectWithSessions, Session, SessionState}; use crate::database::{ProjectWithSessions, Session, SessionState};
use crate::model::{ use crate::model::{
self, DownloadPhase, MODEL_CHOICES, ManagedArtifact, ManagedArtifactState, ModelChoice, self, DownloadPhase, MODEL_CHOICES, ManagedArtifact, ManagedArtifactState, ModelChoice,

View File

@@ -1,6 +1,7 @@
use super::*; use super::*;
use crate::app::git::{GitChangeKind, GitDiffLine, GitDiffLineKind, GitDiffMode, GitDiffRow}; use crate::app::git::{GitChangeKind, GitDiffLine, GitDiffLineKind, GitDiffRow};
use crate::app::{git_diff_new_scroll_id, git_diff_old_scroll_id}; use crate::app::{git_diff_new_scroll_id, git_diff_old_scroll_id};
use crate::config::GitDiffLayout;
use iced::widget::column; use iced::widget::column;
impl App { impl App {
@@ -230,13 +231,13 @@ impl App {
pub(super) fn git_diff_panel(&self) -> Element<'_, Message> { pub(super) fn git_diff_panel(&self) -> Element<'_, Message> {
let diff = self.git_diff.as_ref().unwrap(); let diff = self.git_diff.as_ref().unwrap();
let unified_active = self.git_diff_mode == GitDiffMode::Unified; let unified_active = self.git_diff_layout == GitDiffLayout::Unified;
let split_active = self.git_diff_mode == GitDiffMode::Split; let split_active = self.git_diff_layout == GitDiffLayout::Split;
let mode = container( let mode = container(
row![ row![
button(text("1 column").size(12)) button(text("1 column").size(12))
.padding([4, 10]) .padding([4, 10])
.on_press(Message::SetGitDiffMode(GitDiffMode::Unified)) .on_press(Message::SetGitDiffLayout(GitDiffLayout::Unified))
.style(move |theme, status| segmented_button_style( .style(move |theme, status| segmented_button_style(
theme, theme,
status, status,
@@ -244,7 +245,7 @@ impl App {
)), )),
button(text("2 columns").size(12)) button(text("2 columns").size(12))
.padding([4, 10]) .padding([4, 10])
.on_press(Message::SetGitDiffMode(GitDiffMode::Split)) .on_press(Message::SetGitDiffLayout(GitDiffLayout::Split))
.style(move |theme, status| segmented_button_style( .style(move |theme, status| segmented_button_style(
theme, theme,
status, status,

View File

@@ -199,6 +199,20 @@ impl App {
PreferenceSection::Git, PreferenceSection::Git,
"GIT DIFFS", "GIT DIFFS",
column![ column![
row![
hint(
text("Default layout").size(13).width(Length::Fill),
"Layout used whenever a file diff is opened. The control in the diff can still change that one view.",
),
pick_list(
&GIT_DIFF_LAYOUTS[..],
Some(self.preference_draft.git_diff_layout),
Message::PreferenceGitDiffLayoutChanged,
)
.width(240),
]
.spacing(12)
.align_y(Alignment::Center),
row![ row![
hint( hint(
text("Algorithm").size(13).width(Length::Fill), text("Algorithm").size(13).width(Length::Fill),

View File

@@ -46,6 +46,25 @@ pub const GIT_DIFF_ALGORITHMS: [GitDiffAlgorithm; 3] = [
GitDiffAlgorithm::Minimal, GitDiffAlgorithm::Minimal,
]; ];
pub const GIT_DIFF_LAYOUTS: [GitDiffLayout; 2] = [GitDiffLayout::Unified, GitDiffLayout::Split];
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GitDiffLayout {
#[default]
Unified,
Split,
}
impl fmt::Display for GitDiffLayout {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Unified => "1 column",
Self::Split => "2 columns",
})
}
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum GitDiffAlgorithm { pub enum GitDiffAlgorithm {
@@ -96,6 +115,7 @@ impl fmt::Display for GitDiffWhitespace {
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct GitConfig { pub struct GitConfig {
pub diff_layout: GitDiffLayout,
pub diff_algorithm: GitDiffAlgorithm, pub diff_algorithm: GitDiffAlgorithm,
pub context_lines: u32, pub context_lines: u32,
pub interhunk_lines: u32, pub interhunk_lines: u32,
@@ -107,6 +127,7 @@ pub struct GitConfig {
impl Default for GitConfig { impl Default for GitConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
diff_layout: GitDiffLayout::Unified,
diff_algorithm: GitDiffAlgorithm::Default, diff_algorithm: GitDiffAlgorithm::Default,
context_lines: 3, context_lines: 3,
interhunk_lines: 0, interhunk_lines: 0,
@@ -282,6 +303,7 @@ mod tests {
..RuntimePreferences::default() ..RuntimePreferences::default()
}, },
git: GitConfig { git: GitConfig {
diff_layout: GitDiffLayout::Split,
diff_algorithm: GitDiffAlgorithm::Patience, diff_algorithm: GitDiffAlgorithm::Patience,
context_lines: 5, context_lines: 5,
whitespace: GitDiffWhitespace::IgnoreEndOfLine, whitespace: GitDiffWhitespace::IgnoreEndOfLine,
@@ -297,7 +319,7 @@ mod tests {
"model: glm-5.2\na2ui_enabled: false\n\ "model: glm-5.2\na2ui_enabled: false\n\
generation:\n context_tokens: 65536\n reasoning_mode: none\n\ generation:\n context_tokens: 65536\n reasoning_mode: none\n\
runtime:\n ssd:\n enabled: true\n cache: 64GB\n\ runtime:\n ssd:\n enabled: true\n cache: 64GB\n\
git:\n diff_algorithm: patience\n context_lines: 5\n whitespace: ignore-end-of-line\n" git:\n diff_layout: split\n diff_algorithm: patience\n context_lines: 5\n whitespace: ignore-end-of-line\n"
); );
assert_eq!(Config::load(&path).unwrap(), config); assert_eq!(Config::load(&path).unwrap(), config);
fs::remove_dir_all(&directory).unwrap(); fs::remove_dir_all(&directory).unwrap();