1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-21 21:47:32 +02:00

View filtering (#2680)

This commit is contained in:
Jesse Duffield 2023-07-03 12:57:11 +10:00 committed by GitHub
commit 1a36cb9f3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
120 changed files with 2554 additions and 494 deletions

View File

@ -36,6 +36,9 @@ gui:
- bold
inactiveBorderColor:
- white
searchingActiveBorderColor:
- cyan
- bold
optionsTextColor:
- blue
selectedLineBgColor:

View File

@ -1,7 +1,8 @@
# Documentation Overview
# Documentation Overview
* [Configuration](./Config.md).
* [Custom Commands](./Custom_Command_Keybindings.md)
* [Custom Pagers](./Custom_Pagers.md)
* [Keybindings](./keybindings)
* [Undo/Redo](./Undoing.md)
* [Searching/Filtering](./Searching.md)

21
docs/Searching.md Normal file
View File

@ -0,0 +1,21 @@
# Searching/Filtering
## View searching/filtering
Depending on the currently focused view, hitting '/' will bring up a filter or search prompt. When filtering, the contents of the view will be filtered down to only those lines which match the query string. When searching, the contents of the view are not filtered, but matching lines are highlighted and you can iterate through matches with `n`/`N`.
We intend to support filtering for the files view soon, but at the moment it uses searching. We intend to continue using search for the commits view because you typically care about the commits that come before/after a matching commit.
If you would like both filtering and searching to be enabled on a given view, please raise an issue for this.
## Filtering files by status
You can filter the files view to only show staged/unstaged files by pressing `<c-b>` in the files view.
## Filtering commits by file path
You can filter the commits view to only show commits which contain changes to a given file path.
You can do this in a couple of ways:
1) Start lazygit with the -f flag e.g. `lazygit -f my/path`
2) From within lazygit, press `<c-s>` and then enter the path of the file you want to filter by

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: Previous page
<kbd>.</kbd>: Next page
<kbd>&lt;</kbd>: Scroll to top
<kbd>/</kbd>: Start search
<kbd>&gt;</kbd>: Scroll to bottom
<kbd>/</kbd>: Search the current view by text
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
<kbd>]</kbd>: Next tab
@ -56,6 +56,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: Toggle file tree view
<kbd>/</kbd>: Search the current view by text
</pre>
## Commit summary
@ -96,6 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Copy commit (cherry-pick)
<kbd>C</kbd>: Copy commit range (cherry-pick)
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Search the current view by text
</pre>
## Confirmation panel
@ -111,7 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-o&gt;</kbd>: Copy the file name to the clipboard
<kbd>d</kbd>: View 'discard changes' options
<kbd>&lt;space&gt;</kbd>: Toggle staged
<kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
<kbd>&lt;c-b&gt;</kbd>: Filter files by status
<kbd>c</kbd>: Commit changes
<kbd>w</kbd>: Commit changes without pre-commit hook
<kbd>A</kbd>: Amend last commit
@ -129,6 +131,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: Toggle file tree view
<kbd>M</kbd>: Open external merge tool (git mergetool)
<kbd>f</kbd>: Fetch
<kbd>/</kbd>: Search the current view by text
</pre>
## Local branches
@ -152,6 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Main panel (merging)
@ -190,6 +194,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Edit file
<kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
<kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
<kbd>/</kbd>: Search the current view by text
</pre>
## Main panel (staging)
@ -211,6 +216,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Commit changes
<kbd>w</kbd>: Commit changes without pre-commit hook
<kbd>C</kbd>: Commit changes using git editor
<kbd>/</kbd>: Search the current view by text
</pre>
## Menu
@ -218,6 +224,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: Execute
<kbd>&lt;esc&gt;</kbd>: Close
<kbd>/</kbd>: Filter the current view by text
</pre>
## Reflog
@ -233,6 +240,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Copy commit range (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remote branches
@ -245,9 +253,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: Rebase checked-out branch onto this branch
<kbd>d</kbd>: Delete branch
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>&lt;esc&gt;</kbd>: Return to remotes list
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remotes
@ -257,6 +265,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Add new remote
<kbd>d</kbd>: Remove remote
<kbd>e</kbd>: Edit remote
<kbd>/</kbd>: Filter the current view by text
</pre>
## Stash
@ -268,6 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: New branch
<kbd>r</kbd>: Rename stash
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
## Status
@ -293,6 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Copy commit range (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Search the current view by text
</pre>
## Submodules
@ -306,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Update submodule URL
<kbd>i</kbd>: Initialize submodule
<kbd>b</kbd>: View bulk submodule options
<kbd>/</kbd>: Filter the current view by text
</pre>
## Tags
@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Create tag
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: 前のページ
<kbd>.</kbd>: 次のページ
<kbd>&lt;</kbd>: 最上部までスクロール
<kbd>/</kbd>: 検索を開始
<kbd>&gt;</kbd>: 最下部までスクロール
<kbd>/</kbd>: 検索を開始
<kbd>H</kbd>: 左スクロール
<kbd>L</kbd>: 右スクロール
<kbd>]</kbd>: 次のタブ
@ -53,6 +53,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 新しいブランチを作成
<kbd>r</kbd>: Stashを変更
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
## Sub-commits
@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: 検索を開始
</pre>
## コミット
@ -101,6 +103,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: コミットをコピー (cherry-pick)
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: 検索を開始
</pre>
## コミットファイル
@ -115,6 +118,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: ファイルツリーの表示を切り替え
<kbd>/</kbd>: 検索を開始
</pre>
## コミットメッセージ
@ -135,6 +139,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: サブモジュールのURLを更新
<kbd>i</kbd>: サブモジュールを初期化
<kbd>b</kbd>: View bulk submodule options
<kbd>/</kbd>: Filter the current view by text
</pre>
## ステータス
@ -156,6 +161,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: タグを作成
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
## ファイル
@ -182,6 +188,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: ファイルツリーの表示を切り替え
<kbd>M</kbd>: Git mergetoolを開く
<kbd>f</kbd>: Fetch
<kbd>/</kbd>: 検索を開始
</pre>
## ブランチ
@ -205,6 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: ブランチ名を変更
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
## メインパネル (Merging)
@ -243,6 +251,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: ファイルを編集
<kbd>&lt;space&gt;</kbd>: 行をパッチに追加/削除
<kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
<kbd>/</kbd>: 検索を開始
</pre>
## メインパネル (Staging)
@ -264,6 +273,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: 変更をコミット
<kbd>w</kbd>: pre-commitフックを実行せずに変更をコミット
<kbd>C</kbd>: gitエディタを使用して変更をコミット
<kbd>/</kbd>: 検索を開始
</pre>
## メニュー
@ -271,6 +281,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: 実行
<kbd>&lt;esc&gt;</kbd>: 閉じる
<kbd>/</kbd>: Filter the current view by text
</pre>
## リモート
@ -280,6 +291,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: リモートを新規追加
<kbd>d</kbd>: リモートを削除
<kbd>e</kbd>: リモートを編集
<kbd>/</kbd>: Filter the current view by text
</pre>
## リモートブランチ
@ -292,9 +304,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: Rebase checked-out branch onto this branch
<kbd>d</kbd>: ブランチを削除
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>&lt;esc&gt;</kbd>: リモート一覧に戻る
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
## 参照ログ
@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: コミットを範囲コピー (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: コミットを閲覧
<kbd>/</kbd>: Filter the current view by text
</pre>
## 確認パネル

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: 이전 페이지
<kbd>.</kbd>: 다음 페이지
<kbd>&lt;</kbd>: 맨 위로 스크롤
<kbd>/</kbd>: 검색 시작
<kbd>&gt;</kbd>: 맨 아래로 스크롤
<kbd>/</kbd>: 검색 시작
<kbd>H</kbd>: 우 스크롤
<kbd>L</kbd>: 좌 스크롤
<kbd>]</kbd>: 이전 탭
@ -57,6 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
## Stash
@ -68,6 +69,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 새 브랜치 생성
<kbd>r</kbd>: Rename stash
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: Filter the current view by text
</pre>
## Sub-commits
@ -83,6 +85,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: 검색 시작
</pre>
## 메뉴
@ -90,6 +93,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: 실행
<kbd>&lt;esc&gt;</kbd>: 닫기
<kbd>/</kbd>: Filter the current view by text
</pre>
## 메인 패널 (Merging)
@ -128,6 +132,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: 파일 편집
<kbd>&lt;space&gt;</kbd>: Line(s)을 패치에 추가/삭제
<kbd>&lt;esc&gt;</kbd>: Exit custom patch builder
<kbd>/</kbd>: 검색 시작
</pre>
## 메인 패널 (Staging)
@ -149,6 +154,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: 커밋 변경내용
<kbd>w</kbd>: Commit changes without pre-commit hook
<kbd>C</kbd>: Git 편집기를 사용하여 변경 내용을 커밋합니다.
<kbd>/</kbd>: 검색 시작
</pre>
## 브랜치
@ -172,6 +178,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: 브랜치 이름 변경
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
## 상태
@ -195,6 +202,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: 서브모듈의 URL을 수정
<kbd>i</kbd>: 서브모듈 초기화
<kbd>b</kbd>: View bulk submodule options
<kbd>/</kbd>: Filter the current view by text
</pre>
## 원격
@ -204,6 +212,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 새로운 Remote 추가
<kbd>d</kbd>: Remote를 삭제
<kbd>e</kbd>: Remote를 수정
<kbd>/</kbd>: Filter the current view by text
</pre>
## 원격 브랜치
@ -216,9 +225,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: 체크아웃된 브랜치를 이 브랜치에 리베이스
<kbd>d</kbd>: 브랜치 삭제
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>&lt;esc&gt;</kbd>: 원격목록으로 돌아가기
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
## 커밋
@ -252,6 +261,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: 커밋을 복사 (cherry-pick)
<kbd>C</kbd>: 커밋을 범위로 복사 (cherry-pick)
<kbd>&lt;enter&gt;</kbd>: View selected item's files
<kbd>/</kbd>: 검색 시작
</pre>
## 커밋 파일
@ -266,6 +276,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: 파일 트리뷰로 전환
<kbd>/</kbd>: 검색 시작
</pre>
## 커밋메시지
@ -284,6 +295,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 태그를 생성
<kbd>g</kbd>: View reset options
<kbd>&lt;enter&gt;</kbd>: 커밋 보기
<kbd>/</kbd>: Filter the current view by text
</pre>
## 파일
@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: 파일 트리뷰로 전환
<kbd>M</kbd>: Git mergetool를 열기
<kbd>f</kbd>: Fetch
<kbd>/</kbd>: 검색 시작
</pre>
## 확인 패널

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: Vorige pagina
<kbd>.</kbd>: Volgende pagina
<kbd>&lt;</kbd>: Scroll naar boven
<kbd>/</kbd>: Start met zoeken
<kbd>&gt;</kbd>: Scroll naar beneden
<kbd>/</kbd>: Start met zoeken
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
<kbd>]</kbd>: Volgende tabblad
@ -50,7 +50,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-o&gt;</kbd>: Kopieer de bestandsnaam naar het klembord
<kbd>d</kbd>: Bekijk 'veranderingen ongedaan maken' opties
<kbd>&lt;space&gt;</kbd>: Toggle staged
<kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
<kbd>&lt;c-b&gt;</kbd>: Filter files by status
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
<kbd>A</kbd>: Wijzig laatste commit
@ -68,6 +68,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: Toggle bestandsboom weergave
<kbd>M</kbd>: Open external merge tool (git mergetool)
<kbd>f</kbd>: Fetch
<kbd>/</kbd>: Start met zoeken
</pre>
## Bevestigingspaneel
@ -98,6 +99,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: Hernoem branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Commit bericht
@ -119,6 +121,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: Enter bestand om geselecteerde regels toe te voegen aan de patch
<kbd>`</kbd>: Toggle bestandsboom weergave
<kbd>/</kbd>: Start met zoeken
</pre>
## Commits
@ -152,6 +155,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Kopieer commit (cherry-pick)
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
<kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
<kbd>/</kbd>: Start met zoeken
</pre>
## Menu
@ -159,6 +163,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: Uitvoeren
<kbd>&lt;esc&gt;</kbd>: Sluiten
<kbd>/</kbd>: Filter the current view by text
</pre>
## Mergen
@ -197,6 +202,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Verander bestand
<kbd>&lt;space&gt;</kbd>: Voeg toe/verwijder lijn(en) in patch
<kbd>&lt;esc&gt;</kbd>: Sluit lijn-bij-lijn modus
<kbd>/</kbd>: Start met zoeken
</pre>
## Reflog
@ -212,6 +218,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (gekopieerde) commits selectie
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remote branches
@ -224,9 +231,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: Rebase branch
<kbd>d</kbd>: Verwijder branch
<kbd>u</kbd>: Stel in als upstream van uitgecheckte branch
<kbd>&lt;esc&gt;</kbd>: Ga terug naar remotes lijst
<kbd>g</kbd>: Bekijk reset opties
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remotes
@ -236,6 +243,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Voeg een nieuwe remote toe
<kbd>d</kbd>: Verwijder remote
<kbd>e</kbd>: Wijzig remote
<kbd>/</kbd>: Filter the current view by text
</pre>
## Staging
@ -257,6 +265,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Commit veranderingen
<kbd>w</kbd>: Commit veranderingen zonder pre-commit hook
<kbd>C</kbd>: Commit veranderingen met de git editor
<kbd>/</kbd>: Start met zoeken
</pre>
## Stash
@ -268,6 +277,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Nieuwe branch
<kbd>r</kbd>: Rename stash
<kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
<kbd>/</kbd>: Filter the current view by text
</pre>
## Status
@ -293,6 +303,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Kopieer commit reeks (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (gekopieerde) commits selectie
<kbd>&lt;enter&gt;</kbd>: Bekijk gecommite bestanden
<kbd>/</kbd>: Start met zoeken
</pre>
## Submodules
@ -306,6 +317,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Update submodule URL
<kbd>i</kbd>: Initialiseer submodule
<kbd>b</kbd>: Bekijk bulk submodule opties
<kbd>/</kbd>: Filter the current view by text
</pre>
## Tags
@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Creëer tag
<kbd>g</kbd>: Bekijk reset opties
<kbd>&lt;enter&gt;</kbd>: Bekijk commits
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: Previous page
<kbd>.</kbd>: Next page
<kbd>&lt;</kbd>: Scroll to top
<kbd>/</kbd>: Start search
<kbd>&gt;</kbd>: Scroll to bottom
<kbd>/</kbd>: Search the current view by text
<kbd>H</kbd>: Scroll left
<kbd>L</kbd>: Scroll right
<kbd>]</kbd>: Next tab
@ -82,6 +82,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Kopiuj commit (przebieranie)
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
<kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
<kbd>/</kbd>: Search the current view by text
</pre>
## Confirmation panel
@ -112,6 +113,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: Rename branch
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Main panel (patch building)
@ -127,6 +129,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Edytuj plik
<kbd>&lt;space&gt;</kbd>: Add/Remove line(s) to patch
<kbd>&lt;esc&gt;</kbd>: Wyście z trybu "linia po linii"
<kbd>/</kbd>: Search the current view by text
</pre>
## Menu
@ -134,6 +137,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: Wykonaj
<kbd>&lt;esc&gt;</kbd>: Zamknij
<kbd>/</kbd>: Filter the current view by text
</pre>
## Pliki
@ -142,7 +146,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-o&gt;</kbd>: Copy the file name to the clipboard
<kbd>d</kbd>: Pokaż opcje porzucania zmian
<kbd>&lt;space&gt;</kbd>: Przełącz stan poczekalni
<kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
<kbd>&lt;c-b&gt;</kbd>: Filter files by status
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
<kbd>A</kbd>: Zmień ostatni commit
@ -160,6 +164,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: Toggle file tree view
<kbd>M</kbd>: Open external merge tool (git mergetool)
<kbd>f</kbd>: Pobierz
<kbd>/</kbd>: Search the current view by text
</pre>
## Pliki commita
@ -174,6 +179,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: Enter file to add selected lines to the patch (or toggle directory collapsed)
<kbd>`</kbd>: Toggle file tree view
<kbd>/</kbd>: Search the current view by text
</pre>
## Poczekalnia
@ -195,6 +201,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: Zatwierdź zmiany
<kbd>w</kbd>: Zatwierdź zmiany bez skryptu pre-commit
<kbd>C</kbd>: Zatwierdź zmiany używając edytora
<kbd>/</kbd>: Search the current view by text
</pre>
## Reflog
@ -210,6 +217,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remote branches
@ -222,9 +230,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: Zmiana bazy gałęzi
<kbd>d</kbd>: Usuń gałąź
<kbd>u</kbd>: Set as upstream of checked-out branch
<kbd>&lt;esc&gt;</kbd>: Wróć do listy repozytoriów zdalnych
<kbd>g</kbd>: Wyświetl opcje resetu
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Remotes
@ -234,6 +242,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Add new remote
<kbd>d</kbd>: Remove remote
<kbd>e</kbd>: Edit remote
<kbd>/</kbd>: Filter the current view by text
</pre>
## Scalanie
@ -261,6 +270,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Nowa gałąź
<kbd>r</kbd>: Rename stash
<kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
<kbd>/</kbd>: Filter the current view by text
</pre>
## Status
@ -286,6 +296,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: Kopiuj zakres commitów (przebieranie)
<kbd>&lt;c-r&gt;</kbd>: Reset cherry-picked (copied) commits selection
<kbd>&lt;enter&gt;</kbd>: Przeglądaj pliki commita
<kbd>/</kbd>: Search the current view by text
</pre>
## Submodules
@ -299,6 +310,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: Update submodule URL
<kbd>i</kbd>: Initialize submodule
<kbd>b</kbd>: View bulk submodule options
<kbd>/</kbd>: Filter the current view by text
</pre>
## Tags
@ -310,6 +322,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: Create tag
<kbd>g</kbd>: Wyświetl opcje resetu
<kbd>&lt;enter&gt;</kbd>: View commits
<kbd>/</kbd>: Filter the current view by text
</pre>
## Zwykłe

