Standardise display of range selection across views

We're not fully standardising here: different contexts can store their range state however
they like. What we are standardising on is that now the view is always responsible for
highlighting the selected lines, meaning the context/controller needs to tell the view
where the range start is.

Two convenient benefits from this change:
1) we no longer need bespoke code in integration tests for asserting on selected lines because
we can just ask the view
2) line selection in staging/patch-building/merge-conflicts views now look the same as in
list views i.e. the highlight applies to the whole line (including trailing space)

I also noticed a bug with merge conflicts not rendering the selection on focus though I suspect
it wasn't a bug with any real consequences when the view wasn't displaying the selection.

I'm going to scrap the selectedRangeBgColor config and just let it use the single line
background color. Hopefully nobody cares, but there's really no need for an extra config.
This commit is contained in:
Jesse Duffield 2024-01-14 00:18:05 +11:00
parent c0c3aac02e
commit f3eb180f75
28 changed files with 255 additions and 287 deletions

View file

@ -57,8 +57,6 @@ gui:
- blue
selectedLineBgColor:
- blue # set to `default` to have no background colour
selectedRangeBgColor:
- blue
cherryPickedCommitBgColor:
- cyan
cherryPickedCommitFgColor:
@ -390,15 +388,13 @@ The available attributes are:
## Highlighting the selected line
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` and `selectedRangeBgColor` keys to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
If you don't like the default behaviour of highlighting the selected line with a blue background, you can use the `selectedLineBgColor` key to customise the behaviour. If you just want to embolden the selected line (this was the original default), you can do the following:
```yaml
gui:
theme:
selectedLineBgColor:
- default
selectedRangeBgColor:
- default
```
You can also use the reverse attribute like so:
@ -408,8 +404,6 @@ gui:
theme:
selectedLineBgColor:
- reverse
selectedRangeBgColor:
- reverse
```
## Custom Author Color

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: Next page
<kbd>&lt;</kbd>: Scroll to top
<kbd>&gt;</kbd>: Scroll to bottom
<kbd>v</kbd>: Toggle range select
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: Search the current view by text
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
@ -196,8 +199,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;left&gt;</kbd>: Select previous hunk
<kbd>&lt;right&gt;</kbd>: Select next hunk
<kbd>v</kbd>: Toggle drag select
<kbd>V</kbd>: Toggle drag select
<kbd>v</kbd>: Toggle range select
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Open file
@ -212,8 +214,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;left&gt;</kbd>: Select previous hunk
<kbd>&lt;right&gt;</kbd>: Select next hunk
<kbd>v</kbd>: Toggle drag select
<kbd>V</kbd>: Toggle drag select
<kbd>v</kbd>: Toggle range select
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Open file

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: 次のページ
<kbd>&lt;</kbd>: 最上部までスクロール
<kbd>&gt;</kbd>: 最下部までスクロール
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: 検索を開始
<kbd>H</kbd>: 左スクロール
<kbd>L</kbd>: 右スクロール
@ -270,7 +273,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 前のhunkを選択
<kbd>&lt;right&gt;</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: Hunk選択を切り替え
<kbd>&lt;c-o&gt;</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>o</kbd>: ファイルを開く
@ -286,7 +288,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 前のhunkを選択
<kbd>&lt;right&gt;</kbd>: 次のhunkを選択
<kbd>v</kbd>: 範囲選択を切り替え
<kbd>V</kbd>: 範囲選択を切り替え
<kbd>a</kbd>: Hunk選択を切り替え
<kbd>&lt;c-o&gt;</kbd>: 選択されたテキストをクリップボードにコピー
<kbd>o</kbd>: ファイルを開く

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: 다음 페이지
<kbd>&lt;</kbd>: 맨 위로 스크롤
<kbd>&gt;</kbd>: 맨 아래로 스크롤
<kbd>v</kbd>: 드래그 선택 전환
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: 검색 시작
<kbd>H</kbd>: 우 스크롤
<kbd>L</kbd>: 좌 스크롤
@ -141,7 +144,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 이전 hunk를 선택
<kbd>&lt;right&gt;</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>o</kbd>: 파일 닫기
@ -157,7 +159,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 이전 hunk를 선택
<kbd>&lt;right&gt;</kbd>: 다음 hunk를 선택
<kbd>v</kbd>: 드래그 선택 전환
<kbd>V</kbd>: 드래그 선택 전환
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: 선택한 텍스트를 클립보드에 복사
<kbd>o</kbd>: 파일 닫기

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: Volgende pagina
<kbd>&lt;</kbd>: Scroll naar boven
<kbd>&gt;</kbd>: Scroll naar beneden
<kbd>v</kbd>: Toggle drag selecteer
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: Start met zoeken
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
@ -205,7 +208,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: Selecteer de vorige hunk
<kbd>&lt;right&gt;</kbd>: Selecteer de volgende hunk
<kbd>v</kbd>: Toggle drag selecteer
<kbd>V</kbd>: Toggle drag selecteer
<kbd>a</kbd>: Toggle selecteer hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Open bestand
@ -266,7 +268,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: Selecteer de vorige hunk
<kbd>&lt;right&gt;</kbd>: Selecteer de volgende hunk
<kbd>v</kbd>: Toggle drag selecteer
<kbd>V</kbd>: Toggle drag selecteer
<kbd>a</kbd>: Toggle selecteer hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Open bestand

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: Next page
<kbd>&lt;</kbd>: Scroll to top
<kbd>&gt;</kbd>: Scroll to bottom
<kbd>v</kbd>: Toggle range select
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: Search the current view by text
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
@ -127,8 +130,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;left&gt;</kbd>: Poprzedni kawałek
<kbd>&lt;right&gt;</kbd>: Następny kawałek
<kbd>v</kbd>: Toggle drag select
<kbd>V</kbd>: Toggle drag select
<kbd>v</kbd>: Toggle range select
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Otwórz plik
@ -197,8 +199,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;left&gt;</kbd>: Poprzedni kawałek
<kbd>&lt;right&gt;</kbd>: Następny kawałek
<kbd>v</kbd>: Toggle drag select
<kbd>V</kbd>: Toggle drag select
<kbd>v</kbd>: Toggle range select
<kbd>a</kbd>: Toggle select hunk
<kbd>&lt;c-o&gt;</kbd>: Copy the selected text to the clipboard
<kbd>o</kbd>: Otwórz plik

