diff --git a/docs-master/Config.md b/docs-master/Config.md index 42eff4666a1..b1b4d234053 100644 --- a/docs-master/Config.md +++ b/docs-master/Config.md @@ -70,6 +70,12 @@ gui: # need to pass it separately in the pager command. tabWidth: 4 + # If true, show visual indicators for spaces (middle dot) and tabs (right arrow) + # in diff and staging views. + # This can be toggled from within Lazygit with the 'toggleShowWhitespace' + # keybinding (default ``), but that will not change the default. + showWhitespace: false + # If true, capture mouse events. # When mouse events are captured, it's a little harder to select text: e.g. # requiring you to hold the option key when on macOS. @@ -689,6 +695,7 @@ keybinding: submitEditorText: extrasMenu: '@' toggleWhitespaceInDiffView: + toggleShowWhitespace: increaseContextInDiffView: '}' decreaseContextInDiffView: '{' increaseRenameSimilarityThreshold: ) diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 74dbcc97d4e..453f580d04f 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | Quit | | | `` `` | Suspend the application | | | `` `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | Undo | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | | `` Z `` | Redo | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index f06f1a0bf5f..1f9c024767b 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | 終了 | | | `` `` | Suspend the application | | | `` `` | 空白表示の切り替え | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | 元に戻す | 最後のgitコマンドを元に戻すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 | | `` Z `` | やり直す | 最後のgitコマンドをやり直すために実行するgitコマンドを決定するためにreflogが使用されます。これにはワーキングツリーへの変更は含まれません。コミットのみが考慮されます。 | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 2b96d0aa8de..0b723d36f7f 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | 종료 | | | `` `` | Suspend the application | | | `` `` | 공백문자를 Diff 뷰에서 표시 여부 전환 | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | 되돌리기 (reflog) (실험적) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | | `` Z `` | 다시 실행 (reflog) (실험적) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index f048e2a2cdc..827c17f85f9 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | Quit | | | `` `` | Suspend the application | | | `` `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | Ongedaan maken (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to undo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | | `` Z `` | Redo (via reflog) (experimenteel) | The reflog will be used to determine what git command to run to redo the last git command. This does not include changes to the working tree; only commits are taken into consideration. | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index fcde7bfb40f..e3800abb0f0 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | Wyjdź | | | `` `` | Suspend the application | | | `` `` | Przełącz białe znaki | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | Cofnij | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby cofnąć ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. | | `` Z `` | Ponów | Dziennik reflog zostanie użyty do określenia, jakie polecenie git należy uruchomić, aby ponowić ostatnie polecenie git. Nie obejmuje to zmian w drzewie roboczym; brane są pod uwagę tylko commity. | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 1aacc5722b1..8894b4a22bb 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | Sair | | | `` `` | Suspender a aplicação | | | `` `` | Toggle whitespace | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | Desfazer | O reflog será usado para determinar qual comando git para executar para desfazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. | | `` Z `` | Refazer | O reflog será usado para determinar qual comando git para executar para refazer o último comando git. Isto não inclui mudanças na árvore de trabalho; apenas compromissos são tidos em consideração. | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 0dcb6753e70..7c383008269 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | Выйти | | | `` `` | Suspend the application | | | `` `` | Переключить отображение изменении пробелов в просмотрщике сравнении | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | Отменить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git запустить, чтобы отменить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. | | `` Z `` | Повторить (через reflog) (экспериментальный) | Журнал ссылок (reflog) будет использоваться для определения того, какую команду git нужно запустить, чтобы повторить последнюю команду git. Сюда не входят изменения в рабочем дереве; учитываются только коммиты. | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index d78572f1b8b..cc4a824ebdd 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | 退出 | | | `` `` | 挂起应用程序 | | | `` `` | 切换是否在差异视图中显示空白字符差异 | 切换是否在差异视图中显示空白字符更改。