View File

@ -36,8 +36,8 @@ _Связки клавиш_
<kbd>,</kbd>: Предыдущая страница
<kbd>.</kbd>: Следующая страница
<kbd>&lt;</kbd>: Пролистать наверх
<kbd>/</kbd>: Найти
<kbd>&gt;</kbd>: Прокрутить вниз
<kbd>/</kbd>: Найти
<kbd>H</kbd>: Прокрутить влево
<kbd>L</kbd>: Прокрутить вправо
<kbd>]</kbd>: Следующая вкладка
@ -63,6 +63,7 @@ _Связки клавиш_
<kbd>c</kbd>: Сохранить изменения
<kbd>w</kbd>: Закоммитить изменения без предварительного хука коммита
<kbd>C</kbd>: Сохранить изменения с помощью редактора git
<kbd>/</kbd>: Найти
</pre>
## Главная панель (Обычный)
@ -101,6 +102,7 @@ _Связки клавиш_
<kbd>e</kbd>: Редактировать файл
<kbd>&lt;space&gt;</kbd>: Добавить/удалить строку(и) для патча
<kbd>&lt;esc&gt;</kbd>: Выйти из сборщика пользовательских патчей
<kbd>/</kbd>: Найти
</pre>
## Журнал ссылок (Reflog)
@ -116,6 +118,7 @@ _Связки клавиш_
<kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Сбросить отобранную (скопированную | cherry-picked) выборку коммитов
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
## Коммиты
@ -149,6 +152,7 @@ _Связки клавиш_
<kbd>c</kbd>: Скопировать отобранные коммит (cherry-pick)
<kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
<kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
<kbd>/</kbd>: Найти
</pre>
## Локальные Ветки
@ -172,6 +176,7 @@ _Связки клавиш_
<kbd>R</kbd>: Переименовать ветку
<kbd>u</kbd>: Установить/убрать upstream-ветку
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
## Меню
@ -179,6 +184,7 @@ _Связки клавиш_
<pre>
<kbd>&lt;enter&gt;</kbd>: Выполнить
<kbd>&lt;esc&gt;</kbd>: Закрыть
<kbd>/</kbd>: Filter the current view by text
</pre>
## Панель Подтверждения
@ -201,6 +207,7 @@ _Связки клавиш_
<kbd>C</kbd>: Скопировать несколько отобранных коммитов (cherry-pick)
<kbd>&lt;c-r&gt;</kbd>: Сбросить отобранную (скопированную | cherry-picked) выборку коммитов
<kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
<kbd>/</kbd>: Найти
</pre>
## Подмодули
@ -214,6 +221,7 @@ _Связки клавиш_
<kbd>e</kbd>: Обновить URL подмодуля
<kbd>i</kbd>: Инициализировать подмодуль
<kbd>b</kbd>: Просмотреть параметры массового подмодуля
<kbd>/</kbd>: Filter the current view by text
</pre>
## Сводка коммита
@ -235,6 +243,7 @@ _Связки клавиш_
<kbd>a</kbd>: Переключить все файлы, включённые в патч
<kbd>&lt;enter&gt;</kbd>: Введите файл, чтобы добавить выбранные строки в патч (или свернуть каталог переключения)
<kbd>`</kbd>: Переключить вид дерева файлов
<kbd>/</kbd>: Найти
</pre>
## Статус
@ -256,6 +265,7 @@ _Связки клавиш_
<kbd>n</kbd>: Создать тег
<kbd>g</kbd>: Просмотреть параметры сброса
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
## Удалённые ветки
@ -268,9 +278,9 @@ _Связки клавиш_
<kbd>r</kbd>: Перебазировать переключённую ветку на эту ветку
<kbd>d</kbd>: Удалить ветку
<kbd>u</kbd>: Установить как upstream-ветку переключённую ветку
<kbd>&lt;esc&gt;</kbd>: Вернуться к списку удалённых репозитории
<kbd>g</kbd>: Просмотреть параметры сброса
<kbd>&lt;enter&gt;</kbd>: Просмотреть коммиты
<kbd>/</kbd>: Filter the current view by text
</pre>
## Удалённые репозитории
@ -280,6 +290,7 @@ _Связки клавиш_
<kbd>n</kbd>: Добавить новую удалённую ветку
<kbd>d</kbd>: Удалить удалённую ветку
<kbd>e</kbd>: Редактировать удалённый репозитории
<kbd>/</kbd>: Filter the current view by text
</pre>
## Файлы
@ -306,6 +317,7 @@ _Связки клавиш_
<kbd>`</kbd>: Переключить вид дерева файлов
<kbd>M</kbd>: Открыть внешний инструмент слияния (git mergetool)
<kbd>f</kbd>: Получить изменения
<kbd>/</kbd>: Найти
</pre>
## Хранилище
@ -317,4 +329,5 @@ _Связки клавиш_
<kbd>n</kbd>: Новая ветка
<kbd>r</kbd>: Переименовать хранилище
<kbd>&lt;enter&gt;</kbd>: Просмотреть файлы выбранного элемента
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -36,8 +36,8 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>,</kbd>: 上一页
<kbd>.</kbd>: 下一页
<kbd>&lt;</kbd>: 滚动到顶部
<kbd>/</kbd>: 开始搜索
<kbd>&gt;</kbd>: 滚动到底部
<kbd>/</kbd>: 开始搜索
<kbd>H</kbd>: 向左滚动
<kbd>L</kbd>: 向右滚动
<kbd>]</kbd>: 下一个标签
@ -57,6 +57,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>&lt;c-r&gt;</kbd>: 重置已拣选(复制)的提交
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 分支页面
@ -80,6 +81,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>R</kbd>: 重命名分支
<kbd>u</kbd>: Set/Unset upstream
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 子提交
@ -95,6 +97,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>&lt;c-r&gt;</kbd>: 重置已拣选(复制)的提交
<kbd>&lt;enter&gt;</kbd>: 查看提交的文件
<kbd>/</kbd>: 开始搜索
</pre>
## 子模块
@ -108,6 +111,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: 更新子模块 URL
<kbd>i</kbd>: 初始化子模块
<kbd>b</kbd>: 查看批量子模块选项
<kbd>/</kbd>: Filter the current view by text
</pre>
## 提交
@ -141,6 +145,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: 复制提交(拣选)
<kbd>C</kbd>: 复制提交范围(拣选)
<kbd>&lt;enter&gt;</kbd>: 查看提交的文件
<kbd>/</kbd>: 开始搜索
</pre>
## 提交文件
@ -155,6 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>a</kbd>: Toggle all files included in patch
<kbd>&lt;enter&gt;</kbd>: 输入文件以将所选行添加到补丁中(或切换目录折叠)
<kbd>`</kbd>: 切换文件树视图
<kbd>/</kbd>: 开始搜索
</pre>
## 提交讯息
@ -170,7 +176,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>&lt;c-o&gt;</kbd>: 将文件名复制到剪贴板
<kbd>d</kbd>: 查看'放弃更改'选项
<kbd>&lt;space&gt;</kbd>: 切换暂存状态
<kbd>&lt;c-b&gt;</kbd>: Filter files (staged/unstaged)
<kbd>&lt;c-b&gt;</kbd>: Filter files by status
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>A</kbd>: 修补最后一次提交
@ -188,6 +194,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>`</kbd>: 切换文件树视图
<kbd>M</kbd>: 打开外部合并工具 (git mergetool)
<kbd>f</kbd>: 抓取
<kbd>/</kbd>: 开始搜索
</pre>
## 构建补丁中
@ -203,6 +210,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>e</kbd>: 编辑文件
<kbd>&lt;space&gt;</kbd>: 添加/移除 行到补丁
<kbd>&lt;esc&gt;</kbd>: 退出逐行模式
<kbd>/</kbd>: 开始搜索
</pre>
## 标签页面
@ -214,6 +222,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 创建标签
<kbd>g</kbd>: 查看重置选项
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 正在合并
@ -251,6 +260,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>c</kbd>: 提交更改
<kbd>w</kbd>: 提交更改而无需预先提交钩子
<kbd>C</kbd>: 提交更改(使用编辑器编辑提交信息)
<kbd>/</kbd>: 开始搜索
</pre>
## 正常
@ -282,6 +292,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<pre>
<kbd>&lt;enter&gt;</kbd>: 执行
<kbd>&lt;esc&gt;</kbd>: 关闭
<kbd>/</kbd>: Filter the current view by text
</pre>
## 贮藏
@ -293,6 +304,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: Rename stash
<kbd>&lt;enter&gt;</kbd>: 查看提交的文件
<kbd>/</kbd>: Filter the current view by text
</pre>
## 远程分支
@ -305,9 +317,9 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>r</kbd>: 将已检出的分支变基到该分支
<kbd>d</kbd>: 删除分支
<kbd>u</kbd>: 设置为检出分支的上游
<kbd>&lt;esc&gt;</kbd>: 返回远程仓库列表
<kbd>g</kbd>: 查看重置选项
<kbd>&lt;enter&gt;</kbd>: 查看提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 远程页面
@ -317,4 +329,5 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
<kbd>n</kbd>: 添加新的远程仓库
<kbd>d</kbd>: 删除远程
<kbd>e</kbd>: 编辑远程仓库
<kbd>/</kbd>: Filter the current view by text
</pre>

View File

@ -36,8 +36,8 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>,</kbd>: 上一頁
<kbd>.</kbd>: 下一頁
<kbd>&lt;</kbd>: 捲動到頂部
<kbd>/</kbd>: 開始搜尋
<kbd>&gt;</kbd>: 捲動到底部
<kbd>/</kbd>: 開始搜尋
<kbd>H</kbd>: 向左捲動
<kbd>L</kbd>: 向右捲動
<kbd>]</kbd>: 下一個索引標籤
@ -57,6 +57,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 主視窗 (一般)
@ -101,6 +102,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>c</kbd>: 提交變更
<kbd>w</kbd>: 沒有預提交 hook 就提交更改
<kbd>C</kbd>: 使用 git 編輯器提交變更
<kbd>/</kbd>: 開始搜尋
</pre>
## 主面板 (補丁生成)
@ -116,6 +118,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>e</kbd>: 編輯檔案
<kbd>&lt;space&gt;</kbd>: 向 (或從) 補丁中添加/刪除行
<kbd>&lt;esc&gt;</kbd>: 退出自訂補丁建立器
<kbd>/</kbd>: 開始搜尋
</pre>
## 功能表
@ -123,6 +126,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<pre>
<kbd>&lt;enter&gt;</kbd>: 執行
<kbd>&lt;esc&gt;</kbd>: 關閉
<kbd>/</kbd>: Filter the current view by text
</pre>
## 子提交
@ -138,6 +142,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;c-r&gt;</kbd>: 重設選定的揀選 (複製) 提交
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
<kbd>/</kbd>: 開始搜尋
</pre>
## 子模組
@ -151,6 +156,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>e</kbd>: 更新子模組 URL
<kbd>i</kbd>: 初始化子模組
<kbd>b</kbd>: 查看批量子模組選項
<kbd>/</kbd>: Filter the current view by text
</pre>
## 提交
@ -184,6 +190,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>c</kbd>: 複製提交 (揀選)
<kbd>C</kbd>: 複製提交範圍 (揀選)
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
<kbd>/</kbd>: 開始搜尋
</pre>
## 提交摘要
@ -205,6 +212,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>a</kbd>: 切換所有檔案是否包含在補丁中
<kbd>&lt;enter&gt;</kbd>: 輸入檔案以將選定的行添加至補丁(或切換目錄折疊)
<kbd>`</kbd>: 切換檔案樹狀視圖
<kbd>/</kbd>: 開始搜尋
</pre>
## 收藏 (Stash)
@ -216,6 +224,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>n</kbd>: 新分支
<kbd>r</kbd>: 重新命名收藏
<kbd>&lt;enter&gt;</kbd>: 檢視所選項目的檔案
<kbd>/</kbd>: Filter the current view by text
</pre>
## 本地分支
@ -239,6 +248,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>R</kbd>: 重新命名分支
<kbd>u</kbd>: 設定/取消設定上游
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 標籤
@ -250,6 +260,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>n</kbd>: 建立標籤
<kbd>g</kbd>: 檢視重設選項
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>
## 檔案
@ -276,6 +287,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>`</kbd>: 切換檔案樹狀視圖
<kbd>M</kbd>: 開啟外部合併工具 (git mergetool)
<kbd>f</kbd>: 擷取
<kbd>/</kbd>: 開始搜尋
</pre>
## 狀態
@ -302,6 +314,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>n</kbd>: 新增遠端
<kbd>d</kbd>: 移除遠端
<kbd>e</kbd>: 編輯遠端
<kbd>/</kbd>: Filter the current view by text
</pre>
## 遠端分支
@ -314,7 +327,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B_
<kbd>r</kbd>: 將已檢出的分支變基至此分支
<kbd>d</kbd>: 刪除分支
<kbd>u</kbd>: 將此分支設為當前分支之上游
<kbd>&lt;esc&gt;</kbd>: 返回遠端列表
<kbd>g</kbd>: 檢視重設選項
<kbd>&lt;enter&gt;</kbd>: 檢視提交
<kbd>/</kbd>: Filter the current view by text
</pre>

8
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/integrii/flaggy v1.4.0
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e
@ -67,8 +67,8 @@ require (
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect
golang.org/x/exp v0.0.0-20220318154914-8dddf5d87bd8 // indirect
golang.org/x/net v0.7.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/term v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/sys v0.9.0 // indirect
golang.org/x/term v0.9.0 // indirect
golang.org/x/text v0.10.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)

15
go.sum
View File

@ -72,8 +72,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e h1:NpsrRAbYUmMkxDgNAVSlu3LxtfwdDe140vWVo/VldgA=
github.com/jesseduffield/gocui v0.3.1-0.20230601121845-cb89273fdd4e/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce h1:Xgm21B1an/outcRxnkDfMT6wKb6SKBR05jXOyfPA8WQ=
github.com/jesseduffield/gocui v0.3.1-0.20230702054502-d6c452fc12ce/go.mod h1:dJ/BEUt3OWtaRg/PmuJWendRqREhre9JQ1SLvqrVJ8s=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
@ -206,21 +206,22 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28=
golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=

View File