View file

@ -37,6 +37,9 @@ _Связки клавиш_
<kbd>.</kbd>: Следующая страница
<kbd>&lt;</kbd>: Пролистать наверх
<kbd>&gt;</kbd>: Прокрутить вниз
<kbd>v</kbd>: Переключить выборку перетаскивания
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: Найти
<kbd>H</kbd>: Прокрутить влево
<kbd>L</kbd>: Прокрутить вправо
@ -61,7 +64,6 @@ _Связки клавиш_
<kbd>&lt;left&gt;</kbd>: Выбрать предыдущую часть
<kbd>&lt;right&gt;</kbd>: Выбрать следующую часть
<kbd>v</kbd>: Переключить выборку перетаскивания
<kbd>V</kbd>: Переключить выборку перетаскивания
<kbd>a</kbd>: Переключить выборку частей
<kbd>&lt;c-o&gt;</kbd>: Скопировать выделенный текст в буфер обмена
<kbd>o</kbd>: Открыть файл
@ -106,7 +108,6 @@ _Связки клавиш_
<kbd>&lt;left&gt;</kbd>: Выбрать предыдущую часть
<kbd>&lt;right&gt;</kbd>: Выбрать следующую часть
<kbd>v</kbd>: Переключить выборку перетаскивания
<kbd>V</kbd>: Переключить выборку перетаскивания
<kbd>a</kbd>: Переключить выборку частей
<kbd>&lt;c-o&gt;</kbd>: Скопировать выделенный текст в буфер обмена
<kbd>o</kbd>: Открыть файл

View file

@ -37,6 +37,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>.</kbd>: 下一页
<kbd>&lt;</kbd>: 滚动到顶部
<kbd>&gt;</kbd>: 滚动到底部
<kbd>v</kbd>: 切换拖动选择
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: 开始搜索
<kbd>H</kbd>: 向左滚动
<kbd>L</kbd>: 向右滚动
@ -229,7 +232,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 选择上一个区块
<kbd>&lt;right&gt;</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>&lt;c-o&gt;</kbd>: 将选中文本复制到剪贴板
<kbd>o</kbd>: 打开文件
@ -274,7 +276,6 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;left&gt;</kbd>: 选择上一个区块
<kbd>&lt;right&gt;</kbd>: 选择下一个区块
<kbd>v</kbd>: 切换拖动选择
<kbd>V</kbd>: 切换拖动选择
<kbd>a</kbd>: 切换选择区块
<kbd>&lt;c-o&gt;</kbd>: 将选中文本复制到剪贴板
<kbd>o</kbd>: 打开文件

View file

