From 7797408c4c4a10e3d0f0533b040d91894d7b3816 Mon Sep 17 00:00:00 2001 From: Eric Wheeler Date: Wed, 14 Jan 2026 18:06:33 -0800 Subject: [PATCH] diff: add toggle for word-level diffs Enable users to toggle word-level diffs (git --color-words) from within the application without modifying config files. This provides a more granular view of changes for detailed code review by highlighting word-level modifications instead of full-line diffs. - add ColorWordsInDiffView config option to track state across sessions - add --color-words flag to all diff command builders (commit, diff, stash, working tree) - add Ctrl+X keybinding to toggle color-words mode - prevent toggling in staging contexts where word-level diffs are unsupported - show "(color words)" subtitle indicator when mode is active - add translations for keybinding, tooltip, subtitle, and error message Signed-off-by: Eric Wheeler --- pkg/commands/git_commands/commit.go | 1 + pkg/commands/git_commands/diff.go | 2 ++ pkg/commands/git_commands/stash.go | 1 + pkg/commands/git_commands/working_tree.go | 2 ++ pkg/config/user_config.go | 5 ++++ pkg/gui/controllers/global_controller.go | 10 +++++++ pkg/gui/controllers/helpers/diff_helper.go | 16 ++++++++-- .../controllers/toggle_color_words_action.go | 30 +++++++++++++++++++ pkg/i18n/english.go | 8 +++++ 9 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 pkg/gui/controllers/toggle_color_words_action.go diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 40d2b7319ef..469d8408f57 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -256,6 +256,7 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPaths []string) *oscom Arg("-p"). Arg(hash). ArgIf(self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space"). + ArgIf(self.UserConfig().Git.ColorWordsInDiffView, "--color-words"). Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)). Arg("--"). Arg(filterPaths...). diff --git a/pkg/commands/git_commands/diff.go b/pkg/commands/git_commands/diff.go index 3074e653a80..2431a8a809b 100644 --- a/pkg/commands/git_commands/diff.go +++ b/pkg/commands/git_commands/diff.go @@ -23,6 +23,7 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj { useExtDiff := extDiffCmd != "" useExtDiffGitConfig := self.pagerConfig.GetUseExternalDiffGitConfig() ignoreWhitespace := self.UserConfig().Git.IgnoreWhitespaceInDiffView + colorWords := self.UserConfig().Git.ColorWordsInDiffView return self.cmd.New( NewGitCmd("diff"). @@ -32,6 +33,7 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string) *oscommands.CmdObj { Arg("--submodule"). Arg(fmt.Sprintf("--color=%s", self.pagerConfig.GetColorArg())). ArgIf(ignoreWhitespace, "--ignore-all-space"). + ArgIf(colorWords, "--color-words"). Arg(fmt.Sprintf("--unified=%d", self.UserConfig().Git.DiffContextSize)). Arg(diffArgs...). Dir(self.repoPaths.worktreePath). diff --git a/pkg/commands/git_commands/stash.go b/pkg/commands/git_commands/stash.go index 0e5eb299d50..3eb5259c980 100644 --- a/pkg/commands/git_commands/stash.go +++ b/pkg/commands/git_commands/stash.go @@ -94,6 +94,7 @@ func (self *StashCommands) ShowStashEntryCmdObj(index int) *oscommands.CmdObj { Arg(fmt.Sprintf("--color=%s", self.pagerConfig.GetColorArg())). Arg(fmt.Sprintf("--unified=%d", self.UserConfig().Git.DiffContextSize)). ArgIf(self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space"). + ArgIf(self.UserConfig().Git.ColorWordsInDiffView, "--color-words"). Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)). Arg(fmt.Sprintf("refs/stash@{%d}", index)). Dir(self.repoPaths.worktreePath). diff --git a/pkg/commands/git_commands/working_tree.go b/pkg/commands/git_commands/working_tree.go index e74ccf1e5ed..a125b58538f 100644 --- a/pkg/commands/git_commands/working_tree.go +++ b/pkg/commands/git_commands/working_tree.go @@ -277,6 +277,7 @@ func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain Arg(fmt.Sprintf("--unified=%d", contextSize)). Arg(fmt.Sprintf("--color=%s", colorArg)). ArgIf(!plain && self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space"). + ArgIf(!plain && self.UserConfig().Git.ColorWordsInDiffView, "--color-words"). Arg(fmt.Sprintf("--find-renames=%d%%", self.UserConfig().Git.RenameSimilarityThreshold)). ArgIf(cached, "--cached"). ArgIf(noIndex, "--no-index"). @@ -320,6 +321,7 @@ func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reve Arg(to). ArgIf(reverse, "-R"). ArgIf(!plain && self.UserConfig().Git.IgnoreWhitespaceInDiffView, "--ignore-all-space"). + ArgIf(!plain && self.UserConfig().Git.ColorWordsInDiffView, "--color-words"). Arg("--"). Arg(fileName). Dir(self.repoPaths.worktreePath). diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index bcb5c2f6f85..2fd299e57ce 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -292,6 +292,8 @@ type GitConfig struct { AllBranchesLogCmds []string `yaml:"allBranchesLogCmds"` // If true, git diffs are rendered with the `--ignore-all-space` flag, which ignores whitespace changes. Can be toggled from within Lazygit with ``. IgnoreWhitespaceInDiffView bool `yaml:"ignoreWhitespaceInDiffView"` + // If true, git diffs are rendered with the `--color-words` flag, which shows word-level diffs. Can be toggled from within Lazygit with ``. + ColorWordsInDiffView bool `yaml:"colorWordsInDiffView"` // The number of lines of context to show around each diff hunk. Can be changed from within Lazygit with the `{` and `}` keys. DiffContextSize uint64 `yaml:"diffContextSize"` // The threshold for considering a file to be renamed, in percent. Can be changed from within Lazygit with the `(` and `)` keys. @@ -485,6 +487,7 @@ type KeybindingUniversalConfig struct { SubmitEditorText string `yaml:"submitEditorText"` ExtrasMenu string `yaml:"extrasMenu"` ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"` + ToggleColorWordsInDiffView string `yaml:"toggleColorWordsInDiffView"` IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"` DecreaseContextInDiffView string `yaml:"decreaseContextInDiffView"` IncreaseRenameSimilarityThreshold string `yaml:"increaseRenameSimilarityThreshold"` @@ -849,6 +852,7 @@ func GetDefaultConfig() *UserConfig { BranchLogCmd: "git log --graph --color=always --abbrev-commit --decorate --date=relative --pretty=medium {{branchName}} --", AllBranchesLogCmds: []string{"git log --graph --all --color=always --abbrev-commit --decorate --date=relative --pretty=medium"}, IgnoreWhitespaceInDiffView: false, + ColorWordsInDiffView: false, DiffContextSize: 3, RenameSimilarityThreshold: 50, DisableForcePushing: false, @@ -947,6 +951,7 @@ func GetDefaultConfig() *UserConfig { SubmitEditorText: "", ExtrasMenu: "@", ToggleWhitespaceInDiffView: "", + ToggleColorWordsInDiffView: "", IncreaseContextInDiffView: "}", DecreaseContextInDiffView: "{", IncreaseRenameSimilarityThreshold: ")", diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index 1ae56068561..0596abe0275 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -152,6 +152,12 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type Description: self.c.Tr.ToggleWhitespaceInDiffView, Tooltip: self.c.Tr.ToggleWhitespaceInDiffViewTooltip, }, + { + Key: opts.GetKey(opts.Config.Universal.ToggleColorWordsInDiffView), + Handler: self.toggleColorWords, + Description: self.c.Tr.ToggleColorWordsInDiffView, + Tooltip: self.c.Tr.ToggleColorWordsInDiffViewTooltip, + }, } } @@ -254,6 +260,10 @@ func (self *GlobalController) toggleWhitespace() error { return (&ToggleWhitespaceAction{c: self.c}).Call() } +func (self *GlobalController) toggleColorWords() error { + return (&ToggleColorWordsAction{c: self.c}).Call() +} + func (self *GlobalController) canShowRebaseOptions() *types.DisabledReason { if self.c.Model().WorkingTreeStateAtLastCommitRefresh.None() { return &types.DisabledReason{ diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go index 668ee916adb..5fe63c9d3ae 100644 --- a/pkg/gui/controllers/helpers/diff_helper.go +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -167,11 +167,21 @@ func (self *DiffHelper) WithDiffModeCheck(f func()) { } func (self *DiffHelper) IgnoringWhitespaceSubTitle() string { - if self.c.UserConfig().Git.IgnoreWhitespaceInDiffView { - return self.c.Tr.IgnoreWhitespaceDiffViewSubTitle + ignoreWhitespace := self.c.UserConfig().Git.IgnoreWhitespaceInDiffView + colorWords := self.c.UserConfig().Git.ColorWordsInDiffView + + var subtitle string + if ignoreWhitespace { + subtitle = self.c.Tr.IgnoreWhitespaceDiffViewSubTitle + } + if colorWords { + if subtitle != "" { + subtitle += " | " + } + subtitle += self.c.Tr.ColorWordsDiffViewSubTitle } - return "" + return subtitle } func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error { diff --git a/pkg/gui/controllers/toggle_color_words_action.go b/pkg/gui/controllers/toggle_color_words_action.go new file mode 100644 index 00000000000..2394de6595d --- /dev/null +++ b/pkg/gui/controllers/toggle_color_words_action.go @@ -0,0 +1,30 @@ +package controllers + +import ( + "errors" + + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +type ToggleColorWordsAction struct { + c *ControllerCommon +} + +func (self *ToggleColorWordsAction) Call() error { + contextsThatDontSupportColorWords := []types.ContextKey{ + context.STAGING_MAIN_CONTEXT_KEY, + context.STAGING_SECONDARY_CONTEXT_KEY, + context.PATCH_BUILDING_MAIN_CONTEXT_KEY, + } + + if lo.Contains(contextsThatDontSupportColorWords, self.c.Context().Current().GetKey()) { + return errors.New(self.c.Tr.ColorWordsNotSupportedHere) + } + + self.c.UserConfig().Git.ColorWordsInDiffView = !self.c.UserConfig().Git.ColorWordsInDiffView + + self.c.Context().CurrentSide().HandleFocus(types.OnFocusOpts{}) + return nil +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 970afd498be..537c693b7bf 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -751,6 +751,10 @@ type TranslationSet struct { ToggleWhitespaceInDiffViewTooltip string IgnoreWhitespaceDiffViewSubTitle string IgnoreWhitespaceNotSupportedHere string + ToggleColorWordsInDiffView string + ToggleColorWordsInDiffViewTooltip string + ColorWordsDiffViewSubTitle string + ColorWordsNotSupportedHere string IncreaseContextInDiffView string IncreaseContextInDiffViewTooltip string DecreaseContextInDiffView string @@ -1851,6 +1855,10 @@ func EnglishTranslationSet() *TranslationSet { ToggleWhitespaceInDiffViewTooltip: "Toggle whether or not whitespace changes are shown in the diff view.\n\nThe default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'.", IgnoreWhitespaceDiffViewSubTitle: "(ignoring whitespace)", IgnoreWhitespaceNotSupportedHere: "Ignoring whitespace is not supported in this view", + ToggleColorWordsInDiffView: "Toggle color words", + ToggleColorWordsInDiffViewTooltip: "Toggle whether or not word-level diffs are shown in the diff view.\n\nThe default can be changed in the config file with the key 'git.colorWordsInDiffView'.", + ColorWordsDiffViewSubTitle: "(color words)", + ColorWordsNotSupportedHere: "Color words is not supported in this view", IncreaseContextInDiffView: "Increase diff context size", IncreaseContextInDiffViewTooltip: "Increase the amount of the context shown around changes in the diff view.\n\nThe default can be changed in the config file with the key 'git.diffContextSize'.", DecreaseContextInDiffView: "Decrease diff context size",