@ -23,3 +23,7 @@ func (f *CommitFile) Added() bool {
func (f *CommitFile) Deleted() bool {
return f.ChangeStatus == "D"
}
func (f *CommitFile) GetPath() string {
return f.Name
}

View File

@ -60,15 +60,16 @@ type GuiConfig struct {
}
type ThemeConfig struct {
ActiveBorderColor []string `yaml:"activeBorderColor"`
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
OptionsTextColor []string `yaml:"optionsTextColor"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
UnstagedChangesColor []string `yaml:"unstagedChangesColor"`
DefaultFgColor []string `yaml:"defaultFgColor"`
ActiveBorderColor []string `yaml:"activeBorderColor"`
InactiveBorderColor []string `yaml:"inactiveBorderColor"`
SearchingActiveBorderColor []string `yaml:"searchingActiveBorderColor"`
OptionsTextColor []string `yaml:"optionsTextColor"`
SelectedLineBgColor []string `yaml:"selectedLineBgColor"`
SelectedRangeBgColor []string `yaml:"selectedRangeBgColor"`
CherryPickedCommitBgColor []string `yaml:"cherryPickedCommitBgColor"`
CherryPickedCommitFgColor []string `yaml:"cherryPickedCommitFgColor"`
UnstagedChangesColor []string `yaml:"unstagedChangesColor"`
DefaultFgColor []string `yaml:"defaultFgColor"`
}
type CommitLengthConfig struct {
@ -409,15 +410,16 @@ func GetDefaultConfig() *UserConfig {
TimeFormat: "02 Jan 06",
ShortTimeFormat: time.Kitchen,
Theme: ThemeConfig{
ActiveBorderColor: []string{"green", "bold"},
InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"blue"},
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"cyan"},
CherryPickedCommitFgColor: []string{"blue"},
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
ActiveBorderColor: []string{"green", "bold"},
SearchingActiveBorderColor: []string{"cyan", "bold"},
InactiveBorderColor: []string{"default"},
OptionsTextColor: []string{"blue"},
SelectedLineBgColor: []string{"blue"},
SelectedRangeBgColor: []string{"blue"},
CherryPickedCommitBgColor: []string{"cyan"},
CherryPickedCommitFgColor: []string{"blue"},
UnstagedChangesColor: []string{"red"},
DefaultFgColor: []string{"default"},
},
CommitLength: CommitLengthConfig{Show: true},
SkipNoStagedFilesWarning: false,

View File

@ -135,10 +135,6 @@ func (gui *Gui) getRandomTip() string {
"To escape a mode, for example cherry-picking, patch-building, diffing, or filtering mode, you can just spam the '%s' button. Unless of course you have `quitOnTopLevelReturn` enabled in your config",
formattedKey(config.Universal.Return),
),
fmt.Sprintf(
"To search for a string in your panel, press '%s'",
formattedKey(config.Universal.StartSearch),
),
fmt.Sprintf(
"You can page through the items of a panel using '%s' and '%s'",
formattedKey(config.Universal.PrevPage),

View File

@ -200,9 +200,9 @@ func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error {
func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error {
view, _ := self.gui.c.GocuiGui().View(c.GetViewName())
if view != nil && view.IsSearching() {
if err := self.gui.onSearchEscape(); err != nil {
return err
if opts.NewContextKey != context.SEARCH_CONTEXT_KEY {
if c.GetKind() == types.MAIN_CONTEXT || c.GetKind() == types.TEMPORARY_POPUP {
self.gui.helpers.Search.CancelSearchIfSearching(c)
}
}
@ -234,6 +234,8 @@ func (self *ContextMgr) ActivateContext(c types.Context, opts types.OnFocusOpts)
return err
}
self.gui.helpers.Search.RenderSearchStatus(c)
desiredTitle := c.Title()
if desiredTitle != "" {
v.Title = desiredTitle
@ -326,6 +328,30 @@ func (self *ContextMgr) IsCurrent(c types.Context) bool {
return self.Current().GetKey() == c.GetKey()
}
func (self *ContextMgr) AllFilterable() []types.IFilterableContext {
var result []types.IFilterableContext
for _, context := range self.allContexts.Flatten() {
if ctx, ok := context.(types.IFilterableContext); ok {
result = append(result, ctx)
}
}
return result
}
func (self *ContextMgr) AllSearchable() []types.ISearchableContext {
var result []types.ISearchableContext
for _, context := range self.allContexts.Flatten() {
if ctx, ok := context.(types.ISearchableContext); ok {
result = append(result, ctx)
}
}
return result
}
// all list contexts
func (self *ContextMgr) AllList() []types.IListContext {
var listContexts []types.IListContext

View File

@ -7,7 +7,7 @@ import (
)
type BranchesContext struct {
*BasicViewModel[*models.Branch]
*FilteredListViewModel[*models.Branch]
*ListContextTrait
}
@ -17,11 +17,16 @@ var (
)
func NewBranchesContext(c *ContextCommon) *BranchesContext {
viewModel := NewBasicViewModel(func() []*models.Branch { return c.Model().Branches })
viewModel := NewFilteredListViewModel(
func() []*models.Branch { return c.Model().Branches },
func(branch *models.Branch) []string {
return []string{branch.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetBranchListDisplayStrings(
c.Model().Branches,
viewModel.GetItems(),
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref,
c.Tr,
@ -30,7 +35,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
}
self := &BranchesContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Branches,

View File

@ -13,6 +13,7 @@ type CommitFilesContext struct {
*filetree.CommitFileTreeViewModel
*ListContextTrait
*DynamicTitleBuilder
*SearchTrait
}
var (
@ -38,9 +39,10 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
})
}
return &CommitFilesContext{
ctx := &CommitFilesContext{
CommitFileTreeViewModel: viewModel,
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.CommitFilesDynamicTitle),
SearchTrait: NewSearchTrait(c),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(
NewBaseContext(NewBaseContextOpts{
@ -57,6 +59,13 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
c: c,
},
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
return ctx.HandleFocus(types.OnFocusOpts{})
}))
return ctx
}
func (self *CommitFilesContext) GetSelectedItemId() string {

View File

@ -0,0 +1,93 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
)
type FilteredList[T any] struct {
filteredIndices []int // if nil, we are not filtering
getList func() []T
getFilterFields func(T) []string
filter string
mutex *deadlock.Mutex
}
func NewFilteredList[T any](getList func() []T, getFilterFields func(T) []string) *FilteredList[T] {
return &FilteredList[T]{
getList: getList,
getFilterFields: getFilterFields,
mutex: &deadlock.Mutex{},
}
}
func (self *FilteredList[T]) GetFilter() string {
return self.filter
}
func (self *FilteredList[T]) SetFilter(filter string) {
self.filter = filter
self.applyFilter()
}
func (self *FilteredList[T]) ClearFilter() {
self.SetFilter("")
}
func (self *FilteredList[T]) IsFiltering() bool {
return self.filter != ""
}
func (self *FilteredList[T]) GetFilteredList() []T {
if self.filteredIndices == nil {
return self.getList()
}
return utils.ValuesAtIndices(self.getList(), self.filteredIndices)
}
// TODO: update to just 'Len'
func (self *FilteredList[T]) UnfilteredLen() int {
return len(self.getList())
}
func (self *FilteredList[T]) applyFilter() {
self.mutex.Lock()
defer self.mutex.Unlock()
if self.filter == "" {
self.filteredIndices = nil
} else {
self.filteredIndices = []int{}
for i, item := range self.getList() {
for _, field := range self.getFilterFields(item) {
if self.match(field, self.filter) {
self.filteredIndices = append(self.filteredIndices, i)
break
}
}
}
}
}
func (self *FilteredList[T]) match(haystack string, needle string) bool {
return utils.CaseAwareContains(haystack, needle)
}
func (self *FilteredList[T]) UnfilteredIndex(index int) int {
self.mutex.Lock()
defer self.mutex.Unlock()
if self.filteredIndices == nil {
return index
}
// we use -1 when there are no items
if index == -1 {
return -1
}
return self.filteredIndices[index]
}

View File

@ -0,0 +1,33 @@
package context
type FilteredListViewModel[T any] struct {
*FilteredList[T]
*ListViewModel[T]
}
func NewFilteredListViewModel[T any](getList func() []T, getFilterFields func(T) []string) *FilteredListViewModel[T] {
filteredList := NewFilteredList(getList, getFilterFields)
self := &FilteredListViewModel[T]{
FilteredList: filteredList,
}
listViewModel := NewListViewModel(filteredList.GetFilteredList)
self.ListViewModel = listViewModel
return self
}
// used for type switch
func (self *FilteredListViewModel[T]) IsFilterableContext() {}
func (self *FilteredListViewModel[T]) ClearFilter() {
// Set the selected line index to the unfiltered index of the currently selected line,
// so that the current item is still selected after the filter is cleared.
unfilteredIndex := self.FilteredList.UnfilteredIndex(self.GetSelectedLineIdx())
self.FilteredList.ClearFilter()
self.SetSelectedLineIdx(unfilteredIndex)
}

View File

@ -2,13 +2,13 @@ package context
import "github.com/jesseduffield/lazygit/pkg/gui/context/traits"
type BasicViewModel[T any] struct {
type ListViewModel[T any] struct {
*traits.ListCursor
getModel func() []T
}
func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
self := &BasicViewModel[T]{
func NewListViewModel[T any](getModel func() []T) *ListViewModel[T] {
self := &ListViewModel[T]{
getModel: getModel,
}
@ -17,11 +17,11 @@ func NewBasicViewModel[T any](getModel func() []T) *BasicViewModel[T] {
return self
}
func (self *BasicViewModel[T]) Len() int {
func (self *ListViewModel[T]) Len() int {
return len(self.getModel())
}
func (self *BasicViewModel[T]) GetSelected() T {
func (self *ListViewModel[T]) GetSelected() T {
if self.Len() == 0 {
return Zero[T]()
}
@ -29,6 +29,10 @@ func (self *BasicViewModel[T]) GetSelected() T {
return self.getModel()[self.GetSelectedLineIdx()]
}
func (self *ListViewModel[T]) GetItems() []T {
return self.getModel()
}
func Zero[T any]() T {
return *new(T)
}

View File

@ -13,6 +13,7 @@ import (
type LocalCommitsContext struct {
*LocalCommitsViewModel
*ListContextTrait
*SearchTrait
}
var (
@ -57,8 +58,9 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
)
}
return &LocalCommitsContext{
ctx := &LocalCommitsContext{
LocalCommitsViewModel: viewModel,
SearchTrait: NewSearchTrait(c),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Commits,
@ -73,6 +75,13 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
refreshViewportOnChange: true,
},
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
return ctx.HandleFocus(types.OnFocusOpts{})
}))
return ctx
}
func (self *LocalCommitsContext) GetSelectedItemId() string {
@ -85,7 +94,7 @@ func (self *LocalCommitsContext) GetSelectedItemId() string {
}
type LocalCommitsViewModel struct {
*BasicViewModel[*models.Commit]
*ListViewModel[*models.Commit]
// If this is true we limit the amount of commits we load, for the sake of keeping things fast.
// If the user attempts to scroll past the end of the list, we will load more commits.
@ -97,7 +106,7 @@ type LocalCommitsViewModel struct {
func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel {
self := &LocalCommitsViewModel{
BasicViewModel: NewBasicViewModel(getModel),
ListViewModel: NewListViewModel(getModel),
limitCommits: true,
showWholeGitGraph: c.UserConfig.Git.Log.ShowWholeGraph,
}

View File

@ -56,7 +56,7 @@ func (self *MenuContext) GetSelectedItemId() string {
type MenuViewModel struct {
c *ContextCommon
menuItems []*types.MenuItem
*BasicViewModel[*types.MenuItem]
*FilteredListViewModel[*types.MenuItem]
}
func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
@ -65,7 +65,10 @@ func NewMenuViewModel(c *ContextCommon) *MenuViewModel {
c: c,
}
self.BasicViewModel = NewBasicViewModel(func() []*types.MenuItem { return self.menuItems })
self.FilteredListViewModel = NewFilteredListViewModel(
func() []*types.MenuItem { return self.menuItems },
func(item *types.MenuItem) []string { return item.LabelColumns },
)
return self
}
@ -76,11 +79,12 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem) {
// TODO: move into presentation package
func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]string {
showKeys := slices.Some(self.menuItems, func(item *types.MenuItem) bool {
menuItems := self.FilteredListViewModel.GetItems()
showKeys := slices.Some(menuItems, func(item *types.MenuItem) bool {
return item.Key != nil
})
return slices.Map(self.menuItems, func(item *types.MenuItem) []string {
return slices.Map(menuItems, func(item *types.MenuItem) []string {
displayStrings := item.LabelColumns
if !showKeys {
@ -93,6 +97,7 @@ func (self *MenuViewModel) GetDisplayStrings(_startIdx int, _length int) [][]str
self.c.UserConfig.Keybinding.Universal.Confirm,
self.c.UserConfig.Keybinding.Universal.Select,
self.c.UserConfig.Keybinding.Universal.Return,
self.c.UserConfig.Keybinding.Universal.StartSearch,
}
keyLabel := keybindings.LabelFromKey(item.Key)
keyStyle := style.FgCyan

View File

@ -9,6 +9,7 @@ import (
type PatchExplorerContext struct {
*SimpleContext
*SearchTrait
state *patch_exploring.State
viewTrait *ViewTrait
@ -28,7 +29,7 @@ func NewPatchExplorerContext(
c *ContextCommon,
) *PatchExplorerContext {
return &PatchExplorerContext{
ctx := &PatchExplorerContext{
state: nil,
viewTrait: NewViewTrait(view),
c: c,
@ -42,7 +43,18 @@ func NewPatchExplorerContext(
Focusable: true,
HighlightOnFocus: true,
})),
SearchTrait: NewSearchTrait(c),
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
func(selectedLineIdx int) error {
ctx.GetMutex().Lock()
defer ctx.GetMutex().Unlock()
return ctx.NavigateTo(ctx.c.IsCurrentContext(ctx), selectedLineIdx)
}),
)
return ctx
}
func (self *PatchExplorerContext) IsPatchExplorerContext() {}

View File

@ -9,7 +9,7 @@ import (
)
type ReflogCommitsContext struct {
*BasicViewModel[*models.Commit]
*FilteredListViewModel[*models.Commit]
*ListContextTrait
}
@ -19,11 +19,16 @@ var (
)
func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
viewModel := NewBasicViewModel(func() []*models.Commit { return c.Model().FilteredReflogCommits })
viewModel := NewFilteredListViewModel(
func() []*models.Commit { return c.Model().FilteredReflogCommits },
func(commit *models.Commit) []string {
return []string{commit.ShortSha(), commit.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetReflogCommitListDisplayStrings(
c.Model().FilteredReflogCommits,
viewModel.GetItems(),
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().CherryPicking.SelectedShaSet(),
c.Modes().Diffing.Ref,
@ -35,7 +40,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
}
return &ReflogCommitsContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().ReflogCommits,

View File

@ -7,7 +7,7 @@ import (
)
type RemoteBranchesContext struct {
*BasicViewModel[*models.RemoteBranch]
*FilteredListViewModel[*models.RemoteBranch]
*ListContextTrait
*DynamicTitleBuilder
}
@ -20,15 +20,20 @@ var (
func NewRemoteBranchesContext(
c *ContextCommon,
) *RemoteBranchesContext {
viewModel := NewBasicViewModel(func() []*models.RemoteBranch { return c.Model().RemoteBranches })
viewModel := NewFilteredListViewModel(
func() []*models.RemoteBranch { return c.Model().RemoteBranches },
func(remoteBranch *models.RemoteBranch) []string {
return []string{remoteBranch.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetRemoteBranchListDisplayStrings(c.Model().RemoteBranches, c.Modes().Diffing.Ref)
return presentation.GetRemoteBranchListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
}
return &RemoteBranchesContext{
BasicViewModel: viewModel,
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
FilteredListViewModel: viewModel,
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().RemoteBranches,

View File

@ -7,7 +7,7 @@ import (
)
type RemotesContext struct {
*BasicViewModel[*models.Remote]
*FilteredListViewModel[*models.Remote]
*ListContextTrait
}
@ -17,14 +17,19 @@ var (
)
func NewRemotesContext(c *ContextCommon) *RemotesContext {
viewModel := NewBasicViewModel(func() []*models.Remote { return c.Model().Remotes })
viewModel := NewFilteredListViewModel(
func() []*models.Remote { return c.Model().Remotes },
func(remote *models.Remote) []string {
return []string{remote.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetRemoteListDisplayStrings(c.Model().Remotes, c.Modes().Diffing.Ref)
return presentation.GetRemoteListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
}
return &RemotesContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Remotes,

View File

@ -0,0 +1,74 @@
package context
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/theme"
)
type SearchTrait struct {
c *ContextCommon
searchString string
}
func NewSearchTrait(c *ContextCommon) *SearchTrait {
return &SearchTrait{c: c}
}
func (self *SearchTrait) GetSearchString() string {
return self.searchString
}
func (self *SearchTrait) SetSearchString(searchString string) {
self.searchString = searchString
}
func (self *SearchTrait) ClearSearchString() {
self.SetSearchString("")
}
// used for type switch
func (self *SearchTrait) IsSearchableContext() {}
func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
keybindingConfig := self.c.UserConfig.Keybinding
return func(y int, index int, total int) error {
if total == 0 {
self.c.SetViewContent(
self.c.Views().Search,
fmt.Sprintf(
self.c.Tr.NoMatchesFor,
self.searchString,
theme.OptionsFgColor.Sprintf(self.c.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
),
)
return nil
}
self.c.SetViewContent(
self.c.Views().Search,
fmt.Sprintf(
self.c.Tr.MatchesFor,
self.searchString,
index+1,
total,
theme.OptionsFgColor.Sprintf(
self.c.Tr.SearchKeybindings,
keybindings.Label(keybindingConfig.Universal.NextMatch),
keybindings.Label(keybindingConfig.Universal.PrevMatch),
keybindings.Label(keybindingConfig.Universal.Return),
),
),
)
if err := innerFunc(y); err != nil {
return err
}
return nil
}
}
func (self *SearchTrait) IsSearching() bool {
return self.searchString != ""
}

View File

@ -7,7 +7,7 @@ import (
)
type StashContext struct {
*BasicViewModel[*models.StashEntry]
*FilteredListViewModel[*models.StashEntry]
*ListContextTrait
}
@ -19,14 +19,19 @@ var (
func NewStashContext(
c *ContextCommon,
) *StashContext {
viewModel := NewBasicViewModel(func() []*models.StashEntry { return c.Model().StashEntries })
viewModel := NewFilteredListViewModel(
func() []*models.StashEntry { return c.Model().StashEntries },
func(stashEntry *models.StashEntry) []string {
return []string{stashEntry.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetStashEntryListDisplayStrings(c.Model().StashEntries, c.Modes().Diffing.Ref)
return presentation.GetStashEntryListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
}
return &StashContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Stash,

View File

@ -12,9 +12,12 @@ import (
)
type SubCommitsContext struct {
c *ContextCommon
*SubCommitsViewModel
*ListContextTrait
*DynamicTitleBuilder
*SearchTrait
}
var (
@ -26,7 +29,7 @@ func NewSubCommitsContext(
c *ContextCommon,
) *SubCommitsContext {
viewModel := &SubCommitsViewModel{
BasicViewModel: NewBasicViewModel(
ListViewModel: NewListViewModel(
func() []*models.Commit { return c.Model().SubCommits },
),
ref: nil,
@ -60,8 +63,10 @@ func NewSubCommitsContext(
)
}
return &SubCommitsContext{
ctx := &SubCommitsContext{
c: c,
SubCommitsViewModel: viewModel,
SearchTrait: NewSearchTrait(c),
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@ -78,12 +83,19 @@ func NewSubCommitsContext(
refreshViewportOnChange: true,
},
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
return ctx.HandleFocus(types.OnFocusOpts{})
}))
return ctx
}
type SubCommitsViewModel struct {
// name of the ref that the sub-commits are shown for
ref types.Ref
*BasicViewModel[*models.Commit]
*ListViewModel[*models.Commit]
limitCommits bool
}

View File

@ -7,21 +7,26 @@ import (
)
type SubmodulesContext struct {
*BasicViewModel[*models.SubmoduleConfig]
*FilteredListViewModel[*models.SubmoduleConfig]
*ListContextTrait
}
var _ types.IListContext = (*SubmodulesContext)(nil)
func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext {
viewModel := NewBasicViewModel(func() []*models.SubmoduleConfig { return c.Model().Submodules })
viewModel := NewFilteredListViewModel(
func() []*models.SubmoduleConfig { return c.Model().Submodules },
func(submodule *models.SubmoduleConfig) []string {
return []string{submodule.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules)
return presentation.GetSubmoduleListDisplayStrings(viewModel.GetItems())
}
return &SubmodulesContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Submodules,

View File

@ -7,7 +7,7 @@ import (
)
type SuggestionsContext struct {
*BasicViewModel[*types.Suggestion]
*ListViewModel[*types.Suggestion]
*ListContextTrait
State *SuggestionsContextState
@ -40,11 +40,11 @@ func NewSuggestionsContext(
return presentation.GetSuggestionListDisplayStrings(state.Suggestions)
}
viewModel := NewBasicViewModel(getModel)
viewModel := NewListViewModel(getModel)
return &SuggestionsContext{
State: state,
BasicViewModel: viewModel,
State: state,
ListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Suggestions,

View File

@ -7,7 +7,7 @@ import (
)
type TagsContext struct {
*BasicViewModel[*models.Tag]
*FilteredListViewModel[*models.Tag]
*ListContextTrait
}
@ -19,14 +19,19 @@ var (
func NewTagsContext(
c *ContextCommon,
) *TagsContext {
viewModel := NewBasicViewModel(func() []*models.Tag { return c.Model().Tags })
viewModel := NewFilteredListViewModel(
func() []*models.Tag { return c.Model().Tags },
func(tag *models.Tag) []string {
return []string{tag.Name, tag.Message}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetTagListDisplayStrings(c.Model().Tags, c.Modes().Diffing.Ref)
return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref)
}
return &TagsContext{
BasicViewModel: viewModel,
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Tags,

View File

@ -11,6 +11,7 @@ import (
type WorkingTreeContext struct {
*filetree.FileTreeViewModel
*ListContextTrait
*SearchTrait
}
var _ types.IListContext = (*WorkingTreeContext)(nil)
@ -29,7 +30,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
})
}
return &WorkingTreeContext{
ctx := &WorkingTreeContext{
SearchTrait: NewSearchTrait(c),
FileTreeViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
@ -44,6 +46,13 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
c: c,
},
}
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(selectedLineIdx int) error {
ctx.GetList().SetSelectedLineIdx(selectedLineIdx)
return ctx.HandleFocus(types.OnFocusOpts{})
}))
return ctx
}
func (self *WorkingTreeContext) GetSelectedItemId() string {

View File

@ -99,6 +99,7 @@ func (gui *Gui) resetHelpersAndControllers() {
modeHelper,
appStatusHelper,
),
Search: helpers.NewSearchHelper(helperCommon),
}
gui.CustomCommandsClient = custom_commands.NewClient(
@ -162,6 +163,16 @@ func (gui *Gui) resetHelpersAndControllers() {
sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common)
filterControllerFactory := controllers.NewFilterControllerFactory(common)
for _, context := range gui.c.Context().AllFilterable() {
controllers.AttachControllers(context, filterControllerFactory.Create(context))
}
searchControllerFactory := controllers.NewSearchControllerFactory(common)
for _, context := range gui.c.Context().AllSearchable() {
controllers.AttachControllers(context, searchControllerFactory.Create(context))
}
// allow for navigating between side window contexts
for _, context := range []types.Context{
gui.State.Contexts.Status,
@ -323,6 +334,10 @@ func (gui *Gui) resetHelpersAndControllers() {
suggestionsController,
)
controllers.AttachControllers(gui.State.Contexts.Search,
controllers.NewSearchPromptController(common),
)
controllers.AttachControllers(gui.State.Contexts.Global,
syncController,
undoController,

View File

@ -648,7 +648,7 @@ func (self *FilesController) handleStatusFilterPressed() error {
},
},
{
Label: self.c.Tr.ResetCommitFilterState,
Label: self.c.Tr.ResetFilter,
OnPress: func() error {
return self.setStatusFiltering(filetree.DisplayAll)
},
@ -658,7 +658,7 @@ func (self *FilesController) handleStatusFilterPressed() error {
}
func (self *FilesController) setStatusFiltering(filter filetree.FileTreeDisplayFilter) error {
self.context().FileTreeViewModel.SetFilter(filter)
self.context().FileTreeViewModel.SetStatusFilter(filter)
return self.c.PostRefreshUpdate(self.context())
}

View File

@ -0,0 +1,48 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type FilterControllerFactory struct {
c *ControllerCommon
}
func NewFilterControllerFactory(c *ControllerCommon) *FilterControllerFactory {
return &FilterControllerFactory{
c: c,
}
}
func (self *FilterControllerFactory) Create(context types.IFilterableContext) *FilterController {
return &FilterController{
baseController: baseController{},
c: self.c,
context: context,
}
}
type FilterController struct {
baseController
c *ControllerCommon
context types.IFilterableContext
}
func (self *FilterController) Context() types.Context {
return self.context
}
func (self *FilterController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.StartSearch),
Handler: self.OpenFilterPrompt,
Description: self.c.Tr.StartFilter,
},
}
}
func (self *FilterController) OpenFilterPrompt() error {
return self.c.Helpers().Search.OpenFilterPrompt(self.context)
}

View File

@ -292,7 +292,9 @@ func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string)
}
func (self *ConfirmationHelper) resizeMenu() {
itemCount := self.c.Contexts().Menu.GetList().Len()
// we want the unfiltered length here so that if we're filtering we don't
// resize the window
itemCount := self.c.Contexts().Menu.UnfilteredLen()
offset := 3
panelWidth := self.getPopupPanelWidth()
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset)

View File

@ -46,6 +46,7 @@ type Helpers struct {
Mode *ModeHelper
AppStatus *AppStatusHelper
WindowArrangement *WindowArrangementHelper
Search *SearchHelper
}
func NewStubHelpers() *Helpers {
@ -78,5 +79,6 @@ func NewStubHelpers() *Helpers {
Mode: &ModeHelper{},
AppStatus: &AppStatusHelper{},
WindowArrangement: &WindowArrangementHelper{},
Search: &SearchHelper{},
}
}

View File

@ -464,10 +464,10 @@ func (self *RefreshHelper) refreshStateFiles() error {
// I'd prefer to maintain as little state as possible.
if conflictFileCount > 0 {
if fileTreeViewModel.GetFilter() == filetree.DisplayAll {
fileTreeViewModel.SetFilter(filetree.DisplayConflicted)
fileTreeViewModel.SetStatusFilter(filetree.DisplayConflicted)
}
} else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted {
fileTreeViewModel.SetFilter(filetree.DisplayAll)
fileTreeViewModel.SetStatusFilter(filetree.DisplayAll)
}
self.c.Model().Files = files

View File

@ -0,0 +1,260 @@
package helpers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/theme"
)
// NOTE: this helper supports both filtering and searching. Filtering is when
// the contents of the list are filtered, whereas searching does not actually
// change the contents of the list but instead just highlights the search.
// The general term we use to capture both searching and filtering is...
// 'searching', which is unfortunate but I can't think of a better name.
type SearchHelper struct {
c *HelperCommon
}
func NewSearchHelper(
c *HelperCommon,
) *SearchHelper {
return &SearchHelper{
c: c,
}
}
func (self *SearchHelper) OpenFilterPrompt(context types.IFilterableContext) error {
state := self.searchState()
state.Context = context
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(context.GetFilter())
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
return err
}
return nil
}
func (self *SearchHelper) OpenSearchPrompt(context types.ISearchableContext) error {
state := self.searchState()
state.Context = context
searchString := context.GetSearchString()
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
promptView := self.promptView()
promptView.ClearTextArea()
promptView.TextArea.TypeString(searchString)
promptView.RenderTextArea()
if err := self.c.PushContext(self.c.Contexts().Search); err != nil {
return err
}
return nil
}
func (self *SearchHelper) DisplayFilterStatus(context types.IFilterableContext) {
state := self.searchState()
state.Context = context
searchString := context.GetFilter()
self.searchPrefixView().SetContent(self.c.Tr.FilterPrefix)
promptView := self.promptView()
keybindingConfig := self.c.UserConfig.Keybinding
promptView.SetContent(fmt.Sprintf("matches for '%s' ", searchString) + theme.OptionsFgColor.Sprintf(self.c.Tr.ExitTextFilterMode, keybindings.Label(keybindingConfig.Universal.Return)))
}
func (self *SearchHelper) DisplaySearchStatus(context types.ISearchableContext) {
state := self.searchState()
state.Context = context
self.searchPrefixView().SetContent(self.c.Tr.SearchPrefix)
_ = context.GetView().SelectCurrentSearchResult()
}
func (self *SearchHelper) searchState() *types.SearchState {
return self.c.State().GetRepoState().GetSearchState()
}
func (self *SearchHelper) searchPrefixView() *gocui.View {
return self.c.Views().SearchPrefix
}
func (self *SearchHelper) promptView() *gocui.View {
return self.c.Contexts().Search.GetView()
}
func (self *SearchHelper) promptContent() string {
return self.c.Contexts().Search.GetView().TextArea.GetContent()
}
func (self *SearchHelper) Confirm() error {
state := self.searchState()
if self.promptContent() == "" {
return self.CancelPrompt()
}
switch state.SearchType() {
case types.SearchTypeFilter:
return self.ConfirmFilter()
case types.SearchTypeSearch:
return self.ConfirmSearch()
case types.SearchTypeNone:
return self.c.PopContext()
}
return nil
}
func (self *SearchHelper) ConfirmFilter() error {
// We also do this on each keypress but we do it here again just in case
state := self.searchState()
_, ok := state.Context.(types.IFilterableContext)
if !ok {
self.c.Log.Warnf("Context %s is not filterable", state.Context.GetKey())
return nil
}
self.OnPromptContentChanged(self.promptContent())
return self.c.PopContext()
}
func (self *SearchHelper) ConfirmSearch() error {
state := self.searchState()
context, ok := state.Context.(types.ISearchableContext)
if !ok {
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
return nil
}
searchString := self.promptContent()
context.SetSearchString(searchString)
view := context.GetView()
if err := self.c.PopContext(); err != nil {
return err
}
if err := view.Search(searchString); err != nil {
return err
}
return nil
}
func (self *SearchHelper) CancelPrompt() error {
self.Cancel()
return self.c.PopContext()
}
func (self *SearchHelper) Cancel() {
state := self.searchState()
switch context := state.Context.(type) {
case types.IFilterableContext:
context.ClearFilter()
_ = self.c.PostRefreshUpdate(context)
case types.ISearchableContext:
context.ClearSearchString()
context.GetView().ClearSearch()
default:
// do nothing
}
self.HidePrompt()
}
func (self *SearchHelper) OnPromptContentChanged(searchString string) {
state := self.searchState()
switch context := state.Context.(type) {
case types.IFilterableContext:
context.SetSelectedLineIdx(0)
_ = context.GetView().SetOriginY(0)
context.SetFilter(searchString)
_ = self.c.PostRefreshUpdate(context)
case types.ISearchableContext:
// do nothing
default:
// do nothing (shouldn't land here)
}
}
func (self *SearchHelper) RenderSearchStatus(c types.Context) {
if c.GetKey() == context.SEARCH_CONTEXT_KEY {
return
}
if searchableContext, ok := c.(types.ISearchableContext); ok {
if searchableContext.IsSearching() {
self.setSearchingFrameColor()
self.DisplaySearchStatus(searchableContext)
return
}
}
if filterableContext, ok := c.(types.IFilterableContext); ok {
if filterableContext.IsFiltering() {
self.setSearchingFrameColor()
self.DisplayFilterStatus(filterableContext)
return
}
}
self.HidePrompt()
}
func (self *SearchHelper) CancelSearchIfSearching(c types.Context) {
if searchableContext, ok := c.(types.ISearchableContext); ok {
view := searchableContext.GetView()
if view != nil && view.IsSearching() {
view.ClearSearch()
searchableContext.ClearSearchString()
self.Cancel()
}
return
}
if filterableContext, ok := c.(types.IFilterableContext); ok {
if filterableContext.IsFiltering() {
filterableContext.ClearFilter()
self.Cancel()
}
return
}
}
func (self *SearchHelper) HidePrompt() {
self.setNonSearchingFrameColor()
state := self.searchState()
state.Context = nil
}
func (self *SearchHelper) setSearchingFrameColor() {
self.c.GocuiGui().SelFgColor = theme.SearchingActiveBorderColor
self.c.GocuiGui().SelFrameColor = theme.SearchingActiveBorderColor
}
func (self *SearchHelper) setNonSearchingFrameColor() {
self.c.GocuiGui().SelFgColor = theme.ActiveBorderColor
self.c.GocuiGui().SelFrameColor = theme.ActiveBorderColor
}

View File

@ -55,7 +55,7 @@ func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string,
self.c.Modes().Filtering.Active()
showInfoSection := self.c.UserConfig.Gui.ShowBottomLine ||
self.c.State().GetRepoState().IsSearching() ||
self.c.State().GetRepoState().InSearchPrompt() ||
self.modeHelper.IsAnyModeActive() ||
self.appStatusHelper.HasStatus()
infoSectionSize := 0
@ -174,11 +174,17 @@ func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) {
}
func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box {
if self.c.State().GetRepoState().IsSearching() {
if self.c.State().GetRepoState().InSearchPrompt() {
var prefix string
if self.c.State().GetRepoState().GetSearchState().SearchType() == types.SearchTypeSearch {
prefix = self.c.Tr.SearchPrefix
} else {
prefix = self.c.Tr.FilterPrefix
}
return []*boxlayout.Box{
{
Window: "searchPrefix",
Size: runewidth.StringWidth(self.c.Tr.SearchPrefix),
Size: runewidth.StringWidth(prefix),
},
{
Window: "search",

View File

@ -150,18 +150,7 @@ func (self *ListController) GetKeybindings(opts types.KeybindingsOpts) []*types.
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoTop), Handler: self.HandleGotoTop, Description: self.c.Tr.GotoTop},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollLeft), Handler: self.HandleScrollLeft},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.ScrollRight), Handler: self.HandleScrollRight},
{
Key: opts.GetKey(opts.Config.Universal.StartSearch),
Handler: func() error { self.c.OpenSearch(); return nil },
Description: self.c.Tr.StartSearch,
Tag: "navigation",
},
{
Key: opts.GetKey(opts.Config.Universal.GotoBottom),
Description: self.c.Tr.GotoBottom,
Handler: self.HandleGotoBottom,
Tag: "navigation",
},
{Tag: "navigation", Key: opts.GetKey(opts.Config.Universal.GotoBottom), Handler: self.HandleGotoBottom, Description: self.c.Tr.GotoBottom},
}
}

View File

@ -693,9 +693,7 @@ func (self *LocalCommitsController) openSearch() error {
}
}
self.c.OpenSearch()
return nil
return self.c.Helpers().Search.OpenSearchPrompt(self.context())
}
func (self *LocalCommitsController) gotoBottom() error {

View File

@ -123,12 +123,6 @@ func (self *PatchExplorerController) GetKeybindings(opts types.KeybindingsOpts)
Key: opts.GetKey(opts.Config.Universal.ScrollRight),
Handler: self.withRenderAndFocus(self.HandleScrollRight),
},
{
Tag: "navigation",
Key: opts.GetKey(opts.Config.Universal.StartSearch),
Handler: func() error { self.c.OpenSearch(); return nil },
Description: self.c.Tr.StartSearch,
},
{
Key: opts.GetKey(opts.Config.Universal.CopyToClipboard),
Handler: self.withLock(self.CopySelectedToClipboard),

View File

@ -50,6 +50,19 @@ func (self *QuitActions) confirmQuitDuringUpdate() error {
func (self *QuitActions) Escape() error {
currentContext := self.c.CurrentContext()
switch ctx := currentContext.(type) {
case types.IFilterableContext:
if ctx.IsFiltering() {
self.c.Helpers().Search.Cancel()
return nil
}
case types.ISearchableContext:
if ctx.IsSearching() {
self.c.Helpers().Search.Cancel()
return nil
}
}
parentContext, hasParent := currentContext.GetParentContext()
if hasParent && currentContext != nil && parentContext != nil {
// TODO: think about whether this should be marked as a return rather than adding to the stack

View File

@ -59,11 +59,6 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts)
Handler: self.checkSelected(self.setAsUpstream),
Description: self.c.Tr.SetAsUpstream,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.escape,
Description: self.c.Tr.ReturnToRemotesList,
},
{
Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
Handler: self.checkSelected(self.createResetMenu),
@ -115,10 +110,6 @@ func (self *RemoteBranchesController) checkSelected(callback func(*models.Remote
}
}
func (self *RemoteBranchesController) escape() error {
return self.c.PushContext(self.c.Contexts().Remotes)
}
func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error {
message := fmt.Sprintf("%s '%s'?", self.c.Tr.DeleteRemoteBranchMessage, selectedBranch.FullName())

View File

@ -104,14 +104,16 @@ func (self *RemotesController) enter(remote *models.Remote) error {
if len(remote.Branches) == 0 {
newSelectedLine = -1
}
self.c.Contexts().RemoteBranches.SetSelectedLineIdx(newSelectedLine)
self.c.Contexts().RemoteBranches.SetTitleRef(remote.Name)
remoteBranchesContext := self.c.Contexts().RemoteBranches
remoteBranchesContext.SetSelectedLineIdx(newSelectedLine)
remoteBranchesContext.SetTitleRef(remote.Name)
remoteBranchesContext.SetParentContext(self.Context())
if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil {
if err := self.c.PostRefreshUpdate(remoteBranchesContext); err != nil {
return err
}
return self.c.PushContext(self.c.Contexts().RemoteBranches)
return self.c.PushContext(remoteBranchesContext)
}
func (self *RemotesController) add() error {

View File

@ -0,0 +1,48 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type SearchControllerFactory struct {
c *ControllerCommon
}
func NewSearchControllerFactory(c *ControllerCommon) *SearchControllerFactory {
return &SearchControllerFactory{
c: c,
}
}
func (self *SearchControllerFactory) Create(context types.ISearchableContext) *SearchController {
return &SearchController{
baseController: baseController{},
c: self.c,
context: context,
}
}
type SearchController struct {
baseController
c *ControllerCommon
context types.ISearchableContext
}
func (self *SearchController) Context() types.Context {
return self.context
}
func (self *SearchController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.StartSearch),
Handler: self.OpenSearchPrompt,
Description: self.c.Tr.StartSearch,
},
}
}
func (self *SearchController) OpenSearchPrompt() error {
return self.c.Helpers().Search.OpenSearchPrompt(self.context)
}

View File

@ -0,0 +1,53 @@
package controllers
import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type SearchPromptController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &SearchPromptController{}
func NewSearchPromptController(
common *ControllerCommon,
) *SearchPromptController {
return &SearchPromptController{
baseController: baseController{},
c: common,
}
}
func (self *SearchPromptController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Confirm),
Modifier: gocui.ModNone,
Handler: self.confirm,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Modifier: gocui.ModNone,
Handler: self.cancel,
},
}
}
func (self *SearchPromptController) Context() types.Context {
return self.context()
}
func (self *SearchPromptController) context() types.Context {
return self.c.Contexts().Search
}
func (self *SearchPromptController) confirm() error {
return self.c.Helpers().Search.Confirm()
}
func (self *SearchPromptController) cancel() error {
return self.c.Helpers().Search.CancelPrompt()
}

View File

@ -83,6 +83,7 @@ func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesConte
diffFilesContext.SetCanRebase(opts.CanRebase)
diffFilesContext.SetParentContext(opts.Context)
diffFilesContext.SetWindowName(opts.Context.GetWindowName())
diffFilesContext.ClearSearchString()
if err := self.c.Refresh(types.RefreshOptions{
Scope: []types.RefreshableView{types.COMMIT_FILES},

View File

@ -71,12 +71,15 @@ func (self *SwitchToSubCommitsController) viewCommits() error {
self.setSubCommits(commits)
self.c.Contexts().SubCommits.SetSelectedLineIdx(0)
self.c.Contexts().SubCommits.SetParentContext(self.context)
self.c.Contexts().SubCommits.SetWindowName(self.context.GetWindowName())
self.c.Contexts().SubCommits.SetTitleRef(ref.Description())
self.c.Contexts().SubCommits.SetRef(ref)
self.c.Contexts().SubCommits.SetLimitCommits(true)
subCommitsContext := self.c.Contexts().SubCommits
subCommitsContext.SetSelectedLineIdx(0)
subCommitsContext.SetParentContext(self.context)
subCommitsContext.SetWindowName(self.context.GetWindowName())
subCommitsContext.SetTitleRef(ref.Description())
subCommitsContext.SetRef(ref)
subCommitsContext.SetLimitCommits(true)
subCommitsContext.ClearSearchString()
subCommitsContext.GetView().ClearSearch()
err = self.c.PostRefreshUpdate(self.c.Contexts().SubCommits)
if err != nil {

View File

@ -89,3 +89,14 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo
return matched
}
func (gui *Gui) searchEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false)
v.RenderTextArea()
searchString := v.TextArea.GetContent()
gui.helpers.Search.OnPromptContentChanged(searchString)
return matched
}

View File

@ -34,7 +34,7 @@ type IFileTree interface {
ITree[models.File]
FilterFiles(test func(*models.File) bool) []*models.File
SetFilter(filter FileTreeDisplayFilter)
SetStatusFilter(filter FileTreeDisplayFilter)
Get(index int) *FileNode
GetFile(path string) *models.File
GetAllItems() []*FileNode
@ -91,7 +91,7 @@ func (self *FileTree) FilterFiles(test func(*models.File) bool) []*models.File {
return slices.Filter(self.getFiles(), test)
}
func (self *FileTree) SetFilter(filter FileTreeDisplayFilter) {
func (self *FileTree) SetStatusFilter(filter FileTreeDisplayFilter) {
self.filter = filter
self.SetTree()
}
@ -102,7 +102,7 @@ func (self *FileTree) ToggleShowTree() {
}
func (self *FileTree) Get(index int) *FileNode {
// need to traverse the three depth first until we get to the index.
// need to traverse the tree depth first until we get to the index.
return NewFileNode(self.tree.GetNodeAtIndex(index+1, self.collapsedPaths)) // ignoring root
}

View File

@ -126,8 +126,8 @@ func (self *FileTreeViewModel) findNewSelectedIdx(prevNodes []*FileNode, currNod
return -1
}
func (self *FileTreeViewModel) SetFilter(filter FileTreeDisplayFilter) {
self.IFileTree.SetFilter(filter)
func (self *FileTreeViewModel) SetStatusFilter(filter FileTreeDisplayFilter) {
self.IFileTree.SetStatusFilter(filter)
self.IListCursor.SetSelectedLineIdx(0)
}

View File

@ -201,7 +201,7 @@ type GuiRepoState struct {
SplitMainPanel bool
LimitCommits bool
Searching searchingState
SearchState *types.SearchState
StartupStage types.StartupStage // Allows us to not load everything at once
ContextMgr *ContextMgr
@ -256,8 +256,12 @@ func (self *GuiRepoState) SetScreenMode(value types.WindowMaximisation) {
self.ScreenMode = value
}
func (self *GuiRepoState) IsSearching() bool {
return self.Searching.isSearching
func (self *GuiRepoState) InSearchPrompt() bool {
return self.SearchState.SearchType() != types.SearchTypeNone
}
func (self *GuiRepoState) GetSearchState() *types.SearchState {
return self.SearchState
}
func (self *GuiRepoState) SetSplitMainPanel(value bool) {
@ -268,12 +272,6 @@ func (self *GuiRepoState) GetSplitMainPanel() bool {
return self.SplitMainPanel
}
type searchingState struct {
view *gocui.View
isSearching bool
searchString string
}
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
var err error
gui.git, err = commands.NewGitCommand(
@ -358,6 +356,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.
ContextMgr: NewContextMgr(gui, contextTree),
Contexts: contextTree,
WindowViewNameMap: initialWindowViewNameMap(contextTree),
SearchState: types.NewSearchState(),
}
gui.RepoStateMap[Repo(currentDir)] = gui.State
@ -584,11 +583,12 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
})
deadlock.Opts.Disable = !gui.Debug
gui.g.OnSearchEscape = gui.onSearchEscape
if err := gui.Config.ReloadUserConfig(); err != nil {
return nil
}
userConfig := gui.UserConfig
gui.g.OnSearchEscape = func() error { gui.helpers.Search.Cancel(); return nil }
gui.g.SearchEscapeKey = keybindings.GetKey(userConfig.Keybinding.Universal.Return)
gui.g.NextSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.NextMatch)
gui.g.PrevSearchMatchKey = keybindings.GetKey(userConfig.Keybinding.Universal.PrevMatch)

View File

@ -128,10 +128,6 @@ func (self *guiCommon) Mutexes() types.Mutexes {
return self.gui.Mutexes
}
func (self *guiCommon) OpenSearch() {
_ = self.gui.handleOpenSearch(self.gui.currentViewName())
}
func (self *guiCommon) GocuiGui() *gocui.Gui {
return self.gui.g
}

View File

@ -2,6 +2,7 @@ package gui
import (
"fmt"
"os"
"strings"
"time"
@ -70,7 +71,8 @@ func (self *GuiDriver) Fail(message string) {
self.gui.g.Close()
// need to give the gui time to close
time.Sleep(time.Millisecond * 100)
panic(fullMessage)
fmt.Fprintln(os.Stderr, fullMessage)
panic("Test failed")
}
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`

View File

@ -215,18 +215,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi
Modifier: gocui.ModNone,
Handler: self.scrollUpSecondary,
},
{
ViewName: "search",
Key: opts.GetKey(opts.Config.Universal.Confirm),
Modifier: gocui.ModNone,
Handler: self.handleSearch,
},
{
ViewName: "search",
Key: opts.GetKey(opts.Config.Universal.Return),
Modifier: gocui.ModNone,
Handler: self.handleSearchEscape,
},
{
ViewName: "confirmation",
Key: opts.GetKey(opts.Config.Universal.PrevItem),

View File

@ -132,20 +132,6 @@ func (gui *Gui) layout(g *gocui.Gui) error {
}
view.SelBgColor = theme.GocuiSelectedLineBgColor
// I doubt this is expensive though it's admittedly redundant after the first render
view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.OnSearchSelect))
}
for _, context := range gui.c.Context().AllPatchExplorer() {
context := context
context.GetView().SetOnSelectItem(gui.onSelectItemWrapper(
func(selectedLineIdx int) error {
context.GetMutex().Lock()
defer context.GetMutex().Unlock()
return context.NavigateTo(gui.c.IsCurrentContext(context), selectedLineIdx)
}),
)
}
mainViewWidth, mainViewHeight := gui.Views.Main.Size()