@ -37,6 +37,9 @@ _說明`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B`B`表示 Shift+B_
<kbd>.</kbd>: 下一頁
<kbd>&lt;</kbd>: 捲動到頂部
<kbd>&gt;</kbd>: 捲動到底部
<kbd>v</kbd>: 切換拖曳選擇
<kbd>&lt;s-down&gt;</kbd>: Range select down
<kbd>&lt;s-up&gt;</kbd>: Range select up
<kbd>/</kbd>: 開始搜尋
<kbd>H</kbd>: 向左捲動
<kbd>L</kbd>: 向右捲動
@ -102,7 +105,6 @@ _說明`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B`B`表示 Shift+B_
<kbd>&lt;left&gt;</kbd>: 選擇上一段
<kbd>&lt;right&gt;</kbd>: 選擇下一段
<kbd>v</kbd>: 切換拖曳選擇
<kbd>V</kbd>: 切換拖曳選擇
<kbd>a</kbd>: 切換選擇程式碼塊
<kbd>&lt;c-o&gt;</kbd>: 複製所選文本至剪貼簿
<kbd>o</kbd>: 開啟檔案
@ -124,7 +126,6 @@ _說明`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B`B`表示 Shift+B_
<kbd>&lt;left&gt;</kbd>: 選擇上一段
<kbd>&lt;right&gt;</kbd>: 選擇下一段
<kbd>v</kbd>: 切換拖曳選擇
<kbd>V</kbd>: 切換拖曳選擇
<kbd>a</kbd>: 切換選擇程式碼塊
<kbd>&lt;c-o&gt;</kbd>: 複製所選文本至剪貼簿
<kbd>o</kbd>: 開啟檔案

View file

@ -14,11 +14,6 @@ type patchPresenter struct {
// if true, all following fields are ignored
plain bool
isFocused bool
// first line index for selected cursor range
firstLineIndex int
// last line index for selected cursor range
lastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
incLineIndices *set.Set[int]
}
@ -44,11 +39,6 @@ func formatRangePlain(patch *Patch, startIdx int, endIdx int) string {
}
type FormatViewOpts struct {
IsFocused bool
// first line index for selected cursor range
FirstLineIndex int
// last line index for selected cursor range
LastLineIndex int
// line indices for tagged lines (e.g. lines added to a custom patch)
IncLineIndices *set.Set[int]
}
@ -63,9 +53,6 @@ func formatView(patch *Patch, opts FormatViewOpts) string {
presenter := &patchPresenter{
patch: patch,
plain: false,
isFocused: opts.IsFocused,
firstLineIndex: opts.FirstLineIndex,
lastLineIndex: opts.LastLineIndex,
incLineIndices: includedLineIndices,
}
return presenter.format()
@ -112,7 +99,6 @@ func (self *patchPresenter) format() string {
self.formatLineAux(
hunk.headerContext,
theme.DefaultTextColor,
lineIdx,
false,
),
)
@ -139,23 +125,17 @@ func (self *patchPresenter) patchLineStyle(patchLine *PatchLine) style.TextStyle
func (self *patchPresenter) formatLine(str string, textStyle style.TextStyle, index int) string {
included := self.incLineIndices.Includes(index)
return self.formatLineAux(str, textStyle, index, included)
return self.formatLineAux(str, textStyle, included)
}
// 'selected' means you've got it highlighted with your cursor
// 'included' means the line has been included in the patch (only applicable when
// building a patch)
func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, index int, included bool) string {
func (self *patchPresenter) formatLineAux(str string, textStyle style.TextStyle, included bool) string {
if self.plain {
return str
}
selected := self.isFocused && index >= self.firstLineIndex && index <= self.lastLineIndex
if selected {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor)
}
firstCharStyle := textStyle
if included {
firstCharStyle = firstCharStyle.MergeStyle(style.BgGreen)

View file

@ -197,9 +197,7 @@ func (p *PatchBuilder) RenderPatchForFile(filename string, plain bool, reverse b
if plain {
return patch.FormatPlain()
} else {
return patch.FormatView(FormatViewOpts{
IsFocused: false,
})
return patch.FormatView(FormatViewOpts{})
}
}

View file

@ -154,9 +154,6 @@ type ThemeConfig struct {
// Background color of selected line.
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line
SelectedLineBgColor []string `yaml:"selectedLineBgColor" jsonschema:"minItems=1,uniqueItems=true"`
// Background color of selected range
// See https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor" jsonschema:"minItems=1,uniqueItems=true"`
// Foreground color of copied commit
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor" jsonschema:"minItems=1,uniqueItems=true"`
// Background color of copied commit
@ -622,7 +619,6 @@ func GetDefaultConfig() *UserConfig {
InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"blue"},
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"cyan"},
CherryPickedCommitFgColor: []string{"blue"},
MarkedBaseCommitBgColor: []string{"yellow"},

View file