默认值可在配置文件中通过键 'git.ignoreWhitespaceInDiffView' 更改。 | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | 撤销 | Reflog将用于确定运行哪个git命令来撤消最后一个git命令。这并不包括对工作树的更改,只考虑提交。 | | `` Z `` | 重做 | Reflog将用于确定运行哪个git命令来重做上一个git命令。这并不包括对工作树的更改,只考虑提交。 | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 70294eab9fe..02a82d164da 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -31,6 +31,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` q `` | 結束 | | | `` `` | Suspend the application | | | `` `` | 切換是否在差異檢視中顯示空格變更 | Toggle whether or not whitespace changes are shown in the diff view.

The default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'. | +| `` `` | Toggle show whitespace characters | Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).

The default can be changed in the config file with the key 'gui.showWhitespace'. | | `` z `` | 復原 | 將使用 reflog 確任 git 指令以復原。這不包括工作區更改;只考慮提交。 | | `` Z `` | 取消復原 | 將使用 reflog 確任 git 指令以重作。這不包括工作區更改;只考慮提交。 | diff --git a/docs/Config.md b/docs/Config.md index 9931fda61fe..f49e6e31baf 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -70,6 +70,12 @@ gui: # need to pass it separately in the pager command. tabWidth: 4 + # If true, show visual indicators for spaces (middle dot) and tabs (right arrow) + # in diff and staging views. + # This can be toggled from within Lazygit with the 'toggleShowWhitespace' + # keybinding (default ``), but that will not change the default. + showWhitespace: false + # If true, capture mouse events. # When mouse events are captured, it's a little harder to select text: e.g. # requiring you to hold the option key when on macOS. @@ -674,6 +680,7 @@ keybinding: submitEditorText: extrasMenu: '@' toggleWhitespaceInDiffView: + toggleShowWhitespace: increaseContextInDiffView: '}' decreaseContextInDiffView: '{' increaseRenameSimilarityThreshold: ) diff --git a/pkg/config/user_config.go b/pkg/config/user_config.go index ddb0c087666..fb949b935aa 100644 --- a/pkg/config/user_config.go +++ b/pkg/config/user_config.go @@ -79,6 +79,9 @@ type GuiConfig struct { // The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs. // Note that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command. TabWidth int `yaml:"tabWidth" jsonschema:"minimum=1"` + // If true, show visual indicators for spaces (middle dot) and tabs (right arrow) in diff and staging views. + // This can be toggled from within Lazygit with the 'toggleShowWhitespace' keybinding (default ``), but that will not change the default. + ShowWhitespace bool `yaml:"showWhitespace"` // If true, capture mouse events. // When mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS. MouseEvents bool `yaml:"mouseEvents"` @@ -498,6 +501,7 @@ type KeybindingUniversalConfig struct { SubmitEditorText string `yaml:"submitEditorText"` ExtrasMenu string `yaml:"extrasMenu"` ToggleWhitespaceInDiffView string `yaml:"toggleWhitespaceInDiffView"` + ToggleShowWhitespace string `yaml:"toggleShowWhitespace"` IncreaseContextInDiffView string `yaml:"increaseContextInDiffView"` DecreaseContextInDiffView string `yaml:"decreaseContextInDiffView"` IncreaseRenameSimilarityThreshold string `yaml:"increaseRenameSimilarityThreshold"` @@ -783,6 +787,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { ScrollOffMargin: 2, ScrollOffBehavior: "margin", TabWidth: 4, + ShowWhitespace: false, MouseEvents: true, SkipAmendWarning: false, SkipDiscardChangeWarning: false, @@ -981,6 +986,7 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig { SubmitEditorText: "", ExtrasMenu: "@", ToggleWhitespaceInDiffView: "", + ToggleShowWhitespace: "", IncreaseContextInDiffView: "}", DecreaseContextInDiffView: "{", IncreaseRenameSimilarityThreshold: ")", diff --git a/pkg/gocui/view.go b/pkg/gocui/view.go index 166cb0e2cf8..a327223c443 100644 --- a/pkg/gocui/view.go +++ b/pkg/gocui/view.go @@ -1556,28 +1556,28 @@ func lineWrap(line []cell, columns int) [][]cell { // if currChr == 'g' { // panic(n) // } + isBreakPoint := func(s string) bool { return s == " " || s == "-" || s == "\u00B7" } + if n > columns { // This code is convoluted but we've got comprehensive tests so feel free to do whatever you want // to the code to simplify it so long as our tests still pass. - if currChr == " " { - // if the line ends in a space, we'll omit it. This means there'll be no - // way to distinguish between a clean break and a mid-word break, but - // I think it's worth it. + if currChr == " " || currChr == "\u00B7" { + // omit space or whitespace marker at line end lines = append(lines, line[offset:i]) offset = i + 1 n = 0 } else if currChr == "-" { - // if the last character is hyphen and the width of line is equal to the columns + // retain hyphen at end of line lines = append(lines, line[offset:i]) offset = i n = rw } else if lastWhitespaceIndex != -1 { - // if there is a space in the line and the line is not breaking at a space/hyphen + // if there is a space/hyphen/dot in the line and we're not breaking at one if line[lastWhitespaceIndex].chr == "-" { - // if break occurs at hyphen, we'll retain the hyphen + // retain the hyphen at end of line lines = append(lines, line[offset:lastWhitespaceIndex+1]) } else { - // if break occurs at space, we'll omit the space + // break at space or dot, omit it lines = append(lines, line[offset:lastWhitespaceIndex]) } // Either way, continue *after* the break @@ -1593,7 +1593,7 @@ func lineWrap(line []cell, columns int) [][]cell { n = rw } lastWhitespaceIndex = -1 - } else if line[i].chr == " " || line[i].chr == "-" { + } else if isBreakPoint(line[i].chr) { lastWhitespaceIndex = i } } diff --git a/pkg/gui/context/merge_conflicts_context.go b/pkg/gui/context/merge_conflicts_context.go index 2ab446c066c..0be6753f476 100644 --- a/pkg/gui/context/merge_conflicts_context.go +++ b/pkg/gui/context/merge_conflicts_context.go @@ -5,6 +5,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" ) @@ -91,7 +92,11 @@ func (self *MergeConflictsContext) GetContentToRender() string { } func (self *MergeConflictsContext) setContent() { - self.GetView().SetContent(self.GetContentToRender()) + content := self.GetContentToRender() + if self.c.UserConfig().Gui.ShowWhitespace { + content = utils.ShowWhitespaceCharacters(content, self.c.UserConfig().Gui.TabWidth) + } + self.GetView().SetContent(content) } func (self *MergeConflictsContext) FocusSelection() { diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 334c2e374bc..1b5b86d5dec 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" deadlock "github.com/sasha-s/go-deadlock" ) @@ -90,7 +91,11 @@ func (self *PatchExplorerContext) Render() { } func (self *PatchExplorerContext) setContent() { - self.GetView().SetContent(self.GetContentToRender()) + content := self.GetContentToRender() + if self.c.UserConfig().Gui.ShowWhitespace { + content = utils.ShowWhitespaceCharacters(content, self.c.UserConfig().Gui.TabWidth) + } + self.GetView().SetContent(content) } func (self *PatchExplorerContext) FocusSelection() { diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index 079ad24b5f9..98dd30773b2 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.ToggleShowWhitespace), + Handler: self.toggleShowWhitespace, + Description: self.c.Tr.ToggleShowWhitespace, + Tooltip: self.c.Tr.ToggleShowWhitespaceTooltip, + }, } } @@ -254,6 +260,10 @@ func (self *GlobalController) toggleWhitespace() error { return (&ToggleWhitespaceAction{c: self.c}).Call() } +func (self *GlobalController) toggleShowWhitespace() error { + return (&ToggleShowWhitespaceAction{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/patch_building_helper.go b/pkg/gui/controllers/helpers/patch_building_helper.go index 9369cca93fb..31068e18bcc 100644 --- a/pkg/gui/controllers/helpers/patch_building_helper.go +++ b/pkg/gui/controllers/helpers/patch_building_helper.go @@ -6,6 +6,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" ) type PatchBuildingHelper struct { @@ -98,6 +99,11 @@ func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpt mainContent := context.GetContentToRender() + if self.c.UserConfig().Gui.ShowWhitespace { + mainContent = utils.ShowWhitespaceCharacters(mainContent, self.c.UserConfig().Gui.TabWidth) + secondaryDiff = utils.ShowWhitespaceCharacters(secondaryDiff, self.c.UserConfig().Gui.TabWidth) + } + self.c.Contexts().CustomPatchBuilder.FocusSelection() self.c.RenderToMainViews(types.RefreshMainOpts{ diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go index 55b9c133bd0..6e39371d20c 100644 --- a/pkg/gui/controllers/helpers/staging_helper.go +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -4,6 +4,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" ) type StagingHelper struct { @@ -101,6 +102,11 @@ func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) { self.c.Contexts().Staging.FocusSelection() } + if self.c.UserConfig().Gui.ShowWhitespace { + mainContent = utils.ShowWhitespaceCharacters(mainContent, self.c.UserConfig().Gui.TabWidth) + secondaryContent = utils.ShowWhitespaceCharacters(secondaryContent, self.c.UserConfig().Gui.TabWidth) + } + self.c.RenderToMainViews(types.RefreshMainOpts{ Pair: self.c.MainViewPairs().Staging, Main: &types.ViewUpdateOpts{ diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 31c746750b1..bcc36e41fd9 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -323,6 +323,10 @@ func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts { if c.Git().Patch.PatchBuilder.Active() { patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false) + if c.UserConfig().Gui.ShowWhitespace { + patch = utils.ShowWhitespaceCharacters(patch, c.UserConfig().Gui.TabWidth) + } + return &types.ViewUpdateOpts{ Task: types.NewRenderStringWithoutScrollTask(patch), Title: c.Tr.CustomPatch, diff --git a/pkg/gui/controllers/toggle_show_whitespace_action.go b/pkg/gui/controllers/toggle_show_whitespace_action.go new file mode 100644 index 00000000000..c7c3d0c7bb4 --- /dev/null +++ b/pkg/gui/controllers/toggle_show_whitespace_action.go @@ -0,0 +1,18 @@ +package controllers + +import "github.com/jesseduffield/lazygit/pkg/gui/types" + +type ToggleShowWhitespaceAction struct { + c *ControllerCommon +} + +func (self *ToggleShowWhitespaceAction) Call() error { + self.c.UserConfig().Gui.ShowWhitespace = !self.c.UserConfig().Gui.ShowWhitespace + + if self.c.UserConfig().Gui.ShowWhitespace { + self.c.Toast(self.c.Tr.ShowWhitespaceIndicatorOn) + } + + self.c.Context().CurrentSide().HandleFocus(types.OnFocusOpts{}) + return nil +} diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 3bfc6410002..6c240859b53 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/tasks" + "github.com/jesseduffield/lazygit/pkg/utils" ) func (gui *Gui) newCmdTask(view *gocui.View, cmd *exec.Cmd, prefix string) error { @@ -105,9 +106,15 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { manager, ok := gui.viewBufferManagerMap[view.Name()] if !ok { + writer := io.Writer(view) + writer = &whitespaceFilterWriter{ + writer: writer, + showWhitespace: func() bool { return gui.c.UserConfig().Gui.ShowWhitespace }, + tabWidth: func() int { return gui.c.UserConfig().Gui.TabWidth }, + } manager = tasks.NewViewBufferManager( gui.Log, - view, + writer, func() { // we could clear here, but that actually has the effect of causing a flicker // where the view may contain no content momentarily as the gui refreshes. @@ -144,3 +151,16 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager { return manager } + +type whitespaceFilterWriter struct { + writer io.Writer + showWhitespace func() bool + tabWidth func() int +} + +func (w *whitespaceFilterWriter) Write(p []byte) (n int, err error) { + if w.showWhitespace() { + p = []byte(utils.ShowWhitespaceCharacters(string(p), w.tabWidth())) + } + return w.writer.Write(p) +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 31b18dca049..8174eba61fc 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -770,6 +770,10 @@ type TranslationSet struct { ToggleWhitespaceInDiffViewTooltip string IgnoreWhitespaceDiffViewSubTitle string IgnoreWhitespaceNotSupportedHere string + ToggleShowWhitespace string + ToggleShowWhitespaceTooltip string + ShowWhitespaceIndicatorOn string + ShowWhitespaceIndicatorOff string IncreaseContextInDiffView string IncreaseContextInDiffViewTooltip string DecreaseContextInDiffView string @@ -1893,6 +1897,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", + ToggleShowWhitespace: "Toggle show whitespace characters", + ToggleShowWhitespaceTooltip: "Toggle whether spaces and tabs are rendered with visible markers (· for space, → for tab).\n\nThe default can be changed in the config file with the key 'gui.showWhitespace'.", + ShowWhitespaceIndicatorOn: "(showing whitespace)", + ShowWhitespaceIndicatorOff: "", 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", diff --git a/pkg/utils/lines.go b/pkg/utils/lines.go index 76c73522c4e..4bea1c708a3 100644 --- a/pkg/utils/lines.go +++ b/pkg/utils/lines.go @@ -147,6 +147,10 @@ func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int, tabW originalLineIndices = append(originalLineIndices, originalLineIdx) } + isBreakPoint := func(c rune) bool { + return c == ' ' || c == '-' || c == '·' + } + n := 0 offset := 0 lastWhitespaceIndex := -1 @@ -155,7 +159,7 @@ func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int, tabW n += rw if n > width { - if currChr == ' ' { + if currChr == ' ' || currChr == '·' { appendWrappedLine(line[offset:i]) offset = i + 1 n = 0 @@ -177,7 +181,7 @@ func WrapViewLinesToWidth(wrap bool, editable bool, text string, width int, tabW n = rw } lastWhitespaceIndex = -1 - } else if currChr == ' ' || currChr == '-' { + } else if isBreakPoint(currChr) { lastWhitespaceIndex = i } } diff --git a/pkg/utils/show_whitespace.go b/pkg/utils/show_whitespace.go new file mode 100644 index 00000000000..28b49301a3f --- /dev/null +++ b/pkg/utils/show_whitespace.go @@ -0,0 +1,97 @@ +package utils + +import ( + "strings" + "unicode/utf8" +) + +const ( + spaceMarker = "\u00B7" + tabMarker = "\u2192" + tabFillChar = "-" + + faintOn = "\x1b[2m" + faintOff = "\x1b[22m" +) + +// ShowWhitespaceCharacters replaces spaces and tabs in the given content with +// visual indicators. ANSI escape sequences are preserved and whitespace within +// them is not replaced. Spaces are replaced with middle dots (·) and tabs are +// replaced with a right arrow (→) followed by horizontal lines to fill the +// tab stop, so the layout stays stable when toggling. +func ShowWhitespaceCharacters(content string, tabWidth int) string { + var result strings.Builder + result.Grow(len(content)) + + if tabWidth < 1 { + tabWidth = 4 + } + + inEscape := false + inEscapeCSI := false + col := 0 + + for i := 0; i < len(content); { + if inEscape { + result.WriteByte(content[i]) + if inEscapeCSI { + if (content[i] >= 'a' && content[i] <= 'z') || (content[i] >= 'A' && content[i] <= 'Z') { + inEscape = false + inEscapeCSI = false + } + } else { + if content[i] == '[' { + inEscapeCSI = true + } else { + inEscape = false + } + } + i++ + continue + } + + if content[i] == '\x1b' { + result.WriteByte(content[i]) + inEscape = true + i++ + continue + } + + if content[i] == '\n' { + result.WriteByte(content[i]) + col = 0 + i++ + continue + } + + if content[i] == '\t' { + numFill := tabWidth - (col % tabWidth) + result.WriteString(faintOn) + result.WriteString(tabMarker) + col++ + for f := 1; f < numFill; f++ { + result.WriteString(tabFillChar) + col++ + } + result.WriteString(faintOff) + i++ + continue + } + + if content[i] == ' ' { + result.WriteString(faintOn) + result.WriteString(spaceMarker) + result.WriteString(faintOff) + col++ + i++ + continue + } + + r, size := utf8.DecodeRuneInString(content[i:]) + result.WriteRune(r) + col++ + i += size + } + + return result.String() +} diff --git a/pkg/utils/show_whitespace_test.go b/pkg/utils/show_whitespace_test.go new file mode 100644 index 00000000000..8c45a413d66 --- /dev/null +++ b/pkg/utils/show_whitespace_test.go @@ -0,0 +1,88 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestShowWhitespaceCharacters(t *testing.T) { + dot := faintOn + "\u00B7" + faintOff + arr := faintOn + "\u2192" + fill := "-" + + tests := []struct { + name string + input string + expected string + }{ + { + name: "no whitespace", + input: "hello", + expected: "hello", + }, + { + name: "spaces only", + input: "hello world", + expected: "hello" + dot + "world", + }, + { + name: "tab at column 0", + input: "\thello", + expected: arr + fill + fill + fill + faintOff + "hello", + }, + { + name: "tab at column 1 (after char)", + input: "a\tb", + expected: "a" + arr + fill + fill + faintOff + "b", + }, + { + name: "multiple spaces", + input: "a b", + expected: "a" + dot + dot + "b", + }, + { + name: "newlines preserved", + input: "line1\nline2", + expected: "line1\nline2", + }, + { + name: "ANSI escape preserved", + input: "\x1b[31mred text\x1b[0m", + expected: "\x1b[31mred" + dot + "text\x1b[0m", + }, + { + name: "trailing spaces", + input: "trailing \n", + expected: "trailing" + dot + dot + "\n", + }, + { + name: "leading spaces", + input: " leading", + expected: dot + dot + "leading", + }, + { + name: "tab followed by spaces", + input: "\t hello", + expected: arr + fill + fill + fill + faintOff + dot + dot + "hello", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ShowWhitespaceCharacters(tt.input, 4) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestShowWhitespaceCharacters_DifferentTabWidths(t *testing.T) { + arr := faintOn + "\u2192" + fill := "-" + + result := ShowWhitespaceCharacters("\t", 2) + assert.Equal(t, arr+fill+faintOff, result) + + result = ShowWhitespaceCharacters("\t", 8) + assert.Equal(t, arr+fill+fill+fill+fill+fill+fill+fill+faintOff, result) +} diff --git a/schema-master/config.json b/schema-master/config.json index 549b8087705..62f40e67ddb 100644 --- a/schema-master/config.json +++ b/schema-master/config.json @@ -513,6 +513,11 @@ "description": "The number of spaces per tab; used for everything that's shown in the main view, but probably mostly relevant for diffs.\nNote that when using a pager, the pager has its own tab width setting, so you need to pass it separately in the pager command.", "default": 4 }, + "showWhitespace": { + "type": "boolean", + "description": "If true, show visual indicators for spaces (middle dot) and tabs (right arrow) in diff and staging views.\nThis can be toggled from within Lazygit with the 'toggleShowWhitespace' keybinding (default `\u003cc-v\u003e`), but that will not change the default.", + "default": false + }, "mouseEvents": { "type": "boolean", "description": "If true, capture mouse events.\nWhen mouse events are captured, it's a little harder to select text: e.g. requiring you to hold the option key when on macOS.", @@ -1593,6 +1598,10 @@ "type": "string", "default": "\u003cctrl+w\u003e" }, + "toggleShowWhitespace": { + "type": "string", + "default": "\u003cc-v\u003e" + }, "increaseContextInDiffView": { "type": "string", "default": "}"