View File

@ -46,9 +46,6 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
gui.Views.Menu.Title = opts.Title
gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor
gui.Views.Menu.SetOnSelectItem(gui.onSelectItemWrapper(func(selectedLine int) error {
return nil
}))
gui.Views.Tooltip.Wrap = true
gui.Views.Tooltip.FgColor = theme.GocuiDefaultTextColor

View File

@ -1,103 +0,0 @@
package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func (gui *Gui) handleOpenSearch(viewName string) error {
view, err := gui.g.View(viewName)
if err != nil {
return nil
}
gui.State.Searching.isSearching = true
gui.State.Searching.view = view
gui.Views.Search.ClearTextArea()
if err := gui.c.PushContext(gui.State.Contexts.Search); err != nil {
return err
}
return nil
}
func (gui *Gui) handleSearch() error {
gui.State.Searching.searchString = gui.Views.Search.TextArea.GetContent()
if err := gui.c.PopContext(); err != nil {
return err
}
view := gui.State.Searching.view
if view == nil {
return nil
}
if err := view.Search(gui.State.Searching.searchString); err != nil {
return err
}
return nil
}
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
keybindingConfig := gui.c.UserConfig.Keybinding
return func(y int, index int, total int) error {
if total == 0 {
gui.c.SetViewContent(
gui.Views.Search,
fmt.Sprintf(
gui.Tr.NoMatchesFor,
gui.State.Searching.searchString,
theme.OptionsFgColor.Sprintf(gui.Tr.ExitSearchMode, keybindings.Label(keybindingConfig.Universal.Return)),
),
)
return nil
}
gui.c.SetViewContent(
gui.Views.Search,
fmt.Sprintf(
gui.Tr.MatchesFor,
gui.State.Searching.searchString,
index+1,
total,
theme.OptionsFgColor.Sprintf(
gui.Tr.SearchKeybindings,
keybindings.Label(keybindingConfig.Universal.NextMatch),
keybindings.Label(keybindingConfig.Universal.PrevMatch),
keybindings.Label(keybindingConfig.Universal.Return),
),
),
)
if err := innerFunc(y); err != nil {
return err
}
return nil
}
}
func (gui *Gui) onSearchEscape() error {
gui.State.Searching.isSearching = false
if gui.State.Searching.view != nil {
gui.State.Searching.view.ClearSearch()
gui.State.Searching.view = nil
}
return nil
}
func (gui *Gui) handleSearchEscape() error {
if err := gui.onSearchEscape(); err != nil {
return err
}
if err := gui.c.PopContext(); err != nil {
return err
}
return nil
}