@ -68,8 +68,8 @@ func (self *MergeConflictsContext) IsUserScrolling() bool {
return self.viewModel.userVerticalScrolling
}
func (self *MergeConflictsContext) RenderAndFocus(isFocused bool) error {
self.setContent(isFocused)
func (self *MergeConflictsContext) RenderAndFocus() error {
self.setContent()
self.FocusSelection()
self.c.Render()
@ -77,30 +77,41 @@ func (self *MergeConflictsContext) RenderAndFocus(isFocused bool) error {
return nil
}
func (self *MergeConflictsContext) Render(isFocused bool) error {
self.setContent(isFocused)
func (self *MergeConflictsContext) Render() error {
self.setContent()
self.c.Render()
return nil
}
func (self *MergeConflictsContext) GetContentToRender(isFocused bool) string {
func (self *MergeConflictsContext) GetContentToRender() string {
if self.GetState() == nil {
return ""
}
return mergeconflicts.ColoredConflictFile(self.GetState(), isFocused)
return mergeconflicts.ColoredConflictFile(self.GetState())
}
func (self *MergeConflictsContext) setContent(isFocused bool) {
self.GetView().SetContent(self.GetContentToRender(isFocused))
func (self *MergeConflictsContext) setContent() {
self.GetView().SetContent(self.GetContentToRender())
}
func (self *MergeConflictsContext) FocusSelection() {
if !self.IsUserScrolling() {
_ = self.GetView().SetOriginY(self.GetOriginY())
}
self.SetSelectedLineRange()
}
func (self *MergeConflictsContext) SetSelectedLineRange() {
startIdx, endIdx := self.GetState().GetSelectedRange()
view := self.GetView()
originY := view.OriginY()
// As far as the view is concerned, we are always selecting a range
view.SetRangeSelectStart(startIdx)
view.SetCursorY(endIdx - originY)
}
func (self *MergeConflictsContext) GetOriginY() int {

View file

@ -115,17 +115,10 @@ func (self *PatchExplorerContext) FocusSelection() {
_ = view.SetOriginY(newOriginY)
view.SetCursorY(state.GetSelectedLineIdx() - newOriginY)
// At present this is just bookkeeping: the reason for setting this would be
// so that gocui knows which lines to highlight, but we're currently handling
// highlighting ourselves.
rangeStartLineIdx, isSelectingRange := state.RangeStartLineIdx()
if isSelectingRange {
view.SetRangeSelectStart(rangeStartLineIdx)
} else {
view.CancelRangeSelect()
}
startIdx, endIdx := state.SelectedRange()
// As far as the view is concerned, we are always selecting a range
view.SetRangeSelectStart(startIdx)
view.SetCursorY(endIdx - newOriginY)
}
func (self *PatchExplorerContext) GetContentToRender(isFocused bool) string {

View file

@ -205,7 +205,7 @@ func (self *FilesController) GetOnRenderToMain() func() error {
}
if hasConflicts {
return self.c.Helpers().MergeConflicts.Render(false)
return self.c.Helpers().MergeConflicts.Render()
}
}

View file

@ -69,14 +69,14 @@ func (self *MergeConflictsHelper) EscapeMerge() error {
return nil
}
func (self *MergeConflictsHelper) SetConflictsAndRender(path string, isFocused bool) (bool, error) {
func (self *MergeConflictsHelper) SetConflictsAndRender(path string) (bool, error) {
hasConflicts, err := self.setMergeStateWithoutLock(path)
if err != nil {
return false, err
}
if hasConflicts {
return true, self.context().Render(isFocused)
return true, self.context().Render()
}
return false, nil
@ -100,8 +100,8 @@ func (self *MergeConflictsHelper) context() *context.MergeConflictsContext {
return self.c.Contexts().MergeConflicts
}
func (self *MergeConflictsHelper) Render(isFocused bool) error {
content := self.context().GetContentToRender(isFocused)
func (self *MergeConflictsHelper) Render() error {
content := self.context().GetContentToRender()
var task types.UpdateTask
if self.context().IsUserScrolling() {
@ -127,7 +127,7 @@ func (self *MergeConflictsHelper) RefreshMergeState() error {
return nil
}
hasConflicts, err := self.SetConflictsAndRender(self.c.Contexts().MergeConflicts.GetState().GetPath(), true)
hasConflicts, err := self.SetConflictsAndRender(self.c.Contexts().MergeConflicts.GetState().GetPath())
if err != nil {
return self.c.Error(err)
}

View file

@ -145,7 +145,13 @@ func (self *MergeConflictsController) GetOnFocus() func(types.OnFocusOpts) error
return func(types.OnFocusOpts) error {
self.c.Views().MergeConflicts.Wrap = false
return self.c.Helpers().MergeConflicts.Render(true)
if err := self.c.Helpers().MergeConflicts.Render(); err != nil {
return err
}
self.context().SetSelectedLineRange()
return nil
}
}
@ -313,17 +319,13 @@ func (self *MergeConflictsController) onLastConflictResolved() error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}})
}
func (self *MergeConflictsController) isFocused() bool {
return self.c.CurrentContext().GetKey() == self.context().GetKey()
}
func (self *MergeConflictsController) withRenderAndFocus(f func() error) func() error {
return self.withLock(func() error {
if err := f(); err != nil {
return err
}
return self.context().RenderAndFocus(self.isFocused())
return self.context().RenderAndFocus()
})
}

View file

@ -4,7 +4,6 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
"golang.org/x/exp/slices"
)
@ -143,15 +142,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
gui.State.ViewsSetup = true
}
for _, listContext := range gui.c.Context().AllList() {
view, err := gui.g.View(listContext.GetViewName())
if err != nil {
continue
}
view.SelBgColor = theme.GocuiSelectedLineBgColor
}
mainViewWidth, mainViewHeight := gui.Views.Main.Size()
if mainViewWidth != gui.PrevLayout.MainWidth || mainViewHeight != gui.PrevLayout.MainHeight {
gui.PrevLayout.MainWidth = mainViewWidth

View file

@ -8,7 +8,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
func ColoredConflictFile(state *State, hasFocus bool) string {
func ColoredConflictFile(state *State) string {
content := state.GetContent()
if len(state.conflicts) == 0 {
return content
@ -21,9 +21,6 @@ func ColoredConflictFile(state *State, hasFocus bool) string {
textStyle = style.FgRed
}
if hasFocus && state.conflictIndex < len(state.conflicts) && *state.conflicts[state.conflictIndex] == *conflict && shouldHighlightLine(i, conflict, state.Selection()) {
textStyle = textStyle.MergeStyle(theme.SelectedRangeBgColor).SetBold()
}
if i == conflict.end && len(remainingConflicts) > 0 {
conflict, remainingConflicts = shiftConflict(remainingConflicts)
}
@ -35,8 +32,3 @@ func ColoredConflictFile(state *State, hasFocus bool) string {
func shiftConflict(conflicts []*mergeConflict) (*mergeConflict, []*mergeConflict) {
return conflicts[0], conflicts[1:]
}
func shouldHighlightLine(index int, conflict *mergeConflict, selection Selection) bool {
selectionStart, selectionEnd := selection.bounds(conflict)
return index >= selectionStart && index <= selectionEnd
}

View file

@ -240,12 +240,8 @@ func (s *State) AdjustSelectedLineIdx(change int) {
}
func (s *State) RenderForLineIndices(isFocused bool, includedLineIndices []int) string {
firstLineIdx, lastLineIdx := s.SelectedRange()
includedLineIndicesSet := set.NewFromSlice(includedLineIndices)
return s.patch.FormatView(patch.FormatViewOpts{
IsFocused: isFocused,
FirstLineIndex: firstLineIdx,
LastLineIndex: lastLineIdx,
IncLineIndices: includedLineIndicesSet,
})
}

View file

@ -91,6 +91,7 @@ func (gui *Gui) createAllViews() error {
}
(*mapping.viewPtr).FrameRunes = frameRunes
(*mapping.viewPtr).FgColor = theme.GocuiDefaultTextColor
(*mapping.viewPtr).SelBgColor = theme.GocuiSelectedLineBgColor
}
gui.Views.Options.FgColor = theme.OptionsColor
@ -134,23 +135,18 @@ func (gui *Gui) createAllViews() error {
}
gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges
gui.Views.Staging.Highlight = false
gui.Views.Staging.Wrap = true
gui.Views.StagingSecondary.Title = gui.c.Tr.StagedChanges
gui.Views.StagingSecondary.Highlight = false
gui.Views.StagingSecondary.Wrap = true
gui.Views.PatchBuilding.Title = gui.Tr.Patch
gui.Views.PatchBuilding.Highlight = false
gui.Views.PatchBuilding.Wrap = true
gui.Views.PatchBuildingSecondary.Title = gui.Tr.CustomPatch
gui.Views.PatchBuildingSecondary.Highlight = false
gui.Views.PatchBuildingSecondary.Wrap = true
gui.Views.MergeConflicts.Title = gui.c.Tr.MergeConflictsTitle
gui.Views.MergeConflicts.Highlight = false
gui.Views.MergeConflicts.Wrap = false
gui.Views.Limit.Title = gui.c.Tr.NotEnoughSpace

View file

@ -10,45 +10,24 @@ import (
type ViewDriver struct {
// context is prepended to any error messages e.g. 'context: "current view"'
context string
getView func() *gocui.View
t *TestDriver
getSelectedLinesFn func() ([]string, error)
getSelectedRangeFn func() (int, int, error)
getSelectedLineIdxFn func() (int, error)
context string
getView func() *gocui.View
t *TestDriver
}
func (self *ViewDriver) getSelectedLines() ([]string, error) {
if self.getSelectedLinesFn == nil {
view := self.t.gui.View(self.getView().Name())
return []string{view.SelectedLine()}, nil
}
return self.getSelectedLinesFn()
func (self *ViewDriver) getSelectedLines() []string {
view := self.t.gui.View(self.getView().Name())
return view.SelectedLines()
}
func (self *ViewDriver) getSelectedRange() (int, int, error) {
if self.getSelectedRangeFn == nil {
view := self.t.gui.View(self.getView().Name())
idx := view.SelectedLineIdx()
return idx, idx, nil
}
return self.getSelectedRangeFn()
func (self *ViewDriver) getSelectedRange() (int, int) {
view := self.t.gui.View(self.getView().Name())
return view.SelectedLineRange()
}
// even if you have a selected range, there may still be a line within that range
// which the cursor points at. This function returns that line index.
func (self *ViewDriver) getSelectedLineIdx() (int, error) {
if self.getSelectedLineIdxFn == nil {
view := self.t.gui.View(self.getView().Name())
return view.SelectedLineIdx(), nil
}
return self.getSelectedLineIdxFn()
func (self *ViewDriver) getSelectedLineIdx() int {
view := self.t.gui.View(self.getView().Name())
return view.SelectedLineIdx()
}
// asserts that the view has the expected title
@ -105,7 +84,7 @@ func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver {
content := self.getView().Buffer()
lines := strings.Split(content, "\n")
startIdx, endIdx, err := self.getSelectedRange()
startIdx, endIdx := self.getSelectedRange()
for i := 0; i < len(lines)-len(matchers)+1; i++ {
matches := true
@ -118,10 +97,6 @@ func (self *ViewDriver) ContainsLines(matchers ...*TextMatcher) *ViewDriver {
break
}
if checkIsSelected {
if err != nil {
matches = false
break
}
if lineIdx < startIdx || lineIdx > endIdx {
matches = false
break
@ -181,10 +156,7 @@ func (self *ViewDriver) SelectedLines(matchers ...*TextMatcher) *ViewDriver {
self.validateEnoughLines(matchers)
self.t.assertWithRetries(func() (bool, string) {
selectedLines, err := self.getSelectedLines()
if err != nil {
return false, err.Error()
}
selectedLines := self.getSelectedLines()
selectedContent := strings.Join(selectedLines, "\n")
expectedContent := expectedContentFromMatchers(matchers)
@ -251,19 +223,13 @@ func (self *ViewDriver) assertLines(offset int, matchers ...*TextMatcher) *ViewD
if checkIsSelected {
self.t.assertWithRetries(func() (bool, string) {
startIdx, endIdx, err := self.getSelectedRange()
if err != nil {
return false, err.Error()
}
startIdx, endIdx := self.getSelectedRange()
if lineIdx < startIdx || lineIdx > endIdx {
if startIdx == endIdx {
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected %d, got %d", view.Name(), lineIdx, startIdx)
} else {
lines, err := self.getSelectedLines()
if err != nil {
return false, err.Error()
}
lines := self.getSelectedLines()
return false, fmt.Sprintf("Unexpected selected line index in view '%s'. Expected line %d to be in range %d to %d. Selected lines:\n---\n%s\n---\n\nExpected line: '%s'", view.Name(), lineIdx, startIdx, endIdx, strings.Join(lines, "\n"), matcher.name())
}
}
@ -286,15 +252,11 @@ func (self *ViewDriver) Content(matcher *TextMatcher) *ViewDriver {
return self
}
// asserts on the selected line of the view. If your view has multiple lines selected,
// but also has a concept of a cursor position, this will assert on the line that
// the cursor is on. Otherwise it will assert on the first line of the selection.
// asserts on the selected line of the view. If you are selecting a range,
// you should use the SelectedLines method instead.
func (self *ViewDriver) SelectedLine(matcher *TextMatcher) *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil {
return false, err.Error()
}
selectedLineIdx := self.getSelectedLineIdx()
viewLines := self.getView().BufferLines()
@ -480,11 +442,7 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
}
})
selectedLineIdx, err := self.getSelectedLineIdx()
if err != nil {
self.t.fail(err.Error())
return self
}
selectedLineIdx := self.getSelectedLineIdx()
if selectedLineIdx == matchIndex {
return self.SelectedLine(matcher)
}
@ -507,11 +465,7 @@ func (self *ViewDriver) NavigateToLine(matcher *TextMatcher) *ViewDriver {
for i := 0; i < maxNumKeyPresses; i++ {
keyPress()
idx, err := self.getSelectedLineIdx()
if err != nil {
self.t.fail(err.Error())
return self
}
idx := self.getSelectedLineIdx()
if ok, _ := matcher.test(lines[idx]); ok {
return self
}

View file

@ -2,11 +2,8 @@ package components
import (
"fmt"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
)
type Views struct {
@ -30,95 +27,19 @@ func (self *Views) Secondary() *ViewDriver {
}
func (self *Views) regularView(viewName string) *ViewDriver {
return self.newStaticViewDriver(viewName, nil, nil, nil)
}
func (self *Views) patchExplorerViewByName(viewName string) *ViewDriver {
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := state.PlainRenderSelected()
// the above method returns a string with a trailing newline so we need to remove that before splitting
selectedLines := strings.Split(strings.TrimSuffix(selectedContent, "\n"), "\n")
return selectedLines, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.SelectedRange()
return startIdx, endIdx, nil
},
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.PatchExplorerContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
return state.GetSelectedLineIdx(), nil
},
)
}
// 'static' because it'll always refer to the same view, as opposed to the 'main' view which could actually be
// one of several views, or the 'current' view which depends on focus.
func (self *Views) newStaticViewDriver(
viewName string,
getSelectedLinesFn func() ([]string, error),
getSelectedLineRangeFn func() (int, int, error),
getSelectedLineIdxFn func() (int, error),
) *ViewDriver {
return &ViewDriver{
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
getSelectedLinesFn: getSelectedLinesFn,
getSelectedRangeFn: getSelectedLineRangeFn,
getSelectedLineIdxFn: getSelectedLineIdxFn,
t: self.t,
context: fmt.Sprintf("%s view", viewName),
getView: func() *gocui.View { return self.t.gui.View(viewName) },
t: self.t,
}
}
func (self *Views) MergeConflicts() *ViewDriver {
viewName := "mergeConflicts"
return self.newStaticViewDriver(
viewName,
func() ([]string, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return nil, errors.New("Expected patch explorer to be activated")
}
selectedContent := strings.Split(state.PlainRenderSelected(), "\n")
func (self *Views) patchExplorerViewByName(viewName string) *ViewDriver {
return self.regularView(viewName)
}
return selectedContent, nil
},
func() (int, int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, 0, errors.New("Expected patch explorer to be activated")
}
startIdx, endIdx := state.GetSelectedRange()
return startIdx, endIdx, nil
},
// there is no concept of a cursor in the merge conflicts panel so we just return the start of the selection
func() (int, error) {
ctx := self.t.gui.ContextForView(viewName).(*context.MergeConflictsContext)
state := ctx.GetState()
if state == nil {
return 0, errors.New("Expected patch explorer to be activated")
}
startIdx, _ := state.GetSelectedRange()
return startIdx, nil
},
)
func (self *Views) MergeConflicts() *ViewDriver {
return self.regularView("mergeConflicts")
}
func (self *Views) Commits() *ViewDriver {

View file

@ -260,6 +260,7 @@ var tests = []*components.IntegrationTest{
ui.DoublePopup,
ui.EmptyMenu,
ui.OpenLinkFailure,
ui.RangeSelect,
ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop,
undo.UndoDrop,

View file

@ -0,0 +1,142 @@
package ui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// Here's the state machine we need to verify:
// (no range, press 'v') -> sticky range
// (no range, press arrow) -> no range
// (no range, press shift+arrow) -> nonsticky range
// (sticky range, press 'v') -> no range
// (sticky range, press arrow) -> sticky range
// (sticky range, press shift+arrow) -> nonsticky range
// (nonsticky range, press 'v') -> no range
// (nonsticky range, press arrow) -> no range
// (nonsticky range, press shift+arrow) -> nonsticky range
// Importantly, if you press 'v' when in a nonsticky range, it clears the range,
// so no matter which mode you're in, 'v' will cancel the range.
// And, if you press shift+up/down when in a sticky range, it switches to a non-
// sticky range, meaning if you then press up/down without shift, it clears
// the range.
var RangeSelect = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify range select works as expected in list views and in patch explorer views",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// We're testing the commits view as our representative list context,
// as well as the staging view, and we're using the exact same code to test
// both to ensure they have the exact same behaviour (they are currently implemented
// separately)
// In both views we're going to have 10 lines starting from 'line 1' going down to
// 'line 10'.
fileContent := ""
total := 10
for i := 1; i <= total; i++ {
remaining := total - i + 1
// Commits are displayed in reverse order so to we need to create them in reverse to have them appear as 'line 1', 'line 2' etc.
shell.EmptyCommit(fmt.Sprintf("line %d", remaining))
fileContent = fmt.Sprintf("%sline %d\n", fileContent, i)
}
shell.CreateFile("file1", fileContent)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
assertRangeSelectBehaviour := func(v *ViewDriver) {
v.
SelectedLines(
Contains("line 1"),
).
// (no range, press 'v') -> sticky range
Press(keys.Universal.ToggleRangeSelect).
SelectedLines(
Contains("line 1"),
).
// (sticky range, press arrow) -> sticky range
SelectNextItem().
SelectedLines(
Contains("line 1"),
Contains("line 2"),
).
// (sticky range, press 'v') -> no range
Press(keys.Universal.ToggleRangeSelect).
SelectedLines(
Contains("line 2"),
).
// (no range, press arrow) -> no range
SelectPreviousItem().
SelectedLines(
Contains("line 1"),
).
// (no range, press shift+arrow) -> nonsticky range
Press(keys.Universal.RangeSelectDown).
SelectedLines(
Contains("line 1"),
Contains("line 2"),
).
// (nonsticky range, press shift+arrow) -> nonsticky range
Press(keys.Universal.RangeSelectDown).
SelectedLines(
Contains("line 1"),
Contains("line 2"),
Contains("line 3"),
).
Press(keys.Universal.RangeSelectUp).
SelectedLines(
Contains("line 1"),
Contains("line 2"),
).
// (nonsticky range, press arrow) -> no range
SelectNextItem().
SelectedLines(
Contains("line 3"),
).
Press(keys.Universal.ToggleRangeSelect).
SelectedLines(
Contains("line 3"),
).
SelectNextItem().
SelectedLines(
Contains("line 3"),
Contains("line 4"),
).
// (sticky range, press shift+arrow) -> nonsticky range
Press(keys.Universal.RangeSelectDown).
SelectedLines(
Contains("line 3"),
Contains("line 4"),
Contains("line 5"),
).
SelectNextItem().
SelectedLines(
Contains("line 6"),
).
Press(keys.Universal.RangeSelectDown).
SelectedLines(
Contains("line 6"),
Contains("line 7"),
).
// (nonsticky range, press 'v') -> no range
Press(keys.Universal.ToggleRangeSelect).
SelectedLines(
Contains("line 7"),
)
}
assertRangeSelectBehaviour(t.Views().Commits().Focus())
t.Views().Files().
Focus().
SelectedLine(
Contains("file1"),
).
PressEnter()
assertRangeSelectBehaviour(t.Views().Staging().IsFocused())
},
})

View file

@ -30,9 +30,6 @@ var (
// SelectedLineBgColor is the background color for the selected line
SelectedLineBgColor = style.New()
// SelectedRangeBgColor is the background color of the selected range of lines
SelectedRangeBgColor = style.New()
// CherryPickedCommitColor is the text style when cherry picking a commit
CherryPickedCommitTextStyle = style.New()
@ -52,7 +49,6 @@ func UpdateTheme(themeConfig config.ThemeConfig) {
InactiveBorderColor = GetGocuiStyle(themeConfig.InactiveBorderColor)
SearchingActiveBorderColor = GetGocuiStyle(themeConfig.SearchingActiveBorderColor)
SelectedLineBgColor = GetTextStyle(themeConfig.SelectedLineBgColor, true)
SelectedRangeBgColor = GetTextStyle(themeConfig.SelectedRangeBgColor, true)
cherryPickedCommitBgTextStyle := GetTextStyle(themeConfig.CherryPickedCommitBgColor, true)
cherryPickedCommitFgTextStyle := GetTextStyle(themeConfig.CherryPickedCommitFgColor, false)

View file

@ -176,18 +176,6 @@
"blue"
]
},
"selectedRangeBgColor": {
"items": {
"type": "string"
},
"type": "array",
"minItems": 1,
"uniqueItems": true,
"description": "Background color of selected range\nSee https://github.com/jesseduffield/lazygit/blob/master/docs/Config.md#highlighting-the-selected-line",
"default": [
"blue"
]
},
"cherryPickedCommitFgColor": {
"items": {
"type": "string"
@ -667,6 +655,18 @@
"type": "string",
"default": "\u003e"
},
"toggleRangeSelect": {
"type": "string",
"default": "v"
},
"rangeSelectDown": {
"type": "string",
"default": "\u003cs-down\u003e"
},
"rangeSelectUp": {
"type": "string",
"default": "\u003cs-up\u003e"
},
"prevBlock": {
"type": "string",
"default": "\u003cleft\u003e"

View file

@ -10,8 +10,6 @@ gui:
- bold
inactiveBorderColor:
- black
SelectedRangeBgcolor:
- reverse
# Not important in tests but it creates clutter in demos
showRandomTip: false
animateExplosion: false # takes too long