View File

@ -68,8 +68,6 @@ type IGuiCommon interface {
Context() IContextMgr
ActivateContext(context Context) error
// enters search mode for the current view
OpenSearch()
GetConfig() config.AppConfigurer
GetAppState() *config.AppState
@ -251,7 +249,8 @@ type IRepoStateAccessor interface {
SetCurrentPopupOpts(*CreatePopupPanelOpts)
GetScreenMode() WindowMaximisation
SetScreenMode(WindowMaximisation)
IsSearching() bool
InSearchPrompt() bool
GetSearchState() *SearchState
SetSplitMainPanel(bool)
GetSplitMainPanel() bool
}

View File

@ -87,6 +87,27 @@ type Context interface {
HandleRenderToMain() error
}
type IFilterableContext interface {
Context
IListPanelState
SetFilter(string)
GetFilter() string
ClearFilter()
IsFiltering() bool
IsFilterableContext()
}
type ISearchableContext interface {
Context
SetSearchString(string)
GetSearchString() string
ClearSearchString()
IsSearching() bool
IsSearchableContext()
}
type DiffableContext interface {
Context
@ -104,7 +125,6 @@ type IListContext interface {
GetList() IList
OnSearchSelect(selectedLineIdx int) error
FocusLine()
IsListContext() // used for type switch
}
@ -211,5 +231,7 @@ type IContextMgr interface {
IsCurrent(c Context) bool
ForEach(func(Context))
AllList() []IListContext
AllFilterable() []IFilterableContext
AllSearchable() []ISearchableContext
AllPatchExplorer() []IPatchExplorerContext
}

View File

@ -0,0 +1,31 @@
package types
type SearchType int
const (
SearchTypeNone SearchType = iota
// searching is where matches are highlighted but the content is not filtered down
SearchTypeSearch
// filter is where the list is filtered down to only matches
SearchTypeFilter
)
// TODO: could we remove this entirely?
type SearchState struct {
Context Context
}
func NewSearchState() *SearchState {
return &SearchState{}
}
func (self *SearchState) SearchType() SearchType {
switch self.Context.(type) {
case IFilterableContext:
return SearchTypeFilter
case ISearchableContext:
return SearchTypeSearch
default:
return SearchTypeNone
}
}

View File

@ -91,10 +91,16 @@ func (gui *Gui) createAllViews() error {
gui.Views.Options.Frame = false
gui.Views.SearchPrefix.BgColor = gocui.ColorDefault
gui.Views.SearchPrefix.FgColor = gocui.ColorGreen
gui.Views.SearchPrefix.FgColor = gocui.ColorCyan
gui.Views.SearchPrefix.Frame = false
gui.c.SetViewContent(gui.Views.SearchPrefix, gui.Tr.SearchPrefix)
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorCyan
gui.Views.Search.Editable = true
gui.Views.Search.Frame = false
gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor)
gui.Views.Stash.Title = gui.c.Tr.StashTitle
gui.Views.Commits.Title = gui.c.Tr.CommitsTitle
@ -141,11 +147,6 @@ func (gui *Gui) createAllViews() error {
gui.Views.Status.Title = gui.c.Tr.StatusTitle
gui.Views.Search.BgColor = gocui.ColorDefault
gui.Views.Search.FgColor = gocui.ColorGreen
gui.Views.Search.Editable = true
gui.Views.Search.Frame = false
gui.Views.AppStatus.BgColor = gocui.ColorDefault
gui.Views.AppStatus.FgColor = gocui.ColorCyan
gui.Views.AppStatus.Visible = false

View File

@ -35,7 +35,7 @@ func dutchTranslationSet() TranslationSet {
Scroll: "Scroll",
FilterStagedFiles: "Show only staged files",
FilterUnstagedFiles: "Show only unstaged files",
ResetCommitFilterState: "Reset commit file state filter",
ResetFilter: "Reset commit file state filter",
MergeConflictsTitle: "Merge conflicten",
Checkout: "Uitchecken",
PullWait: "Pullen...",

View File

@ -54,7 +54,7 @@ type TranslationSet struct {
FileFilter string
FilterStagedFiles string
FilterUnstagedFiles string
ResetCommitFilterState string
ResetFilter string
MergeConflictsTitle string
Checkout string
NoChangedFiles string
@ -371,6 +371,7 @@ type TranslationSet struct {
NextScreenMode string
PrevScreenMode string
StartSearch string
StartFilter string
Panel string
Keybindings string
KeybindingsLegend string
@ -536,7 +537,9 @@ type TranslationSet struct {
MatchesFor string
SearchKeybindings string
SearchPrefix string
FilterPrefix string
ExitSearchMode string
ExitTextFilterMode string
Actions Actions
Bisect Bisect
}
@ -741,10 +744,10 @@ func EnglishTranslationSet() TranslationSet {
Scroll: "Scroll",
MergeConflictsTitle: "Merge conflicts",
Checkout: "Checkout",
FileFilter: "Filter files (staged/unstaged)",
FileFilter: "Filter files by status",
FilterStagedFiles: "Show only staged files",
FilterUnstagedFiles: "Show only unstaged files",
ResetCommitFilterState: "Reset filter",
ResetFilter: "Reset filter",
NoChangedFiles: "No changed files",
PullWait: "Pulling...",
PushWait: "Pushing...",
@ -1054,50 +1057,52 @@ func EnglishTranslationSet() TranslationSet {
GitFlowOptions: "Show git-flow options",
NotAGitFlowBranch: "This does not seem to be a git flow branch",
NewGitFlowBranchPrompt: "New {{.branchType}} name:",
IgnoreTracked: "Ignore tracked file",
IgnoreTrackedPrompt: "Are you sure you want to ignore a tracked file?",
ExcludeTracked: "Exclude tracked file",
ExcludeTrackedPrompt: "Are you sure you want to exclude a tracked file?",
ViewResetToUpstreamOptions: "View upstream reset options",
NextScreenMode: "Next screen mode (normal/half/fullscreen)",
PrevScreenMode: "Prev screen mode",
StartSearch: "Start search",
Panel: "Panel",
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
RenameBranch: "Rename branch",
SetUnsetUpstream: "Set/Unset upstream",
NewBranchNamePrompt: "Enter new branch name for branch",
RenameBranchWarning: "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
OpenMenu: "Open menu",
ResetCherryPick: "Reset cherry-picked (copied) commits selection",
NextTab: "Next tab",
PrevTab: "Previous tab",
CantUndoWhileRebasing: "Can't undo while rebasing",
CantRedoWhileRebasing: "Can't redo while rebasing",
MustStashWarning: "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
MustStashTitle: "Must stash",
ConfirmationTitle: "Confirmation panel",
PrevPage: "Previous page",
NextPage: "Next page",
GotoTop: "Scroll to top",
GotoBottom: "Scroll to bottom",
FilteringBy: "Filtering by",
ResetInParentheses: "(Reset)",
OpenFilteringMenu: "View filter-by-path options",
FilterBy: "Filter by",
ExitFilterMode: "Stop filtering by path",
FilterPathOption: "Enter path to filter by",
EnterFileName: "Enter path:",
FilteringMenuTitle: "Filtering",
MustExitFilterModeTitle: "Command not available",
MustExitFilterModePrompt: "Command not available in filtered mode. Exit filtered mode?",
Diff: "Diff",
EnterRefToDiff: "Enter ref to diff",
EnterRefName: "Enter ref:",
ExitDiffMode: "Exit diff mode",
DiffingMenuTitle: "Diffing",
SwapDiff: "Reverse diff direction",
OpenDiffingMenu: "Open diff menu",
IgnoreTracked: "Ignore tracked file",
IgnoreTrackedPrompt: "Are you sure you want to ignore a tracked file?",
ExcludeTracked: "Exclude tracked file",
ExcludeTrackedPrompt: "Are you sure you want to exclude a tracked file?",
ViewResetToUpstreamOptions: "View upstream reset options",
NextScreenMode: "Next screen mode (normal/half/fullscreen)",
PrevScreenMode: "Prev screen mode",
StartSearch: "Search the current view by text",
StartFilter: "Filter the current view by text",
Panel: "Panel",
KeybindingsLegend: "Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b",
RenameBranch: "Rename branch",
SetUnsetUpstream: "Set/Unset upstream",
NewBranchNamePrompt: "Enter new branch name for branch",
RenameBranchWarning: "This branch is tracking a remote. This action will only rename the local branch name, not the name of the remote branch. Continue?",
OpenMenu: "Open menu",
ResetCherryPick: "Reset cherry-picked (copied) commits selection",
NextTab: "Next tab",
PrevTab: "Previous tab",
CantUndoWhileRebasing: "Can't undo while rebasing",
CantRedoWhileRebasing: "Can't redo while rebasing",
MustStashWarning: "Pulling a patch out into the index requires stashing and unstashing your changes. If something goes wrong, you'll be able to access your files from the stash. Continue?",
MustStashTitle: "Must stash",
ConfirmationTitle: "Confirmation panel",
PrevPage: "Previous page",
NextPage: "Next page",
GotoTop: "Scroll to top",
GotoBottom: "Scroll to bottom",
FilteringBy: "Filtering by",
ResetInParentheses: "(Reset)",
OpenFilteringMenu: "View filter-by-path options",
FilterBy: "Filter by",
ExitFilterMode: "Stop filtering by path",
FilterPathOption: "Enter path to filter by",
EnterFileName: "Enter path:",
FilteringMenuTitle: "Filtering",
MustExitFilterModeTitle: "Command not available",
MustExitFilterModePrompt: "Command not available in filter-by-path mode. Exit filter-by-path mode?",
Diff: "Diff",
EnterRefToDiff: "Enter ref to diff",
EnterRefName: "Enter ref:",
ExitDiffMode: "Exit diff mode",
DiffingMenuTitle: "Diffing",
SwapDiff: "Reverse diff direction",
OpenDiffingMenu: "Open diff menu",
// the actual view is the extras view which I intend to give more tabs in future but for now we'll only mention the command log part
OpenExtrasMenu: "Open command log menu",
ShowingGitDiff: "Showing output for:",
@ -1223,9 +1228,11 @@ func EnglishTranslationSet() TranslationSet {
CopyPatchToClipboard: "Copy patch to clipboard",
NoMatchesFor: "No matches for '%s' %s",
ExitSearchMode: "%s: Exit search mode",
ExitTextFilterMode: "%s: Exit filter mode",
MatchesFor: "matches for '%s' (%d of %d) %s", // lowercase because it's after other text
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
SearchPrefix: "Search: ",
FilterPrefix: "Filter: ",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",

View File

@ -61,7 +61,7 @@ func japaneseTranslationSet() TranslationSet {
FileFilter: "ファイルをフィルタ (ステージ/アンステージ)",
FilterStagedFiles: "ステージされたファイルのみを表示",
FilterUnstagedFiles: "ステージされていないファイルのみを表示",
ResetCommitFilterState: "フィルタをリセット",
ResetFilter: "フィルタをリセット",
// NoChangedFiles: "No changed files",
PullWait: "Pull中...",
PushWait: "Push中...",

View File

@ -60,7 +60,7 @@ func koreanTranslationSet() TranslationSet {
FileFilter: "파일을 필터하기 (Staged/unstaged)",
FilterStagedFiles: "Staged된 파일만 표시",
FilterUnstagedFiles: "Stage되지 않은 파일만 표시",
ResetCommitFilterState: "필터 리셋",
ResetFilter: "필터 리셋",
NoChangedFiles: "변경된 파일이 없습니다.",
PullWait: "업데이트 중...",
PushWait: "푸시 중...",

View File

@ -31,7 +31,7 @@ func polishTranslationSet() TranslationSet {
Scroll: "Przewiń",
FilterStagedFiles: "Pokaż tylko pliki w poczekalni",
FilterUnstagedFiles: "Pokaż tylko pliki poza poczekalnią",
ResetCommitFilterState: "Resetuj filtr commitów",
ResetFilter: "Resetuj filtr commitów",
Checkout: "Przełącz",
NoChangedFiles: "Brak zmienionych plików",
PullWait: "Pobieranie zmian...",

View File

@ -79,7 +79,7 @@ func RussianTranslationSet() TranslationSet {
FileFilter: "Фильтровать файлы (проиндексированные/непроиндексированные)",
FilterStagedFiles: "Показывать только проиндексированные файлы",
FilterUnstagedFiles: "Показывать только непроиндексированные файлы",
ResetCommitFilterState: "Сбросить фильтр",
ResetFilter: "Сбросить фильтр",
NoChangedFiles: "Нет изменённых файлов",
PullWait: "Получение и слияние изменении...",
PushWait: "Отправка изменении...",

View File

@ -112,7 +112,7 @@ func traditionalChineseTranslationSet() TranslationSet {
FileFilter: "篩選檔案 (預存/未預存)",
FilterStagedFiles: "僅顯示預存的檔案",
FilterUnstagedFiles: "僅顯示未預存的檔案",
ResetCommitFilterState: "重設篩選",
ResetFilter: "重設篩選",
NoChangedFiles: "沒有變更的檔案",
PullWait: "拉取...",
PushWait: "推送...",

View File

@ -10,9 +10,9 @@ type IntMatcher struct {
func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("equals '%d'", target),
name: fmt.Sprintf("equals %d", target),
testFn: func(value int) (bool, string) {
return value == target, fmt.Sprintf("Expected '%d' to equal '%d'", value, target)
return value == target, fmt.Sprintf("Expected %d to equal %d", value, target)
},
})
@ -21,9 +21,9 @@ func (self *IntMatcher) EqualsInt(target int) *IntMatcher {
func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("greater than '%d'", target),
name: fmt.Sprintf("greater than %d", target),
testFn: func(value int) (bool, string) {
return value > target, fmt.Sprintf("Expected '%d' to greater than '%d'", value, target)
return value > target, fmt.Sprintf("Expected %d to greater than %d", value, target)
},
})
@ -32,9 +32,9 @@ func (self *IntMatcher) GreaterThan(target int) *IntMatcher {
func (self *IntMatcher) LessThan(target int) *IntMatcher {
self.appendRule(matcherRule[int]{
name: fmt.Sprintf("less than '%d'", target),
name: fmt.Sprintf("less than %d", target),
testFn: func(value int) (bool, string) {
return value < target, fmt.Sprintf("Expected '%d' to less than '%d'", value, target)
return value < target, fmt.Sprintf("Expected %d to less than %d", value, target)
},
})

View File

@ -48,6 +48,18 @@ func (self *MenuDriver) TopLines(matchers ...*TextMatcher) *MenuDriver {
return self
}
func (self *MenuDriver) Filter(text string) *MenuDriver {
self.getViewDriver().FilterOrSearch(text)
return self
}
func (self *MenuDriver) LineCount(matcher *IntMatcher) *MenuDriver {
self.getViewDriver().LineCount(matcher)
return self
}
func (self *MenuDriver) checkNecessaryChecksCompleted() {
if !self.hasCheckedTitle {
self.t.Fail("You must check the title of a menu popup by calling Title() before calling Confirm()/Cancel().")

View File

@ -66,7 +66,7 @@ func (self *ViewDriver) Title(expected *TextMatcher) *ViewDriver {
// If you only care about a subset of lines, use the ContainsLines method instead.
func (self *ViewDriver) Lines(matchers ...*TextMatcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.LineCount(len(matchers))
self.LineCount(EqualsInt(len(matchers)))
return self.assertLines(0, matchers...)
}
@ -470,33 +470,60 @@ func (self *ViewDriver) IsEmpty() *ViewDriver {
return self
}
func (self *ViewDriver) LineCount(expectedCount int) *ViewDriver {
if expectedCount == 0 {
return self.IsEmpty()
}
func (self *ViewDriver) LineCount(matcher *IntMatcher) *ViewDriver {
view := self.getView()
self.t.assertWithRetries(func() (bool, string) {
lines := view.BufferLines()
return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
lineCount := self.getLineCount()
ok, _ := matcher.test(lineCount)
return ok, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %s, got %d", view.Name(), matcher.name(), lineCount)
})
return self
}
func (self *ViewDriver) getLineCount() int {
// can't rely entirely on view.BufferLines because it returns 1 even if there's nothing in the view
if strings.TrimSpace(self.getView().Buffer()) == "" {
return 0
}
view := self.getView()
return len(view.BufferLines())
}
func (self *ViewDriver) IsVisible() *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
lines := view.BufferLines()
// if the view has a single blank line (often the case) we want to treat that as having no lines
if len(lines) == 1 && expectedCount == 1 {
actual := strings.TrimSpace(view.Buffer())
return actual != "", fmt.Sprintf("unexpected number of lines in view '%s'. Expected 1, got 0", view.Name())
}
return len(lines) == expectedCount, fmt.Sprintf("unexpected number of lines in view '%s'. Expected %d, got %d", view.Name(), expectedCount, len(lines))
return self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
})
return self
}
func (self *ViewDriver) IsInvisible() *ViewDriver {
self.t.assertWithRetries(func() (bool, string) {
return !self.getView().Visible, fmt.Sprintf("%s: Expected view to be visible, but it was not", self.context)
})
return self
}
// will filter or search depending on whether the view supports filtering/searching
func (self *ViewDriver) FilterOrSearch(text string) *ViewDriver {
self.IsFocused()
self.Press(self.t.keys.Universal.StartSearch).
Tap(func() {
self.t.ExpectSearch().
Type(text).
Confirm()
self.t.Views().Search().IsVisible().Content(Contains(fmt.Sprintf("matches for '%s'", text)))
})
return self
}
// for when you want to make some assertion unrelated to the current view
// without breaking the method chain
func (self *ViewDriver) Tap(f func()) *ViewDriver {

View File

@ -42,6 +42,7 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
Press(keys.Universal.StartSearch).
Tap(func() {
t.ExpectSearch().
Clear().
Type("o").
Confirm()

View File

@ -0,0 +1,117 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DiscardAllDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Discarding all changes in a directory",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
// typically we would use more bespoke shell methods here, but I struggled to find a way to do that,
// and this is copied over from a legacy integration test which did everything in a big shell script
// so I'm just copying it across.
shell.CreateDir("dir")
// common stuff
shell.RunShellCommand(`echo test > dir/both-deleted.txt`)
shell.RunShellCommand(`git checkout -b conflict && git add dir/both-deleted.txt`)
shell.RunShellCommand(`echo bothmodded > dir/both-modded.txt && git add dir/both-modded.txt`)
shell.RunShellCommand(`echo haha > dir/deleted-them.txt && git add dir/deleted-them.txt`)
shell.RunShellCommand(`echo haha2 > dir/deleted-us.txt && git add dir/deleted-us.txt`)
shell.RunShellCommand(`echo mod > dir/modded.txt && git add dir/modded.txt`)
shell.RunShellCommand(`echo mod > dir/modded-staged.txt && git add dir/modded-staged.txt`)
shell.RunShellCommand(`echo del > dir/deleted.txt && git add dir/deleted.txt`)
shell.RunShellCommand(`echo del > dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
shell.RunShellCommand(`echo change-delete > dir/change-delete.txt && git add dir/change-delete.txt`)
shell.RunShellCommand(`echo delete-change > dir/delete-change.txt && git add dir/delete-change.txt`)
shell.RunShellCommand(`echo double-modded > dir/double-modded.txt && git add dir/double-modded.txt`)
shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed.txt && git add dir/renamed.txt`)
shell.RunShellCommand(`git commit -m one`)
// stuff on other branch
shell.RunShellCommand(`git branch conflict_second && git mv dir/both-deleted.txt dir/added-them-changed-us.txt`)
shell.RunShellCommand(`git commit -m "dir/both-deleted.txt renamed in dir/added-them-changed-us.txt"`)
shell.RunShellCommand(`echo blah > dir/both-added.txt && git add dir/both-added.txt`)
shell.RunShellCommand(`echo mod1 > dir/both-modded.txt && git add dir/both-modded.txt`)
shell.RunShellCommand(`rm dir/deleted-them.txt && git add dir/deleted-them.txt`)
shell.RunShellCommand(`echo modded > dir/deleted-us.txt && git add dir/deleted-us.txt`)
shell.RunShellCommand(`git commit -m "two"`)
// stuff on our branch
shell.RunShellCommand(`git checkout conflict_second`)
shell.RunShellCommand(`git mv dir/both-deleted.txt dir/changed-them-added-us.txt`)
shell.RunShellCommand(`git commit -m "both-deleted.txt renamed in dir/changed-them-added-us.txt"`)
shell.RunShellCommand(`echo mod2 > dir/both-modded.txt && git add dir/both-modded.txt`)
shell.RunShellCommand(`echo blah2 > dir/both-added.txt && git add dir/both-added.txt`)
shell.RunShellCommand(`echo modded > dir/deleted-them.txt && git add dir/deleted-them.txt`)
shell.RunShellCommand(`rm dir/deleted-us.txt && git add dir/deleted-us.txt`)
shell.RunShellCommand(`git commit -m "three"`)
shell.RunShellCommand(`git reset --hard conflict_second`)
shell.RunCommandExpectError([]string{"git", "merge", "conflict"})
shell.RunShellCommand(`echo "new" > dir/new.txt`)
shell.RunShellCommand(`echo "new staged" > dir/new-staged.txt && git add dir/new-staged.txt`)
shell.RunShellCommand(`echo mod2 > dir/modded.txt`)
shell.RunShellCommand(`echo mod2 > dir/modded-staged.txt && git add dir/modded-staged.txt`)
shell.RunShellCommand(`rm dir/deleted.txt`)
shell.RunShellCommand(`rm dir/deleted-staged.txt && git add dir/deleted-staged.txt`)
shell.RunShellCommand(`echo change-delete2 > dir/change-delete.txt && git add dir/change-delete.txt`)
shell.RunShellCommand(`rm dir/change-delete.txt`)
shell.RunShellCommand(`rm dir/delete-change.txt && git add dir/delete-change.txt`)
shell.RunShellCommand(`echo "changed" > dir/delete-change.txt`)
shell.RunShellCommand(`echo "change1" > dir/double-modded.txt && git add dir/double-modded.txt`)
shell.RunShellCommand(`echo "change2" > dir/double-modded.txt`)
shell.RunShellCommand(`echo before > dir/added-changed.txt && git add dir/added-changed.txt`)
shell.RunShellCommand(`echo after > dir/added-changed.txt`)
shell.RunShellCommand(`rm dir/renamed.txt && git add dir/renamed.txt`)
shell.RunShellCommand(`echo "renamed\nhaha" > dir/renamed2.txt && git add dir/renamed2.txt`)
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains("UA").Contains("added-them-changed-us.txt"),
Contains("AA").Contains("both-added.txt"),
Contains("DD").Contains("both-deleted.txt"),
Contains("UU").Contains("both-modded.txt"),
Contains("AU").Contains("changed-them-added-us.txt"),
Contains("UD").Contains("deleted-them.txt"),
Contains("DU").Contains("deleted-us.txt"),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("dir")).
Select(Contains("Discard all changes")).
Confirm()
}).
Tap(func() {
t.Common().ContinueOnConflictsResolved()
}).
Lines(
Contains("dir").IsSelected(),
Contains(" M").Contains("added-changed.txt"),
Contains(" D").Contains("change-delete.txt"),
Contains("??").Contains("delete-change.txt"),
Contains(" D").Contains("deleted.txt"),
Contains(" M").Contains("double-modded.txt"),
Contains(" M").Contains("modded.txt"),
Contains("??").Contains("new.txt"),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("dir")).
Select(Contains("Discard all changes")).
Confirm()
}).
IsEmpty()
},
})

View File

@ -0,0 +1,56 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DiscardUnstagedDirChanges = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Discarding unstaged changes in a directory",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.CreateDir("dir")
shell.CreateFileAndAdd("dir/file-one", "original content\n")
shell.Commit("first commit")
shell.UpdateFileAndAdd("dir/file-one", "original content\nnew content\n")
shell.UpdateFile("dir/file-one", "original content\nnew content\neven newer content\n")
shell.CreateDir("dir/subdir")
shell.CreateFile("dir/subdir/unstaged-file-one", "unstaged file")
shell.CreateFile("dir/unstaged-file-two", "unstaged file")
shell.CreateFile("unstaged-file-three", "unstaged file")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("dir").IsSelected(),
Contains("subdir"),
Contains("??").Contains("unstaged-file-one"),
Contains("MM").Contains("file-one"),
Contains("??").Contains("unstaged-file-two"),
Contains("??").Contains("unstaged-file-three"),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("dir")).
Select(Contains("Discard unstaged changes")).
Confirm()
}).
Lines(
Contains("dir").IsSelected(),
Contains("M ").Contains("file-one"),
// this guy remains untouched because it wasn't inside the 'dir' directory
Contains("??").Contains("unstaged-file-three"),
)
t.FileSystem().FileContent("dir/file-one", Equals("original content\nnew content\n"))
},
})

View File

@ -0,0 +1,41 @@
package file
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var DiscardUnstagedFileChanges = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Discarding unstaged changes in a file",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
},
SetupRepo: func(shell *Shell) {
shell.CreateFileAndAdd("file-one", "original content\n")
shell.Commit("first commit")
shell.UpdateFileAndAdd("file-one", "original content\nnew content\n")
shell.UpdateFile("file-one", "original content\nnew content\neven newer content\n")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains("MM").Contains("file-one").IsSelected(),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("file-one")).
Select(Contains("Discard unstaged changes")).
Confirm()
}).
Lines(
Contains("M ").Contains("file-one").IsSelected(),
)
t.FileSystem().FileContent("file-one", Equals("original content\nnew content\n"))
},
})

View File

@ -0,0 +1,84 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterCommitFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Basic commit file filtering by text",
ExtraCmdArgs: []string{},
Skip: true, // skipping until we have implemented file view filtering
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("folder1")
shell.CreateFileAndAdd("folder1/apple-grape", "apple-grape")
shell.CreateFileAndAdd("folder1/apple-orange", "apple-orange")
shell.CreateFileAndAdd("folder1/grape-orange", "grape-orange")
shell.Commit("first commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains(`first commit`).IsSelected(),
).
Press(keys.Universal.Confirm)
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`folder1`).IsSelected(),
Contains(`apple-grape`),
Contains(`apple-orange`),
Contains(`grape-orange`),
).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
Contains(`folder1/grape-orange`),
).
FilterOrSearch("apple").
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
).
Press(keys.Files.ToggleTreeView).
// filter still applies when we toggle tree view
Lines(
Contains(`folder1`),
Contains(`apple-grape`).IsSelected(),
Contains(`apple-orange`),
).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
).
NavigateToLine(Contains(`folder1/apple-orange`)).
Press(keys.Universal.Return).
Lines(
Contains(`folder1/apple-grape`),
// selection is retained after escaping filter mode
Contains(`folder1/apple-orange`).IsSelected(),
Contains(`folder1/grape-orange`),
).
Tap(func() {
t.Views().Search().IsInvisible()
}).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1`),
Contains(`apple-grape`),
Contains(`apple-orange`).IsSelected(),
Contains(`grape-orange`),
).
FilterOrSearch("folder1/grape").
Lines(
// first item is always selected after filtering
Contains(`folder1`).IsSelected(),
Contains(`grape-orange`),
)
},
})

View File

@ -0,0 +1,76 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterFiles = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Basic file filtering by text",
ExtraCmdArgs: []string{},
Skip: true, // Skipping until we have implemented file view filtering
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateDir("folder1")
shell.CreateFile("folder1/apple-grape", "apple-grape")
shell.CreateFile("folder1/apple-orange", "apple-orange")
shell.CreateFile("folder1/grape-orange", "grape-orange")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
Focus().
Lines(
Contains(`folder1`).IsSelected(),
Contains(`apple-grape`),
Contains(`apple-orange`),
Contains(`grape-orange`),
).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
Contains(`folder1/grape-orange`),
).
FilterOrSearch("apple").
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
).
Press(keys.Files.ToggleTreeView).
// filter still applies when we toggle tree view
Lines(
Contains(`folder1`),
Contains(`apple-grape`).IsSelected(),
Contains(`apple-orange`),
).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1/apple-grape`).IsSelected(),
Contains(`folder1/apple-orange`),
).
NavigateToLine(Contains(`folder1/apple-orange`)).
Press(keys.Universal.Return).
Lines(
Contains(`folder1/apple-grape`),
// selection is retained after escaping filter mode
Contains(`folder1/apple-orange`).IsSelected(),
Contains(`folder1/grape-orange`),
).
Tap(func() {
t.Views().Search().IsInvisible()
}).
Press(keys.Files.ToggleTreeView).
Lines(
Contains(`folder1`),
Contains(`apple-grape`),
Contains(`apple-orange`).IsSelected(),
Contains(`grape-orange`),
).
FilterOrSearch("folder1/grape").
Lines(
// first item is always selected after filtering
Contains(`folder1`).IsSelected(),
Contains(`grape-orange`),
)
},
})

View File

@ -0,0 +1,48 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterMenu = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filtering the keybindings menu",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.CreateFile("myfile", "myfile")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Files().
IsFocused().
Lines(
Contains(`??`).Contains(`myfile`).IsSelected(),
).
Press(keys.Universal.OptionMenu).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Keybindings")).
Filter("Toggle staged").
Lines(
// menu has filtered down to the one item that matches the filter
Contains(`Toggle staged`).IsSelected(),
).
Confirm()
})
t.Views().Files().
IsFocused().
Lines(
// file has been staged
Contains(`A `).Contains(`myfile`).IsSelected(),
).
// Upon opening the menu again, the filter should have been reset
Press(keys.Universal.OptionMenu).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Keybindings")).
LineCount(GreaterThan(1))
})
},
})

View File

@ -0,0 +1,59 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var FilterRemoteBranches = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filtering remote branches",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("branch-apple")
shell.EmptyCommit("commit-one")
shell.NewBranch("branch-grape")
shell.NewBranch("branch-orange")
shell.CloneIntoRemote("origin")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Remotes().
Focus().
Lines(
Contains(`origin`).IsSelected(),
).
PressEnter()
t.Views().RemoteBranches().
IsFocused().
Lines(
Contains(`branch-apple`).IsSelected(),
Contains(`branch-grape`),
Contains(`branch-orange`),
).
FilterOrSearch("grape").
Lines(
Contains(`branch-grape`).IsSelected(),
).
// cancel the filter
PressEscape().
Tap(func() {
t.Views().Search().IsInvisible()
}).
Lines(
Contains(`branch-apple`),
Contains(`branch-grape`).IsSelected(),
Contains(`branch-orange`),
).
// return to remotes view
PressEscape()
t.Views().Remotes().
IsFocused().
Lines(
Contains(`origin`).IsSelected(),
)
},
})

View File

@ -0,0 +1,151 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var NestedFilter = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filter in the several nested panels and verify the filters are preserved as you escape back to the surface",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// need to create some branches, each with their own commits
shell.NewBranch("branch-gold")
shell.CreateFileAndAdd("apple", "apple")
shell.CreateFileAndAdd("orange", "orange")
shell.CreateFileAndAdd("grape", "grape")
shell.Commit("commit-knife")
shell.NewBranch("branch-silver")
shell.UpdateFileAndAdd("apple", "apple-2")
shell.UpdateFileAndAdd("orange", "orange-2")
shell.UpdateFileAndAdd("grape", "grape-2")
shell.Commit("commit-spoon")
shell.NewBranch("branch-bronze")
shell.UpdateFileAndAdd("apple", "apple-3")
shell.UpdateFileAndAdd("orange", "orange-3")
shell.UpdateFileAndAdd("grape", "grape-3")
shell.Commit("commit-fork")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains(`branch-bronze`).IsSelected(),
Contains(`branch-silver`),
Contains(`branch-gold`),
).
FilterOrSearch("sil").
Lines(
Contains(`branch-silver`).IsSelected(),
).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains(`commit-spoon`).IsSelected(),
Contains(`commit-knife`),
).
FilterOrSearch("knife").
Lines(
// sub-commits view searches, it doesn't filter, so we haven't filtered down the list
Contains(`commit-spoon`),
Contains(`commit-knife`).IsSelected(),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`apple`).IsSelected(),
Contains(`grape`),
Contains(`orange`),
).
FilterOrSearch("grape").
Lines(
Contains(`apple`),
Contains(`grape`).IsSelected(),
Contains(`orange`),
).
PressEnter()
t.Views().PatchBuilding().
IsFocused().
FilterOrSearch("newline").
SelectedLine(Contains("No newline at end of file")).
PressEscape(). // cancel search
Tap(func() {
t.Views().Search().IsInvisible()
}).
// escape to commit-files view
PressEscape()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`apple`),
Contains(`grape`).IsSelected(),
Contains(`orange`),
).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'grape'"))
}).
// cancel search
PressEscape().
Tap(func() {
t.Views().Search().IsInvisible()
}).
Lines(
Contains(`apple`),
Contains(`grape`).IsSelected(),
Contains(`orange`),
).
// escape to sub-commits view
PressEscape()
t.Views().SubCommits().
IsFocused().
Lines(
Contains(`commit-spoon`),
Contains(`commit-knife`).IsSelected(),
).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'knife'"))
}).
// cancel search
PressEscape().
Tap(func() {
t.Views().Search().IsInvisible()
}).
Lines(
Contains(`commit-spoon`),
// still selected
Contains(`commit-knife`).IsSelected(),
).
// escape to branches view
PressEscape()
t.Views().Branches().
IsFocused().
Lines(
Contains(`branch-silver`).IsSelected(),
).
Tap(func() {
t.Views().Search().IsVisible().Content(Contains("matches for 'sil'"))
}).
// cancel search
PressEscape().
Tap(func() {
t.Views().Search().IsInvisible()
}).
Lines(
Contains(`branch-bronze`),
Contains(`branch-silver`).IsSelected(),
Contains(`branch-gold`),
)
},
})

View File

@ -0,0 +1,106 @@
package filter_and_search
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This one requires some explanation: the sub-commits and diff-file contexts are
// 'transient' in that they are spawned inside a window when you need them, but
// can be relocated elsewhere if you need them somewhere else. So for example if
// I hit enter on a branch I'll see the sub-commits view, but if I then navigate
// to the reflog context and hit enter on a reflog, the sub-commits view is moved
// to the reflog window. This is because we re-use the same view (it's a limitation
// that would be nice to remove in the future).
// Nonetheless, we need to ensure that upon moving the view, the filter is cancelled.
var NestedFilterTransient = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Filter in a transient panel (sub-commits and diff-files) and ensure filter is cancelled when the panel is moved",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
// need to create some branches, each with their own commits
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("file-one", "file-one")
shell.CreateFileAndAdd("file-two", "file-two")
shell.Commit("commit-one")
shell.EmptyCommit("commit-two")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains(`mybranch`).IsSelected(),
).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains(`commit-two`).IsSelected(),
Contains(`commit-one`),
).
FilterOrSearch("one").
Lines(
Contains(`commit-two`),
Contains(`commit-one`).IsSelected(),
)
t.Views().ReflogCommits().
Focus().
SelectedLine(Contains("commit: commit-two")).
PressEnter()
t.Views().SubCommits().
IsFocused().
// the search on the sub-commits context has been cancelled
Lines(
Contains(`commit-two`).IsSelected(),
Contains(`commit-one`),
).
Tap(func() {
t.Views().Search().IsInvisible()
}).
NavigateToLine(Contains("commit-one")).
PressEnter()
// Now let's test the commit files context
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`file-one`).IsSelected(),
Contains(`file-two`),
).
FilterOrSearch("two").
Lines(
Contains(`file-one`),
Contains(`file-two`).IsSelected(),
)
t.Views().Branches().
Focus().
SelectedLine(Contains("mybranch")).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains(`commit-two`).IsSelected(),
Contains(`commit-one`),
).
NavigateToLine(Contains("commit-one")).
PressEnter()
t.Views().CommitFiles().
IsFocused().
// the search on the commit-files context has been cancelled
Lines(
Contains(`file-one`).IsSelected(),
Contains(`file-two`),
).
Tap(func() {
t.Views().Search().IsInvisible()
})
},
})

View File

@ -13,6 +13,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/integration/tests/custom_commands"
"github.com/jesseduffield/lazygit/pkg/integration/tests/diff"
"github.com/jesseduffield/lazygit/pkg/integration/tests/file"
"github.com/jesseduffield/lazygit/pkg/integration/tests/filter_and_search"
"github.com/jesseduffield/lazygit/pkg/integration/tests/filter_by_path"
"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
"github.com/jesseduffield/lazygit/pkg/integration/tests/misc"
@ -87,10 +88,19 @@ var tests = []*components.IntegrationTest{
diff.DiffCommits,
diff.IgnoreWhitespace,
file.DirWithUntrackedFile,
file.DiscardAllDirChanges,
file.DiscardChanges,
file.DiscardStagedChanges,
file.DiscardUnstagedDirChanges,
file.DiscardUnstagedFileChanges,
file.Gitignore,
file.RememberCommitMessageAfterFail,
filter_and_search.FilterCommitFiles,
filter_and_search.FilterFiles,
filter_and_search.FilterMenu,
filter_and_search.FilterRemoteBranches,
filter_and_search.NestedFilter,
filter_and_search.NestedFilterTransient,
filter_by_path.CliArg,
filter_by_path.SelectFile,
filter_by_path.TypeFile,

View File

@ -19,6 +19,9 @@ var (
// InactiveBorderColor is the border color of the inactive active frames
InactiveBorderColor gocui.Attribute
// FilteredActiveBorderColor is the border color of the active frame, when it's being searched/filtered
SearchingActiveBorderColor gocui.Attribute
// GocuiSelectedLineBgColor is the background color for the selected line in gocui
GocuiSelectedLineBgColor gocui.Attribute
@ -44,6 +47,7 @@ var (
func UpdateTheme(themeConfig config.ThemeConfig) {
ActiveBorderColor = GetGocuiStyle(themeConfig.ActiveBorderColor)
InactiveBorderColor = GetGocuiStyle(themeConfig.InactiveBorderColor)
SearchingActiveBorderColor = GetGocuiStyle(themeConfig.SearchingActiveBorderColor)
SelectedLineBgColor = GetTextStyle(themeConfig.SelectedLineBgColor, true)
SelectedRangeBgColor = GetTextStyle(themeConfig.SelectedRangeBgColor, true)

View File

@ -1,21 +0,0 @@
package utils
import (
"sort"
"github.com/jesseduffield/generics/slices"
"github.com/sahilm/fuzzy"
)
func FuzzySearch(needle string, haystack []string) []string {
if needle == "" {
return []string{}
}
matches := fuzzy.Find(needle, haystack)
sort.Sort(matches)
return slices.Map(matches, func(match fuzzy.Match) string {
return match.Str
})
}

View File

@ -1,53 +0,0 @@
package utils
import (
"testing"
"github.com/stretchr/testify/assert"
)
// TestFuzzySearch is a function.
func TestFuzzySearch(t *testing.T) {
type scenario struct {
needle string
haystack []string
expected []string
}
scenarios := []scenario{
{
needle: "",
haystack: []string{"test"},
expected: []string{},
},
{
needle: "test",
haystack: []string{"test"},
expected: []string{"test"},
},
{
needle: "o",
haystack: []string{"a", "o", "e"},
expected: []string{"o"},
},
{
needle: "mybranch",
haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
expected: []string{"mybranch", "my_branch", "this is my branch"},
},
{
needle: "test",
haystack: []string{"not a good match", "this 'test' is a good match", "test"},
expected: []string{"test", "this 'test' is a good match"},
},
{
needle: "test",
haystack: []string{"Test"},
expected: []string{"Test"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
}
}

48
pkg/utils/search.go Normal file
View File

@ -0,0 +1,48 @@
package utils
import (
"sort"
"strings"
"github.com/jesseduffield/generics/slices"
"github.com/sahilm/fuzzy"
)
func FuzzySearch(needle string, haystack []string) []string {
if needle == "" {
return []string{}
}
matches := fuzzy.Find(needle, haystack)
sort.Sort(matches)
return slices.Map(matches, func(match fuzzy.Match) string {
return match.Str
})
}
func CaseAwareContains(haystack, needle string) bool {
// if needle contains an uppercase letter, we'll do a case sensitive search
if ContainsUppercase(needle) {
return strings.Contains(haystack, needle)
}
return CaseInsensitiveContains(haystack, needle)
}
func ContainsUppercase(s string) bool {
for _, r := range s {
if r >= 'A' && r <= 'Z' {
return true
}
}
return false
}
func CaseInsensitiveContains(haystack, needle string) bool {
return strings.Contains(
strings.ToLower(haystack),
strings.ToLower(needle),
)
}

80
pkg/utils/search_test.go Normal file
View File

@ -0,0 +1,80 @@
package utils
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
// TestFuzzySearch is a function.
func TestFuzzySearch(t *testing.T) {
type scenario struct {
needle string
haystack []string
expected []string
}
scenarios := []scenario{
{
needle: "",
haystack: []string{"test"},
expected: []string{},
},
{
needle: "test",
haystack: []string{"test"},
expected: []string{"test"},
},
{
needle: "o",
haystack: []string{"a", "o", "e"},
expected: []string{"o"},
},
{
needle: "mybranch",
haystack: []string{"my_branch", "mybranch", "branch", "this is my branch"},
expected: []string{"mybranch", "my_branch", "this is my branch"},
},
{
needle: "test",
haystack: []string{"not a good match", "this 'test' is a good match", "test"},
expected: []string{"test", "this 'test' is a good match"},
},
{
needle: "test",
haystack: []string{"Test"},
expected: []string{"Test"},
},
}
for _, s := range scenarios {
assert.EqualValues(t, s.expected, FuzzySearch(s.needle, s.haystack))
}
}
func TestCaseInsensitiveContains(t *testing.T) {
testCases := []struct {
haystack string
needle string
expected bool
}{
{"Hello, World!", "world", true}, // Case-insensitive match
{"Hello, World!", "WORLD", true}, // Case-insensitive match
{"Hello, World!", "orl", true}, // Case-insensitive match
{"Hello, World!", "o, W", true}, // Case-insensitive match
{"Hello, World!", "hello", true}, // Case-insensitive match
{"Hello, World!", "Foo", false}, // No match
{"Hello, World!", "Hello, World!!", false}, // No match
{"Hello, World!", "", true}, // Empty needle matches
{"", "Hello", false}, // Empty haystack doesn't match
{"", "", true}, // Empty strings match
{"", " ", false}, // Empty haystack, non-empty needle
{" ", "", true}, // Non-empty haystack, empty needle
}
for i, testCase := range testCases {
result := CaseInsensitiveContains(testCase.haystack, testCase.needle)
assert.Equal(t, testCase.expected, result, fmt.Sprintf("Test case %d failed. Expected '%v', got '%v' for '%s' in '%s'", i, testCase.expected, result, testCase.needle, testCase.haystack))
}
}

View File

@ -113,3 +113,14 @@ func MoveElement[T any](slice []T, from int, to int) []T {
return newSlice
}
func ValuesAtIndices[T any](slice []T, indices []int) []T {
result := make([]T, len(indices))
for i, index := range indices {
// gracefully handling the situation where the index is out of bounds
if index < len(slice) {
result[i] = slice[index]
}
}
return result
}

View File

@ -207,6 +207,10 @@ func (v *View) gotoPreviousMatch() error {
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectCurrentSearchResult() error {
return v.SelectSearchResult(v.searcher.currentSearchIndex)
}
func (v *View) SelectSearchResult(index int) error {
itemCount := len(v.searcher.searchPositions)
if itemCount == 0 {

View File

@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh
// +build 386 amd64 amd64p32 alpha arm arm64 loong64 mipsle mips64le mips64p32le nios2 ppc64le riscv riscv64 sh
//go:build 386 || amd64 || amd64p32 || alpha || arm || arm64 || loong64 || mipsle || mips64le || mips64p32le || nios2 || ppc64le || riscv || riscv64 || sh || wasm
// +build 386 amd64 amd64p32 alpha arm arm64 loong64 mipsle mips64le mips64p32le nios2 ppc64le riscv riscv64 sh wasm
package cpu

View File

@ -50,7 +50,7 @@ if [[ "$GOOS" = "linux" ]]; then
# Use the Docker-based build system
# Files generated through docker (use $cmd so you can Ctl-C the build or run)
$cmd docker build --tag generate:$GOOS $GOOS
$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && /bin/pwd):/build generate:$GOOS
$cmd docker run --interactive --tty --volume $(cd -- "$(dirname -- "$0")/.." && pwd):/build generate:$GOOS
exit
fi

View File

@ -741,7 +741,8 @@ main(void)
e = errors[i].num;
if(i > 0 && errors[i-1].num == e)
continue;
strcpy(buf, strerror(e));
strncpy(buf, strerror(e), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
buf[0] += a - A;
@ -760,7 +761,8 @@ main(void)
e = signals[i].num;
if(i > 0 && signals[i-1].num == e)
continue;
strcpy(buf, strsignal(e));
strncpy(buf, strsignal(e), sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
// lowercase first letter: Bad -> bad, but STREAM -> STREAM.
if(A <= buf[0] && buf[0] <= Z && a <= buf[1] && buf[1] <= z)
buf[0] += a - A;

View File

@ -1699,12 +1699,23 @@ func PtracePokeUser(pid int, addr uintptr, data []byte) (count int, err error) {
return ptracePoke(PTRACE_POKEUSR, PTRACE_PEEKUSR, pid, addr, data)
}
// elfNT_PRSTATUS is a copy of the debug/elf.NT_PRSTATUS constant so
// x/sys/unix doesn't need to depend on debug/elf and thus
// compress/zlib, debug/dwarf, and other packages.
const elfNT_PRSTATUS = 1
func PtraceGetRegs(pid int, regsout *PtraceRegs) (err error) {
return ptracePtr(PTRACE_GETREGS, pid, 0, unsafe.Pointer(regsout))
var iov Iovec
iov.Base = (*byte)(unsafe.Pointer(regsout))
iov.SetLen(int(unsafe.Sizeof(*regsout)))
return ptracePtr(PTRACE_GETREGSET, pid, uintptr(elfNT_PRSTATUS), unsafe.Pointer(&iov))
}
func PtraceSetRegs(pid int, regs *PtraceRegs) (err error) {
return ptracePtr(PTRACE_SETREGS, pid, 0, unsafe.Pointer(regs))
var iov Iovec
iov.Base = (*byte)(unsafe.Pointer(regs))
iov.SetLen(int(unsafe.Sizeof(*regs)))
return ptracePtr(PTRACE_SETREGSET, pid, uintptr(elfNT_PRSTATUS), unsafe.Pointer(&iov))
}
func PtraceSetOptions(pid int, options int) (err error) {
@ -2420,6 +2431,21 @@ func PthreadSigmask(how int, set, oldset *Sigset_t) error {
return rtSigprocmask(how, set, oldset, _C__NSIG/8)
}
//sysnb getresuid(ruid *_C_int, euid *_C_int, suid *_C_int)
//sysnb getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int)
func Getresuid() (ruid, euid, suid int) {
var r, e, s _C_int
getresuid(&r, &e, &s)
return int(r), int(e), int(s)
}
func Getresgid() (rgid, egid, sgid int) {
var r, e, s _C_int
getresgid(&r, &e, &s)
return int(r), int(e), int(s)
}
/*
* Unimplemented
*/

View File

@ -151,6 +151,21 @@ func Getfsstat(buf []Statfs_t, flags int) (n int, err error) {
return
}
//sysnb getresuid(ruid *_C_int, euid *_C_int, suid *_C_int)
//sysnb getresgid(rgid *_C_int, egid *_C_int, sgid *_C_int)
func Getresuid() (ruid, euid, suid int) {
var r, e, s _C_int
getresuid(&r, &e, &s)
return int(r), int(e), int(s)
}
func Getresgid() (rgid, egid, sgid int) {
var r, e, s _C_int
getresgid(&r, &e, &s)
return int(r), int(e), int(s)
}
//sys ioctl(fd int, req uint, arg uintptr) (err error)
//sys ioctlPtr(fd int, req uint, arg unsafe.Pointer) (err error) = SYS_IOCTL
@ -338,8 +353,6 @@ func Uname(uname *Utsname) error {
// getgid
// getitimer
// getlogin
// getresgid
// getresuid
// getthrid
// ktrace
// lfs_bmapv

Some files were not shown because too many files have changed in this diff Show More