mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-28 18:26:03 -04:00
Compare commits
52
Commits
@@ -135,7 +135,7 @@ Lazygit is not my fulltime job but it is a hefty part time job so if you want to
|
||||
|
||||
### Stage individual lines
|
||||
|
||||
Press space on the selected line to stage it, or press `v` to start selecting a range of lines. You can also press `a` to select the entirety of the current hunk.
|
||||
Press `<enter>` on a changed file to focus its diff in the main view. Press `<space>` on the selected line to stage it, or press `v` to start selecting a range of lines. You can also press `a` to switch to hunk selection mode. When a file has both staged and unstaged changes, use `<tab>` to move between the two diff panes; the same actions stage or unstage the selection depending on the pane.
|
||||
|
||||

|
||||
|
||||
@@ -195,7 +195,7 @@ You can create worktrees to have multiple branches going at once without the nee
|
||||
|
||||
You can build a custom patch from an old commit and then remove the patch from the commit, split out a new commit, apply the patch in reverse to the index, and more.
|
||||
|
||||
In this example we have a redundant comment that we want to remove from an old commit. We hit `<enter>` on the commit to view its files, then `<enter>` on a file to focus the patch, then `<space>` to add the comment line to our custom patch, and then `ctrl+p` to view the custom patch options; selecting to remove the patch from the current commit.
|
||||
In this example we have a redundant comment that we want to remove from an old commit. We hit `<enter>` on the commit to view its files, then `<enter>` on a file to focus its diff. From there, `<space>` adds the selected comment line to the custom patch and `ctrl+p` opens the custom patch options, where we choose to remove the patch from the original commit.
|
||||
|
||||
Learn more in the [Rebase magic Youtube tutorial](https://youtu.be/4XaToVut_hs).
|
||||
|
||||
|
||||
+10
-9
@@ -72,7 +72,7 @@ gui:
|
||||
# If true, do not show a warning when amending a commit.
|
||||
skipAmendWarning: false
|
||||
|
||||
# If true, do not show a warning when discarding changes in the staging view.
|
||||
# If true, do not show a warning when discarding changes from a focused diff.
|
||||
skipDiscardChangeWarning: false
|
||||
|
||||
# If true, do not show warning when applying/popping the stash
|
||||
@@ -142,14 +142,13 @@ gui:
|
||||
# - 'top': split the window vertically (side panel on top, main view below)
|
||||
enlargedSideViewLocation: left
|
||||
|
||||
# If true, wrap lines in the staging view to the width of the view. This makes
|
||||
# it much easier to work with diffs that have long lines, e.g. paragraphs of
|
||||
# If true, wrap lines in focused diffs to the width of the view. This makes it
|
||||
# much easier to work with diffs that have long lines, e.g. paragraphs of
|
||||
# markdown text.
|
||||
wrapLinesInStagingView: true
|
||||
wrapLinesInDiffView: true
|
||||
|
||||
# If true, hunk selection mode will be enabled by default when entering the
|
||||
# staging view.
|
||||
useHunkModeInStagingView: true
|
||||
# If true, hunk selection mode will be enabled by default when focusing a diff.
|
||||
useHunkModeInDiffView: true
|
||||
|
||||
# One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko'
|
||||
# | 'ru' | 'pt'
|
||||
@@ -937,7 +936,9 @@ It is used, for example, when pasting a commit message into the commit message p
|
||||
|
||||
## Configuring File Editing
|
||||
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o` acts as if the file was double-clicked in the Finder/Explorer, so it also works for non-text files, whereas `e` opens the file in an editor. `e` can also jump to the right line in the file if you invoke it from the staging panel, for example.
|
||||
There are two commands for opening files, `o` for "open" and `e` for "edit". `o` acts as if the file was double-clicked in the Finder/Explorer, so it also works for non-text files, whereas `e` opens the file in an editor. `e` can also jump to the right line in the file when you invoke it from a focused diff.
|
||||
|
||||
You can also open a line in your editor with the mouse: alt-click or shift-click it. Both modifiers do the same thing, because some terminals only support one or the other. The click leaves the focus and the selection where they are, so it works while you are reading a diff from another panel, or while a popup is open.
|
||||
|
||||
To tell lazygit which editor to use for the `e` command, the easiest way to do that is to provide an editPreset config, e.g.
|
||||
|
||||
@@ -1000,7 +1001,7 @@ When the selected line gets close to the bottom of the window and you hit down-a
|
||||
|
||||
That's the behavior when `gui.scrollOffBehavior` is set to "margin" (the default). If you set `gui.scrollOffBehavior` to "jump", then upon reaching the last line of a view and hitting down-arrow the view will scroll by half a page so that the selection ends up in the middle of the view. This may feel a little jarring because the cursor jumps around when continuously moving down, but it has the advantage that the view doesn't scroll as often.
|
||||
|
||||
This setting applies both to all list views (e.g. commits and branches etc), and to the staging view.
|
||||
This setting applies both to all list views (e.g. commits and branches etc), and to focused diffs.
|
||||
|
||||
## Filtering
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
* `pkg/gui/keybindings`: Contains code for mapping between keybindings and their labels
|
||||
* `pkg/gui/mergeconflicts`: Contains code relating to the handling of merge conflicts
|
||||
* `pkg/gui/modes`: Contains code relating to the state of different modes e.g. cherry picking mode, rebase mode.
|
||||
* `pkg/gui/patch_exploring`: Contains code relating to the state of patch-oriented views like the staging view.
|
||||
* `pkg/gui/popup`: Contains code that lets you easily raise popups
|
||||
* `pkg/gui/presentation`: Contains presentation code i.e. code concerned with rendering content inside views
|
||||
* `pkg/gui/services/custom_commands`: Contains code related to user-defined custom commands.
|
||||
|
||||
@@ -65,7 +65,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Toggle file included in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter file / Toggle directory collapsed | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Toggle file tree view | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
@@ -149,7 +149,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | View stash options | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Stage all | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Stage lines / Collapse directory | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` <enter> `` | Focus file diff / Collapse directory | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Discard | View options for discarding changes to the selected file. |
|
||||
| `` g `` | View upstream reset options | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
@@ -222,18 +222,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Scroll down | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Scroll up | |
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <space> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
@@ -241,44 +242,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (patch building)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` o `` | Open file | Open file in default application. |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <space> `` | Toggle lines in patch | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Exit custom patch builder | |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Main panel (staging)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <space> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Open file | Open file in default application. |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <esc> `` | Return to files panel | |
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Commit changes using git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Search the current view by text | |
|
||||
|
||||
## Menu
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -342,18 +305,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Switch view | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Edit file | Open file in external editor. |
|
||||
| `` <space> `` | Stage | Toggle selection staged / unstaged. |
|
||||
| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Go to previous hunk | |
|
||||
| `` <right>, l `` | Go to next hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Commit staged changes. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
|
||||
@@ -114,7 +114,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` <space> `` | パッチに含めるファイルを切り替え | ファイルがカスタムパッチに含まれるかどうかを切り替えます。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` a `` | すべてのファイルを切り替え | コミットのすべてのファイルをカスタムパッチに追加/削除します。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` <enter> `` | ファイルに入る / ディレクトリの折りたたみを切り替える | ファイルが選択されている場合、そのファイルに入ってカスタムパッチに個々の行を追加/削除できます。ディレクトリが選択されている場合、ディレクトリを切り替えます。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
|
||||
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
|
||||
@@ -191,18 +191,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
@@ -259,44 +260,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
| `` / `` | 現在のビューをテキストでフィルタリング | |
|
||||
|
||||
## メインパネル(ステージング)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <esc> `` | ファイルパネルに戻る | |
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
| `` C `` | Gitエディタを使用して変更をコミット | |
|
||||
| `` <ctrl+f> `` | フィックスアップのベースコミットを検索 | 現在の変更が基づいているコミットを見つけて、コミットの修正/フィックスアップを行います。これにより、ブランチのコミットを一つずつ確認して、どのコミットを修正/フィックスアップすべきかを調べる手間が省けます。詳細はドキュメントを参照: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メインパネル(パッチ作成)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | パッチ内の行を切り替え | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | カスタムパッチビルダーを終了 | |
|
||||
| `` / `` | 現在のビューをテキストで検索 | |
|
||||
|
||||
## メインパネル(マージ中)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -319,18 +282,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 下にスクロール | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 上にスクロール | |
|
||||
| `` <tab> `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 範囲選択を切り替え | |
|
||||
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
|
||||
| `` <space> `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 |
|
||||
| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 |
|
||||
| `` E `` | ハンクを編集 | 選択したハンクを外部エディタで編集します。 |
|
||||
| `` <ctrl+o> `` | 選択したテキストをクリップボードにコピー | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 前のハンクに移動 | |
|
||||
| `` <right>, l `` | 次のハンクに移動 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | サイドパネルに戻る | |
|
||||
| `` c `` | コミット | ステージされた変更をコミットします。 |
|
||||
| `` w `` | pre-commitフックなしで変更をコミット | |
|
||||
|
||||
@@ -83,18 +83,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
@@ -176,18 +177,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 아래로 스크롤 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 위로 스크롤 | |
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
@@ -195,44 +197,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Patch Building)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` o `` | 파일 닫기 | Open file in default application. |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <space> `` | Line(s)을 패치에 추가/삭제 | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Exit custom patch builder | |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 메인 패널 (Staging)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 이전 hunk를 선택 | |
|
||||
| `` <right>, l `` | 다음 hunk를 선택 | |
|
||||
| `` v `` | 드래그 선택 전환 | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | 선택한 텍스트를 클립보드에 복사 | |
|
||||
| `` <space> `` | Staged 전환 | 선택한 행을 staged / unstaged |
|
||||
| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | 파일 닫기 | Open file in default application. |
|
||||
| `` e `` | 파일 편집 | Open file in external editor. |
|
||||
| `` <esc> `` | 파일 목록으로 돌아가기 | |
|
||||
| `` <tab> `` | 패널 전환 | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` c `` | 커밋 변경내용 | 스테이징된 변경 사항 커밋. |
|
||||
| `` w `` | Commit changes without pre-commit hook | |
|
||||
| `` C `` | Git 편집기를 사용하여 변경 내용을 커밋합니다. | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 검색 시작 | |
|
||||
|
||||
## 브랜치
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -375,7 +339,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Toggle file included in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Toggle all files included in patch | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter file to add selected lines to the patch (or toggle directory collapsed) | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 파일 트리뷰로 전환 | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
@@ -425,7 +389,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Stash 옵션 보기 | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | 모든 변경을 Staged/unstaged으로 전환 | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Stage individual hunks/lines for file, or collapse/expand for directory | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` <enter> `` | Stage individual hunks/lines for file, or collapse/expand for directory | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | View 'discard changes' options | View options for discarding changes to the selected file. |
|
||||
| `` g `` | View upstream reset options | |
|
||||
| `` D `` | 초기화 | View reset options for working tree (e.g. nuking the working tree). |
|
||||
|
||||
@@ -72,7 +72,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Bekijk stash opties | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Toggle staged alle | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` <enter> `` | Stage individuele hunks/lijnen | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Bekijk 'veranderingen ongedaan maken' opties | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Bekijk upstream reset opties | |
|
||||
| `` D `` | Resetten | View reset options for working tree (e.g. nuking the working tree). |
|
||||
@@ -144,7 +144,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open externe diff applicatie (git difftool) | |
|
||||
| `` <space> `` | Toggle bestand inbegrepen in patch | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Toggle all files | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Enter bestand om geselecteerde regels toe te voegen aan de patch | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Toggle bestandsboom weergave | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Vouw alle bestanden uit | Vouw alle mappen in de bestandsstructuur uit |
|
||||
@@ -230,18 +230,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Scroll omlaag | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Scroll omhoog | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
@@ -249,22 +250,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Patch bouwen
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Reflog
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -320,18 +305,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
@@ -339,28 +325,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Staging
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Selecteer de vorige hunk | |
|
||||
| `` <right>, l `` | Selecteer de volgende hunk | |
|
||||
| `` v `` | Toggle drag selecteer | |
|
||||
| `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. |
|
||||
| `` <ctrl+o> `` | Copy selected text to clipboard | |
|
||||
| `` <space> `` | Toggle staged | Toggle lijnen staged / unstaged |
|
||||
| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Open bestand | Open bestand in standaardapplicatie. |
|
||||
| `` e `` | Verander bestand | Open bestand in externe editor. |
|
||||
| `` <esc> `` | Ga terug naar het bestanden paneel | |
|
||||
| `` <tab> `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Edit hunk | Edit selected hunk in external editor. |
|
||||
| `` c `` | Commit veranderingen | Commit gestagede wijzigingen. |
|
||||
| `` w `` | Commit veranderingen zonder pre-commit hook | |
|
||||
| `` C `` | Commit veranderingen met de git editor | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Vind de commit waar je huidige wijzigingen bovenop zijn gebouwd met als doel die commit te amenden/fixen. Hierdoor hoef je dit niet met de hand te doen. Zie: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Start met zoeken | |
|
||||
|
||||
## Stash
|
||||
|
||||
| Key | Action | Info |
|
||||
|
||||
@@ -98,18 +98,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
@@ -147,22 +148,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <enter> `` | Pokaż commity | |
|
||||
| `` / `` | Filtruj bieżący widok po tekście | |
|
||||
|
||||
## Główny panel (budowanie łatki)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Przełącz linie w łatce | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Wyjdź z budowniczego niestandardowej łatki | |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Input prompt
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -215,18 +200,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Przewiń w dół | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Przewiń w górę | |
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
@@ -250,28 +236,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Wróć do panelu plików | |
|
||||
|
||||
## Panel główny (zatwierdzanie)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Idź do poprzedniego fragmentu | |
|
||||
| `` <right>, l `` | Idź do następnego fragmentu | |
|
||||
| `` v `` | Przełącz zaznaczenie zakresu | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Kopiuj zaznaczony tekst do schowka | |
|
||||
| `` <space> `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. |
|
||||
| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. |
|
||||
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
|
||||
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
|
||||
| `` <esc> `` | Wróć do panelu plików | |
|
||||
| `` <tab> `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). |
|
||||
| `` E `` | Edytuj fragment | Edytuj wybrany fragment w zewnętrznym edytorze. |
|
||||
| `` c `` | Commit | Zatwierdź zmiany zatwierdzone. |
|
||||
| `` w `` | Zatwierdź zmiany bez hooka pre-commit | |
|
||||
| `` C `` | Zatwierdź zmiany używając edytora git | |
|
||||
| `` <ctrl+f> `` | Znajdź bazowy commit do poprawki | Znajdź commit, na którym opierają się Twoje obecne zmiany, w celu poprawienia/zmiany commita. To pozwala Ci uniknąć przeglądania commitów w Twojej gałęzi jeden po drugim, aby zobaczyć, który commit powinien być poprawiony/zmieniony. Zobacz dokumentację: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Szukaj w bieżącym widoku po tekście | |
|
||||
|
||||
## Panel potwierdzenia
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -326,7 +290,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Otwórz zewnętrzne narzędzie różnic (git difftool) | |
|
||||
| `` <space> `` | Przełącz plik włączony w łatkę | Przełącz, czy plik jest włączony w niestandardową łatkę. Zobacz https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Przełącz wszystkie pliki | Dodaj/usuń wszystkie pliki commita do niestandardowej łatki. Zobacz https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Wejdź do pliku / Przełącz zwiń katalog | Jeśli plik jest wybrany, wejdź do pliku, aby móc dodawać/usuwać poszczególne linie do niestandardowej łatki. Jeśli wybrany jest katalog, przełącz katalog. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Przełącz widok drzewa plików | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
|
||||
@@ -148,7 +148,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Abrir ferramenta de diff externa (git difftool) | |
|
||||
| `` <space> `` | Alternar entre o arquivo incluído no patch | Alternar se o arquivo está incluído no patch personalizado. Veja https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Alternar todos os arquivos | Adicionar/remover todos os arquivos de commit para atualização personalizada. Consulte https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Insira o arquivo / Alternar diretório recolhido | Se um arquivo estiver selecionado, insira o arquivo para que você possa adicionar/remover linhas individuais no patch personalizado. Se um diretório for selecionado, ative o diretório. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Alternar exibição de árvore de arquivo | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Recolher todos os arquivos | Recolher todos os diretórios na árvore de arquivos |
|
||||
| `` = `` | Expandir todos os arquivos | Expandir todos os diretórios na árvore do arquivo |
|
||||
@@ -234,18 +234,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Rolar para baixo | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Rolar para cima | |
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
|
||||
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
| `` w `` | Fazer commit de alterações sem pré-commit | |
|
||||
@@ -253,28 +254,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Encontrar commit da base para corrigir | Encontre o commit em que as suas mudanças atuais estão se baseando, para alterar/consertar o commit. Isso poupa-te você de ter que olhar pelos commits da sua branch um por um para ver qual commit deve ser alterado/consertado<br>Veja a documentação:<br><https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Painel Principal (preparação)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` <space> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
|
||||
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <esc> `` | Retornar ao painel de arquivos | |
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
| `` w `` | Fazer commit de alterações sem pré-commit | |
|
||||
| `` C `` | Enviar alteração usando um editor Git | |
|
||||
| `` <ctrl+f> `` | Encontrar commit da base para corrigir | Encontre o commit em que as suas mudanças atuais estão se baseando, para alterar/consertar o commit. Isso poupa-te você de ter que olhar pelos commits da sua branch um por um para ver qual commit deve ser alterado/consertado<br>Veja a documentação:<br><https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Painel de confirmação
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -299,22 +278,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Retornar ao painel de arquivos | |
|
||||
|
||||
## Painel principal (patch build)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Alternar linhas no caminho | |
|
||||
| `` d `` | Remover linhas do commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Sair do construtor de patch personalizado | |
|
||||
| `` / `` | Pesquisar na visualização atual por texto | |
|
||||
|
||||
## Reflog
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -351,18 +314,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. |
|
||||
| `` v `` | Toggle range select | |
|
||||
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
|
||||
| `` <space> `` | Etapa | Ativar/desativar seleção em staged/unstaged |
|
||||
| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. |
|
||||
| `` E `` | Editar hunk | Editar o local selecionado no editor externo. |
|
||||
| `` <ctrl+o> `` | Copiar texto selecionado para área de transferência | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Ir para o local anterior | |
|
||||
| `` <right>, l `` | Ir para o próximo trecho | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Commit | Submeter mudanças em staging |
|
||||
| `` w `` | Fazer commit de alterações sem pré-commit | |
|
||||
|
||||
@@ -73,18 +73,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Изменить эту часть | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
|
||||
@@ -92,46 +93,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Индексирование)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` o `` | Открыть файл | Open file in default application. |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <esc> `` | Вернуться к панели файлов | |
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` E `` | Изменить эту часть | Edit selected hunk in external editor. |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
|
||||
| `` C `` | Сохранить изменения с помощью редактора git | |
|
||||
| `` <ctrl+f> `` | Find base commit for fixup | Find the commit that your current changes are building upon, for the sake of amending/fixing up the commit. This spares you from having to look through your branch's commits one-by-one to see which commit should be amended/fixed up. See docs: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Главная панель (Обычный)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | Прокрутить вниз | |
|
||||
| `` <mouse wheel up> (fn+down) `` | Прокрутить вверх | |
|
||||
| `` <tab> `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные |
|
||||
| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. |
|
||||
| `` E `` | Изменить эту часть | Edit selected hunk in external editor. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | Exit back to side panel | |
|
||||
| `` c `` | Сохранить изменения | Commit staged changes. |
|
||||
| `` w `` | Закоммитить изменения без предварительного хука коммита | |
|
||||
@@ -155,22 +135,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` <esc> `` | Вернуться к панели файлов | |
|
||||
|
||||
## Главная панель (сборка патчей)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | Выбрать предыдущую часть | |
|
||||
| `` <right>, l `` | Выбрать следующую часть | |
|
||||
| `` v `` | Переключить выборку перетаскивания | |
|
||||
| `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. |
|
||||
| `` <ctrl+o> `` | Скопировать выделенный текст в буфер обмена | |
|
||||
| `` o `` | Открыть файл | Open file in default application. |
|
||||
| `` e `` | Редактировать файл | Open file in external editor. |
|
||||
| `` <space> `` | Добавить/удалить строку(и) для патча | |
|
||||
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
|
||||
| `` <esc> `` | Выйти из сборщика пользовательских патчей | |
|
||||
| `` / `` | Найти | |
|
||||
|
||||
## Журнал ссылок (Reflog)
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -334,7 +298,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | Open external diff tool (git difftool) | |
|
||||
| `` <space> `` | Переключить файлы включённые в патч | Toggle whether the file is included in the custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` a `` | Переключить все файлы, включённые в патч | Add/remove all commit's files to custom patch. See https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches. |
|
||||
| `` <enter> `` | Введите файл, чтобы добавить выбранные строки в патч (или свернуть каталог переключения) | If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory. |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | Переключить вид дерева файлов | Toggle file view between flat and tree layout. Flat layout shows all file paths in a single list, tree layout groups files by directory.<br><br>The default can be changed in the config file with the key 'gui.showFileTree'. |
|
||||
| `` - `` | Collapse all files | Collapse all directories in the files tree |
|
||||
| `` = `` | Expand all files | Expand all directories in the file tree |
|
||||
@@ -419,7 +383,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` s `` | Stash | Stash all changes. For other variations of stashing, use the view stash options keybinding. |
|
||||
| `` S `` | Просмотреть параметры хранилища | View stash options (e.g. stash all, stash staged, stash unstaged). |
|
||||
| `` a `` | Все проиндексированные/непроиндексированные | Toggle staged/unstaged for all files in working tree. |
|
||||
| `` <enter> `` | Проиндексировать отдельные части/строки для файла или свернуть/развернуть для каталога | If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it. |
|
||||
| `` <enter> `` | Проиндексировать отдельные части/строки для файла или свернуть/развернуть для каталога | If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it. |
|
||||
| `` d `` | Просмотреть параметры «отмены изменении» | View options for discarding changes to the selected file. |
|
||||
| `` g `` | Просмотреть параметры сброса upstream-ветки | |
|
||||
| `` D `` | Reset | View reset options for working tree (e.g. nuking the working tree). |
|
||||
|
||||
@@ -178,7 +178,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | 使用外部差异比较工具(git difftool) | |
|
||||
| `` <space> `` | 补丁中包含的切换文件 | 切换文件是否包含在自定义补丁中。请参阅 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` a `` | 操作所有文件 | 添加或删除所有提交中的文件到自定义的补丁中。请参阅 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` <enter> `` | 输入文件以将所选行添加到补丁中(或切换目录折叠) | 如果已选择一个文件,则Enter进入该文件,以便您可以向自定义补丁添加/删除单独的行。如果选择了目录,则切换目录。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 切换文件树视图 | 在平面布局和树布局之间切换文件视图。平面布局在单个列表中显示所有文件路径,树布局按目录分组文件。<br><br>可以在配置文件中使用 'gui.showFileTree' 键更改默认设置。 |
|
||||
| `` - `` | 折叠全部文件 | 折叠文件树中的全部目录 |
|
||||
| `` = `` | 展开全部文件 | 展开文件树中的全部目录 |
|
||||
@@ -249,22 +249,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <enter> `` | 查看提交 | |
|
||||
| `` / `` | 通过文本过滤当前视图 | |
|
||||
|
||||
## 构建补丁中
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 添加/移除 行到补丁 | |
|
||||
| `` d `` | 从提交中移除行 | 从本次提交中移除所选行。此操作会在后台运行交互式变基,因此如果后续提交也修改了这些行,您可能会遇到合并冲突。 |
|
||||
| `` <esc> `` | 退出逐行模式 | |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 标签
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -285,18 +269,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
@@ -320,46 +305,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | 查看合并冲突选项 | 查看用于解决合并冲突的选项。 |
|
||||
| `` <esc> `` | 返回文件面板 | |
|
||||
|
||||
## 正在暂存
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <esc> `` | 返回文件面板 | |
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
| `` C `` | 使用 Git 编辑器提交变更 | |
|
||||
| `` <ctrl+f> `` | 找到用于修复的基准提交 | 找到您当前变更所基于的提交,以便于修正/改进该提交。这样做可以省去您逐一查看分支提交来确定应该修正/改进哪个提交的麻烦。请参阅文档: <https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 开始搜索 | |
|
||||
|
||||
## 正常
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 向下滚动 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 向上滚动 | |
|
||||
| `` <tab> `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 |
|
||||
| `` v `` | 切换拖动选择 | |
|
||||
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
|
||||
| `` <space> `` | 切换暂存状态 | 切换行暂存状态 |
|
||||
| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 |
|
||||
| `` E `` | 编辑代码块 | 在外部编辑器中编辑选中的代码块 |
|
||||
| `` <ctrl+o> `` | 复制选中文本到剪贴板 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 选择上一个区块 | |
|
||||
| `` <right>, l `` | 选择下一个区块 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到侧边面板 | |
|
||||
| `` c `` | 提交变更 | 提交暂存文件 |
|
||||
| `` w `` | 提交变更而无需预先提交钩子 | |
|
||||
|
||||
@@ -52,40 +52,25 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` ] `` | 下一個索引標籤 | |
|
||||
| `` [ `` | 上一個索引標籤 | |
|
||||
|
||||
## 主面板 (補丁生成)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 向 (或從) 補丁中添加/刪除行 | |
|
||||
| `` d `` | 從提交中移除行 | 從本次提交中移除所選行。此操作會在背景執行互動式變基,因此如果後續提交也修改了這些行,您可能會遇到合併衝突。 |
|
||||
| `` <esc> `` | 退出自訂補丁建立器 | |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 主面板(一般)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <mouse wheel down> (fn+up) `` | 向下捲動 | |
|
||||
| `` <mouse wheel up> (fn+down) `` | 向上捲動 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
@@ -109,28 +94,6 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` M `` | 檢視合併衝突選項 | 檢視用於解決合併衝突的選項。 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
|
||||
## 主面板(預存)
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <esc> `` | 返回檔案面板 | |
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
| `` C `` | 使用 git 編輯器提交變更 | |
|
||||
| `` <ctrl+f> `` | 尋找 fixup 的基礎提交 | 找出目前變更所依據的提交,以便 amend/fixup。這樣不必逐一檢視分支中的提交來找出要 amend/fixup 的提交。請見文件:<https://github.com/jesseduffield/lazygit/tree/master/docs/Fixup_Commits.md> |
|
||||
| `` / `` | 搜尋 | |
|
||||
|
||||
## 功能表
|
||||
|
||||
| Key | Action | Info |
|
||||
@@ -245,7 +208,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
| `` <ctrl+t> `` | 開啟外部差異工具 (git difftool) | |
|
||||
| `` <space> `` | 切換檔案是否包含在補丁中 | 切換檔案是否包含在自定義補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` a `` | 切換所有檔案是否包含在補丁中 | 新增或刪除所有提交中的檔案到自定義的補丁中。請參閱 https://github.com/jesseduffield/lazygit#rebase-magic-custom-patches。 |
|
||||
| `` <enter> `` | 輸入檔案以將選定的行添加至補丁(或切換目錄折疊) | 如果已選擇一個檔案,則Enter進入該檔案,以便您可以向自定義補丁新增/刪除單獨的行。如果選擇了目錄,則切換目錄。 |
|
||||
| `` <enter> `` | Focus file diff / Toggle directory | If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it. |
|
||||
| `` ` `` | 顯示檔案樹狀視圖 | 在平面佈局和樹佈局之間切換檔案檢視。平面佈局在單個列表中顯示所有檔案路徑,樹佈局按目錄分組檔案。<br><br>可以在設定檔中使用 'gui.showFileTree' 鍵更改預設設定。 |
|
||||
| `` - `` | 摺疊全部檔案 | 摺疊檔案樹中的全部目錄 |
|
||||
| `` = `` | 展開全部檔案 | 展開檔案樹中的全部目錄 |
|
||||
@@ -370,18 +333,19 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct
|
||||
|
||||
| Key | Action | Info |
|
||||
|-----|--------|-------------|
|
||||
| `` <tab> `` | 切換至另一個面板 (已預存/未預存更改) | 切換到其他檢視(已暫存/未暫存的變更)。 |
|
||||
| `` <tab> `` | Switch diff pane | Switch to the other focused diff pane. |
|
||||
| `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 |
|
||||
| `` v `` | 切換拖曳選擇 | |
|
||||
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
|
||||
| `` <space> `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) |
|
||||
| `` d `` | 刪除變更 (git reset) | 選取未暫存的變更時,使用 `git reset` 捨棄變更。選取已暫存的變更時,取消暫存變更。 |
|
||||
| `` E `` | 編輯程式碼塊 | 在外部編輯器中編輯選中的程式碼塊。 |
|
||||
| `` <ctrl+o> `` | 複製所選文本至剪貼簿 | |
|
||||
| `` <ctrl+o> `` | Copy selected diff lines to clipboard | |
|
||||
| `` <left>, h `` | 選擇上一段 | |
|
||||
| `` <right>, l `` | 選擇下一段 | |
|
||||
| `` N `` | Go to previous file | |
|
||||
| `` n `` | Go to next file | |
|
||||
| `` G `` | Open pull request at selected line | Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found. |
|
||||
| `` <esc> `` | 退出回到側邊面板 | |
|
||||
| `` c `` | 提交變更 | 提交暫存區變更 |
|
||||
| `` w `` | 沒有預提交 hook 就提交更改 | |
|
||||
|
||||
@@ -152,9 +152,6 @@ func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTes
|
||||
|
||||
if integrationTest != nil {
|
||||
integrationTest.SetupConfig(appConfig)
|
||||
// Set this to true so that integration tests don't have to explicitly deal with the hunk
|
||||
// staging hint:
|
||||
appConfig.GetAppState().DidShowHunkStagingHint = true
|
||||
|
||||
// Preserve the changes that the test setup just made to the config, so
|
||||
// they don't get lost when we reload the config while running the test
|
||||
|
||||
@@ -120,9 +120,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
"prompt": tr.PromptTitle,
|
||||
"information": tr.InformationTitle,
|
||||
"main": tr.NormalTitle,
|
||||
"patchBuilding": tr.PatchBuildingTitle,
|
||||
"mergeConflicts": tr.MergingTitle,
|
||||
"staging": tr.StagingTitle,
|
||||
"menu": tr.MenuTitle,
|
||||
"search": tr.SearchTitle,
|
||||
"secondary": tr.SecondaryTitle,
|
||||
@@ -141,12 +139,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
|
||||
}
|
||||
|
||||
func getBindingSections(bindings []*types.Binding, tr *i18n.TranslationSet) []*bindingSection {
|
||||
excludedViews := []string{"stagingSecondary", "patchBuildingSecondary"}
|
||||
bindingsToDisplay := lo.Filter(bindings, func(binding *types.Binding, _ int) bool {
|
||||
if lo.Contains(excludedViews, binding.ViewName) {
|
||||
return false
|
||||
}
|
||||
|
||||
return (binding.Description != "" || binding.Alternative != "") && len(binding.Keys) > 0
|
||||
})
|
||||
|
||||
|
||||
@@ -139,6 +139,11 @@ func NewGitCommandAux(
|
||||
// A patch is built from git's own diff: what a diff renderer would make of it
|
||||
// is a picture of it, not something that can be applied.
|
||||
return workingTreeCommands.ShowFileDiff(from, to, reverse, filename, previousPath, git_commands.DiffModePlain)
|
||||
},
|
||||
func() (string, error) {
|
||||
// Under lazygit's own temp dir, so that it honours the configured location
|
||||
// and is cleaned up with everything else when we exit.
|
||||
return os.MkdirTemp(osCommand.GetTempDir(), "custom-patch-")
|
||||
})
|
||||
patchCommands := git_commands.NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
|
||||
bisectCommands := git_commands.NewBisectCommands(gitCommon)
|
||||
|
||||
@@ -156,6 +156,28 @@ func (self *DiffCommands) DiffCmdObj(diffArgs []string, mode DiffMode) *oscomman
|
||||
)
|
||||
}
|
||||
|
||||
// CustomPatchDiffCmdObj is the command that renders the custom patch being built: a diff
|
||||
// of the two trees the patch was materialized into (PatchCommands.WriteCustomPatchDiffTrees),
|
||||
// under the directory holding them. It goes through the same wiring as any other diff we
|
||||
// show, so the patch is rendered by whatever renders the rest of them, and git works out
|
||||
// how much context to give it.
|
||||
//
|
||||
// git's own path prefixes are suppressed because the trees are named a and b themselves,
|
||||
// which leaves the paths reading like an ordinary diff's over the repo's own paths.
|
||||
func (self *DiffCommands) CustomPatchDiffCmdObj(dir string, mode DiffMode) *oscommands.CmdObj {
|
||||
return self.cmd.New(
|
||||
NewGitCmd("diff").
|
||||
AddCommonDiffArgs(self.diffRendererConfigManager, self.UserConfig(), mode).
|
||||
NoLineEndingConversion().
|
||||
Arg("--no-index").
|
||||
Arg("--no-prefix").
|
||||
Arg(fmt.Sprintf("--color=%s", mode.colorArg(self.diffRendererConfigManager))).
|
||||
Arg("a", "b").
|
||||
Dir(dir).
|
||||
ToArgv(),
|
||||
)
|
||||
}
|
||||
|
||||
// This is a basic generic diff command that can be used for any diff operation
|
||||
// (e.g. copying a diff to the clipboard). It will not use a custom diff renderer,
|
||||
// and does not use user configs such as ignore whitespace.
|
||||
|
||||
@@ -123,6 +123,27 @@ func (self *GitCommandBuilder) GitDirIf(condition bool, path string) *GitCommand
|
||||
return self
|
||||
}
|
||||
|
||||
// NoLineEndingConversion keeps git's line-ending machinery away from files that are not
|
||||
// a working tree's. The trees the custom patch is materialized into hold the bytes git
|
||||
// states the patch in, so a command over them has to read and write those bytes as they
|
||||
// are. On a machine that checks files out with CRLF, `git apply` writes the after tree
|
||||
// in that form while the before tree keeps the LF it was written with. git's own diff
|
||||
// converts both back, but it warns about a round trip through a working tree these
|
||||
// files never belong to, and an external diff renderer is handed the two files as they
|
||||
// stand, one line ending apart in every line.
|
||||
func (self *GitCommandBuilder) NoLineEndingConversion() *GitCommandBuilder {
|
||||
return self.
|
||||
// The setting that converts on most machines, and the one Git for Windows
|
||||
// installs itself with.
|
||||
Config("core.autocrlf=false").
|
||||
// An attributes file outside the repo can still mark the files as text. The
|
||||
// form to keep them in is then the form they are written in.
|
||||
Config("core.eol=lf").
|
||||
// An attribute naming CRLF outright overrides that, and git converts after all.
|
||||
// The warning it gives is about a checkout these files never have.
|
||||
Config("core.safecrlf=false")
|
||||
}
|
||||
|
||||
func (self *GitCommandBuilder) AddCommonDiffArgs(diffRendererConfigManager *config.DiffRendererConfigManager, userConfig *config.UserConfig, mode DiffMode) *GitCommandBuilder {
|
||||
contextSize := userConfig.Git.DiffContextSize
|
||||
useExtDiff := mode == DiffModeRendered && diffRendererConfigManager.GetDiffRendererType() == config.DiffRendererType_ExtDiff
|
||||
|
||||
@@ -2,13 +2,16 @@ package git_commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stefanhaller/git-todo-parser/todo"
|
||||
)
|
||||
|
||||
@@ -20,6 +23,10 @@ type PatchCommands struct {
|
||||
stash *StashCommands
|
||||
|
||||
PatchBuilder *patch.PatchBuilder
|
||||
|
||||
// The version of the patch the diff trees were last written for, so that they are
|
||||
// written again when, and only when, the patch has changed since.
|
||||
treesWrittenForGeneration int
|
||||
}
|
||||
|
||||
func NewPatchCommands(
|
||||
@@ -40,6 +47,88 @@ func NewPatchCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureCustomPatchDiffTrees writes the custom patch's diff trees if what is there no
|
||||
// longer describes the patch. Call it before rendering the patch, which is often — every
|
||||
// time the panel showing it re-renders — while the patch itself changes rarely.
|
||||
func (self *PatchCommands) EnsureCustomPatchDiffTrees() error {
|
||||
if self.PatchBuilder.Generation() == self.treesWrittenForGeneration {
|
||||
return nil
|
||||
}
|
||||
if err := self.WriteCustomPatchDiffTrees(); err != nil {
|
||||
return err
|
||||
}
|
||||
self.treesWrittenForGeneration = self.PatchBuilder.Generation()
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteCustomPatchDiffTrees materializes the custom patch as two file trees under the
|
||||
// directory the patch builder keeps for it: `a` holds each of the patch's files as it is
|
||||
// before the patch, `b` as it is after. Diffing those two trees against each other
|
||||
// (DiffCommands.CustomPatchDiffCmdObj) turns the patch into a diff of real files, which
|
||||
// can then be rendered exactly as any other diff is — through a diff renderer of any
|
||||
// kind, and with git's own idea of how much context to show.
|
||||
//
|
||||
// The trees are named a and b so that the diff's paths, with git's own prefixes
|
||||
// suppressed, come out reading like the a/ and b/ of an ordinary diff, over the real
|
||||
// repo-relative paths.
|
||||
func (self *PatchCommands) WriteCustomPatchDiffTrees() error {
|
||||
dir := self.PatchBuilder.TempDir()
|
||||
if dir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
before := filepath.Join(dir, "a")
|
||||
after := filepath.Join(dir, "b")
|
||||
for _, tree := range []string{before, after} {
|
||||
if err := os.RemoveAll(tree); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(tree, 0o700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, file := range self.PatchBuilder.FilesInPatch() {
|
||||
content, err := self.commit.ShowFileContentCmdObj(self.PatchBuilder.From, file.ContentPath).RunWithOutput()
|
||||
// A file the patch adds has no content on the before side, so git has nothing to
|
||||
// show for it.
|
||||
added := err != nil
|
||||
|
||||
// The before side holds an added file as an empty file rather than not at all, so
|
||||
// that the diff pairs the two sides up and states the file's real path, instead of
|
||||
// reporting a file that only one of the trees has.
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(before, file.Path),
|
||||
lo.Ternary(added, "", content)); err != nil {
|
||||
return err
|
||||
}
|
||||
// The after side is seeded with the same content, for the patch to change; a file
|
||||
// the patch adds is left absent, for the patch to create.
|
||||
if !added {
|
||||
if err := self.os.CreateFileWithContent(filepath.Join(after, file.Path), content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write added files as creations rather than as diffs against an empty file: the
|
||||
// patch is applied in one go, so a file it expects to be there already would make
|
||||
// the whole of it fail.
|
||||
patchText := self.PatchBuilder.PatchToApply(false, false)
|
||||
if strings.TrimSpace(patchText) == "" {
|
||||
// Nothing in the patch, so the two trees are alike and the diff is empty.
|
||||
return nil
|
||||
}
|
||||
patchFilePath, err := self.SaveTemporaryPatch(patchText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return self.cmd.New(NewGitCmd("apply").
|
||||
NoLineEndingConversion().
|
||||
Arg(patchFilePath).
|
||||
Dir(after).
|
||||
ToArgv()).Run()
|
||||
}
|
||||
|
||||
type ApplyPatchOpts struct {
|
||||
ThreeWay bool
|
||||
Cached bool
|
||||
|
||||
@@ -265,17 +265,3 @@ func (self *Patch) AdjustLineNumber(lineNumber int) int {
|
||||
|
||||
return adjustedLineNumber
|
||||
}
|
||||
|
||||
func (self *Patch) IsSingleHunkForWholeFile() bool {
|
||||
if len(self.hunks) != 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// We consider a patch to be a single hunk for the whole file if it has only additions or
|
||||
// deletions but not both, and no context lines. This not quite correct, because it will also
|
||||
// return true for a block of added or deleted lines if the diff context size is 0, but in this
|
||||
// case you wouldn't be able to stage things anyway, so it doesn't matter.
|
||||
bodyLines := self.hunks[0].bodyLines
|
||||
return nLinesWithKind(bodyLines, []PatchLineKind{DELETION, CONTEXT}) == 0 ||
|
||||
nLinesWithKind(bodyLines, []PatchLineKind{ADDITION, CONTEXT}) == 0
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/maps"
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/samber/lo"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -60,12 +62,27 @@ type PatchBuilder struct {
|
||||
|
||||
// loadFileDiff loads the diff of a file, for a given to (typically a commit hash)
|
||||
loadFileDiff loadFileDiffFunc
|
||||
|
||||
// newTempDir makes a directory for the current patch to be materialized into, as
|
||||
// two file trees that can be diffed against each other and so rendered like any
|
||||
// other diff (see PatchCommands.WriteCustomPatchDiffTrees). Its lifetime is the
|
||||
// patch's: made when one is started, removed when it is given up.
|
||||
newTempDir func() (string, error)
|
||||
tempDir string
|
||||
|
||||
// generation counts the changes made to the patch, so that whoever materializes it
|
||||
// can tell whether what they last built still describes it — and rebuild only then,
|
||||
// rather than on every render of it.
|
||||
generation int
|
||||
}
|
||||
|
||||
func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBuilder {
|
||||
func NewPatchBuilder(
|
||||
log *logrus.Entry, loadFileDiff loadFileDiffFunc, newTempDir func() (string, error),
|
||||
) *PatchBuilder {
|
||||
return &PatchBuilder{
|
||||
Log: log,
|
||||
loadFileDiff: loadFileDiff,
|
||||
newTempDir: newTempDir,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +90,9 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.makeTempDir()
|
||||
|
||||
p.To = to
|
||||
p.From = from
|
||||
p.reverse = reverse
|
||||
@@ -91,6 +111,89 @@ func (p *PatchBuilder) snapshotFileInfoMap() map[string]*fileInfo {
|
||||
return p.fileInfoMap
|
||||
}
|
||||
|
||||
// TempDir is the directory the patch is materialized into for rendering, and "" when
|
||||
// there is none — no patch, or a directory we failed to make.
|
||||
func (p *PatchBuilder) TempDir() string {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.tempDir
|
||||
}
|
||||
|
||||
// Generation says which version of the patch this is; see the field.
|
||||
func (p *PatchBuilder) Generation() int {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
return p.generation
|
||||
}
|
||||
|
||||
// makeTempDir replaces the directory the patch is materialized into with a fresh one.
|
||||
// Only call this with the lock held.
|
||||
func (p *PatchBuilder) makeTempDir() {
|
||||
p.removeTempDir()
|
||||
if p.newTempDir == nil {
|
||||
return
|
||||
}
|
||||
dir, err := p.newTempDir()
|
||||
if err != nil {
|
||||
p.Log.Error(err)
|
||||
return
|
||||
}
|
||||
p.tempDir = dir
|
||||
}
|
||||
|
||||
// removeTempDir takes the patch's materialized form away with the patch. Only call this
|
||||
// with the lock held.
|
||||
func (p *PatchBuilder) removeTempDir() {
|
||||
if p.tempDir == "" {
|
||||
return
|
||||
}
|
||||
if err := os.RemoveAll(p.tempDir); err != nil {
|
||||
p.Log.Error(err)
|
||||
}
|
||||
p.tempDir = ""
|
||||
}
|
||||
|
||||
// PatchFile records what materializing the patch needs to know about one of its files:
|
||||
// where the patch expects to find it, and where its content before the patch comes from.
|
||||
type PatchFile struct {
|
||||
// Path is the name the patch knows the file by: for a renamed file, the name it had
|
||||
// before where the patch carries the rename, and the name it was renamed to where the
|
||||
// patch keeps only a content change and leaves the rename behind.
|
||||
Path string
|
||||
// ContentPath is where the file's content before the patch is to be found in the
|
||||
// commit the patch is built from — for a renamed file always the name it had there,
|
||||
// whatever the patch calls it.
|
||||
ContentPath string
|
||||
}
|
||||
|
||||
// FilesInPatch says which files the patch touches, in a stable order, and where each of
|
||||
// them comes from.
|
||||
func (p *PatchBuilder) FilesInPatch() []PatchFile {
|
||||
fileInfoMap := p.snapshotFileInfoMap()
|
||||
|
||||
filenames := maps.Keys(fileInfoMap)
|
||||
sort.Strings(filenames)
|
||||
|
||||
files := make([]PatchFile, 0, len(filenames))
|
||||
for _, filename := range filenames {
|
||||
info := fileInfoMap[filename]
|
||||
if info.mode == UNSELECTED {
|
||||
continue
|
||||
}
|
||||
file := PatchFile{Path: filename, ContentPath: filename}
|
||||
if info.previousPath != "" {
|
||||
file.ContentPath = info.previousPath
|
||||
if info.mode == WHOLE {
|
||||
file.Path = info.previousPath
|
||||
}
|
||||
}
|
||||
files = append(files, file)
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) PatchToApply(reverse bool, turnAddedFilesIntoDiffAgainstEmptyFile bool) string {
|
||||
var patch strings.Builder
|
||||
|
||||
@@ -135,6 +238,7 @@ func (p *PatchBuilder) AddFileWhole(filename string, previousPath string) error
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.addFileWhole(info)
|
||||
|
||||
return nil
|
||||
@@ -146,6 +250,7 @@ func (p *PatchBuilder) RemoveFile(filename string, previousPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
p.generation++
|
||||
p.removeFile(info)
|
||||
|
||||
return nil
|
||||
@@ -182,6 +287,7 @@ func (p *PatchBuilder) AddFileLineRange(filename string, previousPath string, li
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices = lo.Union(info.includedLineIndices, lineIndices)
|
||||
|
||||
@@ -193,6 +299,7 @@ func (p *PatchBuilder) RemoveFileLineRange(filename string, previousPath string,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.generation++
|
||||
info.mode = PART
|
||||
info.includedLineIndices, _ = lo.Difference(info.includedLineIndices, lineIndices)
|
||||
if len(info.includedLineIndices) == 0 {
|
||||
@@ -291,6 +398,110 @@ func (p *PatchBuilder) GetFileStatus(filename string, parent string) PatchStatus
|
||||
return info.mode
|
||||
}
|
||||
|
||||
// LineIdentity says which change line of a file is meant — the line number it has on
|
||||
// the side it belongs to, and whether it is a deletion — without reference to where
|
||||
// that line sits in the file's parsed diff.
|
||||
//
|
||||
// It is how a diff shown in the main view speaks about its lines: what a rendered row
|
||||
// resolves to is a line of a file, while the index of that line in the diff depends on
|
||||
// how much of the diff is being shown and in what order a renderer laid it out.
|
||||
type LineIdentity struct {
|
||||
LineNumber int
|
||||
IsDeletion bool
|
||||
}
|
||||
|
||||
// ChangeLineIndexByIdentity indexes a parsed diff's change lines by their identity. An
|
||||
// addition is numbered in the new file and a deletion in the old one. Two consecutive
|
||||
// deletions share the one new-file position between them, and numbering them in the
|
||||
// old file keeps them apart.
|
||||
func ChangeLineIndexByIdentity(parsed *Patch) map[LineIdentity]int {
|
||||
byIdentity := map[LineIdentity]int{}
|
||||
for idx, line := range parsed.Lines() {
|
||||
switch {
|
||||
case line.IsAddition():
|
||||
byIdentity[LineIdentity{parsed.LineNumberOfLine(idx), false}] = idx
|
||||
case line.IsDeletion():
|
||||
byIdentity[LineIdentity{parsed.OldLineNumberOfLine(idx), true}] = idx
|
||||
}
|
||||
}
|
||||
return byIdentity
|
||||
}
|
||||
|
||||
// ChangeLineIndicesForLines maps the given change lines of a parsed diff to their
|
||||
// indices in it. A line that names no change line of the diff — a context line, or a
|
||||
// line that isn't in the diff at all — contributes nothing.
|
||||
func ChangeLineIndicesForLines(parsed *Patch, lines []LineIdentity) []int {
|
||||
byIdentity := ChangeLineIndexByIdentity(parsed)
|
||||
indices := make([]int, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
if idx, ok := byIdentity[line]; ok {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
// PatchLineIndicesForLines maps change lines of filename to their indices in that
|
||||
// file's diff; the patch is built in terms of those indices. everyChange reports
|
||||
// whether the given lines cover all of the file's changes; this distinguishes acting
|
||||
// on some of a file's lines from acting on the file itself.
|
||||
func (p *PatchBuilder) PatchLineIndicesForLines(
|
||||
filename string, previousPath string, lines []LineIdentity,
|
||||
) (indices []int, everyChange bool, err error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
parsed := Parse(info.diff)
|
||||
selected := set.NewFromSlice(lines)
|
||||
everyChange = lo.EveryBy(maps.Keys(ChangeLineIndexByIdentity(parsed)),
|
||||
func(identity LineIdentity) bool { return selected.Includes(identity) })
|
||||
return ChangeLineIndicesForLines(parsed, lines), everyChange, nil
|
||||
}
|
||||
|
||||
// IncludedLineIdentities says which change lines of filename are in the patch, as the
|
||||
// identities a diff of that file shown anywhere can be compared against. Empty for a
|
||||
// file that is no part of the patch.
|
||||
func (p *PatchBuilder) IncludedLineIdentities(filename string) []LineIdentity {
|
||||
info, ok := p.snapshotFileInfoMap()[filename]
|
||||
if !ok || info.mode == UNSELECTED {
|
||||
return nil
|
||||
}
|
||||
|
||||
included := set.NewFromSlice(info.includedLineIndices)
|
||||
identities := []LineIdentity{}
|
||||
for identity, idx := range ChangeLineIndexByIdentity(Parse(info.diff)) {
|
||||
if included.Includes(idx) {
|
||||
identities = append(identities, identity)
|
||||
}
|
||||
}
|
||||
return identities
|
||||
}
|
||||
|
||||
// IncludedChangeLineIndices says which of filename's change lines are in the patch, as
|
||||
// their indices in the file's diff and in the order the file has them.
|
||||
//
|
||||
// It is how a line of the patch as it is shown names the line of the diff it came from:
|
||||
// all that can be said about a line of the patch is which of the file's changes it is,
|
||||
// its line numbers being the patch's own — a patch that leaves an earlier addition out
|
||||
// numbers everything after it differently from the diff it was built from.
|
||||
func (p *PatchBuilder) IncludedChangeLineIndices(filename string) []int {
|
||||
info, ok := p.snapshotFileInfoMap()[filename]
|
||||
if !ok || info.mode == UNSELECTED {
|
||||
return nil
|
||||
}
|
||||
|
||||
included := set.NewFromSlice(info.includedLineIndices)
|
||||
indices := []int{}
|
||||
for idx, line := range Parse(info.diff).Lines() {
|
||||
if (line.IsAddition() || line.IsDeletion()) && included.Includes(idx) {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func (p *PatchBuilder) GetFileIncLineIndices(filename string, previousPath string) ([]int, error) {
|
||||
info, err := p.getFileInfo(filename, previousPath)
|
||||
if err != nil {
|
||||
@@ -304,6 +515,9 @@ func (p *PatchBuilder) Reset() {
|
||||
p.mutex.Lock()
|
||||
defer p.mutex.Unlock()
|
||||
|
||||
p.generation++
|
||||
p.removeTempDir()
|
||||
|
||||
p.To = ""
|
||||
p.fileInfoMap = map[string]*fileInfo{}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
package patch
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// newTestPatchBuilder returns a patch builder started for a dummy commit, in which
|
||||
// every file's diff is the given one.
|
||||
func newTestPatchBuilder(diff string) *PatchBuilder {
|
||||
patchBuilder := NewPatchBuilder(logrus.New().WithField("test", "test"),
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
return diff, nil
|
||||
},
|
||||
// Nothing here renders the patch, so it needs no directory to be
|
||||
// materialized into.
|
||||
nil)
|
||||
patchBuilder.Start("from", "to", false, true)
|
||||
return patchBuilder
|
||||
}
|
||||
|
||||
// In simpleDiff the deletion "-orange" is line index 6 of the parsed diff (line 2 of
|
||||
// the old file) and the addition "+grape" is index 7 (line 2 of the new file).
|
||||
func TestPatchLineIndicesForLines(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
indices, everyChange, err := patchBuilder.PatchLineIndicesForLines("filename", "", []LineIdentity{
|
||||
{LineNumber: 2, IsDeletion: true}, // -orange
|
||||
{LineNumber: 2, IsDeletion: false}, // +grape
|
||||
{LineNumber: 1, IsDeletion: false}, // " apple", a context line
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{6, 7}, indices, "the context line names no change line")
|
||||
assert.True(t, everyChange, "the two changes are all the diff has")
|
||||
}
|
||||
|
||||
// A renamed file's rename header makes its change lines sit further down the diff, and
|
||||
// its old-file line numbers are of the file under its previous name.
|
||||
func TestPatchLineIndicesForLinesOfARenamedFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(renameWithModificationDiff)
|
||||
|
||||
indices, everyChange, err := patchBuilder.PatchLineIndicesForLines("newname", "oldname", []LineIdentity{
|
||||
{LineNumber: 2, IsDeletion: true}, // -orange
|
||||
{LineNumber: 2, IsDeletion: false}, // +grape
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []int{9, 10}, indices)
|
||||
assert.True(t, everyChange)
|
||||
}
|
||||
|
||||
// everyChange is about the diff alone: whether anything the file changes was left out
|
||||
// of the selection. What that then means for the patch is the caller's question.
|
||||
func TestPatchLineIndicesForLinesEveryChange(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(newFile)
|
||||
|
||||
_, everyChange, err := patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1},
|
||||
{LineNumber: 2},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, everyChange, "the file's third added line is left out")
|
||||
|
||||
_, everyChange, err = patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1},
|
||||
{LineNumber: 2},
|
||||
{LineNumber: 3},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, everyChange)
|
||||
|
||||
// A line the diff doesn't have doesn't stand in for one it does.
|
||||
patchBuilder = newTestPatchBuilder(deletedFile)
|
||||
_, everyChange, err = patchBuilder.PatchLineIndicesForLines("newfile", "", []LineIdentity{
|
||||
{LineNumber: 1, IsDeletion: true},
|
||||
{LineNumber: 2, IsDeletion: true},
|
||||
{LineNumber: 4, IsDeletion: true},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, everyChange)
|
||||
}
|
||||
|
||||
func TestIncludedLineIdentities(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
// A file no part of the patch has nothing included.
|
||||
assert.Empty(t, patchBuilder.IncludedLineIdentities("filename"))
|
||||
|
||||
// With only the deletion in, only its identity comes back.
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{6}))
|
||||
assert.Equal(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
|
||||
// With the addition in as well, both do.
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{7}))
|
||||
assert.ElementsMatch(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}, {LineNumber: 2, IsDeletion: false}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
}
|
||||
|
||||
func TestFilesInPatch(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
// A file no part of the patch is no part of what the patch is materialized from.
|
||||
assert.Empty(t, patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("filename", "", []int{6}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "filename", ContentPath: "filename"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A renamed file's content is under the name it had before whatever the patch calls the
|
||||
// file, and the patch calls it by the name it had before only where it carries the
|
||||
// rename — a partial selection has the rename stripped and names the file by the new one.
|
||||
func TestFilesInPatchOfARenamedFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(renameWithModificationDiff)
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileLineRange("newname", "oldname", []int{9}))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "newname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileWhole("newname", "oldname"))
|
||||
assert.Equal(t,
|
||||
[]PatchFile{{Path: "oldname", ContentPath: "oldname"}},
|
||||
patchBuilder.FilesInPatch())
|
||||
}
|
||||
|
||||
// A file taken into the patch whole has every one of its change lines in it.
|
||||
func TestIncludedLineIdentitiesOfAWholeFile(t *testing.T) {
|
||||
patchBuilder := newTestPatchBuilder(simpleDiff)
|
||||
|
||||
assert.NoError(t, patchBuilder.AddFileWhole("filename", ""))
|
||||
assert.ElementsMatch(t,
|
||||
[]LineIdentity{{LineNumber: 2, IsDeletion: true}, {LineNumber: 2, IsDeletion: false}},
|
||||
patchBuilder.IncludedLineIdentities("filename"))
|
||||
}
|
||||
@@ -921,64 +921,3 @@ func TestAdjustLineNumber(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsSingleHunkForWholeFile(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
testName string
|
||||
patchStr string
|
||||
expectedResult bool
|
||||
}{
|
||||
{
|
||||
testName: "simpleDiff",
|
||||
patchStr: simpleDiff,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToEndOfFile",
|
||||
patchStr: addNewlineToEndOfFile,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "removeNewlinefromEndOfFile",
|
||||
patchStr: removeNewlinefromEndOfFile,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "twoHunks",
|
||||
patchStr: twoHunks,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "twoChangesInOneHunk",
|
||||
patchStr: twoChangesInOneHunk,
|
||||
expectedResult: false,
|
||||
},
|
||||
{
|
||||
testName: "newFile",
|
||||
patchStr: newFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "deletedFile",
|
||||
patchStr: deletedFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "addNewlineToPreviouslyEmptyFile",
|
||||
patchStr: addNewlineToPreviouslyEmptyFile,
|
||||
expectedResult: true,
|
||||
},
|
||||
{
|
||||
testName: "exampleHunk",
|
||||
patchStr: exampleHunk,
|
||||
expectedResult: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.testName, func(t *testing.T) {
|
||||
patch := Parse(s.patchStr)
|
||||
assert.Equal(t, s.expectedResult, patch.IsSingleHunkForWholeFile())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,6 +297,8 @@ func computeMigratedConfig(path string, content []byte, changes *ChangesSet) ([]
|
||||
{[]string{"keybinding", "universal", "cyclePagers"}, "cycleDiffRenderers"},
|
||||
{[]string{"keybinding", "universal", "cyclePagersReverse"}, "cycleDiffRenderersReverse"},
|
||||
{[]string{"gui", "windowSize"}, "screenMode"},
|
||||
{[]string{"gui", "wrapLinesInStagingView"}, "wrapLinesInDiffView"},
|
||||
{[]string{"gui", "useHunkModeInStagingView"}, "useHunkModeInDiffView"},
|
||||
{[]string{"keybinding", "files", "openMergeTool"}, "openMergeOptions"},
|
||||
}
|
||||
|
||||
@@ -898,11 +900,10 @@ func (c *AppConfig) SaveGlobalUserConfig() {
|
||||
// AppState stores data between runs of the app like when the last update check
|
||||
// was performed and which other repos have been checked out
|
||||
type AppState struct {
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
DidShowHunkStagingHint bool
|
||||
LastVersion string // this is the last version the user was using, for the purpose of showing release notes
|
||||
LastUpdateCheck int64
|
||||
RecentRepos []string
|
||||
StartupPopupVersion int
|
||||
LastVersion string // this is the last version the user was using, for the purpose of showing release notes
|
||||
|
||||
// these are for shell commands typed in directly, not for custom commands in the lazygit config.
|
||||
// For backwards compatibility we keep the old name in yaml files.
|
||||
|
||||
@@ -108,6 +108,22 @@ func TestMigrationOfRenamedKeys(t *testing.T) {
|
||||
"Renamed 'gui.windowSize' to 'screenMode'",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Rename staging view options",
|
||||
input: `gui:
|
||||
wrapLinesInStagingView: false
|
||||
useHunkModeInStagingView: true
|
||||
`,
|
||||
expected: `gui:
|
||||
wrapLinesInDiffView: false
|
||||
useHunkModeInDiffView: true
|
||||
`,
|
||||
expectedDidChange: true,
|
||||
expectedChanges: []string{
|
||||
"Renamed 'gui.wrapLinesInStagingView' to 'wrapLinesInDiffView'",
|
||||
"Renamed 'gui.useHunkModeInStagingView' to 'useHunkModeInDiffView'",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
|
||||
@@ -87,7 +87,7 @@ type GuiConfig struct {
|
||||
MouseEvents bool `yaml:"mouseEvents"`
|
||||
// If true, do not show a warning when amending a commit.
|
||||
SkipAmendWarning bool `yaml:"skipAmendWarning"`
|
||||
// If true, do not show a warning when discarding changes in the staging view.
|
||||
// If true, do not show a warning when discarding changes from a focused diff.
|
||||
SkipDiscardChangeWarning bool `yaml:"skipDiscardChangeWarning"`
|
||||
// If true, do not show warning when applying/popping the stash
|
||||
SkipStashWarning bool `yaml:"skipStashWarning"`
|
||||
@@ -122,10 +122,10 @@ type GuiConfig struct {
|
||||
// - 'left': split the window horizontally (side panel on the left, main view on the right)
|
||||
// - 'top': split the window vertically (side panel on top, main view below)
|
||||
EnlargedSideViewLocation string `yaml:"enlargedSideViewLocation"`
|
||||
// If true, wrap lines in the staging view to the width of the view. This makes it much easier to work with diffs that have long lines, e.g. paragraphs of markdown text.
|
||||
WrapLinesInStagingView bool `yaml:"wrapLinesInStagingView"`
|
||||
// If true, hunk selection mode will be enabled by default when entering the staging view.
|
||||
UseHunkModeInStagingView bool `yaml:"useHunkModeInStagingView"`
|
||||
// If true, wrap lines in focused diffs to the width of the view. This makes it much easier to work with diffs that have long lines, e.g. paragraphs of markdown text.
|
||||
WrapLinesInDiffView bool `yaml:"wrapLinesInDiffView"`
|
||||
// If true, hunk selection mode will be enabled by default when focusing a diff.
|
||||
UseHunkModeInDiffView bool `yaml:"useHunkModeInDiffView"`
|
||||
// One of 'auto' (default) | 'en' | 'zh-CN' | 'zh-TW' | 'pl' | 'nl' | 'ja' | 'ko' | 'ru' | 'pt'
|
||||
Language string `yaml:"language" jsonschema:"enum=auto,enum=en,enum=zh-TW,enum=zh-CN,enum=pl,enum=nl,enum=ja,enum=ko,enum=ru"`
|
||||
// Format used when displaying time e.g. commit time.
|
||||
@@ -890,8 +890,8 @@ func GetDefaultConfigForPlatform(platform string) *UserConfig {
|
||||
},
|
||||
MainPanelSplitMode: "flexible",
|
||||
EnlargedSideViewLocation: "left",
|
||||
WrapLinesInStagingView: true,
|
||||
UseHunkModeInStagingView: true,
|
||||
WrapLinesInDiffView: true,
|
||||
UseHunkModeInDiffView: true,
|
||||
Language: "auto",
|
||||
TimeFormat: "02 Jan 06",
|
||||
ShortTimeFormat: time.Kitchen,
|
||||
|
||||
@@ -228,10 +228,6 @@ var ValidCustomCommandContexts = []string{
|
||||
"stash",
|
||||
"normal",
|
||||
"normalSecondary",
|
||||
"staging",
|
||||
"stagingSecondary",
|
||||
"patchBuilding",
|
||||
"patchBuildingSecondary",
|
||||
"mergeConflicts",
|
||||
"menu",
|
||||
"confirmation",
|
||||
|
||||
@@ -242,6 +242,10 @@ func TestUserConfigValidate_enums(t *testing.T) {
|
||||
{value: "commits,subCommits", valid: true},
|
||||
{value: "invalid_value", valid: false},
|
||||
{value: "commits, invalid_value", valid: false},
|
||||
// The staging and patch-building panels are gone, so a config that
|
||||
// still names their contexts is reported rather than fatal.
|
||||
{value: "staging", valid: false},
|
||||
{value: "patchBuilding", valid: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,3 +32,37 @@ func TestMouseReleaseDoesNotBreakDoubleClickDetection(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []bool{false, true}, doubleClicks)
|
||||
}
|
||||
|
||||
func TestASwallowedClickIsNoHalfOfADoubleClick(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
g := newTestGui(t)
|
||||
view, _ := g.SetView("list", 0, 0, 20, 10, 0)
|
||||
doubleClicks := []bool{}
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "list",
|
||||
Key: MouseLeft,
|
||||
Handler: func(opts ViewMouseBindingOpts) error {
|
||||
doubleClicks = append(doubleClicks, opts.IsDoubleClick)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
press := gocuiEventFromTcellEvent(
|
||||
tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonPrimary, tcell.ModNone))
|
||||
release := gocuiEventFromTcellEvent(
|
||||
tcell.NewEventMouse(view.x0+1, view.y0+1, tcell.ButtonNone, tcell.ModNone))
|
||||
|
||||
// A click the client rejects, as it does for one landing behind a popup panel.
|
||||
g.ShouldHandleMouseEvent = func(*View, KeyName) bool { return false }
|
||||
assert.NoError(t, g.onKey(&press))
|
||||
assert.NoError(t, g.onKey(&release))
|
||||
assert.Empty(t, doubleClicks)
|
||||
|
||||
// The same spot clicked again once clicks are accepted. It is a click of its
|
||||
// own, not the second half of the one that nothing acted on.
|
||||
g.ShouldHandleMouseEvent = func(*View, KeyName) bool { return true }
|
||||
assert.NoError(t, g.onKey(&press))
|
||||
|
||||
assert.Equal(t, []bool{false}, doubleClicks)
|
||||
}
|
||||
|
||||
+22
-5
@@ -64,8 +64,8 @@ func TestFlushContentOnly_SkipsUntaintedViews(t *testing.T) {
|
||||
assert.True(t, status.IsTainted(), "status view should be tainted after SetContent")
|
||||
assert.False(t, main.IsTainted(), "main view should not be tainted (was not modified)")
|
||||
|
||||
// flushContentOnly should succeed and clear status tainted flag
|
||||
assert.NoError(t, g.flushContentOnly(g.views))
|
||||
// flushContentOnly should clear status tainted flag
|
||||
g.flushContentOnly(g.views)
|
||||
|
||||
assert.False(t, status.IsTainted(), "status view should not be tainted after flushContentOnly")
|
||||
assert.False(t, main.IsTainted(), "main view should not be tainted after flushContentOnly")
|
||||
@@ -76,11 +76,28 @@ func TestFlushContentOnly_WritesCorrectContent(t *testing.T) {
|
||||
status, _ := setupViews(t, g)
|
||||
|
||||
status.SetContent("Fetching |")
|
||||
assert.NoError(t, g.flushContentOnly(g.views))
|
||||
g.flushContentOnly(g.views)
|
||||
|
||||
assert.Equal(t, "Fetching |", status.Buffer())
|
||||
}
|
||||
|
||||
func TestForceFlushViewsContentOnlyDrawsLineFlash(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
_, main := setupViews(t, g)
|
||||
main.Highlight = true
|
||||
main.SelBgColor = ColorBlue
|
||||
main.SelectedLineColorWidth = 2
|
||||
main.FocusPoint(0, 0, false)
|
||||
|
||||
main.SetLineFlash(0)
|
||||
g.ForceFlushViewsContentOnly(g.Views())
|
||||
|
||||
for x := main.x0 + 1; x <= main.x0+2; x++ {
|
||||
_, style, _ := Screen.Get(x, main.y0+1)
|
||||
assert.True(t, style.HasReverse(), "selection-bar cell at x=%d should flash", x)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessEvent_ContentOnlyEvent_SkipsTaintedCheck(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
status, main := setupViews(t, g)
|
||||
@@ -231,7 +248,7 @@ func TestFlushContentOnly_DoesNotOverdrawHigherZViews(t *testing.T) {
|
||||
assert.False(t, popup.IsTainted(), "popup should not be tainted")
|
||||
|
||||
// flushContentOnly is what spinner ticks ultimately invoke.
|
||||
assert.NoError(t, g.flushContentOnly(g.views))
|
||||
g.flushContentOnly(g.views)
|
||||
|
||||
assert.Equal(t, "P", cellAt(21, 9),
|
||||
"popup region must still show popup content after flushContentOnly; "+
|
||||
@@ -279,7 +296,7 @@ func TestFlushContentOnly_RedrawsTransitivelyOverlappingViews(t *testing.T) {
|
||||
assert.False(t, b.IsTainted())
|
||||
assert.False(t, c.IsTainted())
|
||||
|
||||
assert.NoError(t, g.flushContentOnly(g.views))
|
||||
g.flushContentOnly(g.views)
|
||||
|
||||
// a redrawn (direct).
|
||||
assert.Equal(t, "X", cellAt(5, 5), "a should be redrawn (tainted)")
|
||||
|
||||
+111
-110
@@ -91,6 +91,14 @@ type ViewMouseBinding struct {
|
||||
|
||||
// must be a mouse key
|
||||
Key KeyName
|
||||
|
||||
// If true, this binding is dispatched before ShouldHandleMouseEvent is
|
||||
// consulted, so it fires even when a popup panel is focused and the click
|
||||
// lands on a view other than that panel (which is normally swallowed). This
|
||||
// is the same early phase that hyperlink clicks are handled in; use it for
|
||||
// clicks that must stay live behind a popup, e.g. opening a diff line in the
|
||||
// editor from the main view behind the commit-message panel.
|
||||
HandleWhenPopupPanelFocused bool
|
||||
}
|
||||
|
||||
type ViewMouseBindingOpts struct {
|
||||
@@ -413,13 +421,12 @@ func (g *Gui) Size() (x, y int) {
|
||||
// corner of the terminal. It checks if the position is valid and applies
|
||||
// the given colors.
|
||||
// Should only be used if you know that the given rune is not part of a grapheme cluster.
|
||||
func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) SetRune(x, y int, ch rune, fgColor, bgColor Attribute) {
|
||||
if x < 0 || y < 0 || x >= g.maxX || y >= g.maxY {
|
||||
// swallowing error because it's not that big of a deal
|
||||
return nil
|
||||
return
|
||||
}
|
||||
tcellSetCell(x, y, string(ch), fgColor, bgColor, g.outputMode)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetView creates a new view with its top-left corner at (x0, y0)
|
||||
@@ -1189,7 +1196,8 @@ func (g *Gui) processEvent() error {
|
||||
contentOnly = contentOnly && remainingContentOnly
|
||||
|
||||
if contentOnly {
|
||||
return g.flushContentOnly(g.views)
|
||||
g.flushContentOnly(g.views)
|
||||
return nil
|
||||
}
|
||||
return g.flush()
|
||||
}
|
||||
@@ -1290,7 +1298,7 @@ func (g *Gui) onResize() {
|
||||
}
|
||||
|
||||
// drawFrameEdges draws the horizontal and vertical edges of a view.
|
||||
func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) {
|
||||
runeH, runeV := '─', '│'
|
||||
if len(v.FrameRunes) >= 2 {
|
||||
runeH, runeV = v.FrameRunes[0], v.FrameRunes[1]
|
||||
@@ -1301,14 +1309,10 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
continue
|
||||
}
|
||||
if v.y0 > -1 && v.y0 < g.maxY {
|
||||
if err := g.SetRune(x, v.y0, runeH, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y0, runeH, fgColor, bgColor)
|
||||
}
|
||||
if v.y1 > -1 && v.y1 < g.maxY {
|
||||
if err := g.SetRune(x, v.y1, runeH, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y1, runeH, fgColor, bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,19 +1322,14 @@ func (g *Gui) drawFrameEdges(v *View, fgColor, bgColor Attribute) error {
|
||||
continue
|
||||
}
|
||||
if v.x0 > -1 && v.x0 < g.maxX {
|
||||
if err := g.SetRune(v.x0, y, runeV, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(v.x0, y, runeV, fgColor, bgColor)
|
||||
}
|
||||
if v.x1 > -1 && v.x1 < g.maxX {
|
||||
runeToPrint := calcScrollbarRune(showScrollbar, realScrollbarStart, realScrollbarEnd, y, runeV)
|
||||
|
||||
if err := g.SetRune(v.x1, y, runeToPrint, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(v.x1, y, runeToPrint, fgColor, bgColor)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func calcScrollbarRune(
|
||||
@@ -1430,17 +1429,13 @@ func corner(v *View, directions byte) rune {
|
||||
}
|
||||
|
||||
// drawFrameCorners draws the corners of the view.
|
||||
func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) {
|
||||
if v.y0 == v.y1 {
|
||||
if !g.SupportOverlaps && v.x0 >= 0 && v.x1 >= 0 && v.y0 >= 0 && v.x0 < g.maxX && v.x1 < g.maxX && v.y0 < g.maxY {
|
||||
if err := g.SetRune(v.x0, v.y0, '╶', fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.SetRune(v.x1, v.y0, '╴', fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(v.x0, v.y0, '╶', fgColor, bgColor)
|
||||
g.SetRune(v.x1, v.y0, '╴', fgColor, bgColor)
|
||||
}
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
runeTL, runeTR, runeBL, runeBR := '┌', '┐', '└', '┘'
|
||||
@@ -1461,18 +1456,15 @@ func (g *Gui) drawFrameCorners(v *View, fgColor, bgColor Attribute) error {
|
||||
|
||||
for _, c := range corners {
|
||||
if c.x >= 0 && c.y >= 0 && c.x < g.maxX && c.y < g.maxY {
|
||||
if err := g.SetRune(c.x, c.y, c.ch, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(c.x, c.y, c.ch, fgColor, bgColor)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawTitle draws the title of the view.
|
||||
func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) {
|
||||
if v.y0 < 0 || v.y0 >= g.maxY {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
tabs := v.Tabs
|
||||
@@ -1508,9 +1500,7 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
|
||||
|
||||
x := v.x0 + 2
|
||||
for _, ch := range prefix {
|
||||
if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y0, ch, fgColor, bgColor)
|
||||
x += uniseg.StringWidth(string(ch))
|
||||
}
|
||||
for i, ch := range str {
|
||||
@@ -1533,64 +1523,55 @@ func (g *Gui) drawTitle(v *View, fgColor, bgColor Attribute) error {
|
||||
currentFgColor &= ^AttrBold
|
||||
}
|
||||
}
|
||||
if err := g.SetRune(x, v.y0, ch, currentFgColor, currentBgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y0, ch, currentFgColor, currentBgColor)
|
||||
x += uniseg.StringWidth(string(ch))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawSubtitle draws the subtitle of the view.
|
||||
func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) drawSubtitle(v *View, fgColor, bgColor Attribute) {
|
||||
if v.y0 < 0 || v.y0 >= g.maxY {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
start := v.x1 - 5 - uniseg.StringWidth(v.Subtitle)
|
||||
if start < v.x0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
x := start
|
||||
for _, ch := range v.Subtitle {
|
||||
if x >= v.x1 {
|
||||
break
|
||||
}
|
||||
if err := g.SetRune(x, v.y0, ch, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y0, ch, fgColor, bgColor)
|
||||
x += uniseg.StringWidth(string(ch))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawListFooter draws the footer of a list view, showing something like '1 of 10'
|
||||
func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) error {
|
||||
func (g *Gui) drawListFooter(v *View, fgColor, bgColor Attribute) {
|
||||
if len(v.buf.lines) == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
message := v.Footer
|
||||
|
||||
if v.y1 < 0 || v.y1 >= g.maxY {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
start := v.x1 - 1 - uniseg.StringWidth(message)
|
||||
if start < v.x0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
x := start
|
||||
for _, ch := range message {
|
||||
if x >= v.x1 {
|
||||
break
|
||||
}
|
||||
if err := g.SetRune(x, v.y1, ch, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.SetRune(x, v.y1, ch, fgColor, bgColor)
|
||||
x += uniseg.StringWidth(string(ch))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// flush updates the gui, re-drawing frames and buffers.
|
||||
@@ -1618,40 +1599,35 @@ func (g *Gui) flush() error {
|
||||
}
|
||||
}
|
||||
for _, v := range g.views {
|
||||
if err := g.draw(v); err != nil {
|
||||
return err
|
||||
}
|
||||
g.draw(v)
|
||||
}
|
||||
|
||||
Screen.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Redraws only tainted views and skips the layout pass.
|
||||
// Redraws only dirty views and skips the layout pass.
|
||||
// tcell's cell-level dirty tracking ensures only
|
||||
// actually-changed cells are emitted to the terminal.
|
||||
// Will also redraw any views that overlap tainted views
|
||||
func (g *Gui) flushContentOnly(views []*View) error {
|
||||
// Will also redraw any views that overlap dirty views.
|
||||
func (g *Gui) flushContentOnly(views []*View) {
|
||||
// The screen must not be touched while suspended (see Suspend).
|
||||
if g.isSuspended() {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
for _, v := range viewsToRedrawContentOnly(views) {
|
||||
if err := g.draw(v); err != nil {
|
||||
return err
|
||||
}
|
||||
g.draw(v)
|
||||
}
|
||||
|
||||
Screen.Show()
|
||||
return nil
|
||||
}
|
||||
|
||||
func viewsToRedrawContentOnly(views []*View) []*View {
|
||||
redrawIndexes := set.New[int]()
|
||||
|
||||
for i, v := range views {
|
||||
if !v.IsTainted() && !redrawIndexes.Includes(i) {
|
||||
if !v.NeedsRedraw() && !redrawIndexes.Includes(i) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1681,11 +1657,11 @@ func (g *Gui) ForceLayoutAndRedraw() error {
|
||||
return g.flush()
|
||||
}
|
||||
|
||||
// Redraws only tainted views outside of the normal main
|
||||
// Redraws only dirty views outside of the normal main
|
||||
// loop, without a layout pass. Useful during longer operations that block the
|
||||
// main thread, e.g. to update a spinner in a status view.
|
||||
func (g *Gui) ForceFlushViewsContentOnly(views []*View) error {
|
||||
return g.flushContentOnly(views)
|
||||
func (g *Gui) ForceFlushViewsContentOnly(views []*View) {
|
||||
g.flushContentOnly(views)
|
||||
}
|
||||
|
||||
// hasFocus reports whether a view is drawn as focused. Views that are embedded
|
||||
@@ -1703,9 +1679,9 @@ func outermostView(v *View) *View {
|
||||
}
|
||||
|
||||
// draw manages the cursor and calls the draw function of a view.
|
||||
func (g *Gui) draw(v *View) error {
|
||||
func (g *Gui) draw(v *View) {
|
||||
if !v.Visible || v.y1 < v.y0 || v.x1 < v.x0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
if g.Cursor {
|
||||
@@ -1744,30 +1720,18 @@ func (g *Gui) draw(v *View) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := g.drawFrameEdges(v, frameColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := g.drawFrameCorners(v, frameColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.drawFrameEdges(v, frameColor, bgColor)
|
||||
g.drawFrameCorners(v, frameColor, bgColor)
|
||||
if v.Title != "" || len(v.Tabs) > 0 {
|
||||
if err := g.drawTitle(v, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.drawTitle(v, fgColor, bgColor)
|
||||
}
|
||||
if v.Subtitle != "" {
|
||||
if err := g.drawSubtitle(v, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.drawSubtitle(v, fgColor, bgColor)
|
||||
}
|
||||
if v.Footer != "" && g.ShowListFooter {
|
||||
if err := g.drawListFooter(v, fgColor, bgColor); err != nil {
|
||||
return err
|
||||
}
|
||||
g.drawListFooter(v, fgColor, bgColor)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// onKey manages key-press events. A keybinding handler is called when
|
||||
@@ -1851,6 +1815,25 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
}
|
||||
}
|
||||
|
||||
var mouseOpts ViewMouseBindingOpts
|
||||
if IsMouseKey(ev.Key) {
|
||||
mouseOpts = ViewMouseBindingOpts{
|
||||
X: newX, Y: newY, Key: ev.Key.KeyName(),
|
||||
IsDoubleClick: g.isDoubleClick(newX, newY, ev.Key.KeyName(), v),
|
||||
}
|
||||
|
||||
// Dispatch bindings that opt into firing while a popup panel is focused
|
||||
// before the gate below gets a chance to reject the click.
|
||||
matched, err := g.execMouseKeybindings(v, ev, mouseOpts, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if matched {
|
||||
g.recordClickInfo(newX, newY, ev.Key.KeyName(), v)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if g.ShouldHandleMouseEvent != nil {
|
||||
if !g.ShouldHandleMouseEvent(v, ev.Key.KeyName()) {
|
||||
// Give clients a chance to reject clicks, for example clicks in inactive views
|
||||
@@ -1858,11 +1841,21 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
if ev.Key.KeyName() == MouseLeft && ev.Key.Mod()&ModMotion == 0 {
|
||||
// Bindings match modifiers exactly, so a gesture with a keyboard modifier held
|
||||
// is a gesture of its own, and the bindings that act on a plain click pass it
|
||||
// by. It therefore has to leave the view as it found it: the cursor stays where
|
||||
// the selection is (a list view draws its selection at the cursor), and no
|
||||
// mouse capture begins for a drag that no binding will extend. ModMotion comes
|
||||
// from the mouse rather than the keyboard, and every drag carries it, so it is
|
||||
// masked out here.
|
||||
gestureIsModified := ev.Key.Mod()&^ModMotion != ModNone
|
||||
|
||||
if ev.Key.KeyName() == MouseLeft && ev.Key.Mod() == ModNone {
|
||||
g.captureMouse(v)
|
||||
}
|
||||
|
||||
if !IsMouseScrollKey(ev.Key.KeyName()) && ev.Key.KeyName() != MouseRelease {
|
||||
if !IsMouseScrollKey(ev.Key.KeyName()) && ev.Key.KeyName() != MouseRelease &&
|
||||
!gestureIsModified {
|
||||
cursorX, cursorY := newCx, newCy
|
||||
// A captured drag can report positions outside the view; keep the
|
||||
// view cursor inside its bounds in that case. Handlers still get
|
||||
@@ -1900,9 +1893,8 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
}
|
||||
|
||||
if IsMouseKey(ev.Key) {
|
||||
isDoubleClick := g.recordClickInfo(newX, newY, ev.Key.KeyName(), v)
|
||||
opts := ViewMouseBindingOpts{X: newX, Y: newY, Key: ev.Key.KeyName(), IsDoubleClick: isDoubleClick}
|
||||
matched, err := g.execMouseKeybindings(v, ev, opts)
|
||||
g.recordClickInfo(newX, newY, ev.Key.KeyName(), v)
|
||||
matched, err := g.execMouseKeybindings(v, ev, mouseOpts, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1934,43 +1926,49 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// remember the information for this click, and return true if it was a double click
|
||||
func (g *Gui) recordClickInfo(x, y int, key KeyName, v *View) bool {
|
||||
// isDoubleClick reports whether this click follows one just like it, closely
|
||||
// enough in time to count as a double click.
|
||||
func (g *Gui) isDoubleClick(x, y int, key KeyName, v *View) bool {
|
||||
return g.lastClick != nil &&
|
||||
!IsMouseScrollKey(key) &&
|
||||
key != MouseRelease &&
|
||||
x == g.lastClick.x &&
|
||||
y == g.lastClick.y &&
|
||||
key == g.lastClick.key &&
|
||||
v.Name() == g.lastClick.viewName &&
|
||||
time.Now().Before(g.lastClick.time.Add(DOUBLE_CLICK_THRESHOLD))
|
||||
}
|
||||
|
||||
// recordClickInfo remembers this click as the one a following click is compared
|
||||
// against. Only the clicks that reach a binding are recorded, so a click the
|
||||
// client rejects leaves double-click detection where it was.
|
||||
func (g *Gui) recordClickInfo(x, y int, key KeyName, v *View) {
|
||||
if IsMouseScrollKey(key) {
|
||||
g.lastClick = nil
|
||||
return false
|
||||
return
|
||||
}
|
||||
// A release ends a gesture but is not a click of its own; it must leave
|
||||
// the click info of the press that started it alone, or no double click
|
||||
// could ever be detected.
|
||||
if key == MouseRelease {
|
||||
return false
|
||||
return
|
||||
}
|
||||
|
||||
clickInfo := &clickInfo{
|
||||
g.lastClick = &clickInfo{
|
||||
x: x,
|
||||
y: y,
|
||||
key: key,
|
||||
viewName: v.Name(),
|
||||
time: time.Now(),
|
||||
}
|
||||
|
||||
isDoubleClick := g.lastClick != nil &&
|
||||
clickInfo.x == g.lastClick.x &&
|
||||
clickInfo.y == g.lastClick.y &&
|
||||
clickInfo.key == g.lastClick.key &&
|
||||
clickInfo.viewName == g.lastClick.viewName &&
|
||||
clickInfo.time.Before(g.lastClick.time.Add(DOUBLE_CLICK_THRESHOLD))
|
||||
|
||||
g.lastClick = clickInfo
|
||||
return isDoubleClick
|
||||
}
|
||||
|
||||
func (g *Gui) execMouseKeybindings(view *View, ev *GocuiEvent, opts ViewMouseBindingOpts) (bool, error) {
|
||||
func (g *Gui) execMouseKeybindings(view *View, ev *GocuiEvent, opts ViewMouseBindingOpts, handleWhenPopupPanelFocused bool) (bool, error) {
|
||||
isMatch := func(binding *ViewMouseBinding) bool {
|
||||
return binding.ViewName == view.Name() &&
|
||||
ev.Key.KeyName() == binding.Key &&
|
||||
ev.Key.Mod() == binding.Modifier
|
||||
ev.Key.Mod() == binding.Modifier &&
|
||||
binding.HandleWhenPopupPanelFocused == handleWhenPopupPanelFocused
|
||||
}
|
||||
|
||||
// first pass looks for ones that match the focused view
|
||||
@@ -2145,6 +2143,9 @@ func (g *Gui) Suspend() error {
|
||||
return errors.New("Already suspended")
|
||||
}
|
||||
|
||||
for _, view := range g.views {
|
||||
view.ClearLineFlash()
|
||||
}
|
||||
g.suspended = true
|
||||
|
||||
if err := g.screen.Suspend(); err != nil {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package gocui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gdamore/tcell/v3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAModifiedClickNoBindingWantsLeavesTheViewAlone(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
g := newTestGui(t)
|
||||
view, _ := g.SetView("list", 0, 0, 20, 10, 0)
|
||||
|
||||
clicks := 0
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "list",
|
||||
Key: MouseLeft,
|
||||
Handler: func(ViewMouseBindingOpts) error {
|
||||
clicks++
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
click := func(y int, modifier tcell.ModMask) {
|
||||
for _, button := range []tcell.ButtonMask{tcell.ButtonPrimary, tcell.ButtonNone} {
|
||||
event := gocuiEventFromTcellEvent(
|
||||
tcell.NewEventMouse(view.x0+1, y, button, modifier))
|
||||
assert.NoError(t, g.onKey(&event))
|
||||
}
|
||||
}
|
||||
|
||||
// A plain click is the binding's, and moves the cursor it acts on.
|
||||
click(view.y0+4, tcell.ModNone)
|
||||
assert.Equal(t, 1, clicks)
|
||||
assert.Equal(t, 3, view.CursorY())
|
||||
|
||||
// An alt-click is nobody's here, bindings matching modifiers exactly. It has to
|
||||
// leave the cursor where the selection is, or a list view would draw its
|
||||
// selection on a line its owner never selected.
|
||||
click(view.y0+8, tcell.ModAlt)
|
||||
assert.Equal(t, 1, clicks)
|
||||
assert.Equal(t, 3, view.CursorY())
|
||||
assert.Nil(t, g.mouseCapture, "and no drag begins for it either")
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package gocui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestOnlyBindingsThatOptedInFireBehindAFocusedPopup(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
_, _ = g.SetView("main", 0, 0, 20, 10, 0)
|
||||
|
||||
fired := []string{}
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "main",
|
||||
Key: MouseLeft,
|
||||
Modifier: ModAlt,
|
||||
HandleWhenPopupPanelFocused: true,
|
||||
Handler: func(ViewMouseBindingOpts) error {
|
||||
fired = append(fired, "opted in")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
g.SetViewClickBinding(&ViewMouseBinding{
|
||||
ViewName: "main",
|
||||
Key: MouseLeft,
|
||||
Handler: func(ViewMouseBindingOpts) error {
|
||||
fired = append(fired, "ordinary")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// This is how a client reports that a popup panel has the focus and the click
|
||||
// landed on a view behind it.
|
||||
g.ShouldHandleMouseEvent = func(*View, KeyName) bool { return false }
|
||||
|
||||
assert.NoError(t, g.onKey(&GocuiEvent{
|
||||
Type: eventMouse, MouseX: 3, MouseY: 4,
|
||||
Key: NewKey(MouseLeft, "", ModAlt),
|
||||
}))
|
||||
assert.Equal(t, []string{"opted in"}, fired)
|
||||
|
||||
assert.NoError(t, g.onKey(&GocuiEvent{
|
||||
Type: eventMouse, MouseX: 3, MouseY: 4,
|
||||
Key: NewKey(MouseLeft, "", ModNone),
|
||||
}))
|
||||
assert.Equal(t, []string{"opted in"}, fired,
|
||||
"a binding that didn't opt in must still be swallowed")
|
||||
}
|
||||
@@ -18,7 +18,7 @@ func TestFlushIsNoOpWhileSuspended(t *testing.T) {
|
||||
flush func(g *Gui) error
|
||||
}{
|
||||
{"flush", func(g *Gui) error { return g.flush() }},
|
||||
{"flushContentOnly", func(g *Gui) error { return g.flushContentOnly(g.views) }},
|
||||
{"flushContentOnly", func(g *Gui) error { g.flushContentOnly(g.views); return nil }},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
@@ -67,3 +67,14 @@ func TestResumeSchedulesRedraw(t *testing.T) {
|
||||
assert.Equal(t, eventResize, ev.Type,
|
||||
"resuming must schedule a redraw; without one the screen stays blank until the next event arrives")
|
||||
}
|
||||
|
||||
func TestSuspendClearsLineFlashes(t *testing.T) {
|
||||
g := newTestGui(t)
|
||||
v, err := g.SetView("main", 0, 0, 20, 10, 0)
|
||||
assert.ErrorIs(t, err, ErrUnknownView)
|
||||
v.SetLineFlash(3)
|
||||
|
||||
assert.NoError(t, g.Suspend())
|
||||
assert.Equal(t, -1, v.lineFlashY)
|
||||
assert.NoError(t, g.Resume())
|
||||
}
|
||||
|
||||
@@ -209,6 +209,7 @@ const (
|
||||
|
||||
var (
|
||||
lastMouseKey tcell.ButtonMask = tcell.ButtonNone
|
||||
lastMouseMod tcell.ModMask = tcell.ModNone
|
||||
dragState = NOT_DRAGGING
|
||||
lastX = 0
|
||||
lastY = 0
|
||||
@@ -377,6 +378,12 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
|
||||
if button != tcell.ButtonNone && lastMouseKey == tcell.ButtonNone {
|
||||
newButtonPress = true
|
||||
lastMouseKey = button
|
||||
// The keyboard modifiers held at press time apply to the whole gesture:
|
||||
// the press, every drag event, and the release. Snapshotting them here
|
||||
// keeps a modified press from producing events that match unmodified
|
||||
// bindings, and ignores modifier changes while the button is held.
|
||||
lastMouseMod = tev.Modifiers()
|
||||
mouseMod = Modifier(lastMouseMod)
|
||||
switch button {
|
||||
case tcell.ButtonPrimary:
|
||||
mouseKey = MouseLeft
|
||||
@@ -402,7 +409,8 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
|
||||
case tcell.ButtonMiddle:
|
||||
default:
|
||||
}
|
||||
mouseMod = ModNone
|
||||
mouseMod = Modifier(lastMouseMod)
|
||||
lastMouseMod = tcell.ModNone
|
||||
lastMouseKey = tcell.ButtonNone
|
||||
}
|
||||
default:
|
||||
@@ -433,10 +441,10 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
|
||||
// reaches drag bindings instead of being delivered with the
|
||||
// default MouseRelease key.
|
||||
dragState = DRAGGING
|
||||
mouseMod = ModMotion
|
||||
mouseMod = Modifier(lastMouseMod) | ModMotion
|
||||
mouseKey = MouseLeft
|
||||
case DRAGGING:
|
||||
mouseMod = ModMotion
|
||||
mouseMod = Modifier(lastMouseMod) | ModMotion
|
||||
mouseKey = MouseLeft
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,21 +36,55 @@ func TestMouseReleaseAfterDragIsMouseEvent(t *testing.T) {
|
||||
assert.Equal(t, MouseRelease, releaseEvent.Key.KeyName())
|
||||
}
|
||||
|
||||
func TestMouseReleaseDoesNotKeepPressModifiers(t *testing.T) {
|
||||
func TestWholeGestureCarriesPressModifiers(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
|
||||
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonPrimary, tcell.ModAlt))
|
||||
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonPrimary, tcell.ModAlt))
|
||||
pressEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonPrimary, tcell.ModAlt))
|
||||
dragEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonPrimary, tcell.ModAlt))
|
||||
releaseEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonNone, tcell.ModAlt))
|
||||
|
||||
assert.Equal(t, eventMouse, pressEvent.Type)
|
||||
assert.Equal(t, MouseLeft, pressEvent.Key.KeyName())
|
||||
assert.Equal(t, ModAlt, pressEvent.Key.Mod())
|
||||
assert.Equal(t, eventMouse, dragEvent.Type)
|
||||
assert.Equal(t, MouseLeft, dragEvent.Key.KeyName())
|
||||
assert.Equal(t, ModAlt|ModMotion, dragEvent.Key.Mod())
|
||||
assert.Equal(t, eventMouse, releaseEvent.Type)
|
||||
assert.Equal(t, MouseRelease, releaseEvent.Key.KeyName())
|
||||
assert.Equal(t, ModAlt, releaseEvent.Key.Mod())
|
||||
}
|
||||
|
||||
func TestModifierChangesWhileButtonHeldAreIgnored(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
|
||||
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonPrimary, tcell.ModNone))
|
||||
dragEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonPrimary, tcell.ModAlt))
|
||||
releaseEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonNone, tcell.ModAlt))
|
||||
|
||||
assert.Equal(t, ModMotion, dragEvent.Key.Mod())
|
||||
assert.Equal(t, ModNone, releaseEvent.Key.Mod())
|
||||
}
|
||||
|
||||
func TestModifiedClickWithoutDragCarriesModifierOnPressAndRelease(t *testing.T) {
|
||||
t.Cleanup(resetMouseState)
|
||||
resetMouseState()
|
||||
|
||||
pressEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonPrimary, tcell.ModShift))
|
||||
releaseEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonNone, tcell.ModShift))
|
||||
|
||||
assert.Equal(t, eventMouse, pressEvent.Type)
|
||||
assert.Equal(t, MouseLeft, pressEvent.Key.KeyName())
|
||||
assert.Equal(t, ModShift, pressEvent.Key.Mod())
|
||||
assert.Equal(t, eventMouse, releaseEvent.Type)
|
||||
assert.Equal(t, MouseRelease, releaseEvent.Key.KeyName())
|
||||
assert.Equal(t, ModShift, releaseEvent.Key.Mod())
|
||||
}
|
||||
|
||||
func resetMouseState() {
|
||||
lastMouseKey = tcell.ButtonNone
|
||||
lastMouseMod = tcell.ModNone
|
||||
dragState = NOT_DRAGGING
|
||||
lastX = 0
|
||||
lastY = 0
|
||||
|
||||
+134
-35
@@ -80,18 +80,27 @@ type View struct {
|
||||
// a user starts a range select and then moves the cursor up.
|
||||
rangeSelectStartY int
|
||||
|
||||
// The view line whose selection-width bar is temporarily reversed. A value
|
||||
// of -1 means that no line is flashing.
|
||||
lineFlashY int
|
||||
|
||||
// readBuffer is used for storing unread bytes
|
||||
readBuffer []byte
|
||||
|
||||
// tained is true if the viewLines must be updated
|
||||
tainted bool
|
||||
|
||||
// needsRedraw is true if the view's current state has not been drawn to the
|
||||
// screen yet. A tainted view always needs a redraw, but draw-only state can
|
||||
// require one without invalidating viewLines.
|
||||
needsRedraw bool
|
||||
|
||||
// firstDirtyLine is the index of the lowest line in `lines` that has been
|
||||
// written to or highlighted since viewLines was last refreshed, and whose
|
||||
// cached wrapping (lineType.wrappedCells) may therefore be stale. Lines
|
||||
// below it are unchanged and can reuse their cached wrapping instead of
|
||||
// being re-wrapped, which keeps refreshViewLinesIfNeeded cheap while
|
||||
// scrolling appends new lines to a long buffer.
|
||||
// written to since viewLines was last refreshed, and whose cached wrapping
|
||||
// (lineType.wrappedCells) may therefore be stale. Lines below it are
|
||||
// unchanged and can reuse their cached wrapping instead of being
|
||||
// re-wrapped, which keeps refreshViewLinesIfNeeded cheap while scrolling
|
||||
// appends new lines to a long buffer.
|
||||
firstDirtyLine int
|
||||
|
||||
// the last position that the mouse was hovering over; nil if the mouse is outside of
|
||||
@@ -170,6 +179,17 @@ type View struct {
|
||||
// For content that conveys meaning by color of its own.
|
||||
SelectedLineColorWidth int
|
||||
|
||||
// InclusionGutterMarker is the glyph the inclusion gutter draws on a marked line
|
||||
// (see SetInclusionGutter), and InclusionGutterMarkerColor its color. Both are
|
||||
// set once, when the view is created.
|
||||
InclusionGutterMarker string
|
||||
InclusionGutterMarkerColor Attribute
|
||||
// showInclusionGutter reserves the gutter's columns at the left of every line,
|
||||
// and inclusionGutterMarks, indexed by line of the content, says which lines get
|
||||
// the marker. Set together, via SetInclusionGutter.
|
||||
showInclusionGutter bool
|
||||
inclusionGutterMarks []bool
|
||||
|
||||
// If Frame is true, a border will be drawn around the view.
|
||||
Frame bool
|
||||
|
||||
@@ -263,11 +283,18 @@ type pos struct {
|
||||
// a view whose size has changed, whose content is the same but has to be wrapped
|
||||
// afresh, call RewrapContent instead.
|
||||
func (v *View) clearViewLines() {
|
||||
v.tainted = true
|
||||
v.markViewLinesDirty()
|
||||
v.viewLines = nil
|
||||
v.clearHover()
|
||||
}
|
||||
|
||||
// markViewLinesDirty records that the cached viewLines no longer represent the
|
||||
// view's buffer or wrapping, so both rebuilding and redrawing are required.
|
||||
func (v *View) markViewLinesDirty() {
|
||||
v.tainted = true
|
||||
v.needsRedraw = true
|
||||
}
|
||||
|
||||
// RewrapContent wraps the view's content for the size the view has now, and puts
|
||||
// the positions into that content — the scroll offset, the cursor, a range's
|
||||
// anchor — back on the lines they were on. They are all view lines, which count
|
||||
@@ -782,11 +809,13 @@ func NewView(name string, x0, y0, x1, y1 int, mode OutputMode) *View {
|
||||
Frame: true,
|
||||
Editor: DefaultEditor,
|
||||
tainted: true,
|
||||
needsRedraw: true,
|
||||
outMode: mode,
|
||||
buf: &viewBuffer{ei: newEscapeInterpreter(mode)},
|
||||
searcher: &searcher{},
|
||||
TextArea: &TextArea{},
|
||||
rangeSelectStartY: -1,
|
||||
lineFlashY: -1,
|
||||
TabWidth: 4,
|
||||
}
|
||||
|
||||
@@ -860,6 +889,38 @@ func (v *View) Name() string {
|
||||
return v.name
|
||||
}
|
||||
|
||||
// SetInclusionGutter shows or hides a column reserved at the left of every line, in
|
||||
// which marks — indexed by line of the content — say which lines get
|
||||
// InclusionGutterMarker drawn, on every segment of a line the view wrapped. The
|
||||
// content is drawn shifted past it.
|
||||
//
|
||||
// It is drawn over the content rather than written into it, so the content itself —
|
||||
// and with it what each line of the view means, where a click lands, and how the
|
||||
// lines wrap — is untouched but for the width the gutter takes.
|
||||
func (v *View) SetInclusionGutter(show bool, marks []bool) {
|
||||
v.writeMutex.Lock()
|
||||
changed := v.showInclusionGutter != show
|
||||
v.showInclusionGutter = show
|
||||
v.inclusionGutterMarks = marks
|
||||
v.writeMutex.Unlock()
|
||||
|
||||
if changed {
|
||||
// The gutter takes its columns from the content, so what is left of it wraps
|
||||
// differently, and everything pointing into it has to come along.
|
||||
v.RewrapContent()
|
||||
}
|
||||
}
|
||||
|
||||
// inclusionGutterWidth is how many columns the inclusion gutter takes while it is
|
||||
// shown — the marker plus a column of space before the content — and 0 while it is
|
||||
// not. Only call this with a lock on writeMutex.
|
||||
func (v *View) inclusionGutterWidth() int {
|
||||
if !v.showInclusionGutter {
|
||||
return 0
|
||||
}
|
||||
return uniseg.StringWidth(v.InclusionGutterMarker) + 1
|
||||
}
|
||||
|
||||
// setCharacter sets a character (grapheme cluster) at the given point relative to the view. It applies
|
||||
// the specified colors, taking into account if the cell must be highlighted. Also, it checks if the
|
||||
// position is valid.
|
||||
@@ -906,6 +967,10 @@ func (v *View) setCharacter(x, y int, ch string, fgColor, bgColor Attribute, isW
|
||||
fgColor |= AttrUnderline
|
||||
}
|
||||
|
||||
if v.lineFlashY == v.oy+y && (v.SelectedLineColorWidth == 0 || x < v.SelectedLineColorWidth) {
|
||||
fgColor ^= AttrReverse
|
||||
}
|
||||
|
||||
// Don't display empty characters
|
||||
if ch == "" {
|
||||
ch = " "
|
||||
@@ -1126,7 +1191,7 @@ func (v *View) write(p []byte) {
|
||||
return
|
||||
}
|
||||
|
||||
v.tainted = true
|
||||
v.markViewLinesDirty()
|
||||
// write only ever touches lines from v.buf.wy onwards, so any cached wrapping
|
||||
// below that stays valid.
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, v.buf.wy)
|
||||
@@ -1561,7 +1626,7 @@ func (v *View) SwapInOffscreenRender() {
|
||||
}
|
||||
v.buf = v.offscreen
|
||||
v.offscreen = nil
|
||||
v.tainted = true
|
||||
v.markViewLinesDirty()
|
||||
v.clearHover()
|
||||
}
|
||||
|
||||
@@ -1725,6 +1790,12 @@ func (v *View) IsTainted() bool {
|
||||
return v.tainted
|
||||
}
|
||||
|
||||
func (v *View) NeedsRedraw() bool {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
return v.needsRedraw
|
||||
}
|
||||
|
||||
// draw re-draws the view's contents.
|
||||
func (v *View) draw(isWindowFocused bool) {
|
||||
v.writeMutex.Lock()
|
||||
@@ -1733,6 +1804,7 @@ func (v *View) draw(isWindowFocused bool) {
|
||||
if !v.Visible {
|
||||
return
|
||||
}
|
||||
defer func() { v.needsRedraw = false }()
|
||||
|
||||
v.clearRunes()
|
||||
|
||||
@@ -1764,6 +1836,8 @@ func (v *View) draw(isWindowFocused bool) {
|
||||
|
||||
emptyCell := cell{chr: " ", width: 1, fgColor: ColorDefault, bgColor: ColorDefault}
|
||||
|
||||
gutterWidth := v.inclusionGutterWidth()
|
||||
|
||||
for y, vline := range v.viewLines[start:] {
|
||||
if y >= maxY {
|
||||
break
|
||||
@@ -1778,10 +1852,20 @@ func (v *View) draw(isWindowFocused bool) {
|
||||
trailingCell.bgColor = attrs.bg
|
||||
}
|
||||
|
||||
// The inclusion gutter is blank but for the marker on a marked line, and the
|
||||
// content begins after it. The blanks go through setCharacter like everything
|
||||
// else, so that a selection reaching the left edge covers the gutter too.
|
||||
for gx := range gutterWidth {
|
||||
v.setCharacter(gx, y, " ", v.FgColor, v.BgColor, isWindowFocused)
|
||||
}
|
||||
if gutterWidth > 0 && vline.linesY < len(v.inclusionGutterMarks) && v.inclusionGutterMarks[vline.linesY] {
|
||||
v.setCharacter(0, y, v.InclusionGutterMarker, v.InclusionGutterMarkerColor, v.BgColor, isWindowFocused)
|
||||
}
|
||||
|
||||
// x tracks the current x position in the view, and cellIdx tracks the
|
||||
// index of the cell. If we print a double-sized rune, we increment cellIdx
|
||||
// by one but x by two.
|
||||
x := -v.ox
|
||||
x := gutterWidth - v.ox
|
||||
cellIdx := 0
|
||||
|
||||
var c cell
|
||||
@@ -1795,7 +1879,7 @@ func (v *View) draw(isWindowFocused bool) {
|
||||
|
||||
// no more characters to write so we're only going to be printing empty cells
|
||||
// past this point
|
||||
x = 0
|
||||
x = gutterWidth
|
||||
}
|
||||
|
||||
// if we're out of cells to write, we'll just print empty cells.
|
||||
@@ -1830,10 +1914,11 @@ func (v *View) refreshViewLinesIfNeeded() {
|
||||
return
|
||||
}
|
||||
|
||||
maxX := v.InnerWidth()
|
||||
wrap := 0
|
||||
if v.Wrap {
|
||||
wrap = maxX
|
||||
// The inclusion gutter, while it is shown, takes its columns out of the width
|
||||
// the content has to wrap in.
|
||||
wrap = max(0, v.InnerWidth()-v.inclusionGutterWidth())
|
||||
}
|
||||
|
||||
lineIdx := 0
|
||||
@@ -1844,8 +1929,8 @@ func (v *View) refreshViewLinesIfNeeded() {
|
||||
// Reuse the previously wrapped result for lines that haven't changed
|
||||
// since the last refresh (i.e. below firstDirtyLine) and were wrapped at
|
||||
// the current width. Wrapping is expensive and this loop runs on every
|
||||
// scroll event, so only the lines that were actually just read (or
|
||||
// re-highlighted) should be wrapped afresh.
|
||||
// scroll event, so only the lines that were actually just read should
|
||||
// be wrapped afresh.
|
||||
if line.wrappedCells == nil || line.wrappedColumns != wrap || i >= v.firstDirtyLine {
|
||||
line.wrappedCells = lineWrap(line.cells, wrap)
|
||||
line.wrappedColumns = wrap
|
||||
@@ -1975,6 +2060,26 @@ func (v *View) BufferLines() []string {
|
||||
return lines
|
||||
}
|
||||
|
||||
// MarkedLines returns the lines of the view's content that the inclusion gutter is
|
||||
// marking (see SetInclusionGutter), in the order they appear. Empty while the gutter
|
||||
// is hidden.
|
||||
func (v *View) MarkedLines() []string {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
if !v.showInclusionGutter {
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := []string{}
|
||||
for i, line := range v.buf.lines {
|
||||
if i < len(v.inclusionGutterMarks) && v.inclusionGutterMarks[i] {
|
||||
lines = append(lines, line.cells.String())
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
// DiffLineContent holds what one line of a rendered diff offers to a reader trying
|
||||
// to recover which line of which file it came from: the line's text, which can be
|
||||
// parsed as a unified diff when the rendering preserves one, and the OSC 1717
|
||||
@@ -2219,28 +2324,22 @@ func applySelTextColor(fgColor, selTextColor Attribute) Attribute {
|
||||
return fgColor | selTextColor&AttrStyleBits
|
||||
}
|
||||
|
||||
// SetHighlight toggles highlighting of separate lines, for custom lists
|
||||
// or multiple selection in views.
|
||||
func (v *View) SetHighlight(y int, on bool) {
|
||||
if y < 0 || y >= len(v.buf.lines) {
|
||||
return
|
||||
}
|
||||
// SetLineFlash temporarily marks a view line without moving or changing the
|
||||
// selection. The caller owns the lifetime and clears it with ClearLineFlash.
|
||||
func (v *View) SetLineFlash(viewLine int) {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
cells := make([]cell, 0, len(v.buf.lines[y].cells))
|
||||
for _, c := range v.buf.lines[y].cells {
|
||||
if on {
|
||||
c.bgColor = v.SelBgColor
|
||||
c.fgColor = v.SelFgColor
|
||||
} else {
|
||||
c.bgColor = v.BgColor
|
||||
c.fgColor = v.FgColor
|
||||
}
|
||||
cells = append(cells, c)
|
||||
}
|
||||
v.tainted = true
|
||||
v.firstDirtyLine = min(v.firstDirtyLine, y)
|
||||
v.buf.lines[y].cells = cells
|
||||
v.clearHover()
|
||||
v.lineFlashY = viewLine
|
||||
v.needsRedraw = true
|
||||
}
|
||||
|
||||
func (v *View) ClearLineFlash() {
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.lineFlashY = -1
|
||||
v.needsRedraw = true
|
||||
}
|
||||
|
||||
func lineWrap(line []cell, columns int) [][]cell {
|
||||
|
||||
@@ -1093,6 +1093,32 @@ func TestSelectedLinesOfWrappedContent(t *testing.T) {
|
||||
assert.Equal(t, []string{"a line that wraps"}, v.SelectedLines())
|
||||
}
|
||||
|
||||
func TestLineFlashReversesTheSelectionBarWithoutChangingSelection(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal)
|
||||
v.Highlight = true
|
||||
v.SelBgColor = ColorBlue
|
||||
v.SelectedLineColorWidth = 2
|
||||
v.writeString("one\ntwo\nthree\n")
|
||||
v.FocusPoint(0, 1, false)
|
||||
v.SetLineFlash(1)
|
||||
v.draw(true)
|
||||
|
||||
for x := 1; x <= 2; x++ {
|
||||
_, style, _ := Screen.Get(x, 2)
|
||||
assert.True(t, style.HasReverse(), "selection-bar cell at (%d, 2) should flash", x)
|
||||
}
|
||||
_, style, _ := Screen.Get(3, 2)
|
||||
assert.False(t, style.HasReverse(), "the flash should stop after the selection bar")
|
||||
assert.Equal(t, "two", v.SelectedLine(), "flashing should not change the selection")
|
||||
|
||||
v.ClearLineFlash()
|
||||
v.draw(true)
|
||||
_, style, _ = Screen.Get(1, 2)
|
||||
assert.False(t, style.HasReverse(), "clearing should remove the flash")
|
||||
}
|
||||
|
||||
// Resizing a view throws away the wrapping of its content and wraps it again for
|
||||
// the new width, which moves every line of it to a different view line. The
|
||||
// positions into the view count view lines, so they all have to come along.
|
||||
@@ -1119,3 +1145,91 @@ func TestResizingAWrappingViewKeepsItsPlaceInTheContent(t *testing.T) {
|
||||
|
||||
assert.Equal(t, []string{"another wrapping line"}, v.SelectedLines())
|
||||
}
|
||||
|
||||
// The inclusion gutter reserves columns at the left of every line, draws its marker
|
||||
// on the marked lines only, and moves the content out of the way.
|
||||
func TestInclusionGutter(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
// InnerWidth 10; the frame puts view x=0 at screen x=1.
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal)
|
||||
v.Wrap = true
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
v.writeString("aaa\nbbb\nccc\n")
|
||||
|
||||
// The gutter is two columns wide — the marker and a space; mark the middle line.
|
||||
v.SetInclusionGutter(true, []bool{false, true, false})
|
||||
v.draw(true)
|
||||
|
||||
chr, _, _ := Screen.Get(1, 1)
|
||||
assert.Equal(t, " ", chr, "an unmarked line has no marker")
|
||||
chr, _, _ = Screen.Get(1, 2)
|
||||
assert.Equal(t, "✓", chr, "a marked line has one")
|
||||
chr, _, _ = Screen.Get(1, 3)
|
||||
assert.Equal(t, " ", chr, "an unmarked line has no marker")
|
||||
|
||||
// The content begins after the gutter: view x=2, i.e. screen x=3.
|
||||
chr, _, _ = Screen.Get(3, 1)
|
||||
assert.Equal(t, "a", chr)
|
||||
chr, _, _ = Screen.Get(3, 2)
|
||||
assert.Equal(t, "b", chr)
|
||||
chr, _, _ = Screen.Get(3, 3)
|
||||
assert.Equal(t, "c", chr)
|
||||
|
||||
// Hiding the gutter puts the content back at the left edge.
|
||||
v.SetInclusionGutter(false, nil)
|
||||
v.draw(true)
|
||||
chr, _, _ = Screen.Get(1, 1)
|
||||
assert.Equal(t, "a", chr)
|
||||
}
|
||||
|
||||
// A marked line the view wraps is marked on every segment it is drawn as, so that
|
||||
// the mark doesn't look like it belongs to the first part of the line alone. The
|
||||
// gutter takes its columns out of the width the content wraps in.
|
||||
func TestInclusionGutterMarksEverySegmentOfAWrappedLine(t *testing.T) {
|
||||
WithSimulationScreen(t, 14, 6)
|
||||
|
||||
v := NewView("name", 0, 0, 11, 5, OutputNormal) // InnerWidth 10
|
||||
v.Wrap = true
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
// Ten cells, wrapping at eight once the two-column gutter is shown.
|
||||
v.writeString("0123456789\n")
|
||||
v.SetInclusionGutter(true, []bool{true})
|
||||
v.draw(true)
|
||||
|
||||
chr, _, _ := Screen.Get(1, 1)
|
||||
assert.Equal(t, "✓", chr)
|
||||
chr, _, _ = Screen.Get(3, 1)
|
||||
assert.Equal(t, "0", chr)
|
||||
chr, _, _ = Screen.Get(10, 1)
|
||||
assert.Equal(t, "7", chr, "the content wraps at the width the gutter leaves it")
|
||||
|
||||
chr, _, _ = Screen.Get(1, 2)
|
||||
assert.Equal(t, "✓", chr, "the line's second segment is marked too")
|
||||
chr, _, _ = Screen.Get(3, 2)
|
||||
assert.Equal(t, "8", chr)
|
||||
}
|
||||
|
||||
// Showing the gutter narrows the content, so the content wraps again — and the
|
||||
// positions into it, which count the segments lines are drawn as, have to come
|
||||
// along, as they do for any other change of width.
|
||||
func TestShowingTheInclusionGutterKeepsThePlaceInTheContent(t *testing.T) {
|
||||
v := NewView("name", 0, 0, 11, 10, OutputNormal) // InnerWidth 10
|
||||
v.Wrap = true
|
||||
v.Highlight = true
|
||||
v.InclusionGutterMarker = "✓"
|
||||
|
||||
v.writeString("one\ntwo\nthree\nsomethingfartoolong\n")
|
||||
assert.Equal(t, 5, v.ViewLinesHeight())
|
||||
|
||||
v.FocusPoint(0, 2, false)
|
||||
assert.Equal(t, "three", v.SelectedLine())
|
||||
|
||||
// With eight columns left for the content, the last line wraps into three
|
||||
// segments rather than two.
|
||||
v.SetInclusionGutter(true, []bool{false, false, true, false})
|
||||
assert.Equal(t, 6, v.ViewLinesHeight())
|
||||
assert.Equal(t, "three", v.SelectedLine())
|
||||
}
|
||||
|
||||
@@ -350,18 +350,6 @@ func (self *ContextMgr) AllList() []types.IListContext {
|
||||
return listContexts
|
||||
}
|
||||
|
||||
func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext {
|
||||
var listContexts []types.IPatchExplorerContext
|
||||
|
||||
for _, context := range self.allContexts.Flatten() {
|
||||
if listContext, ok := context.(types.IPatchExplorerContext); ok {
|
||||
listContexts = append(listContexts, listContext)
|
||||
}
|
||||
}
|
||||
|
||||
return listContexts
|
||||
}
|
||||
|
||||
func (self *ContextMgr) ContextForKey(key types.ContextKey) types.Context {
|
||||
self.RLock()
|
||||
defer self.RUnlock()
|
||||
|
||||
@@ -19,16 +19,36 @@ type CommitFilesContext struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ types.IListContext = (*CommitFilesContext)(nil)
|
||||
_ types.DiffableContext = (*CommitFilesContext)(nil)
|
||||
_ types.IFilterableContext = (*CommitFilesContext)(nil)
|
||||
_ types.DiffMainViewContext = (*CommitFilesContext)(nil)
|
||||
_ types.IListContext = (*CommitFilesContext)(nil)
|
||||
_ types.DiffableContext = (*CommitFilesContext)(nil)
|
||||
_ types.IFilterableContext = (*CommitFilesContext)(nil)
|
||||
_ types.DiffMainViewContext = (*CommitFilesContext)(nil)
|
||||
_ types.PullRequestDiffContext = (*CommitFilesContext)(nil)
|
||||
)
|
||||
|
||||
func (self *CommitFilesContext) GetDiffMainViewType() types.DiffMainViewType {
|
||||
return types.DiffMainViewTypePatchBuilding
|
||||
}
|
||||
|
||||
// BranchForPullRequest asks the panel this one was entered from: the files are a
|
||||
// commit's, and which branch's pull request that commit is up for review in is known
|
||||
// there rather than here.
|
||||
func (self *CommitFilesContext) BranchForPullRequest() string {
|
||||
if parent, ok := self.GetParentContext().(types.PullRequestDiffContext); ok {
|
||||
return parent.BranchForPullRequest()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CommitsForPullRequest asks the panel this one was entered from as well: the files
|
||||
// listed here are those of the commits selected there.
|
||||
func (self *CommitFilesContext) CommitsForPullRequest() ([]*models.Commit, string) {
|
||||
if parent, ok := self.GetParentContext().(types.PullRequestDiffContext); ok {
|
||||
return parent.CommitsForPullRequest()
|
||||
}
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
||||
viewModel := filetree.NewCommitFileTreeViewModel(
|
||||
func() []*models.CommitFile { return c.Model().CommitFiles },
|
||||
|
||||
+42
-58
@@ -8,27 +8,23 @@ const (
|
||||
// used as a nil value when passing a context key as an arg
|
||||
NO_CONTEXT types.ContextKey = "none"
|
||||
|
||||
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
|
||||
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
||||
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
||||
FILES_CONTEXT_KEY types.ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
||||
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
||||
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY types.ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY types.ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY types.ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY types.ContextKey = "stash"
|
||||
NORMAL_MAIN_CONTEXT_KEY types.ContextKey = "normal"
|
||||
NORMAL_SECONDARY_CONTEXT_KEY types.ContextKey = "normalSecondary"
|
||||
STAGING_MAIN_CONTEXT_KEY types.ContextKey = "staging"
|
||||
STAGING_SECONDARY_CONTEXT_KEY types.ContextKey = "stagingSecondary"
|
||||
PATCH_BUILDING_MAIN_CONTEXT_KEY types.ContextKey = "patchBuilding"
|
||||
PATCH_BUILDING_SECONDARY_CONTEXT_KEY types.ContextKey = "patchBuildingSecondary"
|
||||
MERGE_CONFLICTS_CONTEXT_KEY types.ContextKey = "mergeConflicts"
|
||||
GLOBAL_CONTEXT_KEY types.ContextKey = "global"
|
||||
STATUS_CONTEXT_KEY types.ContextKey = "status"
|
||||
SNAKE_CONTEXT_KEY types.ContextKey = "snake"
|
||||
FILES_CONTEXT_KEY types.ContextKey = "files"
|
||||
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
|
||||
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
|
||||
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
|
||||
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
|
||||
TAGS_CONTEXT_KEY types.ContextKey = "tags"
|
||||
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
|
||||
REFLOG_COMMITS_CONTEXT_KEY types.ContextKey = "reflogCommits"
|
||||
SUB_COMMITS_CONTEXT_KEY types.ContextKey = "subCommits"
|
||||
COMMIT_FILES_CONTEXT_KEY types.ContextKey = "commitFiles"
|
||||
STASH_CONTEXT_KEY types.ContextKey = "stash"
|
||||
NORMAL_MAIN_CONTEXT_KEY types.ContextKey = "normal"
|
||||
NORMAL_SECONDARY_CONTEXT_KEY types.ContextKey = "normalSecondary"
|
||||
MERGE_CONFLICTS_CONTEXT_KEY types.ContextKey = "mergeConflicts"
|
||||
|
||||
// these shouldn't really be needed for anything but I'm giving them unique keys nonetheless
|
||||
OPTIONS_CONTEXT_KEY types.ContextKey = "options"
|
||||
@@ -66,10 +62,6 @@ var AllContextKeys = []types.ContextKey{
|
||||
STASH_CONTEXT_KEY,
|
||||
NORMAL_MAIN_CONTEXT_KEY,
|
||||
NORMAL_SECONDARY_CONTEXT_KEY,
|
||||
STAGING_MAIN_CONTEXT_KEY,
|
||||
STAGING_SECONDARY_CONTEXT_KEY,
|
||||
PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
|
||||
MERGE_CONFLICTS_CONTEXT_KEY,
|
||||
|
||||
MENU_CONTEXT_KEY,
|
||||
@@ -83,35 +75,31 @@ var AllContextKeys = []types.ContextKey{
|
||||
}
|
||||
|
||||
type ContextTree struct {
|
||||
Global types.Context
|
||||
Status types.Context
|
||||
Snake types.Context
|
||||
Files *WorkingTreeContext
|
||||
Menu *MenuContext
|
||||
Branches *BranchesContext
|
||||
Tags *TagsContext
|
||||
LocalCommits *LocalCommitsContext
|
||||
CommitFiles *CommitFilesContext
|
||||
Remotes *RemotesContext
|
||||
Worktrees *WorktreesContext
|
||||
Submodules *SubmodulesContext
|
||||
RemoteBranches *RemoteBranchesContext
|
||||
ReflogCommits *ReflogCommitsContext
|
||||
SubCommits *SubCommitsContext
|
||||
Stash *StashContext
|
||||
Suggestions *SuggestionsContext
|
||||
Normal *MainContext
|
||||
NormalSecondary *MainContext
|
||||
Staging *PatchExplorerContext
|
||||
StagingSecondary *PatchExplorerContext
|
||||
CustomPatchBuilder *PatchExplorerContext
|
||||
CustomPatchBuilderSecondary types.Context
|
||||
MergeConflicts *MergeConflictsContext
|
||||
Confirmation *ConfirmationContext
|
||||
Prompt *PromptContext
|
||||
CommitMessage *CommitMessageContext
|
||||
CommitDescription types.Context
|
||||
CommandLog types.Context
|
||||
Global types.Context
|
||||
Status types.Context
|
||||
Snake types.Context
|
||||
Files *WorkingTreeContext
|
||||
Menu *MenuContext
|
||||
Branches *BranchesContext
|
||||
Tags *TagsContext
|
||||
LocalCommits *LocalCommitsContext
|
||||
CommitFiles *CommitFilesContext
|
||||
Remotes *RemotesContext
|
||||
Worktrees *WorktreesContext
|
||||
Submodules *SubmodulesContext
|
||||
RemoteBranches *RemoteBranchesContext
|
||||
ReflogCommits *ReflogCommitsContext
|
||||
SubCommits *SubCommitsContext
|
||||
Stash *StashContext
|
||||
Suggestions *SuggestionsContext
|
||||
Normal *MainContext
|
||||
NormalSecondary *MainContext
|
||||
MergeConflicts *MergeConflictsContext
|
||||
Confirmation *ConfirmationContext
|
||||
Prompt *PromptContext
|
||||
CommitMessage *CommitMessageContext
|
||||
CommitDescription types.Context
|
||||
CommandLog types.Context
|
||||
|
||||
// display contexts
|
||||
AppStatus types.Context
|
||||
@@ -149,10 +137,6 @@ func (self *ContextTree) Flatten() []types.Context {
|
||||
self.CommitDescription,
|
||||
|
||||
self.MergeConflicts,
|
||||
self.StagingSecondary,
|
||||
self.Staging,
|
||||
self.CustomPatchBuilderSecondary,
|
||||
self.CustomPatchBuilder,
|
||||
self.NormalSecondary,
|
||||
self.Normal,
|
||||
|
||||
|
||||
@@ -31,16 +31,70 @@ type commitDropIndicator struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ types.IListContext = (*LocalCommitsContext)(nil)
|
||||
_ types.DiffableContext = (*LocalCommitsContext)(nil)
|
||||
_ types.ISearchableContext = (*LocalCommitsContext)(nil)
|
||||
_ types.DiffMainViewContext = (*LocalCommitsContext)(nil)
|
||||
_ types.IListContext = (*LocalCommitsContext)(nil)
|
||||
_ types.DiffableContext = (*LocalCommitsContext)(nil)
|
||||
_ types.ISearchableContext = (*LocalCommitsContext)(nil)
|
||||
_ types.DiffMainViewContext = (*LocalCommitsContext)(nil)
|
||||
_ types.PullRequestDiffContext = (*LocalCommitsContext)(nil)
|
||||
)
|
||||
|
||||
func (self *LocalCommitsContext) GetDiffMainViewType() types.DiffMainViewType {
|
||||
return types.DiffMainViewTypePatchBuilding
|
||||
}
|
||||
|
||||
// BranchForPullRequest returns the checked-out branch: this panel shows its commits,
|
||||
// so a pull request for it is where they are up for review.
|
||||
func (self *LocalCommitsContext) BranchForPullRequest() string {
|
||||
return self.ListContextTrait.c.Model().CheckedOutBranch
|
||||
}
|
||||
|
||||
func (self *LocalCommitsContext) CommitsForPullRequest() ([]*models.Commit, string) {
|
||||
selectedCommits, _, _ := self.GetSelectedItems()
|
||||
commits := commitsShownInDiff(selectedCommits, self.GetSelected(), self.GetSelectedRefRangeForDiffFiles())
|
||||
return commits, pullRequestBaseForCommits(self.GetCommits(), commits)
|
||||
}
|
||||
|
||||
// commitsShownInDiff returns the commits whose combined diff a panel listing a branch's
|
||||
// commits renders into the main view: the selected range where it has a range to diff,
|
||||
// and the commit at the cursor otherwise. The panel hands the same two to
|
||||
// DiffHelper.GetUpdateTaskForRenderingCommitsDiff, so anything acting on the diff on
|
||||
// screen acts on the commits that diff is of.
|
||||
func commitsShownInDiff(
|
||||
selectedCommits []*models.Commit, commitAtCursor *models.Commit, refRange *types.RefRange,
|
||||
) []*models.Commit {
|
||||
if refRange != nil {
|
||||
return selectedCommits
|
||||
}
|
||||
if commitAtCursor == nil {
|
||||
return nil
|
||||
}
|
||||
return []*models.Commit{commitAtCursor}
|
||||
}
|
||||
|
||||
// pullRequestBaseForCommits returns the hash of the commit the diff of the given commits
|
||||
// starts after: the parent of the oldest of them. A pull request holds only the commits
|
||||
// of the branch that are pushed, so a parent that isn't pushed is none of its own. The
|
||||
// diff then starts where the pull request itself does, and "" says so.
|
||||
func pullRequestBaseForCommits(allCommits []*models.Commit, commits []*models.Commit) string {
|
||||
if len(commits) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
oldest := commits[len(commits)-1]
|
||||
if oldest.IsFirstCommit() {
|
||||
return ""
|
||||
}
|
||||
|
||||
parentHash := oldest.Parents()[0]
|
||||
parentIsInPullRequest := lo.ContainsBy(allCommits, func(commit *models.Commit) bool {
|
||||
return commit.Hash() == parentHash && commit.Status == models.StatusPushed
|
||||
})
|
||||
if !parentIsInPullRequest {
|
||||
return ""
|
||||
}
|
||||
return parentHash
|
||||
}
|
||||
|
||||
func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
dropIndicator := &commitDropIndicator{insertionIndex: -1}
|
||||
viewModel := NewLocalCommitsViewModel(
|
||||
|
||||
@@ -4,8 +4,12 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -51,3 +55,101 @@ func TestAddMovingCommitsIndicator(t *testing.T) {
|
||||
},
|
||||
}, items)
|
||||
}
|
||||
|
||||
func TestCommitsShownInDiff(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
newer := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "newer"})
|
||||
older := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "older"})
|
||||
selected := []*models.Commit{newer, older}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
selectedCommit *models.Commit
|
||||
refRange *types.RefRange
|
||||
expected []*models.Commit
|
||||
}{
|
||||
{
|
||||
name: "a range is diffed as a whole",
|
||||
selectedCommit: newer,
|
||||
refRange: &types.RefRange{From: older, To: newer},
|
||||
expected: selected,
|
||||
},
|
||||
{
|
||||
name: "without a range to diff, only the commit at the cursor is",
|
||||
selectedCommit: newer,
|
||||
expected: []*models.Commit{newer},
|
||||
},
|
||||
{
|
||||
name: "nothing is diffed while nothing is selected",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, commitsShownInDiff(selected, s.selectedCommit, s.refRange))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullRequestBaseForCommits(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
commit := func(hash string, parent string, status models.CommitStatus) *models.Commit {
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: hash,
|
||||
Parents: lo.Ternary(parent == "", []string{}, []string{parent}),
|
||||
Status: status,
|
||||
})
|
||||
}
|
||||
|
||||
// A branch of three pushed commits whose tip was amended, on top of a commit that
|
||||
// is in a main branch already, as the panel lists them: newest first.
|
||||
amended := commit("amended", "third", models.StatusUnpushed)
|
||||
third := commit("third", "second", models.StatusPushed)
|
||||
second := commit("second", "first", models.StatusPushed)
|
||||
first := commit("first", "merged", models.StatusPushed)
|
||||
merged := commit("merged", "ancient", models.StatusMerged)
|
||||
ancient := commit("ancient", "", models.StatusMerged)
|
||||
allCommits := []*models.Commit{amended, third, second, first, merged, ancient}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
commits []*models.Commit
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "a single commit starts after its parent",
|
||||
commits: []*models.Commit{second},
|
||||
expected: "first",
|
||||
},
|
||||
{
|
||||
name: "a range starts after the parent of its oldest commit",
|
||||
commits: []*models.Commit{third, second},
|
||||
expected: "first",
|
||||
},
|
||||
{
|
||||
name: "the pull request's first commit starts where the pull request does",
|
||||
commits: []*models.Commit{first},
|
||||
},
|
||||
{
|
||||
name: "so does a range reaching down to it",
|
||||
commits: []*models.Commit{third, second, first},
|
||||
},
|
||||
{
|
||||
name: "and so does the first commit of the repository",
|
||||
commits: []*models.Commit{ancient},
|
||||
},
|
||||
{
|
||||
name: "a parent the panel doesn't list is none of the pull request's",
|
||||
commits: []*models.Commit{commit("elsewhere", "unlisted", models.StatusPushed)},
|
||||
},
|
||||
{
|
||||
name: "nothing is shown",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, pullRequestBaseForCommits(allCommits, s.commits))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
package context
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
deadlock "github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type PatchExplorerContext struct {
|
||||
*SimpleContext
|
||||
*SearchTrait
|
||||
|
||||
state *patch_exploring.State
|
||||
viewTrait *ViewTrait
|
||||
getIncludedLineIndices func() []int
|
||||
c *ContextCommon
|
||||
mutex deadlock.Mutex
|
||||
|
||||
// true if we're inside the OnSelectItem callback; in that case we don't want to update the
|
||||
// search result index.
|
||||
inOnSelectItemCallback bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ types.IPatchExplorerContext = (*PatchExplorerContext)(nil)
|
||||
_ types.ISearchableContext = (*PatchExplorerContext)(nil)
|
||||
)
|
||||
|
||||
func NewPatchExplorerContext(
|
||||
view *gocui.View,
|
||||
windowName string,
|
||||
key types.ContextKey,
|
||||
|
||||
getIncludedLineIndices func() []int,
|
||||
|
||||
c *ContextCommon,
|
||||
) *PatchExplorerContext {
|
||||
ctx := &PatchExplorerContext{
|
||||
state: nil,
|
||||
viewTrait: NewViewTrait(view),
|
||||
c: c,
|
||||
getIncludedLineIndices: getIncludedLineIndices,
|
||||
SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
|
||||
View: view,
|
||||
WindowName: windowName,
|
||||
Key: key,
|
||||
Kind: types.MAIN_CONTEXT,
|
||||
Focusable: true,
|
||||
HasSelectableContent: true,
|
||||
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES,
|
||||
})),
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) IsPatchExplorerContext() {}
|
||||
|
||||
func (self *PatchExplorerContext) GetState() *patch_exploring.State {
|
||||
return self.state
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) SetState(state *patch_exploring.State) {
|
||||
self.state = state
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) GetViewTrait() types.IViewTrait {
|
||||
return self.viewTrait
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) GetIncludedLineIndices() []int {
|
||||
return self.getIncludedLineIndices()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) RenderAndFocus() {
|
||||
self.setContent()
|
||||
|
||||
self.FocusSelection()
|
||||
self.c.Render()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) Render() {
|
||||
self.setContent()
|
||||
|
||||
self.c.Render()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) setContent() {
|
||||
self.GetView().SetContent(self.GetContentToRender())
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) FocusSelection() {
|
||||
view := self.GetView()
|
||||
state := self.GetState()
|
||||
bufferHeight := view.InnerHeight()
|
||||
_, origin := view.Origin()
|
||||
numLines := view.ViewLinesHeight()
|
||||
|
||||
newOriginY := state.CalculateOrigin(origin, bufferHeight, numLines)
|
||||
|
||||
view.SetOriginY(newOriginY)
|
||||
|
||||
startIdx, endIdx := state.SelectedViewRange()
|
||||
// As far as the view is concerned, we are always selecting a range
|
||||
view.SetRangeSelectStart(startIdx)
|
||||
view.SetCursorY(endIdx - newOriginY)
|
||||
|
||||
if !self.inOnSelectItemCallback {
|
||||
view.SetNearestSearchPosition()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) GetContentToRender() string {
|
||||
if self.GetState() == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return self.GetState().RenderForLineIndices(self.GetIncludedLineIndices())
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) NavigateTo(selectedLineIdx int) {
|
||||
self.GetState().SetLineSelectMode()
|
||||
self.GetState().SelectLine(selectedLineIdx)
|
||||
|
||||
self.RenderAndFocus()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) GetMutex() *deadlock.Mutex {
|
||||
return &self.mutex
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) ModelSearchResults(searchStr string, caseSensitive bool) []gocui.SearchPosition {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) OnSearchSelect(selectedLineIdx int) {
|
||||
self.GetMutex().Lock()
|
||||
defer self.GetMutex().Unlock()
|
||||
self.inOnSelectItemCallback = true
|
||||
self.NavigateTo(selectedLineIdx)
|
||||
self.inOnSelectItemCallback = false
|
||||
}
|
||||
|
||||
func (self *PatchExplorerContext) OnViewWidthChanged() {
|
||||
if state := self.GetState(); state != nil {
|
||||
state.OnViewWidthChanged(self.GetView())
|
||||
self.setContent()
|
||||
self.RenderAndFocus()
|
||||
}
|
||||
}
|
||||
@@ -41,48 +41,6 @@ func NewContextTree(c *ContextCommon) *ContextTree {
|
||||
Suggestions: NewSuggestionsContext(c),
|
||||
Normal: NewMainContext(c.Views().Main, "main", NORMAL_MAIN_CONTEXT_KEY, c),
|
||||
NormalSecondary: NewMainContext(c.Views().Secondary, "secondary", NORMAL_SECONDARY_CONTEXT_KEY, c),
|
||||
Staging: NewPatchExplorerContext(
|
||||
c.Views().Staging,
|
||||
"main",
|
||||
STAGING_MAIN_CONTEXT_KEY,
|
||||
func() []int { return nil },
|
||||
c,
|
||||
),
|
||||
StagingSecondary: NewPatchExplorerContext(
|
||||
c.Views().StagingSecondary,
|
||||
"secondary",
|
||||
STAGING_SECONDARY_CONTEXT_KEY,
|
||||
func() []int { return nil },
|
||||
c,
|
||||
),
|
||||
CustomPatchBuilder: NewPatchExplorerContext(
|
||||
c.Views().PatchBuilding,
|
||||
"main",
|
||||
PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
func() []int {
|
||||
file := commitFilesContext.GetSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
|
||||
if err != nil {
|
||||
c.Log.Error(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return includedLineIndices
|
||||
},
|
||||
c,
|
||||
),
|
||||
CustomPatchBuilderSecondary: NewSimpleContext(
|
||||
NewBaseContext(NewBaseContextOpts{
|
||||
Kind: types.MAIN_CONTEXT,
|
||||
View: c.Views().PatchBuildingSecondary,
|
||||
WindowName: "secondary",
|
||||
Key: PATCH_BUILDING_SECONDARY_CONTEXT_KEY,
|
||||
Focusable: false,
|
||||
}),
|
||||
),
|
||||
MergeConflicts: NewMergeConflictsContext(
|
||||
c,
|
||||
),
|
||||
|
||||
@@ -21,16 +21,33 @@ type SubCommitsContext struct {
|
||||
}
|
||||
|
||||
var (
|
||||
_ types.IListContext = (*SubCommitsContext)(nil)
|
||||
_ types.DiffableContext = (*SubCommitsContext)(nil)
|
||||
_ types.ISearchableContext = (*SubCommitsContext)(nil)
|
||||
_ types.DiffMainViewContext = (*SubCommitsContext)(nil)
|
||||
_ types.IListContext = (*SubCommitsContext)(nil)
|
||||
_ types.DiffableContext = (*SubCommitsContext)(nil)
|
||||
_ types.ISearchableContext = (*SubCommitsContext)(nil)
|
||||
_ types.DiffMainViewContext = (*SubCommitsContext)(nil)
|
||||
_ types.PullRequestDiffContext = (*SubCommitsContext)(nil)
|
||||
)
|
||||
|
||||
func (self *SubCommitsContext) GetDiffMainViewType() types.DiffMainViewType {
|
||||
return types.DiffMainViewTypePatchBuilding
|
||||
}
|
||||
|
||||
// BranchForPullRequest returns the branch this panel was entered from, whose commits it
|
||||
// shows. The panel is also entered from a tag, a remote branch and the reflog, none of
|
||||
// which a pull request is made from.
|
||||
func (self *SubCommitsContext) BranchForPullRequest() string {
|
||||
if branch, ok := self.GetRef().(*models.Branch); ok {
|
||||
return branch.Name
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (self *SubCommitsContext) CommitsForPullRequest() ([]*models.Commit, string) {
|
||||
selectedCommits, _, _ := self.GetSelectedItems()
|
||||
commits := commitsShownInDiff(selectedCommits, self.GetSelected(), self.GetSelectedRefRangeForDiffFiles())
|
||||
return commits, pullRequestBaseForCommits(self.GetCommits(), commits)
|
||||
}
|
||||
|
||||
func NewSubCommitsContext(
|
||||
c *ContextCommon,
|
||||
) *SubCommitsContext {
|
||||
|
||||
+3
-33
@@ -52,8 +52,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
|
||||
gpgHelper := helpers.NewGpgHelper(helperCommon)
|
||||
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
|
||||
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
|
||||
stagingHelper := helpers.NewStagingHelper(helperCommon)
|
||||
customPatchHelper := helpers.NewCustomPatchHelper(helperCommon)
|
||||
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
|
||||
searchHelper := helpers.NewSearchHelper(helperCommon)
|
||||
|
||||
@@ -61,8 +60,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
helperCommon,
|
||||
refsHelper,
|
||||
rebaseHelper,
|
||||
patchBuildingHelper,
|
||||
stagingHelper,
|
||||
mergeConflictsHelper,
|
||||
worktreeHelper,
|
||||
searchHelper,
|
||||
@@ -78,7 +75,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
modeHelper := helpers.NewModeHelper(
|
||||
helperCommon,
|
||||
diffHelper,
|
||||
patchBuildingHelper,
|
||||
customPatchHelper,
|
||||
cherryPickHelper,
|
||||
rebaseHelper,
|
||||
bisectHelper,
|
||||
@@ -92,8 +89,7 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
gui.helpers = &helpers.Helpers{
|
||||
Refs: refsHelper,
|
||||
Host: helpers.NewHostHelper(helperCommon),
|
||||
PatchBuilding: patchBuildingHelper,
|
||||
Staging: stagingHelper,
|
||||
CustomPatch: customPatchHelper,
|
||||
Bisect: bisectHelper,
|
||||
Suggestions: suggestionsHelper,
|
||||
Files: helpers.NewFilesHelper(helperCommon),
|
||||
@@ -183,12 +179,8 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
gitFlowController := controllers.NewGitFlowController(common)
|
||||
stashController := controllers.NewStashController(common)
|
||||
commitFilesController := controllers.NewCommitFilesController(common)
|
||||
patchExplorerControllerFactory := controllers.NewPatchExplorerControllerFactory(common)
|
||||
stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false)
|
||||
stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true)
|
||||
mainViewController := controllers.NewMainViewController(common, gui.State.Contexts.Normal, gui.State.Contexts.NormalSecondary)
|
||||
secondaryViewController := controllers.NewMainViewController(common, gui.State.Contexts.NormalSecondary, gui.State.Contexts.Normal)
|
||||
patchBuildingController := controllers.NewPatchBuildingController(common)
|
||||
snakeController := controllers.NewSnakeController(common)
|
||||
reflogCommitsController := controllers.NewReflogCommitsController(common)
|
||||
subCommitsController := controllers.NewSubCommitsController(common)
|
||||
@@ -285,28 +277,6 @@ func (gui *Gui) resetHelpersAndControllers() {
|
||||
)
|
||||
|
||||
// TODO: add scroll controllers for main panels (need to bring some more functionality across for that e.g. reading more from the currently displayed git command)
|
||||
controllers.AttachControllers(gui.State.Contexts.Staging,
|
||||
stagingController,
|
||||
patchExplorerControllerFactory.Create(gui.State.Contexts.Staging),
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.Staging),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.StagingSecondary,
|
||||
stagingSecondaryController,
|
||||
patchExplorerControllerFactory.Create(gui.State.Contexts.StagingSecondary),
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.StagingSecondary),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.CustomPatchBuilder,
|
||||
patchBuildingController,
|
||||
patchExplorerControllerFactory.Create(gui.State.Contexts.CustomPatchBuilder),
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.CustomPatchBuilder),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.CustomPatchBuilderSecondary,
|
||||
verticalScrollControllerFactory.Create(gui.State.Contexts.CustomPatchBuilderSecondary),
|
||||
)
|
||||
|
||||
controllers.AttachControllers(gui.State.Contexts.MergeConflicts,
|
||||
mergeConflictsController,
|
||||
)
|
||||
|
||||
@@ -208,7 +208,7 @@ func (self *BranchesController) GetOnRenderToMain() func() {
|
||||
rendererTask := types.NewRunDiffRendererTask(cmdObj.GetCmd())
|
||||
task = rendererTask
|
||||
|
||||
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
|
||||
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
|
||||
if ok && presentation.ShouldShowPrForBranch(pr, branch.Name, self.c.UserConfig()) {
|
||||
rendererTask.Prefix = presentation.FormatPullRequestHeader(pr, self.c.Tr)
|
||||
rendererTask.Prefix += strings.Repeat("─", self.c.Contexts().Normal.GetView().InnerWidth()) + "\n"
|
||||
@@ -462,7 +462,7 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode
|
||||
|
||||
func (self *BranchesController) getPullRequestURL() (string, error) {
|
||||
branch := self.context().GetSelected()
|
||||
if pr, ok := self.c.Model().PullRequestsMap[branch.Name]; ok {
|
||||
if pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name); ok {
|
||||
return pr.Url, nil
|
||||
}
|
||||
|
||||
@@ -855,15 +855,11 @@ func (self *BranchesController) branchIsReal(branch *models.Branch) *types.Disab
|
||||
}
|
||||
|
||||
func (self *BranchesController) branchHasPR(branch *models.Branch) *types.DisabledReason {
|
||||
if _, ok := self.c.Model().PullRequestsMap[branch.Name]; !ok {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
return nil
|
||||
return self.c.Helpers().Host.NoPullRequestDisabledReason(branch.Name)
|
||||
}
|
||||
|
||||
func (self *BranchesController) openPRInBrowser(branch *models.Branch) error {
|
||||
pr, ok := self.c.Model().PullRequestsMap[branch.Name]
|
||||
pr, ok := self.c.Helpers().Host.PullRequestForBranch(branch.Name)
|
||||
if !ok {
|
||||
// Should be guarded against by the DisabledReason check, but be defensive in case
|
||||
// PullRequestsMap was updated concurrently by a background refresh
|
||||
|
||||
@@ -0,0 +1,461 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// CommitDiffActions implements what a panel showing a commit's diff offers on that diff
|
||||
// in the focused main view. Five panels do: the commit files panel shows the diff of one
|
||||
// file of a commit, and the commits, sub-commits, stash and reflog panels the whole diff
|
||||
// of whatever they have selected. They all offer the same thing and differ only in which
|
||||
// diff they show, so they share this, each saying which diff that is.
|
||||
type CommitDiffActions struct {
|
||||
c *ControllerCommon
|
||||
|
||||
// The panel this belongs to, and what it is showing the diff of — nil when it has
|
||||
// nothing selected, and so no diff.
|
||||
panel types.Context
|
||||
target func() *commitDiffTarget
|
||||
}
|
||||
|
||||
// commitDiffTarget is the diff a panel is showing: the two ends of it, and whether it
|
||||
// belongs to a commit lazygit may rewrite.
|
||||
type commitDiffTarget struct {
|
||||
from string
|
||||
to string
|
||||
canRebase bool
|
||||
}
|
||||
|
||||
var _ types.FocusedMainViewActions = &CommitDiffActions{}
|
||||
|
||||
func NewCommitDiffActions(
|
||||
c *ControllerCommon, panel types.Context, target func() *commitDiffTarget,
|
||||
) *CommitDiffActions {
|
||||
return &CommitDiffActions{c: c, panel: panel, target: target}
|
||||
}
|
||||
|
||||
// PlainDiff hands out the diff the asking pane is showing, for the given files — the
|
||||
// commit's diff as in the main view, only without the commit's message and stat above it,
|
||||
// or the diff the custom patch is previewed as, whose lines are the patch's own rather
|
||||
// than the commit's.
|
||||
//
|
||||
// The patch's own diff is handed out whole: it is only ever as big as the patch, and it
|
||||
// names its files under the trees the patch was materialized into rather than under the
|
||||
// paths asked for.
|
||||
func (self *CommitDiffActions) PlainDiff(pane types.DiffPaneContext, paths []string) string {
|
||||
if self.showsCustomPatch(pane) {
|
||||
return self.customPatchDiff()
|
||||
}
|
||||
|
||||
target := self.target()
|
||||
if target == nil {
|
||||
return ""
|
||||
}
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(target.from, target.to, paths)
|
||||
}
|
||||
|
||||
// customPatchDiff is the diff the custom patch is previewed as, as git writes it — the
|
||||
// diff behind what the pane previewing the patch shows, in which the lines shown there
|
||||
// can be found again.
|
||||
func (self *CommitDiffActions) customPatchDiff() string {
|
||||
treesDir := self.c.Git().Patch.PatchBuilder.TempDir()
|
||||
if treesDir == "" {
|
||||
return ""
|
||||
}
|
||||
// An error means the two trees differ, as they do for any patch with something in
|
||||
// it. We are after the diff itself either way.
|
||||
diff, _ := self.c.Git().Diff.
|
||||
CustomPatchDiffCmdObj(treesDir, git_commands.DiffModePlain).
|
||||
RunWithOutput()
|
||||
return diff
|
||||
}
|
||||
|
||||
// PrimaryAction takes the selected lines into the custom patch being built from this
|
||||
// diff, or back out of it when the first of them is already in — the same toggling the
|
||||
// commit files panel does to a whole file at a time.
|
||||
//
|
||||
// The commit is not touched, so the diff stays as it is: what changes is the patch
|
||||
// beside it, and which of its lines are marked as being in that patch.
|
||||
func (self *CommitDiffActions) PrimaryAction(pane types.DiffPaneContext, firstLineIdx int, lastLineIdx int) error {
|
||||
// In the pane showing the patch, the lines are the patch's own, so there they only
|
||||
// come back out of it.
|
||||
if self.showsCustomPatch(pane) {
|
||||
return self.removePatchLines(pane, firstLineIdx, lastLineIdx)
|
||||
}
|
||||
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextForCustomPatch,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
}
|
||||
|
||||
target := self.target()
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
lines := self.c.Helpers().DiffLine.ChangeLinesInViewRange(pane.GetView(), firstLineIdx, lastLineIdx)
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
from, reverse := self.patchEndpoints(target)
|
||||
// A patch is built from one diff, so building from another one means giving up the
|
||||
// patch there is — which the user is asked about, as entering the patch builder asks.
|
||||
mustDiscardPatch := patchBuilder.Active() && patchBuilder.NewPatchRequired(from, target.to, reverse)
|
||||
return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardPatch,
|
||||
Prompt: self.c.Tr.DiscardPatchConfirm,
|
||||
HandleConfirm: func() error {
|
||||
if mustDiscardPatch {
|
||||
patchBuilder.Reset()
|
||||
}
|
||||
if !patchBuilder.Active() {
|
||||
patchBuilder.Start(from, target.to, reverse, target.canRebase)
|
||||
}
|
||||
|
||||
if err := self.togglePatchLines(lines); err != nil {
|
||||
return err
|
||||
}
|
||||
// Taking the last line back out ends the patch rather than leaving an empty
|
||||
// one, so that the pane previewing it and the marks over the diff go with it.
|
||||
if patchBuilder.IsEmpty() {
|
||||
patchBuilder.Reset()
|
||||
}
|
||||
|
||||
// The diff on screen is the one the marks belong to, so they can be brought up
|
||||
// to date at once rather than waiting for the render below.
|
||||
self.c.Helpers().DiffLine.RefreshInclusionGutter()
|
||||
|
||||
// The selection moves on past the lines just toggled, to the next change of
|
||||
// the diff — which is still there, a toggle leaving the diff as it was, so
|
||||
// hold input back until it has moved: a second press meanwhile would toggle
|
||||
// the same lines straight back.
|
||||
self.c.GocuiGui().BeginBlockingEvents()
|
||||
self.c.Helpers().DiffLine.RevealSelectionAfterAction(pane, pane, firstLineIdx, len(lines),
|
||||
self.c.GocuiGui().EndBlockingEvents)
|
||||
|
||||
// The panel's own render, which is all that is needed: the marks over the diff
|
||||
// and the patch previewed beside it have changed, while the commit has not.
|
||||
self.c.PostRefreshUpdate(self.panel)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// removePatchLines takes the selected lines of the custom patch out of it. The primary
|
||||
// action does this in the pane showing the patch: everything shown there is in the patch
|
||||
// already, so there is nothing else it could mean.
|
||||
//
|
||||
// A line of the patch is named by its position among its file's changes, counted in the
|
||||
// diff the patch is shown as. That position is the same one the line has among the
|
||||
// changes the patch holds for that file. Line numbers would not do: a patch that leaves
|
||||
// an earlier addition out numbers everything after it differently from the commit's diff.
|
||||
func (self *CommitDiffActions) removePatchLines(
|
||||
pane types.DiffPaneContext, firstLineIdx int, lastLineIdx int,
|
||||
) error {
|
||||
lines := self.c.Helpers().DiffLine.ChangeLinesInViewRange(pane.GetView(), firstLineIdx, lastLineIdx)
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
files := self.filesInDiff()
|
||||
for path, ordinals := range self.c.Helpers().DiffLine.ChangeLineOrdinals(self.customPatchDiff(), lines) {
|
||||
filename := self.patchBuilderPath(path)
|
||||
if filename == "" {
|
||||
continue
|
||||
}
|
||||
included := patchBuilder.IncludedChangeLineIndices(filename)
|
||||
indices := []int{}
|
||||
for _, ordinal := range ordinals {
|
||||
if ordinal < len(included) {
|
||||
indices = append(indices, included[ordinal])
|
||||
}
|
||||
}
|
||||
if len(indices) == 0 {
|
||||
continue
|
||||
}
|
||||
if err := patchBuilder.RemoveFileLineRange(filename, files.previousPath(filename), indices); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Taking the last line out ends the patch rather than leaving an empty one, as it does
|
||||
// in the diff beside this pane.
|
||||
if patchBuilder.IsEmpty() {
|
||||
patchBuilder.Reset()
|
||||
}
|
||||
|
||||
self.c.Helpers().DiffLine.RefreshInclusionGutter()
|
||||
|
||||
// The lines are gone from the patch, so the selection carries on from where they were,
|
||||
// as unstaging leaves it. Input is held until it has moved, so that a second press acts
|
||||
// on the patch as it now is.
|
||||
self.c.GocuiGui().BeginBlockingEvents()
|
||||
self.c.Helpers().DiffLine.RevealSelectionAfterAction(pane, pane, firstLineIdx, 0,
|
||||
self.c.GocuiGui().EndBlockingEvents)
|
||||
|
||||
self.c.PostRefreshUpdate(self.panel)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscardSelection takes the selected lines out of the commit they are part of, by
|
||||
// building a patch of exactly those lines and removing that patch from the commit. It is
|
||||
// a rebase, so a later commit that touches the same lines can conflict with it.
|
||||
//
|
||||
// The patch it needs is its own, so a patch being built is given up first — which the
|
||||
// prompt says, there being no way to get it back.
|
||||
func (self *CommitDiffActions) DiscardSelection(pane types.DiffPaneContext, firstLineIdx int, lastLineIdx int) error {
|
||||
target := self.target()
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
lines := self.c.Helpers().DiffLine.ChangeLinesInViewRange(pane.GetView(), firstLineIdx, lastLineIdx)
|
||||
if len(lines) == 0 {
|
||||
return nil
|
||||
}
|
||||
commitIndex := self.indexOfTargetCommit(target)
|
||||
if commitIndex == -1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
prompt := lo.Ternary(patchBuilder.IsEmpty(),
|
||||
self.c.Tr.DiscardLinesFromCommitPrompt,
|
||||
self.c.Tr.DiscardLinesFromCommitPromptWithReset)
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardLinesFromCommitTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
from, reverse := self.patchEndpoints(target)
|
||||
patchBuilder.Reset()
|
||||
patchBuilder.Start(from, target.to, reverse, target.canRebase)
|
||||
if err := self.togglePatchLines(lines); err != nil {
|
||||
return err
|
||||
}
|
||||
if patchBuilder.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// The rebase runs on a worker, which may not read the model, so the commits
|
||||
// it rewrites are taken here.
|
||||
commits := self.c.Model().Commits
|
||||
return self.c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{
|
||||
Message: self.c.Tr.RebasingStatus,
|
||||
HideWorkingTreeState: true,
|
||||
}, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
|
||||
err := self.c.Git().Patch.DeletePatchesFromCommit(commits, commitIndex)
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
})
|
||||
},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// DiscardSelectionDisabledReason says why the selected lines can't be taken out of the
|
||||
// commit: doing so rewrites it, which is only ours to do for a commit of the branch we
|
||||
// are on, and not while a rebase is already under way. In the pane previewing the custom
|
||||
// patch there is nothing to discard from — the lines there are the patch's, and space
|
||||
// takes them back out of it.
|
||||
func (self *CommitDiffActions) DiscardSelectionDisabledReason(pane types.DiffPaneContext) *types.DisabledReason {
|
||||
if self.showsCustomPatch(pane) {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CannotDiscardFromCustomPatchView, ShowErrorInPanel: true}
|
||||
}
|
||||
target := self.target()
|
||||
if target == nil || !target.canRebase {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CanOnlyDiscardFromLocalCommits, ShowErrorInPanel: true}
|
||||
}
|
||||
if self.c.Git().Status.WorkingTreeState().Any() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantPatchWhileRebasingError, ShowErrorInPanel: true}
|
||||
}
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return &types.DisabledReason{
|
||||
Text: fmt.Sprintf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView),
|
||||
ShowErrorInPanel: true,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchInclusion says which lines of the commit's diff are in the custom patch being
|
||||
// built from it. nil when there is no such patch: none is being built at all, or the one
|
||||
// being built is of another diff, whose lines are not these however alike they look.
|
||||
func (self *CommitDiffActions) PatchInclusion() func(types.DiffLineInfo) bool {
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
target := self.target()
|
||||
if !patchBuilder.Active() || target == nil {
|
||||
return nil
|
||||
}
|
||||
from, reverse := self.patchEndpoints(target)
|
||||
if patchBuilder.NewPatchRequired(from, target.to, reverse) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Which lines of a file are in the patch is asked of the patch builder per file, and
|
||||
// a diff can span many, so each is asked about when a line of it first comes up.
|
||||
includedByPath := map[string]*set.Set[patch.LineIdentity]{}
|
||||
return func(info types.DiffLineInfo) bool {
|
||||
path := self.patchBuilderPath(info.Path)
|
||||
if path == "" {
|
||||
return false
|
||||
}
|
||||
included, asked := includedByPath[path]
|
||||
if !asked {
|
||||
included = set.NewFromSlice(patchBuilder.IncludedLineIdentities(path))
|
||||
includedByPath[path] = included
|
||||
}
|
||||
return included.Includes(info.PatchLineIdentity())
|
||||
}
|
||||
}
|
||||
|
||||
// togglePatchLines takes the given lines of the commit's diff into the custom patch, or
|
||||
// out of it. The first line of the selection decides which of the two happens, once for
|
||||
// the whole selection: pointing at a line that is already in the patch takes the whole
|
||||
// selection out of it, as toggling a selection of files in the commit files panel does.
|
||||
func (self *CommitDiffActions) togglePatchLines(lines []types.DiffLineInfo) error {
|
||||
patchBuilder := self.c.Git().Patch.PatchBuilder
|
||||
|
||||
// The files the selection covers, in the order the diff shows them, and per file the
|
||||
// lines of it that are selected: a patch is built a file at a time, while a selection
|
||||
// can span several of them.
|
||||
paths := []string{}
|
||||
linesByPath := map[string][]patch.LineIdentity{}
|
||||
for _, line := range lines {
|
||||
path := self.patchBuilderPath(line.Path)
|
||||
if path == "" {
|
||||
continue
|
||||
}
|
||||
if _, seen := linesByPath[path]; !seen {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
linesByPath[path] = append(linesByPath[path], line.PatchLineIdentity())
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
files := self.filesInDiff()
|
||||
indicesByPath := map[string][]int{}
|
||||
wholeFileByPath := map[string]bool{}
|
||||
for _, path := range paths {
|
||||
indices, everyChange, err := patchBuilder.PatchLineIndicesForLines(
|
||||
path, files.previousPath(path), linesByPath[path])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indicesByPath[path] = indices
|
||||
|
||||
// Selecting every change of a file the commit adds or deletes is selecting the
|
||||
// file: what the commit did to it is not something its content lines can carry,
|
||||
// so a patch of those alone would move the content and leave the file behind.
|
||||
wholeFileByPath[path] = everyChange && files.isWholeFileOperation(path)
|
||||
}
|
||||
|
||||
included, err := patchBuilder.GetFileIncLineIndices(paths[0], files.previousPath(paths[0]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removing := len(indicesByPath[paths[0]]) > 0 && lo.Contains(included, indicesByPath[paths[0]][0])
|
||||
|
||||
for _, path := range paths {
|
||||
if len(indicesByPath[path]) == 0 {
|
||||
continue
|
||||
}
|
||||
previousPath := files.previousPath(path)
|
||||
var err error
|
||||
switch {
|
||||
case wholeFileByPath[path] && removing:
|
||||
err = patchBuilder.RemoveFile(path, previousPath)
|
||||
case wholeFileByPath[path]:
|
||||
err = patchBuilder.AddFileWhole(path, previousPath)
|
||||
case removing:
|
||||
err = patchBuilder.RemoveFileLineRange(path, previousPath, indicesByPath[path])
|
||||
default:
|
||||
err = patchBuilder.AddFileLineRange(path, previousPath, indicesByPath[path])
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commitDiffFiles records what the diff a patch is being built from says about each of
|
||||
// the files it covers, by the path the diff shows them under.
|
||||
type commitDiffFiles map[string]*models.CommitFile
|
||||
|
||||
// previousPath says what a file of the diff was called before, and is empty for one
|
||||
// that wasn't renamed. A renamed file's diff only comes out as a rename when git is
|
||||
// asked about both of its paths, and its lines are numbered in the file under its old
|
||||
// name, so the patch builder has to be told the old path along with them.
|
||||
func (self commitDiffFiles) previousPath(path string) string {
|
||||
if file, ok := self[path]; ok {
|
||||
return file.PreviousPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// isWholeFileOperation reports whether what the commit did to this file is something
|
||||
// its diff's content lines don't say: creating it or deleting it, which the file
|
||||
// header carries and a patch built from lines alone would leave out.
|
||||
func (self commitDiffFiles) isWholeFileOperation(path string) bool {
|
||||
file, ok := self[path]
|
||||
return ok && (file.Added() || file.Deleted())
|
||||
}
|
||||
|
||||
// filesInDiff asks git which files the diff a patch is being built from covers, and
|
||||
// what it does to each.
|
||||
func (self *CommitDiffActions) filesInDiff() commitDiffFiles {
|
||||
target := self.target()
|
||||
if target == nil {
|
||||
return nil
|
||||
}
|
||||
from, reverse := self.patchEndpoints(target)
|
||||
files, err := self.c.Git().Loaders.CommitFileLoader.GetFilesInDiff(from, target.to, reverse)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return lo.SliceToMap(files, func(file *models.CommitFile) (string, *models.CommitFile) {
|
||||
return file.Path, file
|
||||
})
|
||||
}
|
||||
|
||||
// patchEndpoints gives the two ends of the diff a patch is built from. They are the ends
|
||||
// of the diff shown, except in diffing mode, where what is shown is a diff against
|
||||
// another ref, possibly the other way around.
|
||||
func (self *CommitDiffActions) patchEndpoints(target *commitDiffTarget) (string, bool) {
|
||||
return self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(target.from)
|
||||
}
|
||||
|
||||
// patchBuilderPath turns the absolute path a diff line carries into the repo-relative
|
||||
// one the patch builder keys a file by, and "" for a path that is no file of this repo.
|
||||
func (self *CommitDiffActions) patchBuilderPath(path string) string {
|
||||
return repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), path)
|
||||
}
|
||||
|
||||
// indexOfTargetCommit finds the commit the diff belongs to among the commits of the
|
||||
// branch we are on, which is how a rebase is told which commit to rewrite. -1 when it
|
||||
// isn't one of them, in which case there is nothing we can rewrite.
|
||||
func (self *CommitDiffActions) indexOfTargetCommit(target *commitDiffTarget) int {
|
||||
return lo.IndexOf(
|
||||
lo.Map(self.c.Model().Commits, func(commit *models.Commit, _ int) string { return commit.Hash() }),
|
||||
target.to)
|
||||
}
|
||||
|
||||
// showsCustomPatch reports whether the given main pane is the one previewing the custom
|
||||
// patch being built, rather than the commit's diff — which for a commit's diff is always
|
||||
// the lower one.
|
||||
func (self *CommitDiffActions) showsCustomPatch(pane types.DiffPaneContext) bool {
|
||||
return pane.GetKey() == self.c.Contexts().NormalSecondary.GetKey()
|
||||
}
|
||||
@@ -23,6 +23,9 @@ type CommitFilesController struct {
|
||||
baseController
|
||||
*ListControllerTrait[*filetree.CommitFileNode]
|
||||
c *ControllerCommon
|
||||
|
||||
// what this panel offers on the diff it shows in the focused main view
|
||||
diffActions *CommitDiffActions
|
||||
}
|
||||
|
||||
var _ types.IController = &CommitFilesController{}
|
||||
@@ -30,7 +33,7 @@ var _ types.IController = &CommitFilesController{}
|
||||
func NewCommitFilesController(
|
||||
c *ControllerCommon,
|
||||
) *CommitFilesController {
|
||||
return &CommitFilesController{
|
||||
controller := &CommitFilesController{
|
||||
baseController: baseController{},
|
||||
c: c,
|
||||
ListControllerTrait: NewListControllerTrait(
|
||||
@@ -40,6 +43,18 @@ func NewCommitFilesController(
|
||||
c.Contexts().CommitFiles.GetSelectedItems,
|
||||
),
|
||||
}
|
||||
controller.diffActions = NewCommitDiffActions(c, c.Contexts().CommitFiles, controller.diffTarget)
|
||||
return controller
|
||||
}
|
||||
|
||||
// diffTarget is the commit whose files this panel is showing. Its main view shows the
|
||||
// diff of that commit.
|
||||
func (self *CommitFilesController) diffTarget() *commitDiffTarget {
|
||||
if self.context().GetRef() == nil && self.context().GetRefRange() == nil {
|
||||
return nil
|
||||
}
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
return &commitDiffTarget{from: from, to: to, canRebase: self.context().GetCanRebase()}
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
@@ -109,8 +124,8 @@ func (self *CommitFilesController) GetKeybindings(opts types.KeybindingsOpts) []
|
||||
Keys: opts.GetKeys(opts.Config.Universal.GoInto),
|
||||
Handler: self.withItem(self.enter),
|
||||
GetDisabledReason: self.require(self.singleItemSelected()),
|
||||
Description: self.c.Tr.EnterCommitFile,
|
||||
Tooltip: self.c.Tr.EnterCommitFileTooltip,
|
||||
Description: self.c.Tr.FocusCommitFileDiff,
|
||||
Tooltip: self.c.Tr.FocusCommitFileDiffTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.ToggleTreeView),
|
||||
@@ -193,15 +208,7 @@ func (self *CommitFilesController) GetOnRenderToMain() func() {
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the commit's diff for the given files. Both panes show the same
|
||||
// diff here — the secondary one shows the custom patch built from it, which is not a
|
||||
// diff of the commit — so which pane asks makes no difference.
|
||||
func (self *CommitFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
from, to := self.context().GetFromAndToForDiff()
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
|
||||
return self.diffActions
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) copyDiffToClipboard(paths []string, toastMessage string) error {
|
||||
@@ -550,41 +557,15 @@ func (self *CommitFilesController) currentFromToReverseForPatchBuilding() (strin
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) enter(node *filetree.CommitFileNode) error {
|
||||
return self.enterCommitFile(node, types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1})
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode, opts types.OnFocusOpts) error {
|
||||
if node.File == nil {
|
||||
return self.handleToggleCommitFileDirCollapsed(node)
|
||||
}
|
||||
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextForCustomPatch,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
}
|
||||
return focusMainView(self.c, self.context(), -1)
|
||||
}
|
||||
|
||||
from, to, reverse := self.currentFromToReverseForPatchBuilding()
|
||||
mustDiscardPatch := self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.NewPatchRequired(from, to, reverse)
|
||||
return self.c.ConfirmIf(mustDiscardPatch, types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardPatch,
|
||||
Prompt: self.c.Tr.DiscardPatchConfirm,
|
||||
HandleConfirm: func() error {
|
||||
if mustDiscardPatch {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
}
|
||||
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
if err := self.startPatchBuilder(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
self.c.Context().Push(self.c.Contexts().CustomPatchBuilder, opts)
|
||||
self.c.Helpers().PatchBuilding.ShowHunkStagingHint()
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
func (self *CommitFilesController) GetOnDoubleClick() func() error {
|
||||
return self.withItemGraceful(self.enter)
|
||||
}
|
||||
|
||||
func (self *CommitFilesController) handleToggleCommitFileDirCollapsed(node *filetree.CommitFileNode) error {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
@@ -51,10 +49,6 @@ func (self *ContextLinesController) Context() types.Context {
|
||||
}
|
||||
|
||||
func (self *ContextLinesController) Increase() error {
|
||||
if err := self.checkCanChangeContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.c.UserConfig().Git.DiffContextSize < math.MaxUint64 {
|
||||
self.c.UserConfig().Git.DiffContextSize++
|
||||
}
|
||||
@@ -62,10 +56,6 @@ func (self *ContextLinesController) Increase() error {
|
||||
}
|
||||
|
||||
func (self *ContextLinesController) Decrease() error {
|
||||
if err := self.checkCanChangeContext(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.c.UserConfig().Git.DiffContextSize > 0 {
|
||||
self.c.UserConfig().Git.DiffContextSize--
|
||||
}
|
||||
@@ -76,27 +66,11 @@ func (self *ContextLinesController) applyChange() error {
|
||||
self.c.Toast(fmt.Sprintf(self.c.Tr.DiffContextSizeChanged, self.c.UserConfig().Git.DiffContextSize))
|
||||
|
||||
currentContext := self.c.Context().CurrentSide()
|
||||
switch currentContext.GetKey() {
|
||||
// we make an exception for our staging and patch building contexts because they actually need to refresh their state afterwards.
|
||||
case context.PATCH_BUILDING_MAIN_CONTEXT_KEY:
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.PATCH_BUILDING}})
|
||||
case context.STAGING_MAIN_CONTEXT_KEY, context.STAGING_SECONDARY_CONTEXT_KEY:
|
||||
self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STAGING}})
|
||||
default:
|
||||
// The diff is about to be rendered again with more or less context around
|
||||
// each change, which reads as the lines you were looking at moving up or down
|
||||
// the view; keep them where they are instead.
|
||||
self.c.Helpers().DiffLine.PreserveDiffPositionOnRerender(self.c.Contexts().Normal.GetView())
|
||||
self.c.Helpers().DiffLine.PreserveDiffPositionOnRerender(self.c.Contexts().NormalSecondary.GetView())
|
||||
currentContext.HandleRenderToMain()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *ContextLinesController) checkCanChangeContext() error {
|
||||
if self.c.Git().Patch.PatchBuilder.Active() {
|
||||
return errors.New(self.c.Tr.CantChangeContextSizeError)
|
||||
}
|
||||
|
||||
// The diff is about to be rendered again with more or less context around
|
||||
// each change, which reads as the lines you were looking at moving up or down
|
||||
// the view; keep them where they are instead.
|
||||
self.c.Helpers().DiffLine.PreserveDiffPositionOnRerender(self.c.Contexts().Normal.GetView())
|
||||
self.c.Helpers().DiffLine.PreserveDiffPositionOnRerender(self.c.Contexts().NormalSecondary.GetView())
|
||||
currentContext.HandleRenderToMain()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func (self *CustomPatchOptionsMenuAction) Call() error {
|
||||
{
|
||||
Label: self.c.Tr.ResetPatch,
|
||||
Tooltip: self.c.Tr.ResetPatchTooltip,
|
||||
OnPress: self.c.Helpers().PatchBuilding.Reset,
|
||||
OnPress: self.c.Helpers().CustomPatch.Reset,
|
||||
Keys: menuKey('c'),
|
||||
},
|
||||
{
|
||||
@@ -123,15 +123,7 @@ func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessary() {
|
||||
if self.c.Context().Current().GetKey() == self.c.Contexts().CustomPatchBuilder.GetKey() {
|
||||
self.c.Helpers().PatchBuilding.Escape()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
@@ -142,8 +134,6 @@ func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error {
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
toCommitIndex := self.c.Contexts().LocalCommits.GetSelectedLineIdx()
|
||||
@@ -155,8 +145,6 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() erro
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
mustStash := self.c.Helpers().WorkingTree.IsWorkingTreeDirtyExceptSubmodules()
|
||||
return self.c.ConfirmIf(mustStash, types.ConfirmOpts{
|
||||
Title: self.c.Tr.MustStashTitle,
|
||||
@@ -174,8 +162,6 @@ func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
@@ -209,8 +195,6 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error {
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
self.c.Helpers().Commits.OpenCommitMessagePanel(
|
||||
&helpers.OpenCommitMessagePanelOpts{
|
||||
@@ -244,8 +228,6 @@ func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommitBefore() e
|
||||
}
|
||||
|
||||
func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
|
||||
self.returnFocusFromPatchExplorerIfNecessary()
|
||||
|
||||
affectedUnstagedFiles := self.getAffectedUnstagedFiles()
|
||||
|
||||
mustStageFiles := len(affectedUnstagedFiles) > 0
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// Removes '+' or '-' from the beginning of each line in the diff string, except
|
||||
// when both '+' and '-' lines are present, or diff header lines, in which case
|
||||
// the diff is returned unchanged. This is useful for copying parts of diffs to
|
||||
// the clipboard in order to paste them into code.
|
||||
func dropDiffPrefix(diff string) string {
|
||||
lines := strings.Split(strings.TrimRight(diff, "\n"), "\n")
|
||||
|
||||
const (
|
||||
PLUS int = iota
|
||||
MINUS
|
||||
CONTEXT
|
||||
OTHER
|
||||
)
|
||||
|
||||
linesByType := lo.GroupBy(lines, func(line string) int {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "+"):
|
||||
return PLUS
|
||||
case strings.HasPrefix(line, "-"):
|
||||
return MINUS
|
||||
case strings.HasPrefix(line, " "):
|
||||
return CONTEXT
|
||||
}
|
||||
return OTHER
|
||||
})
|
||||
|
||||
hasLinesOfType := func(lineType int) bool { return len(linesByType[lineType]) > 0 }
|
||||
|
||||
keepPrefix := hasLinesOfType(OTHER) || (hasLinesOfType(PLUS) && hasLinesOfType(MINUS))
|
||||
if keepPrefix {
|
||||
return diff
|
||||
}
|
||||
|
||||
return strings.Join(lo.Map(lines, func(line string, _ int) string { return line[1:] + "\n" }), "")
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package controllers
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
|
||||
@@ -130,3 +131,14 @@ func isInDir(path string, dir string) bool {
|
||||
// "." is the root item, which contains every file
|
||||
return dir == "." || strings.HasPrefix(path, dir+"/")
|
||||
}
|
||||
|
||||
// repoRelativePath turns the absolute path a diff line carries into the one git speaks
|
||||
// of the file: relative to the worktree, with forward slashes. It is "" for a path that
|
||||
// is no file of this repo, which a diff renderer's own naming of a line can produce.
|
||||
func repoRelativePath(worktreePath string, absolutePath string) string {
|
||||
relativePath, err := filepath.Rel(worktreePath, absolutePath)
|
||||
if err != nil || strings.HasPrefix(relativePath, "..") {
|
||||
return ""
|
||||
}
|
||||
return filepath.ToSlash(relativePath)
|
||||
}
|
||||
|
||||
@@ -415,9 +415,7 @@ func (self *FilesController) GetFocusedMainViewDiffSource() types.FocusedMainVie
|
||||
}
|
||||
|
||||
func (self *FilesController) GetOnDoubleClick() func() error {
|
||||
return self.withItemGraceful(func(node *filetree.FileNode) error {
|
||||
return self.press([]*filetree.FileNode{node})
|
||||
})
|
||||
return self.enter
|
||||
}
|
||||
|
||||
// if we are dealing with a status for which there is no key in this map,
|
||||
@@ -694,7 +692,7 @@ func (self *FilesController) getSelectedFile() *models.File {
|
||||
}
|
||||
|
||||
func (self *FilesController) enter() error {
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "", ClickedViewLineIdx: -1})
|
||||
return self.enterFile(-1)
|
||||
}
|
||||
|
||||
func (self *FilesController) collapseAll() error {
|
||||
@@ -713,7 +711,10 @@ func (self *FilesController) expandAll() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
|
||||
// enterFile focuses the diff of the selected file, which is where the commands that
|
||||
// act on its lines live. clickedViewLineIdx is the row of the diff a click landed on,
|
||||
// or -1 when the diff wasn't clicked.
|
||||
func (self *FilesController) enterFile(clickedViewLineIdx int) error {
|
||||
node := self.context().GetSelected()
|
||||
if node == nil {
|
||||
return nil
|
||||
@@ -739,11 +740,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error {
|
||||
return self.switchToMerge()
|
||||
}
|
||||
|
||||
context := lo.Ternary(opts.ClickedWindowName == "secondary", self.c.Contexts().StagingSecondary, self.c.Contexts().Staging)
|
||||
self.c.Context().Push(context, opts)
|
||||
self.c.Helpers().PatchBuilding.ShowHunkStagingHint()
|
||||
|
||||
return nil
|
||||
return focusMainView(self.c, self.context(), clickedViewLineIdx)
|
||||
}
|
||||
|
||||
// conflictResolutionHint formats a conflict description for the main view,
|
||||
@@ -1534,7 +1531,7 @@ func (self *FilesController) handleStashSave(stashFunc func(message string) erro
|
||||
}
|
||||
|
||||
func (self *FilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error {
|
||||
return self.EnterFile(types.OnFocusOpts{ClickedWindowName: "main", ClickedViewLineIdx: opts.Y})
|
||||
return self.enterFile(opts.Y)
|
||||
}
|
||||
|
||||
func (self *FilesController) fetch() error {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package helpers
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
|
||||
type CustomPatchHelper struct {
|
||||
c *HelperCommon
|
||||
}
|
||||
|
||||
func NewCustomPatchHelper(c *HelperCommon) *CustomPatchHelper {
|
||||
return &CustomPatchHelper{c: c}
|
||||
}
|
||||
|
||||
func (self *CustomPatchHelper) Reset() error {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.COMMIT_FILES},
|
||||
})
|
||||
|
||||
// Render again so that the pane that was previewing the patch goes with it. The
|
||||
// panel asked to do that is the side panel rather than whichever context has the
|
||||
// focus. Both main panes are rendered by the panel beneath them, so a reset from
|
||||
// within the focused main view has to go through that panel too.
|
||||
self.c.PostRefreshUpdate(self.c.Context().CurrentSide())
|
||||
return nil
|
||||
}
|
||||
@@ -227,7 +227,7 @@ func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error {
|
||||
// AdjustLineNumber is used to adjust a line number in the diff that's currently
|
||||
// being viewed, so that it corresponds to the line number in the actual working
|
||||
// copy state of the file. It is used when clicking on a delta hyperlink in a
|
||||
// diff, or when pressing `e` in the staging or patch building panels. It works
|
||||
// diff, or when pressing `e` in a focused diff. It works
|
||||
// by getting a diff of what's being viewed in the main view against the working
|
||||
// copy, and then using that diff to adjust the line number.
|
||||
// path is the file path of the file being viewed
|
||||
@@ -238,7 +238,7 @@ func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error {
|
||||
func (self *DiffHelper) AdjustLineNumber(path string, linenumber int, viewname string) int {
|
||||
switch viewname {
|
||||
|
||||
case "main", "patchBuilding":
|
||||
case "main":
|
||||
if diffableContext, ok := self.c.Context().CurrentSide().(types.DiffableContext); ok {
|
||||
ref := diffableContext.RefForAdjustingLineNumberInDiff()
|
||||
if len(ref) != 0 {
|
||||
@@ -249,7 +249,7 @@ func (self *DiffHelper) AdjustLineNumber(path string, linenumber int, viewname s
|
||||
// unstaged changes view of the Files panel; no need to adjust line
|
||||
// numbers in this case
|
||||
|
||||
case "secondary", "stagingSecondary":
|
||||
case "secondary":
|
||||
return self.adjustLineNumber(linenumber, "--", path)
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ func (self *DiffLineHelper) diffLineIdentitiesAt(
|
||||
|
||||
if renderingStatesDiffLines(contents) {
|
||||
if identities := self.diffLineIdentitiesFromRecords(contents[bufferLineIdx].Metadata); len(identities) > 0 {
|
||||
return identities, true
|
||||
return self.inRepoTerms(view, identities), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -76,7 +76,7 @@ func (self *DiffLineHelper) diffLineIdentitiesAt(
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return []types.DiffLineInfo{self.diffLineInfo(parsed)}, true
|
||||
return self.inRepoTerms(view, []types.DiffLineInfo{self.diffLineInfo(parsed)}), true
|
||||
}
|
||||
|
||||
// diffLineIdentitiesFromRecords recovers the identity of every diff line the row's
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
@@ -32,7 +35,81 @@ func (self *DiffLineHelper) DiffLinesInViewRange(view *gocui.View, first int, la
|
||||
previousBufferLine = bufferLine
|
||||
infos = append(infos, identities[bufferLine]...)
|
||||
}
|
||||
return infos
|
||||
return self.inRepoTerms(view, infos)
|
||||
}
|
||||
|
||||
// ChangeLineOrdinals says, for each of the given change lines, which of its file's
|
||||
// changes it is in the given diff — its place among them, counted from the top of the
|
||||
// file — keyed by file. Lines the diff doesn't have are left out.
|
||||
//
|
||||
// It is how a line is named in something built out of a diff rather than being that diff:
|
||||
// the custom patch holds the lines it was given in the order the file has them, so a
|
||||
// place among a file's changes is a line of the patch.
|
||||
func (self *DiffLineHelper) ChangeLineOrdinals(
|
||||
diff string, infos []types.DiffLineInfo,
|
||||
) map[string][]int {
|
||||
ordinals := map[patchLine]int{}
|
||||
counts := map[string]int{}
|
||||
for _, parsed := range parseAllDiffLinesFromBuffer(strings.Split(diff, "\n")) {
|
||||
if !parsed.ok {
|
||||
continue
|
||||
}
|
||||
info := self.diffLineInfo(parsed.parsed)
|
||||
if !info.IsChange() {
|
||||
continue
|
||||
}
|
||||
ordinals[patchLineOf(info)] = counts[info.Path]
|
||||
counts[info.Path]++
|
||||
}
|
||||
|
||||
ordinalsByPath := map[string][]int{}
|
||||
for _, info := range infos {
|
||||
if ordinal, ok := ordinals[patchLineOf(info)]; ok {
|
||||
ordinalsByPath[info.Path] = append(ordinalsByPath[info.Path], ordinal)
|
||||
}
|
||||
}
|
||||
return ordinalsByPath
|
||||
}
|
||||
|
||||
// inRepoTerms brings the paths of lines recovered from a view into the repo's terms.
|
||||
//
|
||||
// They are in them already for a diff of the repo's own files. The pane previewing the
|
||||
// custom patch, though, shows a diff of the two trees the patch was materialized into: a
|
||||
// diff renderer states the path it was handed there, which is under the tree's own name,
|
||||
// while the diff's text names the trees where an ordinary diff has git's a/ and b/
|
||||
// prefixes and so needs nothing.
|
||||
func (self *DiffLineHelper) inRepoTerms(view *gocui.View, infos []types.DiffLineInfo) []types.DiffLineInfo {
|
||||
if !self.ShowsCustomPatch(view) {
|
||||
return infos
|
||||
}
|
||||
|
||||
worktreePath := self.c.Git().RepoPaths.WorktreePath()
|
||||
treesDir := self.c.Git().Patch.PatchBuilder.TempDir()
|
||||
return lo.Map(infos, func(info types.DiffLineInfo, _ int) types.DiffLineInfo {
|
||||
info.Path = repoPathOfTreePath(info.Path, treesDir, worktreePath)
|
||||
return info
|
||||
})
|
||||
}
|
||||
|
||||
// repoPathOfTreePath maps a path under one of the trees the custom patch was materialized
|
||||
// into to the file of the repo it stands for: the path is the tree's name followed by the
|
||||
// file's own, stated either against the directory holding the trees or against the repo,
|
||||
// depending on how the renderer that stated it was given it.
|
||||
func repoPathOfTreePath(path string, treesDir string, worktreePath string) string {
|
||||
root := worktreePath
|
||||
if treesDir != "" && strings.HasPrefix(path, treesDir+string(filepath.Separator)) {
|
||||
root = treesDir
|
||||
}
|
||||
relativePath, err := filepath.Rel(root, path)
|
||||
if err != nil {
|
||||
return path
|
||||
}
|
||||
|
||||
segments := strings.Split(filepath.ToSlash(relativePath), "/")
|
||||
if len(segments) > 1 && (segments[0] == "a" || segments[0] == "b") {
|
||||
relativePath = filepath.Join(segments[1:]...)
|
||||
}
|
||||
return filepath.Join(worktreePath, relativePath)
|
||||
}
|
||||
|
||||
// ChangeLinesInViewRange returns the change lines — the additions and deletions —
|
||||
@@ -143,8 +220,7 @@ func (self *DiffLineHelper) IsChangeLine(view *gocui.View, viewLineIdx int) bool
|
||||
// is shown as one solid block of changes — every row of its diff a change of the same
|
||||
// kind, no context — which is what a newly added or deleted file looks like. That is
|
||||
// the case where widening the selection to the change block would select the file
|
||||
// entire, so hunk mode drops to a single line there instead. It asks of a rendered
|
||||
// diff the question patch.Patch.IsSingleHunkForWholeFile asks of a patch.
|
||||
// entire, so hunk mode drops to a single line there instead.
|
||||
//
|
||||
// It says false while the diff is still being read in, since the rows that would
|
||||
// answer otherwise — a context line, a change of the other kind — may not have
|
||||
|
||||
@@ -549,3 +549,59 @@ func patchLineOf(info types.DiffLineInfo) patchLine {
|
||||
return patchLine{path: info.Path, kind: types.DiffLineContext, line: info.NewLine}
|
||||
}
|
||||
}
|
||||
|
||||
// RevealSelectionAfterAction moves a diff pane's selection to the change that takes the
|
||||
// place of the one just acted on, once the changed diff has re-rendered. Call it with
|
||||
// the pane acted in, the pane the work carries on in, and the first line of the
|
||||
// selection, before triggering the re-render.
|
||||
//
|
||||
// The line acted on is gone from the diff, so what is remembered is its place among the
|
||||
// diff's changes: the next change moves up into it, which is where you want to be to
|
||||
// carry on. A range collapses to a single line at its start, and hunk mode selects the
|
||||
// whole block it lands in, so that pressing the key again acts on the next hunk. The
|
||||
// target pane inherits that select mode, this being the same piece of work continuing
|
||||
// in another pane — and shows no selection until the restore places one, so that what
|
||||
// it was left showing the last time it was used doesn't appear for a frame.
|
||||
//
|
||||
// advanceBy moves on by that many changes past the place remembered, for an action that
|
||||
// leaves the diff as it was: lines taken into a custom patch are still in the commit's
|
||||
// diff, so the place remembered is still the line acted on, and carrying on means going
|
||||
// past the lines just dealt with rather than staying on them.
|
||||
//
|
||||
// done, which may be nil, is called once the selection is where it belongs, or once it
|
||||
// turns out that no render is coming to put it there — for a caller that must not let
|
||||
// the user act again in between.
|
||||
func (self *DiffLineHelper) RevealSelectionAfterAction(
|
||||
source types.DiffPaneContext, target types.DiffPaneContext, firstLineIdx int, advanceBy int, done func(),
|
||||
) {
|
||||
ordinal, ok := self.ChangeLineOrdinal(source.GetView(), firstLineIdx)
|
||||
if !ok {
|
||||
if done != nil {
|
||||
done()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
sel := source.DiffSelectState()
|
||||
if sel.Mode == types.DiffSelectModeRange {
|
||||
sel.Mode = types.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
}
|
||||
*target.DiffSelectState() = *sel
|
||||
selectHunk := sel.Mode == types.DiffSelectModeHunk
|
||||
|
||||
targetView := target.GetView()
|
||||
if target != source {
|
||||
target.SetHasSelectableContent(false)
|
||||
self.c.Context().UpdateSelectionHighlights()
|
||||
}
|
||||
|
||||
self.RevealChangeLineAtOrdinal(targetView, ordinal+advanceBy, func(viewLine int) {
|
||||
if selectHunk {
|
||||
self.SelectChangeBlock(target, viewLine, true)
|
||||
return
|
||||
}
|
||||
targetView.CancelRangeSelect()
|
||||
self.ShowSelectionAtLine(targetView, viewLine, true)
|
||||
}, done)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func (self *DiffLineHelper) EstablishSelection(mainContext *context.MainContext,
|
||||
// there is nothing else to offer. Line by line it is simply the first change line on
|
||||
// screen. ok is false when the viewport shows no change at all.
|
||||
func (self *DiffLineHelper) changeToSelectOnScreen(view *gocui.View) (int, bool) {
|
||||
if self.c.UserConfig().Gui.UseHunkModeInStagingView {
|
||||
if self.c.UserConfig().Gui.UseHunkModeInDiffView {
|
||||
return self.FirstChangeBlockInView(view)
|
||||
}
|
||||
return self.FirstChangeLineInView(view)
|
||||
@@ -81,7 +81,7 @@ func (self *DiffLineHelper) changeToSelectOnScreen(view *gocui.View) (int, bool)
|
||||
// except over a file shown as one solid block of changes, where it would select the
|
||||
// whole file — see IsSingleHunkForWholeFile.
|
||||
func (self *DiffLineHelper) hunkModeApplies(view *gocui.View, changeViewLine int) bool {
|
||||
return self.c.UserConfig().Gui.UseHunkModeInStagingView &&
|
||||
return self.c.UserConfig().Gui.UseHunkModeInDiffView &&
|
||||
!self.IsSingleHunkForWholeFile(view, changeViewLine)
|
||||
}
|
||||
|
||||
@@ -131,3 +131,70 @@ func (self *DiffLineHelper) SelectedHunkBounds(view *gocui.View) (int, int, bool
|
||||
}
|
||||
return self.ChangeBlockBounds(view, anchor)
|
||||
}
|
||||
|
||||
// RefreshInclusionGutter updates the marks drawn over the diff in the main pane, which
|
||||
// say which of its lines are in the custom patch being built from it.
|
||||
//
|
||||
// They are shown while the focused main view holds the focus — either of its panes, so
|
||||
// that moving between the diff and the patch previewed beside it doesn't make them come
|
||||
// and go — and only over a diff a patch is being built from: a patch built from some
|
||||
// other commit says nothing about the lines of this one.
|
||||
//
|
||||
// Call it whenever either of those can have changed: as a pane's content settles, when
|
||||
// the focus arrives or leaves, and when the patch itself changes.
|
||||
func (self *DiffLineHelper) RefreshInclusionGutter() {
|
||||
view := self.c.Contexts().Normal.GetView()
|
||||
|
||||
included := self.patchInclusion()
|
||||
if included == nil {
|
||||
view.SetInclusionGutter(false, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resolved := self.resolveDiffLines(view.DiffLineContents())
|
||||
marks := make([]bool, len(resolved))
|
||||
showsChanges := false
|
||||
for i, row := range resolved {
|
||||
if !row.ok || !row.info.IsChange() {
|
||||
continue
|
||||
}
|
||||
showsChanges = true
|
||||
marks[i] = included(row.info)
|
||||
}
|
||||
|
||||
// Nothing to mark and nowhere to mark it: the pane is showing a message rather than
|
||||
// a diff, or a diff with nothing in it.
|
||||
if !showsChanges {
|
||||
view.SetInclusionGutter(false, nil)
|
||||
return
|
||||
}
|
||||
view.SetInclusionGutter(true, marks)
|
||||
}
|
||||
|
||||
// patchInclusion asks the panel whose diff the focused main view is showing which of
|
||||
// that diff's lines are in the custom patch being built from it, and answers nil where
|
||||
// there is no such patch — including when the focus is elsewhere, the marks being an
|
||||
// affordance of the focused view.
|
||||
func (self *DiffLineHelper) patchInclusion() func(types.DiffLineInfo) bool {
|
||||
if !self.mainViewIsFocused() {
|
||||
return nil
|
||||
}
|
||||
// The panel beneath is found from the pane that holds the focus, which is not always
|
||||
// the one the diff is in: moving to the pane beside it takes the other off the stack.
|
||||
sidePanel := self.c.Context().NextInStack(self.c.Context().CurrentStatic())
|
||||
if sidePanel == nil {
|
||||
return nil
|
||||
}
|
||||
actions, ok := sidePanel.GetFocusedMainViewDiffSource().(types.FocusedMainViewActions)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return actions.PatchInclusion()
|
||||
}
|
||||
|
||||
// ShowsCustomPatch reports whether the given view is the one previewing the custom patch
|
||||
// being built, which is the lower pane while a patch is being built from the diff in the
|
||||
// upper one.
|
||||
func (self *DiffLineHelper) ShowsCustomPatch(view *gocui.View) bool {
|
||||
return view == self.c.Contexts().NormalSecondary.GetView() && self.patchInclusion() != nil
|
||||
}
|
||||
|
||||
@@ -28,8 +28,7 @@ type Helpers struct {
|
||||
MergeConflicts *MergeConflictsHelper
|
||||
CherryPick *CherryPickHelper
|
||||
Host *HostHelper
|
||||
PatchBuilding *PatchBuildingHelper
|
||||
Staging *StagingHelper
|
||||
CustomPatch *CustomPatchHelper
|
||||
GPG *GpgHelper
|
||||
Upstream *UpstreamHelper
|
||||
AmendHelper *AmendHelper
|
||||
@@ -68,8 +67,7 @@ func NewStubHelpers() *Helpers {
|
||||
MergeConflicts: &MergeConflictsHelper{},
|
||||
CherryPick: &CherryPickHelper{},
|
||||
Host: &HostHelper{},
|
||||
PatchBuilding: &PatchBuildingHelper{},
|
||||
Staging: &StagingHelper{},
|
||||
CustomPatch: &CustomPatchHelper{},
|
||||
GPG: &GpgHelper{},
|
||||
Upstream: &UpstreamHelper{},
|
||||
AmendHelper: &AmendHelper{},
|
||||
|
||||
@@ -2,9 +2,12 @@ package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/hosting_service"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// this helper just wraps our hosting_service package
|
||||
// this helper wraps our hosting_service package, and answers what we know about the
|
||||
// repo's pull requests on it
|
||||
|
||||
type HostHelper struct {
|
||||
c *HelperCommon
|
||||
@@ -18,6 +21,24 @@ func NewHostHelper(
|
||||
}
|
||||
}
|
||||
|
||||
// PullRequestForBranch returns the pull request the given local branch is the head of,
|
||||
// and false where it has none. That covers knowing of no pull requests at all: the repo
|
||||
// may not be on GitHub, or the answer may not have arrived yet.
|
||||
func (self *HostHelper) PullRequestForBranch(branchName string) (*models.GithubPullRequest, bool) {
|
||||
pr, ok := self.c.Model().PullRequestsMap[branchName]
|
||||
return pr, ok
|
||||
}
|
||||
|
||||
// NoPullRequestDisabledReason disables a command that acts on a branch's pull request
|
||||
// while that branch has none.
|
||||
func (self *HostHelper) NoPullRequestDisabledReason(branchName string) *types.DisabledReason {
|
||||
if _, ok := self.PullRequestForBranch(branchName); !ok {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *HostHelper) GetPullRequestURL(from string, to string) (string, error) {
|
||||
mgr, err := self.getHostingServiceMgr()
|
||||
if err != nil {
|
||||
|
||||
@@ -14,7 +14,7 @@ type ModeHelper struct {
|
||||
c *HelperCommon
|
||||
|
||||
diffHelper *DiffHelper
|
||||
patchBuildingHelper *PatchBuildingHelper
|
||||
customPatchHelper *CustomPatchHelper
|
||||
cherryPickHelper *CherryPickHelper
|
||||
mergeAndRebaseHelper *MergeAndRebaseHelper
|
||||
bisectHelper *BisectHelper
|
||||
@@ -24,7 +24,7 @@ type ModeHelper struct {
|
||||
func NewModeHelper(
|
||||
c *HelperCommon,
|
||||
diffHelper *DiffHelper,
|
||||
patchBuildingHelper *PatchBuildingHelper,
|
||||
customPatchHelper *CustomPatchHelper,
|
||||
cherryPickHelper *CherryPickHelper,
|
||||
mergeAndRebaseHelper *MergeAndRebaseHelper,
|
||||
bisectHelper *BisectHelper,
|
||||
@@ -32,7 +32,7 @@ func NewModeHelper(
|
||||
return &ModeHelper{
|
||||
c: c,
|
||||
diffHelper: diffHelper,
|
||||
patchBuildingHelper: patchBuildingHelper,
|
||||
customPatchHelper: customPatchHelper,
|
||||
cherryPickHelper: cherryPickHelper,
|
||||
mergeAndRebaseHelper: mergeAndRebaseHelper,
|
||||
bisectHelper: bisectHelper,
|
||||
@@ -71,9 +71,9 @@ func (self *ModeHelper) Statuses() []ModeStatus {
|
||||
return self.withResetButton(self.c.Tr.BuildingPatch, style.FgYellow.SetBold())
|
||||
},
|
||||
CancelLabel: func() string {
|
||||
return self.c.Tr.ExitCustomPatchBuilder
|
||||
return self.c.Tr.ResetCustomPatch
|
||||
},
|
||||
Reset: self.patchBuildingHelper.Reset,
|
||||
Reset: self.customPatchHelper.Reset,
|
||||
},
|
||||
{
|
||||
IsActive: self.c.Modes().Filtering.Active,
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type PatchBuildingHelper struct {
|
||||
c *HelperCommon
|
||||
}
|
||||
|
||||
func NewPatchBuildingHelper(
|
||||
c *HelperCommon,
|
||||
) *PatchBuildingHelper {
|
||||
return &PatchBuildingHelper{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingHelper) ShowHunkStagingHint() {
|
||||
if !self.c.AppState.DidShowHunkStagingHint && self.c.UserConfig().Gui.UseHunkModeInStagingView {
|
||||
self.c.AppState.DidShowHunkStagingHint = true
|
||||
self.c.SaveAppStateAndLogError()
|
||||
|
||||
message := fmt.Sprintf(self.c.Tr.HunkStagingHint, self.c.UserConfig().Keybinding.Main.ToggleSelectHunk)
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Prompt: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// takes us from the patch building panel back to the commit files panel
|
||||
func (self *PatchBuildingHelper) Escape() {
|
||||
self.c.Context().Pop()
|
||||
}
|
||||
|
||||
// kills the custom patch and returns us back to the commit files panel if needed
|
||||
func (self *PatchBuildingHelper) Reset() error {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
|
||||
if self.c.Context().CurrentStatic().GetKind() != types.SIDE_CONTEXT {
|
||||
self.Escape()
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.COMMIT_FILES},
|
||||
})
|
||||
|
||||
// refreshing the current context so that the secondary panel is hidden if necessary.
|
||||
self.c.PostRefreshUpdate(self.c.Context().Current())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) {
|
||||
selectedLineIdx := -1
|
||||
if opts.ClickedWindowName == "main" {
|
||||
selectedLineIdx = opts.ClickedViewLineIdx
|
||||
}
|
||||
|
||||
if !self.c.Git().Patch.PatchBuilder.Active() {
|
||||
self.Escape()
|
||||
return
|
||||
}
|
||||
|
||||
// get diff from commit file that's currently selected
|
||||
file := self.c.Contexts().CommitFiles.GetSelectedFile()
|
||||
if file == nil {
|
||||
return
|
||||
}
|
||||
|
||||
from, to := self.c.Contexts().CommitFiles.GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, file.Path, file.PreviousPath, git_commands.DiffModePlain)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(patch.RenderPatchForFileOpts{
|
||||
Filename: file.Path,
|
||||
PreviousPath: file.PreviousPath,
|
||||
Plain: false,
|
||||
Reverse: false,
|
||||
TurnAddedFilesIntoDiffAgainstEmptyFile: true,
|
||||
})
|
||||
|
||||
context := self.c.Contexts().CustomPatchBuilder
|
||||
|
||||
oldState := context.GetState()
|
||||
|
||||
state := patch_exploring.NewState(diff, selectedLineIdx, context.GetView(), oldState, self.c.UserConfig().Gui.UseHunkModeInStagingView)
|
||||
context.SetState(state)
|
||||
if state == nil {
|
||||
self.Escape()
|
||||
return
|
||||
}
|
||||
|
||||
mainContent := context.GetContentToRender()
|
||||
|
||||
self.c.Contexts().CustomPatchBuilder.FocusSelection()
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
Pair: self.c.MainViewPairs().PatchBuilding,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
Task: types.NewRenderStringWithoutScrollTask(mainContent),
|
||||
Title: self.c.Tr.Patch,
|
||||
},
|
||||
Secondary: &types.ViewUpdateOpts{
|
||||
Task: types.NewRenderStringWithoutScrollTask(secondaryDiff),
|
||||
Title: self.c.Tr.CustomPatch,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -30,8 +30,6 @@ type RefreshHelper struct {
|
||||
c *HelperCommon
|
||||
refsHelper *RefsHelper
|
||||
mergeAndRebaseHelper *MergeAndRebaseHelper
|
||||
patchBuildingHelper *PatchBuildingHelper
|
||||
stagingHelper *StagingHelper
|
||||
mergeConflictsHelper *MergeConflictsHelper
|
||||
worktreeHelper *WorktreeHelper
|
||||
searchHelper *SearchHelper
|
||||
@@ -62,8 +60,6 @@ func NewRefreshHelper(
|
||||
c *HelperCommon,
|
||||
refsHelper *RefsHelper,
|
||||
mergeAndRebaseHelper *MergeAndRebaseHelper,
|
||||
patchBuildingHelper *PatchBuildingHelper,
|
||||
stagingHelper *StagingHelper,
|
||||
mergeConflictsHelper *MergeConflictsHelper,
|
||||
worktreeHelper *WorktreeHelper,
|
||||
searchHelper *SearchHelper,
|
||||
@@ -72,8 +68,6 @@ func NewRefreshHelper(
|
||||
c: c,
|
||||
refsHelper: refsHelper,
|
||||
mergeAndRebaseHelper: mergeAndRebaseHelper,
|
||||
patchBuildingHelper: patchBuildingHelper,
|
||||
stagingHelper: stagingHelper,
|
||||
mergeConflictsHelper: mergeConflictsHelper,
|
||||
worktreeHelper: worktreeHelper,
|
||||
searchHelper: searchHelper,
|
||||
@@ -248,8 +242,6 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
||||
|
||||
var scopeSet *set.Set[types.RefreshableView]
|
||||
if len(options.Scope) == 0 {
|
||||
// not refreshing staging/patch-building unless explicitly requested because we only need
|
||||
// to refresh those while focused.
|
||||
scopeSet = set.NewFromSlice([]types.RefreshableView{
|
||||
types.COMMITS,
|
||||
types.BRANCHES,
|
||||
@@ -261,7 +253,6 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
||||
types.WORKTREES,
|
||||
types.STATUS,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
types.PULL_REQUESTS,
|
||||
})
|
||||
} else {
|
||||
@@ -516,37 +507,9 @@ func (self *RefreshHelper) performRefresh(options types.RefreshOptions, calledFr
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.STAGING) {
|
||||
refresh("staging", func() {
|
||||
fileWg.Wait()
|
||||
// Bounce onto the UI thread so this runs after the files
|
||||
// scope's model-update bounce — RefreshStagingPanel reads
|
||||
// Model.Files (via Files.GetSelected) and would otherwise
|
||||
// see the pre-refresh model. Guard on the generation so a
|
||||
// repo switch mid-refresh drops it, like the model bounces.
|
||||
self.onUIThreadUnlessRepoChanged(env, func() {
|
||||
self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.PATCH_BUILDING) {
|
||||
refresh("patch building", func() {
|
||||
// Bounce onto the UI thread, like the staging panel above:
|
||||
// RefreshPatchBuildingPanel reads the commit-files selection and
|
||||
// sets the patch view's origin, neither of which may run off the UI
|
||||
// thread. Guard on the generation so a repo switch mid-refresh drops
|
||||
// it, like the model bounces.
|
||||
self.onUIThreadUnlessRepoChanged(env, func() {
|
||||
self.patchBuildingHelper.RefreshPatchBuildingPanel(types.OnFocusOpts{})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
if scopeSet.Includes(types.MERGE_CONFLICTS) {
|
||||
refresh("merge conflicts", func() {
|
||||
// Bounce onto the UI thread, like the staging and patch-building
|
||||
// panels above: RefreshMergeState reads the current context and
|
||||
// Bounce onto the UI thread: RefreshMergeState reads the current context and
|
||||
// renders (or escapes) the merge-conflicts view, none of which may
|
||||
// run off the UI thread.
|
||||
self.onUIThreadUnlessRepoChanged(env, func() {
|
||||
@@ -676,8 +639,6 @@ func getScopeNames(scopes []types.RefreshableView) []string {
|
||||
types.WORKTREES: "worktrees",
|
||||
types.STATUS: "status",
|
||||
types.BISECT_INFO: "bisect",
|
||||
types.STAGING: "staging",
|
||||
types.PATCH_BUILDING: "patchBuilding",
|
||||
types.MERGE_CONFLICTS: "mergeConflicts",
|
||||
types.COMMIT_FILES: "commitFiles",
|
||||
types.PULL_REQUESTS: "pullRequests",
|
||||
@@ -1086,17 +1047,31 @@ type capturedCommitFilesState struct {
|
||||
from string
|
||||
to string
|
||||
reverse bool
|
||||
// Whether there is a commit to load the files of at all. The panel is only ever
|
||||
// pointed at one by being entered, and a patch can now be built from a commit's diff
|
||||
// without that — after which anything that refreshes the panel would otherwise be
|
||||
// asking for the files of nothing.
|
||||
hasCommit bool
|
||||
}
|
||||
|
||||
// captureCommitFilesState reads the commit-files refresh's diff endpoints into
|
||||
// an immutable snapshot. It must run on the UI thread.
|
||||
func (self *RefreshHelper) captureCommitFilesState() capturedCommitFilesState {
|
||||
from, to := self.c.Contexts().CommitFiles.GetFromAndToForDiff()
|
||||
commitFilesContext := self.c.Contexts().CommitFiles
|
||||
if commitFilesContext.GetRef() == nil && commitFilesContext.GetRefRange() == nil {
|
||||
return capturedCommitFilesState{}
|
||||
}
|
||||
|
||||
from, to := commitFilesContext.GetFromAndToForDiff()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from)
|
||||
return capturedCommitFilesState{from: from, to: to, reverse: reverse}
|
||||
return capturedCommitFilesState{from: from, to: to, reverse: reverse, hasCommit: true}
|
||||
}
|
||||
|
||||
func (self *RefreshHelper) refreshCommitFilesContext(captured capturedCommitFilesState, env refreshEnv) error {
|
||||
if !captured.hasCommit {
|
||||
return nil
|
||||
}
|
||||
|
||||
files, err := env.git.Loaders.CommitFileLoader.GetFilesInDiff(captured.from, captured.to, captured.reverse)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -50,7 +50,6 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions
|
||||
types.REFLOG,
|
||||
types.WORKTREES,
|
||||
types.BISECT_INFO,
|
||||
types.STAGING,
|
||||
}
|
||||
if options.RefreshPullRequests {
|
||||
scope = append(scope, types.PULL_REQUESTS)
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
package helpers
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type StagingHelper struct {
|
||||
c *HelperCommon
|
||||
}
|
||||
|
||||
func NewStagingHelper(
|
||||
c *HelperCommon,
|
||||
) *StagingHelper {
|
||||
return &StagingHelper{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: used from outside this file
|
||||
func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) {
|
||||
secondaryFocused := self.secondaryStagingFocused()
|
||||
mainFocused := self.mainStagingFocused()
|
||||
|
||||
// this method could be called when the staging panel is not being used,
|
||||
// in which case we don't want to do anything.
|
||||
if !mainFocused && !secondaryFocused {
|
||||
return
|
||||
}
|
||||
|
||||
mainSelectedLineIdx := -1
|
||||
secondarySelectedLineIdx := -1
|
||||
if focusOpts.ClickedViewLineIdx > 0 {
|
||||
if secondaryFocused {
|
||||
secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx
|
||||
} else {
|
||||
mainSelectedLineIdx = focusOpts.ClickedViewLineIdx
|
||||
}
|
||||
}
|
||||
|
||||
mainContext := self.c.Contexts().Staging
|
||||
secondaryContext := self.c.Contexts().StagingSecondary
|
||||
|
||||
var file *models.File
|
||||
node := self.c.Contexts().Files.GetSelected()
|
||||
if node != nil {
|
||||
file = node.File
|
||||
}
|
||||
|
||||
if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) {
|
||||
self.handleStagingEscape()
|
||||
return
|
||||
}
|
||||
|
||||
mainDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, false)
|
||||
secondaryDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, true)
|
||||
|
||||
// grabbing locks here and releasing before we finish the function
|
||||
// because pushing say the secondary context could mean entering this function
|
||||
// again, and we don't want to have a deadlock
|
||||
mainContext.GetMutex().Lock()
|
||||
secondaryContext.GetMutex().Lock()
|
||||
|
||||
hunkMode := self.c.UserConfig().Gui.UseHunkModeInStagingView
|
||||
mainContext.SetState(
|
||||
patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetView(), mainContext.GetState(), hunkMode),
|
||||
)
|
||||
|
||||
secondaryContext.SetState(
|
||||
patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetView(), secondaryContext.GetState(), hunkMode),
|
||||
)
|
||||
|
||||
mainState := mainContext.GetState()
|
||||
secondaryState := secondaryContext.GetState()
|
||||
|
||||
mainContent := mainContext.GetContentToRender()
|
||||
secondaryContent := secondaryContext.GetContentToRender()
|
||||
|
||||
mainContext.GetMutex().Unlock()
|
||||
secondaryContext.GetMutex().Unlock()
|
||||
|
||||
if mainState == nil && secondaryState == nil {
|
||||
self.handleStagingEscape()
|
||||
return
|
||||
}
|
||||
|
||||
if mainState == nil && !secondaryFocused {
|
||||
self.c.Context().Push(secondaryContext, focusOpts)
|
||||
return
|
||||
}
|
||||
|
||||
if secondaryState == nil && secondaryFocused {
|
||||
self.c.Context().Push(mainContext, focusOpts)
|
||||
return
|
||||
}
|
||||
|
||||
if secondaryFocused {
|
||||
self.c.Contexts().StagingSecondary.FocusSelection()
|
||||
} else {
|
||||
self.c.Contexts().Staging.FocusSelection()
|
||||
}
|
||||
|
||||
self.c.RenderToMainViews(types.RefreshMainOpts{
|
||||
Pair: self.c.MainViewPairs().Staging,
|
||||
Main: &types.ViewUpdateOpts{
|
||||
Task: types.NewRenderStringWithoutScrollTask(mainContent),
|
||||
Title: self.c.Tr.UnstagedChanges,
|
||||
},
|
||||
Secondary: &types.ViewUpdateOpts{
|
||||
Task: types.NewRenderStringWithoutScrollTask(secondaryContent),
|
||||
Title: self.c.Tr.StagedChanges,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *StagingHelper) handleStagingEscape() {
|
||||
self.c.Context().Push(self.c.Contexts().Files, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
func (self *StagingHelper) secondaryStagingFocused() bool {
|
||||
return self.c.Context().CurrentStatic().GetKey() == self.c.Contexts().StagingSecondary.GetKey()
|
||||
}
|
||||
|
||||
func (self *StagingHelper) mainStagingFocused() bool {
|
||||
return self.c.Context().CurrentStatic().GetKey() == self.c.Contexts().Staging.GetKey()
|
||||
}
|
||||
@@ -667,15 +667,11 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) checkedOutBranchHasPR() *types.DisabledReason {
|
||||
branch := self.c.Model().CheckedOutBranch
|
||||
if _, ok := self.c.Model().PullRequestsMap[branch]; !ok {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NoPullRequestForBranch, ShowErrorInPanel: true}
|
||||
}
|
||||
return nil
|
||||
return self.c.Helpers().Host.NoPullRequestDisabledReason(self.c.Model().CheckedOutBranch)
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) openPRInBrowser() error {
|
||||
pr, ok := self.c.Model().PullRequestsMap[self.c.Model().CheckedOutBranch]
|
||||
pr, ok := self.c.Helpers().Host.PullRequestForBranch(self.c.Model().CheckedOutBranch)
|
||||
if !ok {
|
||||
// Should be guarded against by the DisabledReason check, but be defensive in case
|
||||
// PullRequestsMap was updated concurrently by a background refresh
|
||||
@@ -722,17 +718,30 @@ func (self *LocalCommitsController) GetOnRenderToMain() func() {
|
||||
}
|
||||
}
|
||||
|
||||
// secondaryPatchPanelUpdateOpts renders the custom patch being built into the pane
|
||||
// beside the diff it is being built from, as a diff of the two trees the patch is
|
||||
// materialized into. This way it is shown by whatever renders the rest of the diffs,
|
||||
// and its lines can be pointed at and taken back out of the patch.
|
||||
func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts {
|
||||
if c.Git().Patch.PatchBuilder.Active() {
|
||||
patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false)
|
||||
|
||||
return &types.ViewUpdateOpts{
|
||||
Task: types.NewMainViewDiffStringTask(patch),
|
||||
Title: c.Tr.CustomPatch,
|
||||
}
|
||||
if !c.Git().Patch.PatchBuilder.Active() {
|
||||
return nil
|
||||
}
|
||||
|
||||
return nil
|
||||
// A render of the same patch reuses the trees; only a change to the patch writes them
|
||||
// again.
|
||||
if err := c.Git().Patch.EnsureCustomPatchDiffTrees(); err != nil {
|
||||
c.Log.Error(err)
|
||||
}
|
||||
|
||||
// The same mode as the diff beside it: both panes of the pair have to agree about
|
||||
// whether what they show can be acted on.
|
||||
mode := c.Helpers().DiffLine.MainViewDiffMode()
|
||||
cmdObj := c.Git().Diff.CustomPatchDiffCmdObj(c.Git().Patch.PatchBuilder.TempDir(), mode)
|
||||
|
||||
return &types.ViewUpdateOpts{
|
||||
Task: types.NewMainViewDiffTask(cmdObj.GetCmd(), mode),
|
||||
Title: c.Tr.CustomPatch,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) squashDown(selectedCommits []*models.Commit, startIdx int, endIdx int) error {
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
@@ -15,10 +22,13 @@ type MainViewController struct {
|
||||
context *context.MainContext
|
||||
otherContext *context.MainContext
|
||||
|
||||
dragAutoscroller *helpers.DragAutoscroller
|
||||
draggingWithMouse bool
|
||||
dragAutoscroller *helpers.DragAutoscroller
|
||||
draggingWithMouse bool
|
||||
lineFlashGeneration uint64
|
||||
}
|
||||
|
||||
const editedLineFlashDuration = 200 * time.Millisecond
|
||||
|
||||
var _ types.IController = &MainViewController{}
|
||||
|
||||
func NewMainViewController(
|
||||
@@ -46,8 +56,8 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.togglePanel,
|
||||
Description: self.c.Tr.ToggleStagingView,
|
||||
Tooltip: self.c.Tr.ToggleStagingViewTooltip,
|
||||
Description: self.c.Tr.ToggleDiffPane,
|
||||
Tooltip: self.c.Tr.ToggleDiffPaneTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
@@ -80,22 +90,31 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
Tooltip: self.c.Tr.EditFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
||||
Handler: self.primaryAction,
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
||||
Handler: self.primaryAction,
|
||||
// The description is of the working tree's diff, which is where the key does
|
||||
// the thing users know it for; over a commit's diff it says so for itself.
|
||||
Description: self.c.Tr.Stage,
|
||||
DescriptionFunc: self.workingTreeActionDescription(self.c.Tr.Stage),
|
||||
DescriptionFunc: self.diffActionDescription(self.c.Tr.Stage, self.c.Tr.ToggleSelectionForPatch),
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
Tooltip: self.c.Tr.StageSelectionTooltip,
|
||||
DisplayOnScreen: true,
|
||||
// Over a commit's diff the key toggles lines in the custom patch, which the
|
||||
// description says for itself; there is nothing to add to it.
|
||||
TooltipFunc: self.diffActionDescription(self.c.Tr.StageSelectionTooltip, ""),
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
||||
Handler: self.discardSelection,
|
||||
Description: self.c.Tr.DiscardSelection,
|
||||
DescriptionFunc: self.workingTreeActionDescription(self.c.Tr.DiscardSelection),
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
DescriptionFunc: self.diffActionDescription(self.c.Tr.DiscardSelection, self.c.Tr.RemoveSelectionFromPatch),
|
||||
GetDisabledReason: self.discardSelectionDisabledReason,
|
||||
Tooltip: self.c.Tr.DiscardSelectionTooltip,
|
||||
DisplayOnScreen: true,
|
||||
// Over a commit's diff the key rewrites the commit rather than touching the
|
||||
// index, which is worth the warning the other tooltip carries.
|
||||
TooltipFunc: self.diffActionDescription(
|
||||
self.c.Tr.DiscardSelectionTooltip, self.c.Tr.RemoveSelectionFromPatchTooltip),
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.EditSelectHunk),
|
||||
@@ -108,8 +127,8 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard),
|
||||
Handler: self.copySelection,
|
||||
Description: self.c.Tr.CopySelectedTextToClipboard,
|
||||
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.CopySelectedTextToClipboard),
|
||||
Description: self.c.Tr.CopySelectedDiffLinesToClipboard,
|
||||
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.CopySelectedDiffLinesToClipboard),
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
},
|
||||
{
|
||||
@@ -140,6 +159,14 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty
|
||||
DescriptionFunc: self.diffSelectionDescriptionText(self.c.Tr.NextFileInDiff),
|
||||
GetDisabledReason: self.diffSelectionDisabledReason,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Commits.OpenPullRequestInBrowser),
|
||||
Handler: self.openPullRequestAtSelectedLine,
|
||||
Description: self.c.Tr.OpenPullRequestAtSelectedLine,
|
||||
DescriptionFunc: self.pullRequestDescription(self.c.Tr.OpenPullRequestAtSelectedLine),
|
||||
GetDisabledReason: self.openPullRequestDisabledReason,
|
||||
Tooltip: self.c.Tr.OpenPullRequestAtSelectedLineTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Return),
|
||||
Handler: self.escape,
|
||||
@@ -232,6 +259,20 @@ func (self *MainViewController) GetMouseKeybindings(opts types.KeybindingsOpts)
|
||||
Key: gocui.MouseRelease,
|
||||
Handler: self.onDragRelease,
|
||||
},
|
||||
{
|
||||
ViewName: self.context.GetViewName(),
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModAlt,
|
||||
Handler: self.editClickedLine,
|
||||
HandleWhenPopupPanelFocused: true,
|
||||
},
|
||||
{
|
||||
ViewName: self.context.GetViewName(),
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModShift,
|
||||
Handler: self.editClickedLine,
|
||||
HandleWhenPopupPanelFocused: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,6 +280,14 @@ func (self *MainViewController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
// GetOnFocus brings on the marks over the lines that are in the custom patch, which
|
||||
// are an affordance of the focused view, so they arrive with the focus.
|
||||
func (self *MainViewController) GetOnFocus() func(types.OnFocusOpts) {
|
||||
return func(types.OnFocusOpts) {
|
||||
self.c.Helpers().DiffLine.RefreshInclusionGutter()
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MainViewController) togglePanel() error {
|
||||
if !self.otherContext.GetView().Visible {
|
||||
return nil
|
||||
@@ -291,17 +340,22 @@ func (self *MainViewController) isDiffView() bool {
|
||||
return self.diffMainViewType() != types.DiffMainViewTypeNone
|
||||
}
|
||||
|
||||
// sidePanelBeneath returns the side panel this pane is showing the content of, and nil
|
||||
// when there is none. The IsInStack guard is essential: NextInStack panics for a context
|
||||
// that isn't in the stack, and GetKeybindings (which leads here) also runs for off-stack
|
||||
// panes — at startup and while generating the cheatsheets, where the stack is empty.
|
||||
func (self *MainViewController) sidePanelBeneath() types.Context {
|
||||
if !self.c.Context().IsInStack(self.context) {
|
||||
return nil
|
||||
}
|
||||
return self.c.Context().NextInStack(self.context)
|
||||
}
|
||||
|
||||
// diffMainViewType reports what the diff in the focused main view belongs to, taken
|
||||
// from the side panel beneath it, or DiffMainViewTypeNone when this pane isn't on the
|
||||
// stack or has no diff panel beneath it. The IsInStack guard is essential:
|
||||
// NextInStack panics for a context that isn't in the stack, and GetKeybindings (which
|
||||
// leads here) also runs for off-stack panes — at startup and while generating the
|
||||
// cheatsheets, where the stack is empty.
|
||||
// stack or has no diff panel beneath it.
|
||||
func (self *MainViewController) diffMainViewType() types.DiffMainViewType {
|
||||
if !self.c.Context().IsInStack(self.context) {
|
||||
return types.DiffMainViewTypeNone
|
||||
}
|
||||
if diffContext, ok := self.c.Context().NextInStack(self.context).(types.DiffMainViewContext); ok {
|
||||
if diffContext, ok := self.sidePanelBeneath().(types.DiffMainViewContext); ok {
|
||||
return diffContext.GetDiffMainViewType()
|
||||
}
|
||||
return types.DiffMainViewTypeNone
|
||||
@@ -311,10 +365,7 @@ func (self *MainViewController) diffMainViewType() types.DiffMainViewType {
|
||||
// hand out the diff it rendered there. nil when this pane isn't on the stack, or the
|
||||
// panel beneath shows no diff.
|
||||
func (self *MainViewController) diffSource() types.FocusedMainViewDiffSource {
|
||||
if !self.c.Context().IsInStack(self.context) {
|
||||
return nil
|
||||
}
|
||||
sidePanel := self.c.Context().NextInStack(self.context)
|
||||
sidePanel := self.sidePanelBeneath()
|
||||
if sidePanel == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -340,56 +391,6 @@ func (self *MainViewController) primaryAction() error {
|
||||
return actions.PrimaryAction(self.context, first, last)
|
||||
}
|
||||
|
||||
// revealSelectionAfterAction moves the selection to the change that takes the place of
|
||||
// the one just acted on, once the changed diff has re-rendered. Call it from the panel's
|
||||
// action handler with the pane it acted in, the pane the work carries on in, and the
|
||||
// first line of the selection, before triggering the re-render.
|
||||
//
|
||||
// The line acted on is gone from the diff, so what is remembered is its place among the
|
||||
// diff's changes: the next change moves up into it, which is where you want to be to
|
||||
// carry on. A range collapses to a single line at its start, and hunk mode selects the
|
||||
// whole block it lands in, so that pressing the key again acts on the next hunk. The
|
||||
// target pane inherits that select mode, this being the same piece of work continuing
|
||||
// in another pane — and shows no selection until the restore places one, so that what
|
||||
// it was left showing the last time it was used doesn't appear for a frame.
|
||||
//
|
||||
// done is called once the selection is where it belongs, or once it turns out that no
|
||||
// render is coming to put it there, for a caller that must not let the user act again
|
||||
// in between.
|
||||
func revealSelectionAfterAction(
|
||||
c *ControllerCommon, source types.DiffPaneContext, target types.DiffPaneContext,
|
||||
firstLineIdx int, done func(),
|
||||
) {
|
||||
ordinal, ok := c.Helpers().DiffLine.ChangeLineOrdinal(source.GetView(), firstLineIdx)
|
||||
if !ok {
|
||||
done()
|
||||
return
|
||||
}
|
||||
|
||||
sel := source.DiffSelectState()
|
||||
if sel.Mode == types.DiffSelectModeRange {
|
||||
sel.Mode = types.DiffSelectModeLine
|
||||
sel.RangeIsSticky = false
|
||||
}
|
||||
*target.DiffSelectState() = *sel
|
||||
selectHunk := sel.Mode == types.DiffSelectModeHunk
|
||||
|
||||
targetView := target.GetView()
|
||||
if target != source {
|
||||
target.SetHasSelectableContent(false)
|
||||
c.Context().UpdateSelectionHighlights()
|
||||
}
|
||||
|
||||
c.Helpers().DiffLine.RevealChangeLineAtOrdinal(targetView, ordinal, func(viewLine int) {
|
||||
if selectHunk {
|
||||
c.Helpers().DiffLine.SelectChangeBlock(target, viewLine, true)
|
||||
return
|
||||
}
|
||||
targetView.CancelRangeSelect()
|
||||
c.Helpers().DiffLine.ShowSelectionAtLine(targetView, viewLine, true)
|
||||
}, done)
|
||||
}
|
||||
|
||||
// discardSelection takes the selected diff lines back out of what they are part of,
|
||||
// which — like the primary action — is the panel's business, and so is the re-render
|
||||
// that follows.
|
||||
@@ -431,12 +432,37 @@ func (self *MainViewController) workingTreeAction(action func() error) func() er
|
||||
// workingTreeActionDescription gives a command's description only where the command
|
||||
// applies — over the working tree's diff — so that it is listed there and nowhere else.
|
||||
func (self *MainViewController) workingTreeActionDescription(description string) func() string {
|
||||
return self.diffActionDescription(description, "")
|
||||
}
|
||||
|
||||
// diffActionDescription describes a command in the words that suit the diff it applies
|
||||
// to: acting on the working tree's diff stages, acting on a commit's builds a custom
|
||||
// patch. Over content that is no diff at all the command doesn't apply, and describes
|
||||
// itself as nothing, which keeps it out of the keybindings menu there.
|
||||
func (self *MainViewController) diffActionDescription(staging string, patchBuilding string) func() string {
|
||||
return func() string {
|
||||
if self.diffMainViewType() != types.DiffMainViewTypeStaging {
|
||||
switch self.diffMainViewType() {
|
||||
case types.DiffMainViewTypeStaging:
|
||||
return staging
|
||||
case types.DiffMainViewTypePatchBuilding:
|
||||
return patchBuilding
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// pullRequestDescription describes a command that acts on the pull request of the
|
||||
// branch the diff belongs to. Over a diff that belongs to no branch (the working tree's,
|
||||
// a stash entry's) it describes it as nothing; this keeps the command out of the
|
||||
// keybindings menu there.
|
||||
func (self *MainViewController) pullRequestDescription(description string) func() string {
|
||||
return self.diffSelectionDescription(func() string {
|
||||
if self.pullRequestBranch() == "" {
|
||||
return ""
|
||||
}
|
||||
return description
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// copySelection copies the selected diff lines to the clipboard — not as the diff
|
||||
@@ -517,11 +543,103 @@ func (self *MainViewController) diffSelectionDisabledReason() *types.DisabledRea
|
||||
return nil
|
||||
}
|
||||
|
||||
// discardSelectionDisabledReason disables discarding while there is nothing to discard,
|
||||
// and where the panel beneath won't have it: taking lines out of a commit means
|
||||
// rewriting it, which isn't always something we may do.
|
||||
func (self *MainViewController) discardSelectionDisabledReason() *types.DisabledReason {
|
||||
if reason := self.diffSelectionDisabledReason(); reason != nil {
|
||||
return reason
|
||||
}
|
||||
if actions := self.focusedMainViewActions(); actions != nil {
|
||||
return actions.DiscardSelectionDisabledReason(self.context)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openPullRequestDisabledReason disables opening a line in the pull request where the
|
||||
// pull request has no view of what is on screen. The branch may have no pull request,
|
||||
// and the pane may be showing a diff that is not the commit's own: a diff against
|
||||
// another ref, or the custom patch, whose lines sit at the numbers the patch gives them
|
||||
// rather than the commit's.
|
||||
func (self *MainViewController) openPullRequestDisabledReason() *types.DisabledReason {
|
||||
if reason := self.diffSelectionDisabledReason(); reason != nil {
|
||||
return reason
|
||||
}
|
||||
if self.c.Modes().Diffing.Active() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NotAvailableInDiffingMode}
|
||||
}
|
||||
if self.c.Helpers().DiffLine.ShowsCustomPatch(self.context.GetView()) {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NotAvailableForCustomPatch}
|
||||
}
|
||||
if reason := self.c.Helpers().Host.NoPullRequestDisabledReason(self.pullRequestBranch()); reason != nil {
|
||||
return reason
|
||||
}
|
||||
return self.commitsOutsidePullRequestDisabledReason()
|
||||
}
|
||||
|
||||
// commitsOutsidePullRequestDisabledReason disables opening a line of a diff whose
|
||||
// commits the pull request doesn't hold: it holds the commits of its branch that are on
|
||||
// the remote, so an unpushed commit is none of its own, and neither is one that is in a
|
||||
// main branch already and so from before the branch. Asked for such a commit, its pages
|
||||
// say they can't find it.
|
||||
func (self *MainViewController) commitsOutsidePullRequestDisabledReason() *types.DisabledReason {
|
||||
commits, _ := self.pullRequestCommits()
|
||||
if lo.EveryBy(commits, func(commit *models.Commit) bool {
|
||||
return commit.Status == models.StatusPushed
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(commits) == 1 {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CommitNotInPullRequest}
|
||||
}
|
||||
return &types.DisabledReason{Text: self.c.Tr.CommitsNotInPullRequest}
|
||||
}
|
||||
|
||||
func (self *MainViewController) onClickInAlreadyFocusedView(opts gocui.ViewMouseBindingOpts) error {
|
||||
self.selectClickedDiffLine(opts.Y)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *MainViewController) editClickedLine(opts gocui.ViewMouseBindingOpts) error {
|
||||
var flashGeneration uint64
|
||||
err := self.editDiffLine(opts.Y, func() {
|
||||
self.lineFlashGeneration++
|
||||
flashGeneration = self.lineFlashGeneration
|
||||
self.context.GetView().SetLineFlash(self.lineToFlash(opts.Y))
|
||||
self.c.GocuiGui().ForceFlushViewsContentOnly(self.c.GocuiGui().Views())
|
||||
})
|
||||
if flashGeneration != 0 {
|
||||
time.AfterFunc(editedLineFlashDuration, func() {
|
||||
self.c.OnUIThreadContentOnlyBackground(func() error {
|
||||
if self.lineFlashGeneration == flashGeneration {
|
||||
self.context.GetView().ClearLineFlash()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// lineToFlash returns the view line to flash for an edit of the line clicked at the
|
||||
// given one. The editor is sent to where that line begins, so a long line wrapped over
|
||||
// several view lines is flashed at the first of them. If the editor wraps the line too,
|
||||
// its cursor ends up on the flashed line. When the line begins above the top of the
|
||||
// viewport, the clicked line is flashed, as the only part of the line on screen.
|
||||
func (self *MainViewController) lineToFlash(clickedViewLine int) int {
|
||||
view := self.context.GetView()
|
||||
bufferLine, ok := view.BufferLineForViewLine(clickedViewLine)
|
||||
if !ok {
|
||||
return clickedViewLine
|
||||
}
|
||||
firstViewLine, ok := view.ViewLineForBufferLine(bufferLine)
|
||||
if !ok || firstViewLine < view.OriginY() {
|
||||
return clickedViewLine
|
||||
}
|
||||
return firstViewLine
|
||||
}
|
||||
|
||||
func (self *MainViewController) onClickInOtherViewOfMainViewPair(opts gocui.ViewMouseBindingOpts) error {
|
||||
// Carry the select mode over from the pane we're leaving, so that clicking into
|
||||
// the other pane keeps hunk mode even the first time we enter it — its own mode
|
||||
@@ -580,6 +698,10 @@ func (self *MainViewController) GetOnFocusLost() func(types.OnFocusLostOpts) {
|
||||
self.draggingWithMouse = false
|
||||
self.c.GocuiGui().CancelMouseCapture()
|
||||
}
|
||||
// Where the focus has gone is already known here, so asking again keeps the
|
||||
// patch marks over a move to the pane beside this one, and takes them away
|
||||
// when the focus leaves the pair.
|
||||
self.c.Helpers().DiffLine.RefreshInclusionGutter()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -970,11 +1092,17 @@ func (self *MainViewController) editLine() error {
|
||||
if !view.Highlight {
|
||||
return nil
|
||||
}
|
||||
return self.editDiffLine(view.SelectedLineIdx(), nil)
|
||||
}
|
||||
|
||||
info, ok := self.c.Helpers().DiffLine.GetDiffLineInfo(view, view.SelectedLineIdx())
|
||||
func (self *MainViewController) editDiffLine(viewLine int, beforeEdit func()) error {
|
||||
info, ok := self.c.Helpers().DiffLine.GetDiffLineInfo(self.context.GetView(), viewLine)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if beforeEdit != nil {
|
||||
beforeEdit()
|
||||
}
|
||||
|
||||
// A file-header row points at the file as a whole rather than at a line in it, so
|
||||
// it opens the file without jumping anywhere — as pressing edit on a file in a side
|
||||
@@ -989,6 +1117,104 @@ func (self *MainViewController) editLine() error {
|
||||
return self.c.Helpers().Files.EditFileAtLine(info.Path, lineNumber)
|
||||
}
|
||||
|
||||
// openPullRequestAtSelectedLine opens the pull request of the branch whose commit the
|
||||
// main view is showing the diff of, at the line the selection is on, so that the line
|
||||
// can be commented on there.
|
||||
func (self *MainViewController) openPullRequestAtSelectedLine() error {
|
||||
pr, ok := self.c.Helpers().Host.PullRequestForBranch(self.pullRequestBranch())
|
||||
if !ok {
|
||||
// Guarded against by the disabled reason, but a refresh in the background may
|
||||
// have taken the pull request away since it was asked.
|
||||
return errors.New(self.c.Tr.NoPullRequestForBranch)
|
||||
}
|
||||
|
||||
commits, baseHash := self.pullRequestCommits()
|
||||
if len(commits) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
view := self.context.GetView()
|
||||
info, ok := self.c.Helpers().DiffLine.GetDiffLineInfo(view, view.SelectedLineIdx())
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), info.Path)
|
||||
if relativePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenPullRequest)
|
||||
url := githubPullRequestLineURL(pr.Url, githubCommitRange(commits, baseHash), relativePath, info)
|
||||
return self.c.OS().OpenLink(url)
|
||||
}
|
||||
|
||||
// pullRequestBranch returns the branch whose pull request would show the diff in this
|
||||
// pane, as the panel beneath names it, and "" where no pull request shows it.
|
||||
func (self *MainViewController) pullRequestBranch() string {
|
||||
prContext, ok := self.sidePanelBeneath().(types.PullRequestDiffContext)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return prContext.BranchForPullRequest()
|
||||
}
|
||||
|
||||
// pullRequestCommits returns the commits whose diff the pane is showing, and the commit
|
||||
// that diff starts after, as the panel beneath names them. The diff's line numbers are
|
||||
// the ones the pull request's page for those commits shows.
|
||||
func (self *MainViewController) pullRequestCommits() ([]*models.Commit, string) {
|
||||
prContext, ok := self.sidePanelBeneath().(types.PullRequestDiffContext)
|
||||
if !ok {
|
||||
return nil, ""
|
||||
}
|
||||
return prContext.CommitsForPullRequest()
|
||||
}
|
||||
|
||||
// githubPullRequestLineURL builds the URL of a line of a file, in the diff a pull request
|
||||
// shows for the given commits. The file is named by the SHA-256 of its path as git spells
|
||||
// it, and the line by which side of the diff it is on.
|
||||
//
|
||||
// GitHub documents none of this; the form was read off the URLs its own pages carry (see
|
||||
// https://github.com/orgs/community/discussions/55764).
|
||||
func githubPullRequestLineURL(
|
||||
prURL string, commitRange string, relativePath string, info types.DiffLineInfo,
|
||||
) string {
|
||||
pathHash := sha256.Sum256([]byte(relativePath))
|
||||
anchor := "diff-" + hex.EncodeToString(pathHash[:]) + githubDiffLineSuffix(info)
|
||||
return fmt.Sprintf("%s/changes/%s#%s", prURL, commitRange, anchor)
|
||||
}
|
||||
|
||||
// githubCommitRange names the commits a pull request is to show the diff of: a single
|
||||
// commit by its hash, and a range of them as the commit the diff starts after, then the
|
||||
// commit it ends at. A range that starts where the pull request itself does names BASE
|
||||
// as the commit it starts after, the keyword its pages use for the commit the pull
|
||||
// request was opened against; naming that commit by its hash gets a page that says it
|
||||
// can't find those commits.
|
||||
func githubCommitRange(commits []*models.Commit, baseHash string) string {
|
||||
newest := commits[0].Hash()
|
||||
if len(commits) == 1 {
|
||||
return newest
|
||||
}
|
||||
if baseHash == "" {
|
||||
baseHash = "BASE"
|
||||
}
|
||||
return baseHash + ".." + newest
|
||||
}
|
||||
|
||||
// githubDiffLineSuffix names a line within a file's diff: R for the new version of the
|
||||
// file, L for the old one, which is where a deleted line is found. Some rows are no line
|
||||
// of the file at all (the header naming it, or a marker like "\ No newline at end of
|
||||
// file"); those name none, and the anchor points at the file itself.
|
||||
func githubDiffLineSuffix(info types.DiffLineInfo) string {
|
||||
switch info.Type {
|
||||
case types.DiffLineDeleted:
|
||||
return fmt.Sprintf("L%d", info.OldLine)
|
||||
case types.DiffLineAdded, types.DiffLineContext, types.DiffLineHunkHeader:
|
||||
return fmt.Sprintf("R%d", info.NewLine)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (self *MainViewController) openSearch() error {
|
||||
if manager := self.c.GetViewBufferManagerForView(self.context.GetView()); manager != nil {
|
||||
manager.ReadToEnd(func() {
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGithubPullRequestLineURL(t *testing.T) {
|
||||
const prURL = "https://github.com/jesseduffield/lazygit/pull/1234"
|
||||
const commitHash = "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b"
|
||||
|
||||
// The anchor names the file by the SHA-256 of its repo-relative path, taken over
|
||||
// exactly those bytes: no leading slash, no trailing newline, forward slashes.
|
||||
const fileHash = "067980d6efc4249367ceb61b0d93a00bca100a0ddb6d4a72b6dbb0eb9d3825cc" // "dir/file1"
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
path string
|
||||
info types.DiffLineInfo
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "an added line is on the right side of the diff",
|
||||
path: "dir/file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineAdded, NewLine: 12},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R12",
|
||||
},
|
||||
{
|
||||
name: "a deleted line is on the left side, at the line it sat on",
|
||||
path: "dir/file1",
|
||||
// A deletion's NewLine is only where it sits in the new version of the
|
||||
// file; the line it is, is the old one.
|
||||
info: types.DiffLineInfo{Type: types.DiffLineDeleted, NewLine: 12, OldLine: 34},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "L34",
|
||||
},
|
||||
{
|
||||
name: "a context line is on the right side too",
|
||||
path: "dir/file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineContext, NewLine: 7, OldLine: 5},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R7",
|
||||
},
|
||||
{
|
||||
name: "a hunk header points at the first line of its hunk",
|
||||
path: "dir/file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineHunkHeader, NewLine: 20},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash + "R20",
|
||||
},
|
||||
{
|
||||
name: "the header naming a file points at the file alone",
|
||||
path: "dir/file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineFileHeader},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash,
|
||||
},
|
||||
{
|
||||
name: "a row that is no line of the file points at the file alone",
|
||||
path: "dir/file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineOther},
|
||||
expected: prURL + "/changes/" + commitHash + "#diff-" + fileHash,
|
||||
},
|
||||
{
|
||||
name: "a file at the root of the repo",
|
||||
path: "file1",
|
||||
info: types.DiffLineInfo{Type: types.DiffLineAdded, NewLine: 1},
|
||||
expected: prURL + "/changes/" + commitHash +
|
||||
"#diff-c147efcfc2d7ea666a9e4f5187b115c90903f0fc896a56df9a6ef5d8f3fc9f31R1",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, githubPullRequestLineURL(prURL, commitHash, s.path, s.info))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGithubCommitRange(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
newest := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "newest"})
|
||||
oldest := models.NewCommit(hashPool, models.NewCommitOpts{Hash: "oldest"})
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
commits []*models.Commit
|
||||
baseHash string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "a single commit is named on its own",
|
||||
commits: []*models.Commit{newest},
|
||||
baseHash: "parent",
|
||||
expected: "newest",
|
||||
},
|
||||
{
|
||||
name: "a range is named as the commits it lies between",
|
||||
commits: []*models.Commit{newest, oldest},
|
||||
baseHash: "parent",
|
||||
expected: "parent..newest",
|
||||
},
|
||||
{
|
||||
name: "a range starting where the pull request does lies above BASE",
|
||||
commits: []*models.Commit{newest, oldest},
|
||||
expected: "BASE..newest",
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.Equal(t, s.expected, githubCommitRange(s.commits, s.baseHash))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func (self *OptionsMenuAction) Call() error {
|
||||
if binding.GetDisabledReason != nil {
|
||||
disabledReason = binding.GetDisabledReason()
|
||||
}
|
||||
tooltip := binding.Tooltip
|
||||
tooltip := binding.GetTooltip()
|
||||
if len(binding.Keys) > 1 {
|
||||
if tooltip != "" {
|
||||
tooltip += "\n\n"
|
||||
|
||||
@@ -1,277 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type PatchBuildingController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
}
|
||||
|
||||
var _ types.IController = &PatchBuildingController{}
|
||||
|
||||
func NewPatchBuildingController(
|
||||
c *ControllerCommon,
|
||||
) *PatchBuildingController {
|
||||
return &PatchBuildingController{
|
||||
baseController: baseController{},
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
return []*types.Binding{
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.OpenFile),
|
||||
Handler: self.OpenFile,
|
||||
Description: self.c.Tr.OpenFile,
|
||||
Tooltip: self.c.Tr.OpenFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Edit),
|
||||
Handler: self.EditFile,
|
||||
Description: self.c.Tr.EditFile,
|
||||
Tooltip: self.c.Tr.EditFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
||||
Handler: self.ToggleSelectionAndRefresh,
|
||||
Description: self.c.Tr.ToggleSelectionForPatch,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
||||
Handler: self.discardSelection,
|
||||
GetDisabledReason: self.getDisabledReasonForDiscard,
|
||||
Description: self.c.Tr.RemoveSelectionFromPatch,
|
||||
Tooltip: self.c.Tr.RemoveSelectionFromPatchTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Return),
|
||||
Handler: self.Escape,
|
||||
Description: self.c.Tr.ExitCustomPatchBuilder,
|
||||
DescriptionFunc: self.EscapeDescription,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) Context() types.Context {
|
||||
return self.c.Contexts().CustomPatchBuilder
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) context() types.IPatchExplorerContext {
|
||||
return self.c.Contexts().CustomPatchBuilder
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) {
|
||||
return func(opts types.OnFocusOpts) {
|
||||
// no need to change wrap on the secondary view because it can't be interacted with
|
||||
self.c.Views().PatchBuilding.Wrap = self.c.UserConfig().Gui.WrapLinesInStagingView
|
||||
|
||||
self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) {
|
||||
return func(opts types.OnFocusLostOpts) {
|
||||
self.context().SetState(nil)
|
||||
|
||||
self.c.Views().PatchBuilding.Wrap = true
|
||||
|
||||
if self.c.Git().Patch.PatchBuilder.IsEmpty() {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) OpenFile() error {
|
||||
self.context().GetMutex().Lock()
|
||||
defer self.context().GetMutex().Unlock()
|
||||
|
||||
path := self.c.Contexts().CommitFiles.GetSelectedPath()
|
||||
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Helpers().Files.OpenFile(path)
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) EditFile() error {
|
||||
self.context().GetMutex().Lock()
|
||||
defer self.context().GetMutex().Unlock()
|
||||
|
||||
path := self.c.Contexts().CommitFiles.GetSelectedPath()
|
||||
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
lineNumber := self.context().GetState().CurrentLineNumber()
|
||||
lineNumber = self.c.Helpers().Diff.AdjustLineNumber(path, lineNumber, self.context().GetViewName())
|
||||
return self.c.Helpers().Files.EditFileAtLine(path, lineNumber)
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) ToggleSelectionAndRefresh() error {
|
||||
if err := self.toggleSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Scope: []types.RefreshableView{types.PATCH_BUILDING, types.COMMIT_FILES},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) toggleSelection() error {
|
||||
self.context().GetMutex().Lock()
|
||||
defer self.context().GetMutex().Unlock()
|
||||
|
||||
file := self.c.Contexts().CommitFiles.GetSelectedFile()
|
||||
if file == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
state := self.context().GetState()
|
||||
|
||||
// Get added/deleted lines in the selected patch range
|
||||
lineIndicesToToggle := state.LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange()
|
||||
if len(lineIndicesToToggle) == 0 {
|
||||
// Only context lines or header lines selected, so nothing to do
|
||||
return nil
|
||||
}
|
||||
|
||||
includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(file.Path, file.PreviousPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange
|
||||
firstSelectedChangeLineIsStaged := lo.Contains(includedLineIndices, lineIndicesToToggle[0])
|
||||
if firstSelectedChangeLineIsStaged {
|
||||
toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange
|
||||
}
|
||||
|
||||
// add range of lines to those set for the file
|
||||
if err := toggleFunc(file.Path, file.PreviousPath, lineIndicesToToggle); err != nil {
|
||||
// might actually want to return an error here
|
||||
self.c.Log.Error(err)
|
||||
}
|
||||
|
||||
if state.SelectingRange() {
|
||||
state.SetLineSelectMode()
|
||||
}
|
||||
|
||||
state.SelectNextStageableLineOfSameIncludedState(self.context().GetIncludedLineIndices(), firstSelectedChangeLineIsStaged)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) getDisabledReasonForDiscard() *types.DisabledReason {
|
||||
if !self.c.Git().Patch.PatchBuilder.CanRebase {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CanOnlyDiscardFromLocalCommits, ShowErrorInPanel: true}
|
||||
}
|
||||
if self.c.Git().Status.WorkingTreeState().Any() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.CantPatchWhileRebasingError, ShowErrorInPanel: true}
|
||||
}
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
text := fmt.Sprintf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
return &types.DisabledReason{Text: text, ShowErrorInPanel: true}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) discardSelection() error {
|
||||
prompt := lo.Ternary(self.c.Git().Patch.PatchBuilder.IsEmpty(),
|
||||
self.c.Tr.DiscardLinesFromCommitPrompt,
|
||||
self.c.Tr.DiscardLinesFromCommitPromptWithReset)
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardLinesFromCommitTitle,
|
||||
Prompt: prompt,
|
||||
HandleConfirm: func() error {
|
||||
return self.discardSelectionFromCommit()
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) discardSelectionFromCommit() error {
|
||||
// Reset the current patch if there is one.
|
||||
if !self.c.Git().Patch.PatchBuilder.IsEmpty() {
|
||||
self.c.Git().Patch.PatchBuilder.Reset()
|
||||
}
|
||||
|
||||
if err := self.toggleSelection(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if self.c.Git().Patch.PatchBuilder.IsEmpty() {
|
||||
return nil
|
||||
}
|
||||
|
||||
commits := self.c.Model().Commits
|
||||
commitIndex := self.getPatchCommitIndex()
|
||||
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
|
||||
err := self.c.Git().Patch.DeletePatchesFromCommit(commits, commitIndex)
|
||||
// Escape pops the patch-building context, so run it on the UI thread
|
||||
// before the refresh below.
|
||||
_ = self.c.GocuiGui().OnUIThreadAndWait(func() {
|
||||
self.c.Helpers().PatchBuilding.Escape()
|
||||
})
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
|
||||
err, types.RefreshOptions{})
|
||||
})
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) getPatchCommitIndex() int {
|
||||
for index, commit := range self.c.Model().Commits {
|
||||
if commit.Hash() == self.c.Git().Patch.PatchBuilder.To {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) Escape() error {
|
||||
context := self.c.Contexts().CustomPatchBuilder
|
||||
state := context.GetState()
|
||||
|
||||
if state.SelectingRange() || state.SelectingHunkEnabledByUser() {
|
||||
state.SetLineSelectMode()
|
||||
self.c.PostRefreshUpdate(context)
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.Helpers().PatchBuilding.Escape()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchBuildingController) EscapeDescription() string {
|
||||
context := self.c.Contexts().CustomPatchBuilder
|
||||
if state := context.GetState(); state != nil {
|
||||
if state.SelectingRange() {
|
||||
return self.c.Tr.DismissRangeSelect
|
||||
}
|
||||
|
||||
if state.SelectingHunkEnabledByUser() {
|
||||
return self.c.Tr.SelectLineByLine
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.Tr.ExitCustomPatchBuilder
|
||||
}
|
||||
@@ -1,416 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type PatchExplorerControllerFactory struct {
|
||||
c *ControllerCommon
|
||||
}
|
||||
|
||||
func NewPatchExplorerControllerFactory(c *ControllerCommon) *PatchExplorerControllerFactory {
|
||||
return &PatchExplorerControllerFactory{
|
||||
c: c,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerControllerFactory) Create(context types.IPatchExplorerContext) *PatchExplorerController {
|
||||
controller := &PatchExplorerController{
|
||||
baseController: baseController{},
|
||||
c: self.c,
|
||||
context: context,
|
||||
}
|
||||
controller.dragAutoscroller = helpers.NewDragAutoscroller(
|
||||
self.c.HelperCommon,
|
||||
context,
|
||||
controller.canDragAutoscroll,
|
||||
controller.handleDragAutoscroll,
|
||||
)
|
||||
return controller
|
||||
}
|
||||
|
||||
type PatchExplorerController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
|
||||
context types.IPatchExplorerContext
|
||||
dragAutoscroller *helpers.DragAutoscroller
|
||||
draggingWithMouse bool
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
return []*types.Binding{
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.PrevItem),
|
||||
Handler: self.withRenderAndFocus(self.HandlePrevLine),
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.NextItem),
|
||||
Handler: self.withRenderAndFocus(self.HandleNextLine),
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.RangeSelectUp),
|
||||
Handler: self.withRenderAndFocus(self.HandlePrevLineRange),
|
||||
Description: self.c.Tr.RangeSelectUp,
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.RangeSelectDown),
|
||||
Handler: self.withRenderAndFocus(self.HandleNextLineRange),
|
||||
Description: self.c.Tr.RangeSelectDown,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.PrevHunk),
|
||||
Handler: self.withRenderAndFocus(self.HandlePrevHunk),
|
||||
Description: self.c.Tr.PrevHunk,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.NextHunk),
|
||||
Handler: self.withRenderAndFocus(self.HandleNextHunk),
|
||||
Description: self.c.Tr.NextHunk,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.ToggleRangeSelect),
|
||||
Handler: self.withRenderAndFocus(self.HandleToggleSelectRange),
|
||||
Description: self.c.Tr.ToggleRangeSelect,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.ToggleSelectHunk),
|
||||
Handler: self.withRenderAndFocus(self.HandleToggleSelectHunk),
|
||||
Description: self.c.Tr.ToggleSelectHunk,
|
||||
DescriptionFunc: func() string {
|
||||
if state := self.context.GetState(); state != nil && state.SelectingHunk() {
|
||||
return self.c.Tr.SelectLineByLine
|
||||
}
|
||||
return self.c.Tr.SelectHunk
|
||||
},
|
||||
Tooltip: self.c.Tr.ToggleSelectHunkTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.PrevPage),
|
||||
Handler: self.withRenderAndFocus(self.HandlePrevPage),
|
||||
Description: self.c.Tr.PrevPage,
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.NextPage),
|
||||
Handler: self.withRenderAndFocus(self.HandleNextPage),
|
||||
Description: self.c.Tr.NextPage,
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.GotoTop),
|
||||
Handler: self.withRenderAndFocus(self.HandleGotoTop),
|
||||
Description: self.c.Tr.GotoTop,
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.GotoBottom),
|
||||
Description: self.c.Tr.GotoBottom,
|
||||
Handler: self.withRenderAndFocus(self.HandleGotoBottom),
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.ScrollLeft),
|
||||
Handler: self.withRenderAndFocus(self.HandleScrollLeft),
|
||||
},
|
||||
{
|
||||
Tag: "navigation",
|
||||
Keys: opts.GetKeys(opts.Config.Universal.ScrollRight),
|
||||
Handler: self.withRenderAndFocus(self.HandleScrollRight),
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard),
|
||||
Handler: self.withLock(self.CopySelectedToClipboard),
|
||||
Description: self.c.Tr.CopySelectedTextToClipboard,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{
|
||||
{
|
||||
ViewName: self.context.GetViewName(),
|
||||
Key: gocui.MouseLeft,
|
||||
Handler: func(opts gocui.ViewMouseBindingOpts) error {
|
||||
if self.isFocused() {
|
||||
return self.withRenderAndFocus(self.HandleMouseDown)()
|
||||
}
|
||||
|
||||
self.c.Context().Push(self.context, types.OnFocusOpts{
|
||||
ClickedWindowName: self.context.GetWindowName(),
|
||||
ClickedViewLineIdx: opts.Y,
|
||||
})
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
ViewName: self.context.GetViewName(),
|
||||
Key: gocui.MouseLeft,
|
||||
Modifier: gocui.ModMotion,
|
||||
Handler: self.handleMouseDrag,
|
||||
},
|
||||
{
|
||||
ViewName: self.context.GetViewName(),
|
||||
Key: gocui.MouseRelease,
|
||||
Handler: func(gocui.ViewMouseBindingOpts) error { return self.handleDragRelease() },
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) handleMouseDrag(opts gocui.ViewMouseBindingOpts) error {
|
||||
if err := self.withLock(func() error {
|
||||
self.context.GetState().DragSelectLine(opts.Y)
|
||||
self.renderDragSelection()
|
||||
return nil
|
||||
})(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.draggingWithMouse = true
|
||||
originY, _ := self.context.GetViewTrait().ViewPortYBounds()
|
||||
self.dragAutoscroller.Update(opts.Y - originY)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) canDragAutoscroll(int) bool {
|
||||
state := self.context.GetState()
|
||||
return state != nil && state.SelectingRange()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) handleDragAutoscroll(viewIndex int) bool {
|
||||
if !self.canDragAutoscroll(0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if err := self.withLock(func() error {
|
||||
self.context.GetState().DragSelectLine(viewIndex)
|
||||
self.renderDragSelection()
|
||||
return nil
|
||||
})(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) renderDragSelection() {
|
||||
view := self.context.GetView()
|
||||
state := self.context.GetState()
|
||||
originY := view.OriginY()
|
||||
startIndex, _ := state.SelectedViewRange()
|
||||
view.SetRangeSelectStart(startIndex)
|
||||
view.SetCursorY(state.GetSelectedViewLineIdx() - originY)
|
||||
self.context.Render()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) handleDragRelease() error {
|
||||
self.draggingWithMouse = false
|
||||
self.dragAutoscroller.Cancel()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) GetOnFocusLost() func(types.OnFocusLostOpts) {
|
||||
return func(types.OnFocusLostOpts) {
|
||||
self.dragAutoscroller.Cancel()
|
||||
if self.draggingWithMouse {
|
||||
self.draggingWithMouse = false
|
||||
self.c.GocuiGui().CancelMouseCapture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandlePrevLine() error {
|
||||
before := self.context.GetState().GetSelectedViewLineIdx()
|
||||
self.context.GetState().CycleSelection(false)
|
||||
after := self.context.GetState().GetSelectedViewLineIdx()
|
||||
|
||||
if self.context.GetState().SelectingLine() {
|
||||
checkScrollUp(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleNextLine() error {
|
||||
before := self.context.GetState().GetSelectedViewLineIdx()
|
||||
self.context.GetState().CycleSelection(true)
|
||||
after := self.context.GetState().GetSelectedViewLineIdx()
|
||||
|
||||
if self.context.GetState().SelectingLine() {
|
||||
checkScrollDown(self.context.GetViewTrait(), self.c.UserConfig(), before, after)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandlePrevLineRange() error {
|
||||
s := self.context.GetState()
|
||||
|
||||
s.CycleRange(false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleNextLineRange() error {
|
||||
s := self.context.GetState()
|
||||
|
||||
s.CycleRange(true)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandlePrevHunk() error {
|
||||
self.context.GetState().SelectPreviousHunk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleNextHunk() error {
|
||||
self.context.GetState().SelectNextHunk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleToggleSelectRange() error {
|
||||
self.context.GetState().ToggleStickySelectRange()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleToggleSelectHunk() error {
|
||||
self.context.GetState().ToggleSelectHunk()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleScrollLeft() error {
|
||||
self.context.GetViewTrait().ScrollLeft()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleScrollRight() error {
|
||||
self.context.GetViewTrait().ScrollRight()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandlePrevPage() error {
|
||||
self.context.GetState().AdjustSelectedLineIdx(-self.context.GetViewTrait().PageDelta())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleNextPage() error {
|
||||
self.context.GetState().AdjustSelectedLineIdx(self.context.GetViewTrait().PageDelta())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleGotoTop() error {
|
||||
self.context.GetState().SelectTop()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleGotoBottom() error {
|
||||
self.context.GetState().SelectBottom()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) HandleMouseDown() error {
|
||||
self.context.GetState().SelectNewLineForRange(self.context.GetViewTrait().SelectedLineIdx())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) CopySelectedToClipboard() error {
|
||||
selected := self.context.GetState().PlainRenderSelected()
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard)
|
||||
if err := self.c.OS().CopyToClipboard(dropDiffPrefix(selected)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Removes '+' or '-' from the beginning of each line in the diff string, except
|
||||
// when both '+' and '-' lines are present, or diff header lines, in which case
|
||||
// the diff is returned unchanged. This is useful for copying parts of diffs to
|
||||
// the clipboard in order to paste them into code.
|
||||
func dropDiffPrefix(diff string) string {
|
||||
lines := strings.Split(strings.TrimRight(diff, "\n"), "\n")
|
||||
|
||||
const (
|
||||
PLUS int = iota
|
||||
MINUS
|
||||
CONTEXT
|
||||
OTHER
|
||||
)
|
||||
|
||||
linesByType := lo.GroupBy(lines, func(line string) int {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "+"):
|
||||
return PLUS
|
||||
case strings.HasPrefix(line, "-"):
|
||||
return MINUS
|
||||
case strings.HasPrefix(line, " "):
|
||||
return CONTEXT
|
||||
}
|
||||
return OTHER
|
||||
})
|
||||
|
||||
hasLinesOfType := func(lineType int) bool { return len(linesByType[lineType]) > 0 }
|
||||
|
||||
keepPrefix := hasLinesOfType(OTHER) || (hasLinesOfType(PLUS) && hasLinesOfType(MINUS))
|
||||
if keepPrefix {
|
||||
return diff
|
||||
}
|
||||
|
||||
return strings.Join(lo.Map(lines, func(line string, _ int) string { return line[1:] + "\n" }), "")
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) isFocused() bool {
|
||||
return self.c.Context().Current().GetKey() == self.context.GetKey()
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) withRenderAndFocus(f func() error) func() error {
|
||||
return self.withLock(func() error {
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.context.RenderAndFocus()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (self *PatchExplorerController) withLock(f func() error) func() error {
|
||||
return func() error {
|
||||
self.context.GetMutex().Lock()
|
||||
defer self.context.GetMutex().Unlock()
|
||||
|
||||
if self.context.GetState() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return f()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,9 @@ type ReflogCommitsController struct {
|
||||
baseController
|
||||
*ListControllerTrait[*models.Commit]
|
||||
c *ControllerCommon
|
||||
|
||||
// what this panel offers on the diff it shows in the focused main view
|
||||
diffActions *CommitDiffActions
|
||||
}
|
||||
|
||||
var _ types.IController = &ReflogCommitsController{}
|
||||
@@ -17,7 +20,7 @@ var _ types.IController = &ReflogCommitsController{}
|
||||
func NewReflogCommitsController(
|
||||
c *ControllerCommon,
|
||||
) *ReflogCommitsController {
|
||||
return &ReflogCommitsController{
|
||||
controller := &ReflogCommitsController{
|
||||
baseController: baseController{},
|
||||
ListControllerTrait: NewListControllerTrait(
|
||||
c,
|
||||
@@ -27,6 +30,19 @@ func NewReflogCommitsController(
|
||||
),
|
||||
c: c,
|
||||
}
|
||||
controller.diffActions = NewCommitDiffActions(c, c.Contexts().ReflogCommits, controller.diffTarget)
|
||||
return controller
|
||||
}
|
||||
|
||||
// diffTarget is the reflog entry the panel has selected, whose diff its main view
|
||||
// shows. A reflog entry is never a commit of the checked-out branch as far as we are
|
||||
// concerned, so nothing here may be rewritten.
|
||||
func (self *ReflogCommitsController) diffTarget() *commitDiffTarget {
|
||||
commit := self.context().GetSelected()
|
||||
if commit == nil {
|
||||
return nil
|
||||
}
|
||||
return &commitDiffTarget{from: commit.ParentRefName(), to: commit.RefName()}
|
||||
}
|
||||
|
||||
func (self *ReflogCommitsController) Context() types.Context {
|
||||
@@ -38,17 +54,7 @@ func (self *ReflogCommitsController) context() *context.ReflogCommitsContext {
|
||||
}
|
||||
|
||||
func (self *ReflogCommitsController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the reflog entry's diff for the given files — the same diff its
|
||||
// main view shows, only without the entry's message and stat above it.
|
||||
func (self *ReflogCommitsController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
commit := self.context().GetSelected()
|
||||
if commit == nil {
|
||||
return ""
|
||||
}
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(commit.ParentRefName(), commit.RefName(), paths)
|
||||
return self.diffActions
|
||||
}
|
||||
|
||||
func (self *ReflogCommitsController) GetOnRenderToMain() func() {
|
||||
@@ -71,6 +77,7 @@ func (self *ReflogCommitsController) GetOnRenderToMain() func() {
|
||||
Title: "Reflog Entry",
|
||||
Task: task,
|
||||
},
|
||||
Secondary: secondaryPatchPanelUpdateOpts(self.c),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
type StagingController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
|
||||
context types.IPatchExplorerContext
|
||||
otherContext types.IPatchExplorerContext
|
||||
|
||||
// if true, we're dealing with the secondary context i.e. dealing with staged file changes
|
||||
staged bool
|
||||
}
|
||||
|
||||
var _ types.IController = &StagingController{}
|
||||
|
||||
func NewStagingController(
|
||||
c *ControllerCommon,
|
||||
context types.IPatchExplorerContext,
|
||||
otherContext types.IPatchExplorerContext,
|
||||
staged bool,
|
||||
) *StagingController {
|
||||
return &StagingController{
|
||||
baseController: baseController{},
|
||||
c: c,
|
||||
context: context,
|
||||
otherContext: otherContext,
|
||||
staged: staged,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StagingController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
return []*types.Binding{
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Select),
|
||||
Handler: self.ToggleStaged,
|
||||
Description: self.c.Tr.Stage,
|
||||
Tooltip: self.c.Tr.StageSelectionTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Remove),
|
||||
Handler: self.DiscardSelection,
|
||||
Description: self.c.Tr.DiscardSelection,
|
||||
Tooltip: self.c.Tr.DiscardSelectionTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.OpenFile),
|
||||
Handler: self.OpenFile,
|
||||
Description: self.c.Tr.OpenFile,
|
||||
Tooltip: self.c.Tr.OpenFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Edit),
|
||||
Handler: self.EditFile,
|
||||
Description: self.c.Tr.EditFile,
|
||||
Tooltip: self.c.Tr.EditFileTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.Return),
|
||||
Handler: self.Escape,
|
||||
Description: self.c.Tr.ReturnToFilesPanel,
|
||||
DescriptionFunc: self.EscapeDescription,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Universal.TogglePanel),
|
||||
Handler: self.TogglePanel,
|
||||
Description: self.c.Tr.ToggleStagingView,
|
||||
Tooltip: self.c.Tr.ToggleStagingViewTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Main.EditSelectHunk),
|
||||
Handler: self.EditHunkAndRefresh,
|
||||
Description: self.c.Tr.EditHunk,
|
||||
Tooltip: self.c.Tr.EditHunkTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.CommitChanges),
|
||||
Handler: self.c.Helpers().WorkingTree.HandleCommitPress,
|
||||
Description: self.c.Tr.Commit,
|
||||
Tooltip: self.c.Tr.CommitTooltip,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.CommitChangesWithoutHook),
|
||||
Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress,
|
||||
Description: self.c.Tr.CommitChangesWithoutHook,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.CommitChangesWithEditor),
|
||||
Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress,
|
||||
Description: self.c.Tr.CommitChangesWithEditor,
|
||||
},
|
||||
{
|
||||
Keys: opts.GetKeys(opts.Config.Files.FindBaseCommitForFixup),
|
||||
Handler: self.c.Helpers().FixupHelper.HandleFindBaseCommitForFixupPress,
|
||||
Description: self.c.Tr.FindBaseCommitForFixup,
|
||||
Tooltip: self.c.Tr.FindBaseCommitForFixupTooltip,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StagingController) Context() types.Context {
|
||||
return self.context
|
||||
}
|
||||
|
||||
func (self *StagingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding {
|
||||
return []*gocui.ViewMouseBinding{}
|
||||
}
|
||||
|
||||
func (self *StagingController) GetOnFocus() func(types.OnFocusOpts) {
|
||||
return func(opts types.OnFocusOpts) {
|
||||
wrap := self.c.UserConfig().Gui.WrapLinesInStagingView
|
||||
self.c.Views().Staging.Wrap = wrap
|
||||
self.c.Views().StagingSecondary.Wrap = wrap
|
||||
|
||||
self.c.Helpers().Staging.RefreshStagingPanel(opts)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StagingController) GetOnFocusLost() func(types.OnFocusLostOpts) {
|
||||
return func(opts types.OnFocusLostOpts) {
|
||||
self.context.SetState(nil)
|
||||
|
||||
if opts.NewContextKey != self.otherContext.GetKey() {
|
||||
self.c.Views().Staging.Wrap = true
|
||||
self.c.Views().StagingSecondary.Wrap = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (self *StagingController) OpenFile() error {
|
||||
self.context.GetMutex().Lock()
|
||||
defer self.context.GetMutex().Unlock()
|
||||
|
||||
path := self.FilePath()
|
||||
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return self.c.Helpers().Files.OpenFile(path)
|
||||
}
|
||||
|
||||
func (self *StagingController) EditFile() error {
|
||||
self.context.GetMutex().Lock()
|
||||
defer self.context.GetMutex().Unlock()
|
||||
|
||||
path := self.FilePath()
|
||||
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
lineNumber := self.context.GetState().CurrentLineNumber()
|
||||
lineNumber = self.c.Helpers().Diff.AdjustLineNumber(path, lineNumber, self.context.GetViewName())
|
||||
return self.c.Helpers().Files.EditFileAtLine(path, lineNumber)
|
||||
}
|
||||
|
||||
func (self *StagingController) Escape() error {
|
||||
if self.context.GetState().SelectingRange() || self.context.GetState().SelectingHunkEnabledByUser() {
|
||||
self.context.GetState().SetLineSelectMode()
|
||||
self.c.PostRefreshUpdate(self.context)
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.Context().Pop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) EscapeDescription() string {
|
||||
if state := self.context.GetState(); state != nil {
|
||||
if state.SelectingRange() {
|
||||
return self.c.Tr.DismissRangeSelect
|
||||
}
|
||||
|
||||
if state.SelectingHunkEnabledByUser() {
|
||||
return self.c.Tr.SelectLineByLine
|
||||
}
|
||||
}
|
||||
|
||||
return self.c.Tr.ReturnToFilesPanel
|
||||
}
|
||||
|
||||
func (self *StagingController) TogglePanel() error {
|
||||
if self.otherContext.GetState() != nil {
|
||||
self.c.Context().Push(self.otherContext, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) ToggleStaged() error {
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
}
|
||||
|
||||
return self.applySelectionAndRefresh(self.staged)
|
||||
}
|
||||
|
||||
func (self *StagingController) DiscardSelection() error {
|
||||
if self.c.UserConfig().Git.DiffContextSize == 0 {
|
||||
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToDiscard,
|
||||
self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)
|
||||
}
|
||||
|
||||
return self.c.ConfirmIf(!self.staged && !self.c.UserConfig().Gui.SkipDiscardChangeWarning,
|
||||
types.ConfirmOpts{
|
||||
Title: self.c.Tr.DiscardChangeTitle,
|
||||
Prompt: self.c.Tr.DiscardChangePrompt,
|
||||
HandleConfirm: func() error { return self.applySelectionAndRefresh(true) },
|
||||
})
|
||||
}
|
||||
|
||||
func (self *StagingController) applySelectionAndRefresh(reverse bool) error {
|
||||
if err := self.applySelection(reverse); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Block input until the refresh has landed: it rebuilds the staging panel
|
||||
// and moves the selection to the next stageable change, and a quick second
|
||||
// keypress must act on that, not on the stale pre-refresh diff.
|
||||
self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) applySelection(reverse bool) error {
|
||||
self.context.GetMutex().Lock()
|
||||
defer self.context.GetMutex().Unlock()
|
||||
|
||||
state := self.context.GetState()
|
||||
path := self.FilePath()
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
firstLineIdx, lastLineIdx := state.SelectedPatchRange()
|
||||
patchToApply := patch.
|
||||
Parse(state.GetDiff()).
|
||||
Transform(patch.TransformOpts{
|
||||
Reverse: reverse,
|
||||
IncludedLineIndices: patch.ExpandRange(firstLineIdx, lastLineIdx),
|
||||
FileNameOverride: path,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
||||
if patchToApply == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// apply the patch then refresh this panel
|
||||
// create a new temp file with the patch, then call git apply with that patch
|
||||
self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
|
||||
err := self.c.Git().Patch.ApplyPatch(
|
||||
patchToApply,
|
||||
git_commands.ApplyPatchOpts{
|
||||
Reverse: reverse,
|
||||
Cached: !reverse || self.staged,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if state.SelectingRange() {
|
||||
firstLine, _ := state.SelectedViewRange()
|
||||
state.SelectLine(firstLine)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) EditHunkAndRefresh() error {
|
||||
if err := self.editHunk(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Block input like applySelectionAndRefresh does; the refresh rebuilds the
|
||||
// staging panel from the post-edit diff.
|
||||
self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) editHunk() error {
|
||||
self.context.GetMutex().Lock()
|
||||
defer self.context.GetMutex().Unlock()
|
||||
|
||||
state := self.context.GetState()
|
||||
path := self.FilePath()
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
hunkStartIdx, hunkEndIdx := state.CurrentHunkBounds()
|
||||
patchText := patch.
|
||||
Parse(state.GetDiff()).
|
||||
Transform(patch.TransformOpts{
|
||||
Reverse: self.staged,
|
||||
IncludedLineIndices: patch.ExpandRange(hunkStartIdx, hunkEndIdx),
|
||||
FileNameOverride: path,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
||||
patchFilepath, err := self.c.Git().Patch.SaveTemporaryPatch(patchText)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lineOffset := 3
|
||||
lineIdxInHunk := state.GetSelectedPatchLineIdx() - hunkStartIdx
|
||||
if err := self.c.Helpers().Files.EditFileAtLineAndWait(patchFilepath, lineIdxInHunk+lineOffset); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
editedPatchText, err := self.c.Git().File.Cat(patchFilepath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
|
||||
|
||||
lineCount := strings.Count(editedPatchText, "\n") + 1
|
||||
newPatchText := patch.
|
||||
Parse(editedPatchText).
|
||||
Transform(patch.TransformOpts{
|
||||
IncludedLineIndices: patch.ExpandRange(0, lineCount),
|
||||
FileNameOverride: path,
|
||||
}).
|
||||
FormatPlain()
|
||||
|
||||
if err := self.c.Git().Patch.ApplyPatch(
|
||||
newPatchText,
|
||||
git_commands.ApplyPatchOpts{
|
||||
Reverse: self.staged,
|
||||
Cached: true,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *StagingController) FilePath() string {
|
||||
return self.c.Contexts().Files.GetSelectedPath()
|
||||
}
|
||||
@@ -108,6 +108,7 @@ func (self *StashController) GetOnRenderToMain() func() {
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Task: task,
|
||||
},
|
||||
Secondary: secondaryPatchPanelUpdateOpts(self.c),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ func (self *SubCommitsController) GetOnRenderToMain() func() {
|
||||
SubTitle: self.c.Helpers().Diff.IgnoringWhitespaceSubTitle(),
|
||||
Task: task,
|
||||
},
|
||||
Secondary: secondaryPatchPanelUpdateOpts(self.c),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,17 +25,48 @@ type SwitchToDiffFilesController struct {
|
||||
baseController
|
||||
c *ControllerCommon
|
||||
context CanSwitchToDiffFiles
|
||||
|
||||
// what this panel offers on the diff it shows in the focused main view
|
||||
diffActions *CommitDiffActions
|
||||
}
|
||||
|
||||
func NewSwitchToDiffFilesController(
|
||||
c *ControllerCommon,
|
||||
context CanSwitchToDiffFiles,
|
||||
) *SwitchToDiffFilesController {
|
||||
return &SwitchToDiffFilesController{
|
||||
controller := &SwitchToDiffFilesController{
|
||||
baseController: baseController{},
|
||||
c: c,
|
||||
context: context,
|
||||
}
|
||||
controller.diffActions = NewCommitDiffActions(c, context, controller.diffTarget)
|
||||
return controller
|
||||
}
|
||||
|
||||
// diffTarget is the commit — or stash entry, or range of commits — the panel has
|
||||
// selected, whose whole diff its main view shows.
|
||||
func (self *SwitchToDiffFilesController) diffTarget() *commitDiffTarget {
|
||||
ref := self.context.GetSelectedRef()
|
||||
if ref == nil {
|
||||
return nil
|
||||
}
|
||||
refRange := self.context.GetSelectedRefRangeForDiffFiles()
|
||||
from, to := context.FromAndToForDiff(ref, refRange)
|
||||
return &commitDiffTarget{from: from, to: to, canRebase: self.canRebase(ref, refRange)}
|
||||
}
|
||||
|
||||
// canRebase reports whether the given selection is one lazygit may rewrite: the panel
|
||||
// has to allow it in the first place, a range of commits can't be rewritten as one,
|
||||
// and in diffing mode what the main view shows is a diff against another ref rather
|
||||
// than the commit itself, unless that other ref is the selected commit.
|
||||
func (self *SwitchToDiffFilesController) canRebase(ref models.Ref, refRange *types.RefRange) bool {
|
||||
if !self.context.CanRebase() {
|
||||
return false
|
||||
}
|
||||
if self.c.Modes().Diffing.Active() {
|
||||
return self.c.Modes().Diffing.Ref == ref.RefName()
|
||||
}
|
||||
return refRange == nil
|
||||
}
|
||||
|
||||
func (self *SwitchToDiffFilesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
|
||||
@@ -66,19 +97,7 @@ func (self *SwitchToDiffFilesController) GetOnDoubleClick() func() error {
|
||||
}
|
||||
|
||||
func (self *SwitchToDiffFilesController) GetFocusedMainViewDiffSource() types.FocusedMainViewDiffSource {
|
||||
return self
|
||||
}
|
||||
|
||||
// PlainDiff hands out the diff of the panel's selected commit (or range of them) for
|
||||
// the given files — the same diff its main view shows, only without the commit's
|
||||
// message and stat above it.
|
||||
func (self *SwitchToDiffFilesController) PlainDiff(_ types.DiffPaneContext, paths []string) string {
|
||||
ref := self.context.GetSelectedRef()
|
||||
if ref == nil {
|
||||
return ""
|
||||
}
|
||||
from, to := context.FromAndToForDiff(ref, self.context.GetSelectedRefRangeForDiffFiles())
|
||||
return self.c.Helpers().Diff.PlainDiffBetweenRefs(from, to, paths)
|
||||
return self.diffActions
|
||||
}
|
||||
|
||||
func (self *SwitchToDiffFilesController) enter() error {
|
||||
@@ -86,16 +105,7 @@ func (self *SwitchToDiffFilesController) enter() error {
|
||||
refsRange := self.context.GetSelectedRefRangeForDiffFiles()
|
||||
commitFilesContext := self.c.Contexts().CommitFiles
|
||||
|
||||
canRebase := self.context.CanRebase()
|
||||
if canRebase {
|
||||
if self.c.Modes().Diffing.Active() {
|
||||
if self.c.Modes().Diffing.Ref != ref.RefName() {
|
||||
canRebase = false
|
||||
}
|
||||
} else if refsRange != nil {
|
||||
canRebase = false
|
||||
}
|
||||
}
|
||||
canRebase := self.canRebase(ref, refsRange)
|
||||
|
||||
commitFilesContext.ClearFilter()
|
||||
commitFilesContext.ReInit(ref, refsRange)
|
||||
|
||||
@@ -71,20 +71,28 @@ func (self *SwitchToFocusedMainViewController) onClickSecondary(opts gocui.ViewM
|
||||
}
|
||||
|
||||
func (self *SwitchToFocusedMainViewController) handleFocusMainView() error {
|
||||
return focusMainView(self.c, self.context, -1)
|
||||
}
|
||||
|
||||
func focusMainView(c *ControllerCommon, source types.Context, clickedLineIdx int) error {
|
||||
// Usually the main pane, but the content can be in the secondary one alone: a file
|
||||
// with nothing but staged changes shows them there.
|
||||
mainViewContext := self.c.Contexts().Normal
|
||||
if self.c.State().GetRepoState().GetMainPanes() == types.SecondaryPaneOnly {
|
||||
mainViewContext = self.c.Contexts().NormalSecondary
|
||||
mainViewContext := c.Contexts().Normal
|
||||
if c.State().GetRepoState().GetMainPanes() == types.SecondaryPaneOnly {
|
||||
mainViewContext = c.Contexts().NormalSecondary
|
||||
}
|
||||
return self.focusMainView(mainViewContext, -1)
|
||||
return focusMainViewPane(c, source, mainViewContext, clickedLineIdx)
|
||||
}
|
||||
|
||||
func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *context.MainContext, clickedLineIdx int) error {
|
||||
mainViewContext.ClearSearchString()
|
||||
self.c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
return focusMainViewPane(self.c, self.context, mainViewContext, clickedLineIdx)
|
||||
}
|
||||
|
||||
if _, ok := self.context.(types.DiffMainViewContext); !ok {
|
||||
func focusMainViewPane(c *ControllerCommon, source types.Context, mainViewContext *context.MainContext, clickedLineIdx int) error {
|
||||
mainViewContext.ClearSearchString()
|
||||
c.Context().Push(mainViewContext, types.OnFocusOpts{})
|
||||
|
||||
if _, ok := source.(types.DiffMainViewContext); !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -93,13 +101,13 @@ func (self *SwitchToFocusedMainViewController) focusMainView(mainViewContext *co
|
||||
// is. Now that the user wants to act on it, it is re-rendered as git's own diff — the
|
||||
// panel below decides that for itself, from the same question — and the selection
|
||||
// goes on that instead of on rows we can't place.
|
||||
if self.c.Helpers().DiffLine.MainViewDiffMode() == git_commands.DiffModeRaw {
|
||||
self.c.Helpers().DiffLine.RenderFocusedMainViewAgain(mainViewContext.GetView(), self.context, func() {
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
if c.Helpers().DiffLine.MainViewDiffMode() == git_commands.DiffModeRaw {
|
||||
c.Helpers().DiffLine.RenderFocusedMainViewAgain(mainViewContext.GetView(), source, func() {
|
||||
c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
self.c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
c.Helpers().DiffLine.EstablishSelection(mainViewContext, clickedLineIdx)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,30 +1,10 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ToggleWhitespaceAction struct {
|
||||
c *ControllerCommon
|
||||
}
|
||||
|
||||
func (self *ToggleWhitespaceAction) Call() error {
|
||||
contextsThatDontSupportIgnoringWhitespace := []types.ContextKey{
|
||||
context.STAGING_MAIN_CONTEXT_KEY,
|
||||
context.STAGING_SECONDARY_CONTEXT_KEY,
|
||||
context.PATCH_BUILDING_MAIN_CONTEXT_KEY,
|
||||
}
|
||||
|
||||
if lo.Contains(contextsThatDontSupportIgnoringWhitespace, self.c.Context().Current().GetKey()) {
|
||||
// Ignoring whitespace is not supported in these views. Let the user
|
||||
// know that it's not going to work in case they try to turn it on.
|
||||
return errors.New(self.c.Tr.IgnoreWhitespaceNotSupportedHere)
|
||||
}
|
||||
|
||||
self.c.UserConfig().Git.IgnoreWhitespaceInDiffView = !self.c.UserConfig().Git.IgnoreWhitespaceInDiffView
|
||||
|
||||
// You toggle this to see whether what you are looking at is more than
|
||||
|
||||
@@ -2,7 +2,6 @@ package controllers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
@@ -100,6 +99,12 @@ func (self *WorkingTreeDiffActions) DiscardSelection(pane types.DiffPaneContext,
|
||||
})
|
||||
}
|
||||
|
||||
// DiscardSelectionDisabledReason is nil: a change of the working tree can always be
|
||||
// thrown away, and one in the index always taken back out of it.
|
||||
func (self *WorkingTreeDiffActions) DiscardSelectionDisabledReason(types.DiffPaneContext) *types.DisabledReason {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EditHunk opens the git hunk holding the selection in an editor, as a patch against
|
||||
// the index, and applies whatever comes back. It is how you stage something the diff
|
||||
// can't express — half of a changed line, or a change written differently from either
|
||||
@@ -122,7 +127,8 @@ func (self *WorkingTreeDiffActions) EditHunk(
|
||||
}
|
||||
|
||||
parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, onStagedSide))
|
||||
lineIndices := changeLineIndices(parsedPatch, infos[:1])
|
||||
lineIndices := patch.ChangeLineIndicesForLines(parsedPatch,
|
||||
[]patch.LineIdentity{infos[0].PatchLineIdentity()})
|
||||
if len(lineIndices) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -181,6 +187,12 @@ func (self *WorkingTreeDiffActions) EditHunk(
|
||||
return nil
|
||||
}
|
||||
|
||||
// PatchInclusion is nil: a custom patch is built from a commit's diff, never from the
|
||||
// working tree's, so no line of this diff is ever in one.
|
||||
func (self *WorkingTreeDiffActions) PatchInclusion() func(types.DiffLineInfo) bool {
|
||||
return nil
|
||||
}
|
||||
|
||||
// diffLineSelection resolves what the user has selected in a pane of the focused main
|
||||
// view to the change lines to act on, and reports whether they are the staged side of
|
||||
// the diff — which is a question about the pane, so it is the same for every file of a
|
||||
@@ -249,11 +261,11 @@ func (self *WorkingTreeDiffActions) applyDiffLineSelection(
|
||||
// fileForDiffLinePath maps the absolute path a diff line carries to the working tree
|
||||
// file it belongs to, or nil for a path that is no file of this repo's working tree.
|
||||
func (self *WorkingTreeDiffActions) fileForDiffLinePath(path string) *models.File {
|
||||
relativePath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), path)
|
||||
if err != nil {
|
||||
relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), path)
|
||||
if relativePath == "" {
|
||||
return nil
|
||||
}
|
||||
return self.context().FileTreeViewModel.GetFile(filepath.ToSlash(relativePath))
|
||||
return self.context().FileTreeViewModel.GetFile(relativePath)
|
||||
}
|
||||
|
||||
// applyDiffLines applies the given change lines of one file — a line, a hunk, a range —
|
||||
@@ -280,7 +292,10 @@ func (self *WorkingTreeDiffActions) applyDiffLines(
|
||||
) (bool, error) {
|
||||
parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, git_commands.DiffModePlain, sourceCached))
|
||||
|
||||
patchLineIndices := changeLineIndices(parsedPatch, infos)
|
||||
patchLineIndices := patch.ChangeLineIndicesForLines(parsedPatch,
|
||||
lo.Map(infos, func(info types.DiffLineInfo, _ int) patch.LineIdentity {
|
||||
return info.PatchLineIdentity()
|
||||
}))
|
||||
|
||||
changesLeft := len(patchLineIndices) < changeLineCount(parsedPatch)
|
||||
|
||||
@@ -311,42 +326,6 @@ func (self *WorkingTreeDiffActions) applyDiffLines(
|
||||
return changesLeft, self.c.Git().Patch.ApplyPatch(patchToApply, opts)
|
||||
}
|
||||
|
||||
// changeLineIndices says which lines of a parsed patch the given diff rows are, as the
|
||||
// indices a patch is built in terms of. Each row is looked for by where it sits in the
|
||||
// file: an addition by its place in the new version, a deletion by its place in the
|
||||
// old one, which is what tells the two halves of a changed line apart.
|
||||
func changeLineIndices(parsedPatch *patch.Patch, infos []types.DiffLineInfo) []int {
|
||||
type changeLine struct {
|
||||
lineNumber int
|
||||
isDeletion bool
|
||||
}
|
||||
selected := set.New[changeLine]()
|
||||
for _, info := range infos {
|
||||
if info.Type == types.DiffLineDeleted {
|
||||
selected.Add(changeLine{info.OldLine, true})
|
||||
} else {
|
||||
selected.Add(changeLine{info.NewLine, false})
|
||||
}
|
||||
}
|
||||
|
||||
var indices []int
|
||||
for idx, line := range parsedPatch.Lines() {
|
||||
var key changeLine
|
||||
switch {
|
||||
case line.IsAddition():
|
||||
key = changeLine{parsedPatch.LineNumberOfLine(idx), false}
|
||||
case line.IsDeletion():
|
||||
key = changeLine{parsedPatch.OldLineNumberOfLine(idx), true}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
if selected.Includes(key) {
|
||||
indices = append(indices, idx)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
// changeLineCount returns how many of a patch's lines are changes rather than context
|
||||
// or header. A selection of the whole diff covers exactly that many.
|
||||
func changeLineCount(p *patch.Patch) int {
|
||||
@@ -378,7 +357,7 @@ func (self *WorkingTreeDiffActions) revealSelectionInPaneItLandsIn(
|
||||
// that, and until it has been the selection is still on lines that aren't there any
|
||||
// more — so a key pressed meanwhile would act on nothing.
|
||||
self.c.GocuiGui().BeginBlockingEvents()
|
||||
revealSelectionAfterAction(self.c, pane, target, firstLineIdx,
|
||||
self.c.Helpers().DiffLine.RevealSelectionAfterAction(pane, target, firstLineIdx, 0,
|
||||
self.c.GocuiGui().EndBlockingEvents)
|
||||
}
|
||||
|
||||
|
||||
@@ -175,8 +175,6 @@ func (self *guiCommon) RenderToMainViews(opts types.RefreshMainOpts) {
|
||||
func (self *guiCommon) MainViewPairs() types.MainViewPairs {
|
||||
return types.MainViewPairs{
|
||||
Normal: self.gui.normalMainContextPair(),
|
||||
Staging: self.gui.stagingMainContextPair(),
|
||||
PatchBuilding: self.gui.patchBuildingMainContextPair(),
|
||||
MergeConflicts: self.gui.mergingMainContextPair(),
|
||||
}
|
||||
}
|
||||
|
||||
+19
-11
@@ -51,34 +51,40 @@ func (self *GuiDriver) PressKeysRapidly(keyStrs ...string) {
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Click(x, y int) {
|
||||
self.ClickWithModifier(x, y, gocui.ModNone)
|
||||
}
|
||||
|
||||
// ClickWithModifier clicks with a keyboard modifier held down for the whole
|
||||
// gesture, as a terminal reports it.
|
||||
func (self *GuiDriver) ClickWithModifier(x, y int, modifier gocui.Modifier) {
|
||||
self.CheckAllToastsAcknowledged()
|
||||
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonNone)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary, modifier)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonNone, modifier)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) ClickAndHold(x, y int) {
|
||||
self.CheckAllToastsAcknowledged()
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary, gocui.ModNone)
|
||||
}
|
||||
|
||||
// MouseMove reports the mouse at a new position with the left button still
|
||||
// held down, i.e. a drag movement. (No test needs pointer motion without a
|
||||
// button held, so that variant doesn't exist.)
|
||||
func (self *GuiDriver) MouseMove(x, y int) {
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonPrimary, gocui.ModNone)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) ScrollWheelDown(x, y int) {
|
||||
self.replayMouseEvent(x, y, tcell.WheelDown)
|
||||
self.replayMouseEvent(x, y, tcell.WheelDown, gocui.ModNone)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) MouseRelease(x, y int) {
|
||||
self.replayMouseEvent(x, y, tcell.ButtonNone)
|
||||
self.replayMouseEvent(x, y, tcell.ButtonNone, gocui.ModNone)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) MouseReleaseWithoutWaiting(x, y int) {
|
||||
self.replayMouseEventWithoutWaiting(x, y, tcell.ButtonNone)
|
||||
self.replayMouseEventWithoutWaiting(x, y, tcell.ButtonNone, gocui.ModNone)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) WaitUntilIdle() {
|
||||
@@ -89,14 +95,16 @@ func (self *GuiDriver) OnUIThreadAndWait(f func()) {
|
||||
_ = self.gui.g.OnUIThreadAndWait(f)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) replayMouseEvent(x, y int, buttons tcell.ButtonMask) {
|
||||
self.replayMouseEventWithoutWaiting(x, y, buttons)
|
||||
func (self *GuiDriver) replayMouseEvent(x, y int, buttons tcell.ButtonMask, modifier gocui.Modifier) {
|
||||
self.replayMouseEventWithoutWaiting(x, y, buttons, modifier)
|
||||
self.waitTillIdle()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) replayMouseEventWithoutWaiting(x, y int, buttons tcell.ButtonMask) {
|
||||
func (self *GuiDriver) replayMouseEventWithoutWaiting(
|
||||
x, y int, buttons tcell.ButtonMask, modifier gocui.Modifier,
|
||||
) {
|
||||
self.gui.g.ReplayMouseEvent(gocui.NewTcellMouseEventWrapper(
|
||||
tcell.NewEventMouse(x, y, buttons, 0),
|
||||
tcell.NewEventMouse(x, y, buttons, tcell.ModMask(modifier)),
|
||||
0,
|
||||
))
|
||||
}
|
||||
|
||||
+84
-19
@@ -1,6 +1,8 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
@@ -68,6 +70,10 @@ func (gui *Gui) RefreshMainView(opts *types.ViewUpdateOpts, context types.Contex
|
||||
|
||||
if mainContext := gui.mainContextForView(view); mainContext != nil {
|
||||
mainContext.SetContentIsDiff(types.ContentIsDiff(opts.Task))
|
||||
// A diff is wrapped as the user asks; anything else a main pane shows is prose
|
||||
// or a log, and reads as badly cut off at the edge of the pane as it would
|
||||
// anywhere else.
|
||||
view.Wrap = !mainContext.ContentIsDiff() || gui.c.UserConfig().Gui.WrapLinesInDiffView
|
||||
}
|
||||
|
||||
if err := gui.runTaskForView(view, opts.Task); err != nil {
|
||||
@@ -82,20 +88,6 @@ func (gui *Gui) normalMainContextPair() types.MainContextPair {
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) stagingMainContextPair() types.MainContextPair {
|
||||
return types.NewMainContextPair(
|
||||
gui.State.Contexts.Staging,
|
||||
gui.State.Contexts.StagingSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) patchBuildingMainContextPair() types.MainContextPair {
|
||||
return types.NewMainContextPair(
|
||||
gui.State.Contexts.CustomPatchBuilder,
|
||||
gui.State.Contexts.CustomPatchBuilderSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
func (gui *Gui) mergingMainContextPair() types.MainContextPair {
|
||||
return types.NewMainContextPair(
|
||||
gui.State.Contexts.MergeConflicts,
|
||||
@@ -106,8 +98,6 @@ func (gui *Gui) mergingMainContextPair() types.MainContextPair {
|
||||
func (gui *Gui) allMainContextPairs() []types.MainContextPair {
|
||||
return []types.MainContextPair{
|
||||
gui.normalMainContextPair(),
|
||||
gui.stagingMainContextPair(),
|
||||
gui.patchBuildingMainContextPair(),
|
||||
gui.mergingMainContextPair(),
|
||||
}
|
||||
}
|
||||
@@ -118,6 +108,7 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) {
|
||||
// Before the render is triggered, so that the pane the focus moves into can be
|
||||
// told where to put its selection as it renders.
|
||||
gui.followFocusIntoWorkablePane(opts)
|
||||
gui.keepDiffSelectionAcrossACommitRewrite(opts)
|
||||
|
||||
gui.moveMainContextPairToTop(opts.Pair)
|
||||
|
||||
@@ -244,6 +235,71 @@ func (gui *Gui) followFocusIntoWorkablePane(opts types.RefreshMainOpts) {
|
||||
gui.State.ContextMgr.Push(target, types.OnFocusOpts{})
|
||||
}
|
||||
|
||||
// keepDiffSelectionAcrossACommitRewrite arranges for a selection in the focused main
|
||||
// view to come back on the same change of the diff when the render about to happen is
|
||||
// of a different diff from the one on screen. This happens when a commit is rewritten
|
||||
// under the user, by moving a patch out of it, discarding lines from it, or undoing
|
||||
// either. The selection is then left at a position in a rendering that no longer exists.
|
||||
//
|
||||
// What is remembered is which change of the diff the selection was on rather than which
|
||||
// line of which file, a rewrite being precisely a change to those lines: the change that
|
||||
// takes its place is where the work carries on.
|
||||
//
|
||||
// It is asked of every render, and does nothing unless all three of these hold: there
|
||||
// is a selection to keep; nothing more precise is already waiting to be put back (the
|
||||
// position preserves and the post-action reveals know better where their selection
|
||||
// belongs); and the diff really is another one. A plain refresh re-renders the same
|
||||
// diff, where the selection, possibly a range the user is in the middle of making, is
|
||||
// still exactly right.
|
||||
func (gui *Gui) keepDiffSelectionAcrossACommitRewrite(opts types.RefreshMainOpts) {
|
||||
// The focused main view's two panes only: no other pair has a diff selection.
|
||||
if opts.Pair.Main.GetKey() != context.NORMAL_MAIN_CONTEXT_KEY {
|
||||
return
|
||||
}
|
||||
|
||||
current := gui.State.ContextMgr.CurrentStatic().GetKey()
|
||||
for _, pane := range []struct {
|
||||
context types.Context
|
||||
update *types.ViewUpdateOpts
|
||||
}{
|
||||
{opts.Pair.Main, opts.Main},
|
||||
{opts.Pair.Secondary, opts.Secondary},
|
||||
} {
|
||||
if pane.update == nil || pane.context.GetKey() != current {
|
||||
continue
|
||||
}
|
||||
mainContext := gui.mainContextForView(pane.context.GetView())
|
||||
if mainContext == nil || !mainContext.GetView().Highlight {
|
||||
continue
|
||||
}
|
||||
manager := gui.getViewBufferManagerForView(mainContext.GetView())
|
||||
if manager == nil || manager.HasRestoreForNextTask() {
|
||||
continue
|
||||
}
|
||||
key, ok := diffTaskCommandKey(pane.update.Task)
|
||||
if !ok || key == manager.GetTaskKey() {
|
||||
continue
|
||||
}
|
||||
|
||||
first, _ := mainContext.GetView().SelectedLineRange()
|
||||
gui.helpers.DiffLine.RevealSelectionAfterAction(mainContext, mainContext, first, 0, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// diffTaskCommandKey returns the key the given render will be remembered under. Two
|
||||
// renders of the same diff have the same key, so comparing keys says whether a render
|
||||
// is of the diff already on screen. ok is false for a render that is a message rather
|
||||
// than a diff.
|
||||
func diffTaskCommandKey(task types.UpdateTask) (string, bool) {
|
||||
switch task := task.(type) {
|
||||
case *types.RunCommandTask:
|
||||
return strings.Join(task.Cmd.Args, " "), true
|
||||
case *types.RunDiffRendererTask:
|
||||
return strings.Join(task.Cmd.Args, " "), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// onlyWorkablePane returns the main pane a render leaves as the only one worth having
|
||||
// the focus in, or nil when that is true of both of them or of neither. Being shown is
|
||||
// not the same as being worth working in: a pane the layout keeps around for the sake
|
||||
@@ -308,8 +364,11 @@ func (gui *Gui) clearMainView(mainContext types.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// updateDiffSelectionVisibility works out whether a main pane holds anything for a
|
||||
// selection to sit on, from what it is now showing: only beneath a panel whose main
|
||||
// updateDiffPaneDecorations re-derives what is drawn over a main pane's content, rather
|
||||
// than being part of it: whether a selection is shown, and which lines are marked as
|
||||
// being in the custom patch.
|
||||
//
|
||||
// A pane holds something for a selection to sit on only beneath a panel whose main
|
||||
// view is a diff, only while the pane is showing that diff rather than a message like
|
||||
// "No changed files", and only while the diff holds something to select — never over
|
||||
// one with nothing in it, such as a binary file's or an empty commit's. Whether the
|
||||
@@ -321,7 +380,7 @@ func (gui *Gui) clearMainView(mainContext types.Context) {
|
||||
// still being read can leave the question open (see diffPaneHasSomethingToSelect). The
|
||||
// pane never answers from the render before it, and a render that leaves the question
|
||||
// open is read on until it doesn't, so the answer is always about what is there.
|
||||
func (gui *Gui) updateDiffSelectionVisibility(view *gocui.View, contentIsComplete bool) {
|
||||
func (gui *Gui) updateDiffPaneDecorations(view *gocui.View, contentIsComplete bool) {
|
||||
mainContext := gui.mainContextForView(view)
|
||||
if mainContext == nil {
|
||||
return
|
||||
@@ -337,6 +396,12 @@ func (gui *Gui) updateDiffSelectionVisibility(view *gocui.View, contentIsComplet
|
||||
} else {
|
||||
gui.readOnUntilTheDiffPaneCanTell(view)
|
||||
}
|
||||
|
||||
// The marks are over the diff in the upper pane; the lower one shows the patch
|
||||
// they are marks of.
|
||||
if view == gui.Views.Main {
|
||||
gui.helpers.DiffLine.RefreshInclusionGutter()
|
||||
}
|
||||
}
|
||||
|
||||
// dropAnAnswerAboutAnotherRender takes away what the pane worked out about the content
|
||||
|
||||
@@ -51,6 +51,11 @@ func (gui *Gui) newRenderTask(view *gocui.View, cmd *exec.Cmd, prefix string) er
|
||||
return gui.newCmdTask(view, cmd, prefix)
|
||||
}
|
||||
|
||||
// The key the render is remembered under says which diff it is of, so that a
|
||||
// re-render of the same diff can be told from a render of another one. Take
|
||||
// it before anything else can touch the command's arguments.
|
||||
cmdStr := strings.Join(cmd.Args, " ")
|
||||
|
||||
// Mark the view as loading synchronously now, before the layout pass: the
|
||||
// actual task is created in afterLayout (below), which runs after layout, so
|
||||
// without this the next layout pass would clamp the scroll position to the
|
||||
@@ -81,8 +86,6 @@ func (gui *Gui) newRenderTask(view *gocui.View, cmd *exec.Cmd, prefix string) er
|
||||
gui.c.ErrorToast(err.Error())
|
||||
}
|
||||
|
||||
cmdStr := strings.Join(cmd.Args, " ")
|
||||
|
||||
// This communicates to diff renderers that we're in a very simple
|
||||
// terminal that they should not expect to have much capabilities.
|
||||
// Moving the cursor, clearing the screen, or querying for colors are among such "advanced" capabilities.
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package patch_exploring
|
||||
|
||||
func calculateOrigin(currentOrigin int, bufferHeight int, numLines int, firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) int {
|
||||
needToSeeIdx, wantToSeeIdx := getNeedAndWantLineIdx(firstLineIdx, lastLineIdx, selectedLineIdx, mode)
|
||||
|
||||
return calculateNewOriginWithNeededAndWantedIdx(currentOrigin, bufferHeight, numLines, needToSeeIdx, wantToSeeIdx)
|
||||
}
|
||||
|
||||
// we want to scroll our origin so that the index we need to see is in view
|
||||
// and the other index we want to see (e.g. the other side of a line range)
|
||||
// is as close to being in view as possible.
|
||||
func calculateNewOriginWithNeededAndWantedIdx(currentOrigin int, bufferHeight int, numLines int, needToSeeIdx int, wantToSeeIdx int) int {
|
||||
origin := currentOrigin
|
||||
if needToSeeIdx < currentOrigin || needToSeeIdx >= currentOrigin+bufferHeight {
|
||||
origin = max(min(needToSeeIdx-bufferHeight/2, numLines-bufferHeight), 0)
|
||||
}
|
||||
|
||||
bottom := origin + bufferHeight
|
||||
|
||||
if wantToSeeIdx < origin {
|
||||
requiredChange := origin - wantToSeeIdx
|
||||
allowedChange := bottom - needToSeeIdx
|
||||
return origin - min(requiredChange, allowedChange)
|
||||
} else if wantToSeeIdx >= bottom {
|
||||
requiredChange := wantToSeeIdx + 1 - bottom
|
||||
allowedChange := needToSeeIdx - origin
|
||||
return origin + min(requiredChange, allowedChange)
|
||||
}
|
||||
return origin
|
||||
}
|
||||
|
||||
func getNeedAndWantLineIdx(firstLineIdx int, lastLineIdx int, selectedLineIdx int, mode selectMode) (int, int) {
|
||||
switch mode {
|
||||
case LINE:
|
||||
return selectedLineIdx, selectedLineIdx
|
||||
case RANGE:
|
||||
if selectedLineIdx == firstLineIdx {
|
||||
return firstLineIdx, lastLineIdx
|
||||
}
|
||||
return lastLineIdx, firstLineIdx
|
||||
case HUNK:
|
||||
return firstLineIdx, lastLineIdx
|
||||
default:
|
||||
// we should never land here
|
||||
panic("unknown mode")
|
||||
}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package patch_exploring
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewOrigin(t *testing.T) {
|
||||
type scenario struct {
|
||||
name string
|
||||
origin int
|
||||
bufferHeight int
|
||||
numLines int
|
||||
firstLineIdx int
|
||||
lastLineIdx int
|
||||
selectedLineIdx int
|
||||
selectMode selectMode
|
||||
expected int
|
||||
}
|
||||
|
||||
scenarios := []scenario{
|
||||
{
|
||||
name: "selection above scroll window, enough room to put it in the middle",
|
||||
origin: 250,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 210,
|
||||
lastLineIdx: 210,
|
||||
selectedLineIdx: 210,
|
||||
selectMode: LINE,
|
||||
expected: 160,
|
||||
},
|
||||
{
|
||||
name: "selection above scroll window, not enough room to put it in the middle",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 10,
|
||||
lastLineIdx: 10,
|
||||
selectedLineIdx: 10,
|
||||
selectMode: LINE,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "selection below scroll window, enough room to put it in the middle",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 150,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 150,
|
||||
selectMode: LINE,
|
||||
expected: 100,
|
||||
},
|
||||
{
|
||||
name: "selection below scroll window, not enough room to put it in the middle",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
numLines: 200,
|
||||
firstLineIdx: 199,
|
||||
lastLineIdx: 199,
|
||||
selectedLineIdx: 199,
|
||||
selectMode: LINE,
|
||||
expected: 100,
|
||||
},
|
||||
{
|
||||
name: "selection within scroll window",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 50,
|
||||
lastLineIdx: 50,
|
||||
selectedLineIdx: 50,
|
||||
selectMode: LINE,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "range ending below scroll window with selection at end of range",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 150,
|
||||
selectMode: RANGE,
|
||||
expected: 50,
|
||||
},
|
||||
{
|
||||
name: "range ending below scroll window with selection at beginning of range",
|
||||
origin: 0,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 40,
|
||||
selectMode: RANGE,
|
||||
expected: 40,
|
||||
},
|
||||
{
|
||||
name: "range starting above scroll window with selection at beginning of range",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 150,
|
||||
selectedLineIdx: 40,
|
||||
selectMode: RANGE,
|
||||
expected: 40,
|
||||
},
|
||||
{
|
||||
name: "hunk extending beyond both bounds of scroll window",
|
||||
origin: 50,
|
||||
bufferHeight: 100,
|
||||
numLines: 500,
|
||||
firstLineIdx: 40,
|
||||
lastLineIdx: 200,
|
||||
selectedLineIdx: 70,
|
||||
selectMode: HUNK,
|
||||
expected: 40,
|
||||
},
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
assert.EqualValues(t, s.expected, calculateOrigin(s.origin, s.bufferHeight, s.numLines, s.firstLineIdx, s.lastLineIdx, s.selectedLineIdx, s.selectMode))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,438 +0,0 @@
|
||||
package patch_exploring
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/generics/set"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// State represents the current state of the patch explorer context i.e. when
|
||||
// you're staging a file or you're building a patch from an existing commit
|
||||
// this struct holds the info about the diff you're interacting with and what's currently selected.
|
||||
type State struct {
|
||||
// These are in terms of view lines (wrapped), not patch lines
|
||||
selectedLineIdx int
|
||||
rangeStartLineIdx int
|
||||
// If a range is sticky, it means we expand the range when we move up or down.
|
||||
// Otherwise, we cancel the range when we move up or down.
|
||||
rangeIsSticky bool
|
||||
diff string
|
||||
patch *patch.Patch
|
||||
selectMode selectMode
|
||||
|
||||
// Array of indices of the wrapped lines indexed by a patch line index
|
||||
viewLineIndices []int
|
||||
// Array of indices of the original patch lines indexed by a wrapped view line index
|
||||
patchLineIndices []int
|
||||
|
||||
// whether the user has switched to hunk mode manually; if hunk mode is on
|
||||
// but this is false, then hunk mode was enabled because the config makes it
|
||||
// on by default.
|
||||
// this makes a difference for whether we want to escape out of hunk mode
|
||||
userEnabledHunkMode bool
|
||||
}
|
||||
|
||||
// these represent what select mode we're in
|
||||
type selectMode int
|
||||
|
||||
const (
|
||||
LINE selectMode = iota
|
||||
RANGE
|
||||
HUNK
|
||||
)
|
||||
|
||||
func NewState(diff string, selectedLineIdx int, view *gocui.View, oldState *State, useHunkModeByDefault bool) *State {
|
||||
if oldState != nil && diff == oldState.diff && selectedLineIdx == -1 {
|
||||
// if we're here then we can return the old state. If selectedLineIdx was not -1
|
||||
// then that would mean we were trying to click and potentially drag a range, which
|
||||
// is why in that case we continue below
|
||||
return oldState
|
||||
}
|
||||
|
||||
patch := patch.Parse(diff)
|
||||
|
||||
if !patch.ContainsChanges() {
|
||||
return nil
|
||||
}
|
||||
|
||||
viewLineIndices, patchLineIndices := wrapPatchLines(diff, view)
|
||||
|
||||
rangeStartLineIdx := 0
|
||||
if oldState != nil {
|
||||
rangeStartLineIdx = oldState.rangeStartLineIdx
|
||||
}
|
||||
|
||||
selectMode := LINE
|
||||
if useHunkModeByDefault && !patch.IsSingleHunkForWholeFile() {
|
||||
selectMode = HUNK
|
||||
}
|
||||
|
||||
userEnabledHunkMode := false
|
||||
if oldState != nil {
|
||||
userEnabledHunkMode = oldState.userEnabledHunkMode
|
||||
}
|
||||
|
||||
// if we have clicked from the outside to focus the main view we'll pass in a non-negative line index so that we can instantly select that line
|
||||
if selectedLineIdx >= 0 {
|
||||
// Clamp to the number of wrapped view lines; index might be out of
|
||||
// bounds if a custom diff renderer is being used which produces more lines
|
||||
selectedLineIdx = min(selectedLineIdx, len(viewLineIndices)-1)
|
||||
|
||||
selectMode = RANGE
|
||||
rangeStartLineIdx = selectedLineIdx
|
||||
} else if oldState != nil {
|
||||
// if we previously had a selectMode of RANGE, we want that to now be line again (or hunk, if that's the default)
|
||||
if oldState.selectMode != RANGE {
|
||||
selectMode = oldState.selectMode
|
||||
}
|
||||
oldPatchLineIdx := oldState.patchLineIndices[oldState.selectedLineIdx]
|
||||
newPatchLineIdx := patch.GetNextChangeIdx(oldPatchLineIdx)
|
||||
// When staging an addition from a consecutive changes block, the unselected deletions get
|
||||
// reordered to appear before the remaining additions in the new diff. This can cause the
|
||||
// cursor to land on a deletion at the same patch line index where the staged addition used
|
||||
// to be. In that case, skip forward past any deletions, then call GetNextChangeIdx from the
|
||||
// first non-deletion position, which correctly lands on the next meaningful change.
|
||||
newLines := patch.Lines()
|
||||
if newPatchLineIdx == oldPatchLineIdx &&
|
||||
oldState.patch.Lines()[oldPatchLineIdx].IsAddition() &&
|
||||
newLines[newPatchLineIdx].IsDeletion() &&
|
||||
patch.HunkOldStartForLine(newPatchLineIdx) == oldState.patch.HunkOldStartForLine(oldPatchLineIdx) {
|
||||
for newPatchLineIdx < len(newLines) && newLines[newPatchLineIdx].IsDeletion() {
|
||||
newPatchLineIdx++
|
||||
}
|
||||
newPatchLineIdx = patch.GetNextChangeIdx(newPatchLineIdx)
|
||||
}
|
||||
selectedLineIdx = viewLineIndices[newPatchLineIdx]
|
||||
} else {
|
||||
selectedLineIdx = viewLineIndices[patch.GetNextChangeIdx(0)]
|
||||
}
|
||||
|
||||
return &State{
|
||||
patch: patch,
|
||||
selectedLineIdx: selectedLineIdx,
|
||||
selectMode: selectMode,
|
||||
rangeStartLineIdx: rangeStartLineIdx,
|
||||
rangeIsSticky: false,
|
||||
diff: diff,
|
||||
viewLineIndices: viewLineIndices,
|
||||
patchLineIndices: patchLineIndices,
|
||||
userEnabledHunkMode: userEnabledHunkMode,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) OnViewWidthChanged(view *gocui.View) {
|
||||
if !view.Wrap {
|
||||
return
|
||||
}
|
||||
|
||||
selectedPatchLineIdx := s.patchLineIndices[s.selectedLineIdx]
|
||||
var rangeStartPatchLineIdx int
|
||||
if s.selectMode == RANGE {
|
||||
rangeStartPatchLineIdx = s.patchLineIndices[s.rangeStartLineIdx]
|
||||
}
|
||||
s.viewLineIndices, s.patchLineIndices = wrapPatchLines(s.diff, view)
|
||||
s.selectedLineIdx = s.viewLineIndices[selectedPatchLineIdx]
|
||||
if s.selectMode == RANGE {
|
||||
s.rangeStartLineIdx = s.viewLineIndices[rangeStartPatchLineIdx]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) GetSelectedPatchLineIdx() int {
|
||||
return s.patchLineIndices[s.selectedLineIdx]
|
||||
}
|
||||
|
||||
func (s *State) GetSelectedViewLineIdx() int {
|
||||
return s.selectedLineIdx
|
||||
}
|
||||
|
||||
func (s *State) GetDiff() string {
|
||||
return s.diff
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelectHunk() {
|
||||
if s.selectMode == HUNK {
|
||||
s.selectMode = LINE
|
||||
} else {
|
||||
s.selectMode = HUNK
|
||||
s.userEnabledHunkMode = true
|
||||
|
||||
// If we are not currently on a change line, select the next one (or the
|
||||
// previous one if there is no next one):
|
||||
s.selectedLineIdx = s.viewLineIndices[s.patch.GetNextChangeIdx(
|
||||
s.patchLineIndices[s.selectedLineIdx])]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) ToggleStickySelectRange() {
|
||||
s.ToggleSelectRange(true)
|
||||
}
|
||||
|
||||
func (s *State) ToggleSelectRange(sticky bool) {
|
||||
if s.SelectingRange() {
|
||||
s.selectMode = LINE
|
||||
} else {
|
||||
s.selectMode = RANGE
|
||||
s.rangeStartLineIdx = s.selectedLineIdx
|
||||
s.rangeIsSticky = sticky
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SetRangeIsSticky(value bool) {
|
||||
s.rangeIsSticky = value
|
||||
}
|
||||
|
||||
func (s *State) SelectingHunk() bool {
|
||||
return s.selectMode == HUNK
|
||||
}
|
||||
|
||||
func (s *State) SelectingHunkEnabledByUser() bool {
|
||||
return s.selectMode == HUNK && s.userEnabledHunkMode
|
||||
}
|
||||
|
||||
func (s *State) SelectingRange() bool {
|
||||
return s.selectMode == RANGE && (s.rangeIsSticky || s.rangeStartLineIdx != s.selectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) SelectingLine() bool {
|
||||
return s.selectMode == LINE
|
||||
}
|
||||
|
||||
func (s *State) SetLineSelectMode() {
|
||||
s.selectMode = LINE
|
||||
}
|
||||
|
||||
func (s *State) DismissHunkSelectMode() {
|
||||
if s.SelectingHunk() {
|
||||
s.selectMode = LINE
|
||||
}
|
||||
}
|
||||
|
||||
// For when you move the cursor without holding shift (meaning if we're in
|
||||
// a non-sticky range select, we'll cancel it)
|
||||
func (s *State) SelectLine(newSelectedLineIdx int) {
|
||||
if s.selectMode == RANGE && !s.rangeIsSticky {
|
||||
s.selectMode = LINE
|
||||
}
|
||||
|
||||
s.selectLineWithoutRangeCheck(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) clampLineIdx(lineIdx int) int {
|
||||
return lo.Clamp(lineIdx, 0, len(s.patchLineIndices)-1)
|
||||
}
|
||||
|
||||
// This just moves the cursor without caring about range select
|
||||
func (s *State) selectLineWithoutRangeCheck(newSelectedLineIdx int) {
|
||||
s.selectedLineIdx = s.clampLineIdx(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) SelectNewLineForRange(newSelectedLineIdx int) {
|
||||
s.rangeStartLineIdx = s.clampLineIdx(newSelectedLineIdx)
|
||||
|
||||
s.selectMode = RANGE
|
||||
|
||||
s.selectLineWithoutRangeCheck(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) DragSelectLine(newSelectedLineIdx int) {
|
||||
s.selectMode = RANGE
|
||||
|
||||
s.selectLineWithoutRangeCheck(newSelectedLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) CycleSelection(forward bool) {
|
||||
if s.SelectingHunk() {
|
||||
if forward {
|
||||
s.SelectNextHunk()
|
||||
} else {
|
||||
s.SelectPreviousHunk()
|
||||
}
|
||||
} else {
|
||||
s.CycleLine(forward)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectPreviousHunk() {
|
||||
patchLines := s.patch.Lines()
|
||||
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
|
||||
nextNonChangeLine := patchLineIdx
|
||||
for nextNonChangeLine >= 0 && patchLines[nextNonChangeLine].IsChange() {
|
||||
nextNonChangeLine--
|
||||
}
|
||||
nextChangeLine := nextNonChangeLine
|
||||
for nextChangeLine >= 0 && !patchLines[nextChangeLine].IsChange() {
|
||||
nextChangeLine--
|
||||
}
|
||||
if nextChangeLine >= 0 {
|
||||
// Now we found a previous hunk, but we're on its last line. Skip to the beginning.
|
||||
for nextChangeLine > 0 && patchLines[nextChangeLine-1].IsChange() {
|
||||
nextChangeLine--
|
||||
}
|
||||
s.selectedLineIdx = s.viewLineIndices[nextChangeLine]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectNextHunk() {
|
||||
patchLines := s.patch.Lines()
|
||||
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
|
||||
nextNonChangeLine := patchLineIdx
|
||||
for nextNonChangeLine < len(patchLines) && patchLines[nextNonChangeLine].IsChange() {
|
||||
nextNonChangeLine++
|
||||
}
|
||||
nextChangeLine := nextNonChangeLine
|
||||
for nextChangeLine < len(patchLines) && !patchLines[nextChangeLine].IsChange() {
|
||||
nextChangeLine++
|
||||
}
|
||||
if nextChangeLine < len(patchLines) {
|
||||
s.selectedLineIdx = s.viewLineIndices[nextChangeLine]
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) CycleLine(forward bool) {
|
||||
change := 1
|
||||
if !forward {
|
||||
change = -1
|
||||
}
|
||||
|
||||
s.SelectLine(s.selectedLineIdx + change)
|
||||
}
|
||||
|
||||
// This is called when we use shift+arrow to expand the range (i.e. a non-sticky
|
||||
// range)
|
||||
func (s *State) CycleRange(forward bool) {
|
||||
if !s.SelectingRange() {
|
||||
s.ToggleSelectRange(false)
|
||||
}
|
||||
|
||||
s.SetRangeIsSticky(false)
|
||||
|
||||
change := 1
|
||||
if !forward {
|
||||
change = -1
|
||||
}
|
||||
|
||||
s.selectLineWithoutRangeCheck(s.selectedLineIdx + change)
|
||||
}
|
||||
|
||||
// returns first and last patch line index of current hunk
|
||||
func (s *State) CurrentHunkBounds() (int, int) {
|
||||
hunkIdx := s.patch.HunkContainingLine(s.patchLineIndices[s.selectedLineIdx])
|
||||
start := s.patch.HunkStartIdx(hunkIdx)
|
||||
end := s.patch.HunkEndIdx(hunkIdx)
|
||||
return start, end
|
||||
}
|
||||
|
||||
func (s *State) selectionRangeForCurrentBlockOfChanges() (int, int) {
|
||||
patchLines := s.patch.Lines()
|
||||
patchLineIdx := s.patchLineIndices[s.selectedLineIdx]
|
||||
|
||||
patchStart := patchLineIdx
|
||||
for patchStart > 0 && patchLines[patchStart-1].IsChange() {
|
||||
patchStart--
|
||||
}
|
||||
|
||||
patchEnd := patchLineIdx
|
||||
for patchEnd < len(patchLines)-1 && patchLines[patchEnd+1].IsChange() {
|
||||
patchEnd++
|
||||
}
|
||||
|
||||
viewStart, viewEnd := s.viewLineIndices[patchStart], s.viewLineIndices[patchEnd]
|
||||
|
||||
// Increase viewEnd in case the last patch line is wrapped to more than one view line.
|
||||
for viewEnd < len(s.patchLineIndices)-1 && s.patchLineIndices[viewEnd] == s.patchLineIndices[viewEnd+1] {
|
||||
viewEnd++
|
||||
}
|
||||
|
||||
return viewStart, viewEnd
|
||||
}
|
||||
|
||||
func (s *State) SelectedViewRange() (int, int) {
|
||||
switch s.selectMode {
|
||||
case HUNK:
|
||||
return s.selectionRangeForCurrentBlockOfChanges()
|
||||
case RANGE:
|
||||
if s.rangeStartLineIdx > s.selectedLineIdx {
|
||||
return s.selectedLineIdx, s.rangeStartLineIdx
|
||||
}
|
||||
return s.rangeStartLineIdx, s.selectedLineIdx
|
||||
case LINE:
|
||||
return s.selectedLineIdx, s.selectedLineIdx
|
||||
default:
|
||||
// should never happen
|
||||
return 0, 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) SelectedPatchRange() (int, int) {
|
||||
start, end := s.SelectedViewRange()
|
||||
return s.patchLineIndices[start], s.patchLineIndices[end]
|
||||
}
|
||||
|
||||
// Returns the line indices of the selected patch range that are changes (i.e. additions or deletions)
|
||||
func (s *State) LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange() []int {
|
||||
viewStart, viewEnd := s.SelectedViewRange()
|
||||
patchStart, patchEnd := s.patchLineIndices[viewStart], s.patchLineIndices[viewEnd]
|
||||
lines := s.patch.Lines()
|
||||
indices := []int{}
|
||||
for i := patchStart; i <= patchEnd; i++ {
|
||||
if lines[i].IsChange() {
|
||||
indices = append(indices, i)
|
||||
}
|
||||
}
|
||||
return indices
|
||||
}
|
||||
|
||||
func (s *State) CurrentLineNumber() int {
|
||||
return s.patch.LineNumberOfLine(s.patchLineIndices[s.selectedLineIdx])
|
||||
}
|
||||
|
||||
func (s *State) AdjustSelectedLineIdx(change int) {
|
||||
s.DismissHunkSelectMode()
|
||||
s.SelectLine(s.selectedLineIdx + change)
|
||||
}
|
||||
|
||||
func (s *State) RenderForLineIndices(includedLineIndices []int) string {
|
||||
includedLineIndicesSet := set.NewFromSlice(includedLineIndices)
|
||||
return s.patch.FormatView(patch.FormatViewOpts{
|
||||
IncLineIndices: includedLineIndicesSet,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *State) PlainRenderSelected() string {
|
||||
firstLineIdx, lastLineIdx := s.SelectedPatchRange()
|
||||
return s.patch.FormatRangePlain(firstLineIdx, lastLineIdx)
|
||||
}
|
||||
|
||||
func (s *State) SelectBottom() {
|
||||
s.DismissHunkSelectMode()
|
||||
s.SelectLine(len(s.patchLineIndices) - 1)
|
||||
}
|
||||
|
||||
func (s *State) SelectTop() {
|
||||
s.DismissHunkSelectMode()
|
||||
s.SelectLine(0)
|
||||
}
|
||||
|
||||
func (s *State) CalculateOrigin(currentOrigin int, bufferHeight int, numLines int) int {
|
||||
firstLineIdx, lastLineIdx := s.SelectedViewRange()
|
||||
|
||||
return calculateOrigin(currentOrigin, bufferHeight, numLines, firstLineIdx, lastLineIdx, s.GetSelectedViewLineIdx(), s.selectMode)
|
||||
}
|
||||
|
||||
func wrapPatchLines(diff string, view *gocui.View) ([]int, []int) {
|
||||
_, viewLineIndices, patchLineIndices := utils.WrapViewLinesToWidth(
|
||||
view.Wrap, view.Editable, strings.TrimSuffix(diff, "\n"), view.InnerWidth(), view.TabWidth)
|
||||
return viewLineIndices, patchLineIndices
|
||||
}
|
||||
|
||||
func (s *State) SelectNextStageableLineOfSameIncludedState(includedLines []int, included bool) {
|
||||
_, lastLineIdx := s.SelectedPatchRange()
|
||||
patchLineIdx, found := s.patch.GetNextChangeIdxOfSameIncludedState(lastLineIdx+1, includedLines, included)
|
||||
if found {
|
||||
s.SelectLine(s.viewLineIndices[patchLineIdx])
|
||||
}
|
||||
}
|
||||
@@ -230,6 +230,7 @@ M file1
|
||||
func(from string, to string, reverse bool, filename string, previousPath string) (string, error) {
|
||||
return "", nil
|
||||
},
|
||||
nil,
|
||||
)
|
||||
patchBuilder.Start("from", "to", false, false)
|
||||
result := RenderCommitFileTree(viewModel, patchBuilder, false, &config.CustomIconsConfig{})
|
||||
|
||||
@@ -94,7 +94,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error {
|
||||
f := func(tasks.TaskOpts) error {
|
||||
return gui.g.OnUIThreadAndWaitBackground(func() {
|
||||
gui.c.SetViewContent(view, str)
|
||||
gui.updateDiffSelectionVisibility(view, true)
|
||||
gui.updateDiffPaneDecorations(view, true)
|
||||
gui.reApplySearch(view)
|
||||
})
|
||||
}
|
||||
@@ -116,7 +116,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in
|
||||
return gui.g.OnUIThreadAndWaitBackground(func() {
|
||||
gui.c.SetViewContent(view, str)
|
||||
view.SetOrigin(originX, originY)
|
||||
gui.updateDiffSelectionVisibility(view, true)
|
||||
gui.updateDiffPaneDecorations(view, true)
|
||||
gui.reApplySearch(view)
|
||||
})
|
||||
}
|
||||
@@ -138,7 +138,7 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e
|
||||
return gui.g.OnUIThreadAndWaitBackground(func() {
|
||||
gui.c.ResetViewOrigin(view)
|
||||
gui.c.SetViewContent(view, str)
|
||||
gui.updateDiffSelectionVisibility(view, true)
|
||||
gui.updateDiffPaneDecorations(view, true)
|
||||
gui.reApplySearch(view)
|
||||
})
|
||||
}
|
||||
@@ -177,7 +177,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
|
||||
// to say whether there is anything to select, and for a diff that
|
||||
// opens with a long diffstat it isn't.
|
||||
gui.c.OnUIThreadContentOnly(func() error {
|
||||
gui.updateDiffSelectionVisibility(view, false)
|
||||
gui.updateDiffPaneDecorations(view, false)
|
||||
return nil
|
||||
})
|
||||
},
|
||||
@@ -196,7 +196,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
|
||||
view.SetOrigin(0, newOriginY)
|
||||
}
|
||||
|
||||
gui.updateDiffSelectionVisibility(view, true)
|
||||
gui.updateDiffPaneDecorations(view, true)
|
||||
gui.clampDiffSelectionToContent(view)
|
||||
gui.reApplySearch(view)
|
||||
},
|
||||
@@ -210,7 +210,7 @@ func (gui *Gui) getManager(view *gocui.View) *tasks.ViewBufferManager {
|
||||
// The content the pane is being given is on display from here on, so
|
||||
// what is drawn over it is settled against that content rather than
|
||||
// against the render before it.
|
||||
gui.updateDiffSelectionVisibility(view, false)
|
||||
gui.updateDiffPaneDecorations(view, false)
|
||||
},
|
||||
func() gocui.Task {
|
||||
// A background task: rendering content into a view is display
|
||||
|
||||
+37
-18
@@ -1,11 +1,10 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/patch_exploring"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/sasha-s/go-deadlock"
|
||||
)
|
||||
|
||||
type ContextKind int
|
||||
@@ -215,6 +214,29 @@ const (
|
||||
DiffMainViewTypePatchBuilding
|
||||
)
|
||||
|
||||
// PullRequestDiffContext is implemented by the side panel contexts that show, in
|
||||
// their focused main view, the diff of a commit of a branch: the commits panel and
|
||||
// the sub-commits panel, and the commit files panel entered from either of them. A
|
||||
// pull request for that branch has a view of that commit, so a line of the diff can
|
||||
// be pointed at in it. A panel showing a diff that no pull request has a view of (the
|
||||
// working tree's, a stash entry's, a reflog entry's) doesn't implement this.
|
||||
type PullRequestDiffContext interface {
|
||||
Context
|
||||
|
||||
// BranchForPullRequest returns the local branch whose pull request would show
|
||||
// the diff in the main view, and "" where no branch does: the panel may have
|
||||
// nothing selected, HEAD may be detached, or what was drilled into may be a tag
|
||||
// or a remote branch rather than a local one.
|
||||
BranchForPullRequest() string
|
||||
|
||||
// CommitsForPullRequest returns the commits whose combined diff the main view is
|
||||
// showing, newest first as the panel lists them, together with the hash of the
|
||||
// commit that diff starts after: the parent of the oldest of them, where the
|
||||
// pull request has that parent as one of its own commits, and "" where the diff
|
||||
// starts where the pull request itself does.
|
||||
CommitsForPullRequest() ([]*models.Commit, string)
|
||||
}
|
||||
|
||||
// DiffPaneContext is one of the two panes the main section can show, as the thing
|
||||
// that holds a diff with a selection in it. The panels that act on such a selection
|
||||
// are handed the pane it was made in, and speak to it through this.
|
||||
@@ -256,8 +278,20 @@ type FocusedMainViewActions interface {
|
||||
PrimaryAction(pane DiffPaneContext, firstLineIdx int, lastLineIdx int) error
|
||||
|
||||
// DiscardSelection takes the selected diff lines back out of whatever they are part
|
||||
// of: the working tree for the files panel.
|
||||
// of: the working tree for the files panel, the commit itself for the panels showing
|
||||
// a commit's diff.
|
||||
DiscardSelection(pane DiffPaneContext, firstLineIdx int, lastLineIdx int) error
|
||||
|
||||
// DiscardSelectionDisabledReason says why the selection can't be discarded where it
|
||||
// is, and nil when it can. Taking lines out of a commit means rewriting it, which
|
||||
// isn't always something we may do; the working tree has no such condition.
|
||||
DiscardSelectionDisabledReason(pane DiffPaneContext) *DisabledReason
|
||||
|
||||
// PatchInclusion says which lines of the diff this panel shows are in the custom
|
||||
// patch being built from it. The marks over those lines are drawn from this. nil
|
||||
// where nothing about this diff is being built into a patch, which is always so
|
||||
// for a diff that can't be.
|
||||
PatchInclusion() func(info DiffLineInfo) bool
|
||||
}
|
||||
|
||||
type IListContext interface {
|
||||
@@ -279,20 +313,6 @@ type IListContext interface {
|
||||
IndexForGotoBottom() int
|
||||
}
|
||||
|
||||
type IPatchExplorerContext interface {
|
||||
Context
|
||||
|
||||
GetState() *patch_exploring.State
|
||||
SetState(*patch_exploring.State)
|
||||
GetIncludedLineIndices() []int
|
||||
RenderAndFocus()
|
||||
Render()
|
||||
GetContentToRender() string
|
||||
NavigateTo(selectedLineIdx int)
|
||||
GetMutex() *deadlock.Mutex
|
||||
IsPatchExplorerContext() // used for type switch
|
||||
}
|
||||
|
||||
type IViewTrait interface {
|
||||
FocusPoint(yIdx int, scrollIntoView bool)
|
||||
SetRangeSelectStart(yIdx int)
|
||||
@@ -428,5 +448,4 @@ type IContextMgr interface {
|
||||
AllList() []IListContext
|
||||
AllFilterable() []IFilterableContext
|
||||
AllSearchable() []ISearchableContext
|
||||
AllPatchExplorer() []IPatchExplorerContext
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package types
|
||||
|
||||
import "github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
|
||||
// DiffLineType classifies a row of a rendered diff.
|
||||
type DiffLineType int
|
||||
|
||||
@@ -41,6 +43,16 @@ func (self DiffLineInfo) IsChange() bool {
|
||||
return self.Type == DiffLineAdded || self.Type == DiffLineDeleted
|
||||
}
|
||||
|
||||
// PatchLineIdentity says which change line of the file the row is, in the terms a patch
|
||||
// of that file is built and read in: an addition by where it sits in the new version of
|
||||
// the file, a deletion by where it sat in the old one. Only meaningful for a change row.
|
||||
func (self DiffLineInfo) PatchLineIdentity() patch.LineIdentity {
|
||||
if self.Type == DiffLineDeleted {
|
||||
return patch.LineIdentity{LineNumber: self.OldLine, IsDeletion: true}
|
||||
}
|
||||
return patch.LineIdentity{LineNumber: self.NewLine}
|
||||
}
|
||||
|
||||
// IsContent reports whether the row is a line of the file itself — a change or a
|
||||
// context line — as opposed to a header or a marker. Those are the rows that have a
|
||||
// position in the file, and so can be looked for in another rendering of the same
|
||||
|
||||
@@ -39,6 +39,11 @@ type Binding struct {
|
||||
|
||||
// to be displayed if the keybinding is highlighted from within a menu
|
||||
Tooltip string
|
||||
// TooltipFunc is used instead of Tooltip if non-nil, for a command whose tooltip
|
||||
// depends on context, as DescriptionFunc is for its description — and with the
|
||||
// same two conditions: it must not be an expensive call, and a generic Tooltip
|
||||
// must still be given, since that is the one the cheatsheet prints.
|
||||
TooltipFunc func() string
|
||||
|
||||
// Function to decide whether the command is enabled, and why. If this
|
||||
// returns an empty string, it is; if it returns a non-empty string, it is
|
||||
@@ -59,6 +64,13 @@ func (b *Binding) GetDescription() string {
|
||||
return b.Description
|
||||
}
|
||||
|
||||
func (b *Binding) GetTooltip() string {
|
||||
if b.TooltipFunc != nil {
|
||||
return b.TooltipFunc()
|
||||
}
|
||||
return b.Tooltip
|
||||
}
|
||||
|
||||
func (b *Binding) GetShortDescription() string {
|
||||
if b.ShortDescriptionFunc != nil {
|
||||
return b.ShortDescriptionFunc()
|
||||
|
||||
@@ -16,8 +16,6 @@ const (
|
||||
WORKTREES
|
||||
STATUS
|
||||
SUBMODULES
|
||||
STAGING
|
||||
PATCH_BUILDING
|
||||
MERGE_CONFLICTS
|
||||
COMMIT_FILES
|
||||
// not actually views. Will refactor this later
|
||||
|
||||
@@ -31,8 +31,6 @@ const (
|
||||
type MainViewPairs struct {
|
||||
Normal MainContextPair
|
||||
MergeConflicts MainContextPair
|
||||
Staging MainContextPair
|
||||
PatchBuilding MainContextPair
|
||||
}
|
||||
|
||||
type ViewUpdateOpts struct {
|
||||
@@ -69,9 +67,6 @@ func NewRenderStringTask(str string) *RenderStringTask {
|
||||
|
||||
type RenderStringWithoutScrollTask struct {
|
||||
Str string
|
||||
|
||||
// contentIsDiff marks a string that is a panel's own diff; see ContentIsDiff.
|
||||
contentIsDiff bool
|
||||
}
|
||||
|
||||
func (t *RenderStringWithoutScrollTask) IsUpdateTask() {}
|
||||
@@ -80,16 +75,6 @@ func NewRenderStringWithoutScrollTask(str string) *RenderStringWithoutScrollTask
|
||||
return &RenderStringWithoutScrollTask{Str: str}
|
||||
}
|
||||
|
||||
// NewMainViewDiffStringTask returns the task for rendering a diff we hold as text
|
||||
// rather than as a command to run — the custom patch being built, which we assemble
|
||||
// ourselves. The view stays where it is, the patch being rendered again on every
|
||||
// change to it.
|
||||
func NewMainViewDiffStringTask(str string) UpdateTask {
|
||||
task := NewRenderStringWithoutScrollTask(str)
|
||||
task.contentIsDiff = true
|
||||
return task
|
||||
}
|
||||
|
||||
type RenderStringWithScrollTask struct {
|
||||
Str string
|
||||
OriginX int
|
||||
@@ -166,8 +151,6 @@ func NewMainViewDiffTaskWithPrefix(cmd *exec.Cmd, prefix string, mode git_comman
|
||||
// such a render that there is anything to point at.
|
||||
func ContentIsDiff(task UpdateTask) bool {
|
||||
switch task := task.(type) {
|
||||
case *RenderStringWithoutScrollTask:
|
||||
return task.contentIsDiff
|
||||
case *RunCommandTask:
|
||||
return task.contentIsDiff
|
||||
case *RunDiffRendererTask:
|
||||
|
||||
@@ -15,13 +15,9 @@ type Views struct {
|
||||
Commits *gocui.View
|
||||
Stash *gocui.View
|
||||
|
||||
Main *gocui.View
|
||||
Secondary *gocui.View
|
||||
Staging *gocui.View
|
||||
StagingSecondary *gocui.View
|
||||
PatchBuilding *gocui.View
|
||||
PatchBuildingSecondary *gocui.View
|
||||
MergeConflicts *gocui.View
|
||||
Main *gocui.View
|
||||
Secondary *gocui.View
|
||||
MergeConflicts *gocui.View
|
||||
|
||||
Options *gocui.View
|
||||
Confirmation *gocui.View
|
||||
|
||||
+9
-14
@@ -41,10 +41,6 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
|
||||
{viewPtr: &gui.Views.SubCommits, name: "subCommits"},
|
||||
{viewPtr: &gui.Views.CommitFiles, name: "commitFiles"},
|
||||
|
||||
{viewPtr: &gui.Views.Staging, name: "staging"},
|
||||
{viewPtr: &gui.Views.StagingSecondary, name: "stagingSecondary"},
|
||||
{viewPtr: &gui.Views.PatchBuilding, name: "patchBuilding"},
|
||||
{viewPtr: &gui.Views.PatchBuildingSecondary, name: "patchBuildingSecondary"},
|
||||
{viewPtr: &gui.Views.MergeConflicts, name: "mergeConflicts"},
|
||||
{viewPtr: &gui.Views.Secondary, name: "secondary"},
|
||||
{viewPtr: &gui.Views.Main, name: "main"},
|
||||
@@ -106,16 +102,12 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Search.Frame = false
|
||||
gui.Views.Search.Editor = gocui.EditorFunc(gui.searchEditor)
|
||||
|
||||
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.Staging, gui.Views.StagingSecondary, gui.Views.PatchBuilding, gui.Views.PatchBuildingSecondary, gui.Views.MergeConflicts} {
|
||||
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.MergeConflicts} {
|
||||
view.Wrap = true
|
||||
view.UnderlineHyperLinksOnlyOnHover = true
|
||||
view.AutoRenderHyperLinks = true
|
||||
}
|
||||
|
||||
gui.Views.Staging.Wrap = true
|
||||
gui.Views.StagingSecondary.Wrap = true
|
||||
gui.Views.PatchBuilding.Wrap = true
|
||||
gui.Views.PatchBuildingSecondary.Wrap = true
|
||||
gui.Views.MergeConflicts.Wrap = false
|
||||
gui.Views.Limit.Wrap = true
|
||||
|
||||
@@ -177,6 +169,13 @@ func (gui *Gui) createAllViews() error {
|
||||
gui.Views.Main.SelectedLineColorWidth = 2
|
||||
gui.Views.Secondary.SelectedLineColorWidth = 2
|
||||
|
||||
// A tick, for the lines of a commit's diff that are in the custom patch being
|
||||
// built. A plus would collide with a diff's own plus column.
|
||||
gui.Views.Main.InclusionGutterMarker = "✓"
|
||||
gui.Views.Main.InclusionGutterMarkerColor = gocui.ColorGreen
|
||||
gui.Views.Secondary.InclusionGutterMarker = "✓"
|
||||
gui.Views.Secondary.InclusionGutterMarkerColor = gocui.ColorGreen
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -230,19 +229,15 @@ func (gui *Gui) configureViewProperties() {
|
||||
gui.Views.Submodules.Title = gui.c.Tr.SubmodulesTitle
|
||||
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
|
||||
gui.Views.Files.Title = gui.c.Tr.FilesTitle
|
||||
gui.Views.PatchBuilding.Title = gui.c.Tr.Patch
|
||||
gui.Views.PatchBuildingSecondary.Title = gui.c.Tr.CustomPatch
|
||||
gui.Views.MergeConflicts.Title = gui.c.Tr.MergeConflictsTitle
|
||||
gui.Views.Limit.Title = gui.c.Tr.NotEnoughSpace
|
||||
gui.Views.Status.Title = gui.c.Tr.StatusTitle
|
||||
gui.Views.Staging.Title = gui.c.Tr.UnstagedChanges
|
||||
gui.Views.StagingSecondary.Title = gui.c.Tr.StagedChanges
|
||||
gui.Views.CommitMessage.Title = gui.c.Tr.CommitSummary
|
||||
gui.Views.CommitDescription.Title = gui.c.Tr.CommitDescriptionTitle
|
||||
gui.Views.Extras.Title = gui.c.Tr.CommandLog
|
||||
gui.Views.Snake.Title = gui.c.Tr.SnakeTitle
|
||||
|
||||
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.Staging, gui.Views.StagingSecondary, gui.Views.PatchBuilding, gui.Views.PatchBuildingSecondary, gui.Views.MergeConflicts} {
|
||||
for _, view := range []*gocui.View{gui.Views.Main, gui.Views.Secondary, gui.Views.MergeConflicts} {
|
||||
view.Title = gui.c.Tr.DiffTitle
|
||||
view.CanScrollPastBottom = gui.c.UserConfig().Gui.ScrollPastBottom
|
||||
view.TabWidth = gui.c.UserConfig().Gui.TabWidth
|
||||
|
||||
+28
-33
@@ -21,7 +21,6 @@ type TranslationSet struct {
|
||||
EasterEgg string
|
||||
UnstagedChanges string
|
||||
StagedChanges string
|
||||
StagingTitle string
|
||||
MergingTitle string
|
||||
NormalTitle string
|
||||
LogTitle string
|
||||
@@ -294,6 +293,8 @@ type TranslationSet struct {
|
||||
UnsupportedGitService string
|
||||
CopyPullRequestURL string
|
||||
OpenPullRequestInBrowser string
|
||||
OpenPullRequestAtSelectedLine string
|
||||
OpenPullRequestAtSelectedLineTooltip string
|
||||
NoPullRequestForBranch string
|
||||
NoBranchOnRemote string
|
||||
Fetch string
|
||||
@@ -311,16 +312,19 @@ type TranslationSet struct {
|
||||
ToggleSelectHunk string
|
||||
SelectHunk string
|
||||
NothingToSelectInDiff string
|
||||
NotAvailableInDiffingMode string
|
||||
NotAvailableForCustomPatch string
|
||||
CommitNotInPullRequest string
|
||||
CommitsNotInPullRequest string
|
||||
SelectLineByLine string
|
||||
ToggleSelectHunkTooltip string
|
||||
HunkStagingHint string
|
||||
ToggleSelectionForPatch string
|
||||
RemoveSelectionFromPatch string
|
||||
RemoveSelectionFromPatchTooltip string
|
||||
EditHunk string
|
||||
EditHunkTooltip string
|
||||
ToggleStagingView string
|
||||
ToggleStagingViewTooltip string
|
||||
ToggleDiffPane string
|
||||
ToggleDiffPaneTooltip string
|
||||
ReturnToFilesPanel string
|
||||
FastForward string
|
||||
FastForwardTooltip string
|
||||
@@ -360,7 +364,6 @@ type TranslationSet struct {
|
||||
CommitMenuTitle string
|
||||
RemotesTitle string
|
||||
RemoteBranchesTitle string
|
||||
PatchBuildingTitle string
|
||||
InformationTitle string
|
||||
SecondaryTitle string
|
||||
ReflogCommitsTitle string
|
||||
@@ -472,6 +475,7 @@ type TranslationSet struct {
|
||||
CheckoutCommitFileTooltip string
|
||||
CannotCheckoutWithModifiedFilesErr string
|
||||
CanOnlyDiscardFromLocalCommits string
|
||||
CannotDiscardFromCustomPatchView string
|
||||
CannotDiscardFromMultipleCommits string
|
||||
Remove string
|
||||
DiscardOldFileChangeTooltip string
|
||||
@@ -563,9 +567,9 @@ type TranslationSet struct {
|
||||
PatchOptionsTitle string
|
||||
NoPatchError string
|
||||
EmptyPatchError string
|
||||
EnterCommitFile string
|
||||
EnterCommitFileTooltip string
|
||||
ExitCustomPatchBuilder string
|
||||
FocusCommitFileDiff string
|
||||
FocusCommitFileDiffTooltip string
|
||||
ResetCustomPatch string
|
||||
ExitFocusedMainView string
|
||||
EnterUpstream string
|
||||
InvalidUpstream string
|
||||
@@ -735,7 +739,7 @@ type TranslationSet struct {
|
||||
CopyTagToClipboard string
|
||||
CopyPathToClipboard string
|
||||
CommitPrefixPatternError string
|
||||
CopySelectedTextToClipboard string
|
||||
CopySelectedDiffLinesToClipboard string
|
||||
SelectedDiffLinesCopiedToast string
|
||||
SelectionNotFoundInDiffToast string
|
||||
NoFilesStagedTitle string
|
||||
@@ -821,7 +825,6 @@ type TranslationSet struct {
|
||||
ToggleWhitespaceInDiffView string
|
||||
ToggleWhitespaceInDiffViewTooltip string
|
||||
IgnoreWhitespaceDiffViewSubTitle string
|
||||
IgnoreWhitespaceNotSupportedHere string
|
||||
IncreaseContextInDiffView string
|
||||
IncreaseContextInDiffViewTooltip string
|
||||
DecreaseContextInDiffView string
|
||||
@@ -863,7 +866,6 @@ type TranslationSet struct {
|
||||
SortOrderPrompt string
|
||||
SortCommits string
|
||||
SortCommitsTooltip string
|
||||
CantChangeContextSizeError string
|
||||
CantChangeRenameThresholdError string
|
||||
OpenCommitInBrowser string
|
||||
ViewBisectOptions string
|
||||
@@ -1185,15 +1187,6 @@ const englishNonReloadableConfigWarning = `The following config settings were ch
|
||||
|
||||
{{configs}}`
|
||||
|
||||
const englishHunkStagingHint = `Hunk selection mode is now the default for staging. If you want to stage individual lines, press '%s' to switch to line-by-line mode.
|
||||
|
||||
If you prefer to use line-by-line mode by default (like in earlier lazygit versions), add
|
||||
|
||||
gui:
|
||||
useHunkModeInStagingView: false
|
||||
|
||||
to your lazygit config.`
|
||||
|
||||
// exporting this so we can use it in tests
|
||||
func EnglishTranslationSet() *TranslationSet {
|
||||
return &TranslationSet{
|
||||
@@ -1207,7 +1200,6 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
EasterEgg: "Easter egg",
|
||||
UnstagedChanges: "Unstaged changes",
|
||||
StagedChanges: "Staged changes",
|
||||
StagingTitle: "Main panel (staging)",
|
||||
MergingTitle: "Main panel (merging)",
|
||||
NormalTitle: "Main panel (normal)",
|
||||
LogTitle: "Log",
|
||||
@@ -1481,6 +1473,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CreatePullRequest: `Create pull request`,
|
||||
CopyPullRequestURL: `Copy pull request URL to clipboard`,
|
||||
OpenPullRequestInBrowser: `Open pull request in browser`,
|
||||
OpenPullRequestAtSelectedLine: `Open pull request at selected line`,
|
||||
OpenPullRequestAtSelectedLineTooltip: "Open the branch's pull request in your browser, at the line the selection is on, so that you can comment on it there. Only pull requests on GitHub are found.",
|
||||
NoPullRequestForBranch: `No pull request found for this branch`,
|
||||
NoBranchOnRemote: `This branch doesn't exist on remote. You need to push it to remote first.`,
|
||||
Fetch: `Fetch`,
|
||||
@@ -1490,8 +1484,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
ExpandAll: "Expand all files",
|
||||
ExpandAllTooltip: "Expand all directories in the file tree",
|
||||
DisabledInFlatView: "Not available in flat view",
|
||||
FileEnter: `Stage lines / Collapse directory`,
|
||||
FileEnterTooltip: "If the selected item is a file, focus the staging view so you can stage individual hunks/lines. If the selected item is a directory, collapse/expand it.",
|
||||
FileEnter: `Focus file diff / Collapse directory`,
|
||||
FileEnterTooltip: "If the selected item is a file, focus its diff so you can act on individual hunks or lines. If it is a directory, collapse or expand it.",
|
||||
StageSelectionTooltip: `Toggle selection staged / unstaged.`,
|
||||
DiscardSelection: `Discard`,
|
||||
DiscardSelectionTooltip: "When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change.",
|
||||
@@ -1499,17 +1493,20 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
DismissRangeSelect: "Dismiss range select",
|
||||
ToggleSelectHunk: "Toggle hunk selection",
|
||||
NothingToSelectInDiff: "There is nothing to select here",
|
||||
NotAvailableInDiffingMode: "Not available in diffing mode",
|
||||
NotAvailableForCustomPatch: "Not available for the custom patch",
|
||||
CommitNotInPullRequest: "This commit is not part of the pull request",
|
||||
CommitsNotInPullRequest: "Not all of these commits are part of the pull request",
|
||||
SelectHunk: "Select hunks",
|
||||
SelectLineByLine: "Select line-by-line",
|
||||
ToggleSelectHunkTooltip: "Toggle line-by-line vs. hunk selection mode.",
|
||||
HunkStagingHint: englishHunkStagingHint,
|
||||
ToggleSelectionForPatch: `Toggle lines in patch`,
|
||||
RemoveSelectionFromPatch: `Remove lines from commit`,
|
||||
RemoveSelectionFromPatchTooltip: "Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines.",
|
||||
EditHunk: `Edit hunk`,
|
||||
EditHunkTooltip: "Edit selected hunk in external editor.",
|
||||
ToggleStagingView: "Switch view",
|
||||
ToggleStagingViewTooltip: "Switch to other view (staged/unstaged changes).",
|
||||
ToggleDiffPane: "Switch diff pane",
|
||||
ToggleDiffPaneTooltip: "Switch to the other focused diff pane.",
|
||||
ReturnToFilesPanel: `Return to files panel`,
|
||||
FastForward: `Fast-forward`,
|
||||
FastForwardTooltip: "Fast-forward selected branch from its upstream. If the branch has diverged from its upstream because the upstream branch was rewritten, and it has no commits of its own, it is reset to its upstream instead. This needs reflogs to be enabled; a bare repository doesn't keep them by default (core.logAllRefUpdates).",
|
||||
@@ -1547,7 +1544,6 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CommitMenuTitle: "Commit Menu",
|
||||
RemotesTitle: "Remotes",
|
||||
RemoteBranchesTitle: "Remote branches",
|
||||
PatchBuildingTitle: "Main panel (patch building)",
|
||||
InformationTitle: "Information",
|
||||
SecondaryTitle: "Secondary",
|
||||
ReflogCommitsTitle: "Reflog",
|
||||
@@ -1665,6 +1661,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CheckoutCommitFileTooltip: "Checkout file. This replaces the file in your working tree with the version from the selected commit.",
|
||||
CannotCheckoutWithModifiedFilesErr: "You have local modifications for the file(s) you are trying to check out. You need to stash or discard these first.",
|
||||
CanOnlyDiscardFromLocalCommits: "Changes can only be discarded from local commits",
|
||||
CannotDiscardFromCustomPatchView: "Lines shown here are the custom patch's; press space to take them back out of it",
|
||||
CannotDiscardFromMultipleCommits: "Changes cannot be discarded from a multiselection of commits",
|
||||
Remove: "Remove",
|
||||
DiscardOldFileChangeTooltip: "Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file.",
|
||||
@@ -1757,9 +1754,9 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
PatchOptionsTitle: "Patch options",
|
||||
NoPatchError: "No patch created yet. To start building a patch, use 'space' on a commit file or enter to add specific lines",
|
||||
EmptyPatchError: "Patch is still empty. Add some files or lines to your patch first.",
|
||||
EnterCommitFile: "Enter file / Toggle directory collapsed",
|
||||
EnterCommitFileTooltip: "If a file is selected, enter the file so that you can add/remove individual lines to the custom patch. If a directory is selected, toggle the directory.",
|
||||
ExitCustomPatchBuilder: `Exit custom patch builder`,
|
||||
FocusCommitFileDiff: "Focus file diff / Toggle directory",
|
||||
FocusCommitFileDiffTooltip: "If a file is selected, focus its diff so you can act on individual lines. If it is a directory, collapse or expand it.",
|
||||
ResetCustomPatch: `Reset custom patch`,
|
||||
ExitFocusedMainView: "Exit back to side panel",
|
||||
EnterUpstream: `Enter upstream as '<remote> <branchname>'`,
|
||||
InvalidUpstream: "Invalid upstream. Must be in the format '<remote> <branchname>'",
|
||||
@@ -1926,7 +1923,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CopyBranchNameToClipboard: "Copy branch name to clipboard",
|
||||
CopyTagToClipboard: "Copy tag to clipboard",
|
||||
CopyPathToClipboard: "Copy path to clipboard",
|
||||
CopySelectedTextToClipboard: "Copy selected text to clipboard",
|
||||
CopySelectedDiffLinesToClipboard: "Copy selected diff lines to clipboard",
|
||||
SelectedDiffLinesCopiedToast: "Selected diff lines copied to clipboard",
|
||||
SelectionNotFoundInDiffToast: "Nothing in the selection could be found in the diff",
|
||||
CommitPrefixPatternError: "Error in commitPrefix pattern",
|
||||
@@ -2012,7 +2009,6 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
ToggleWhitespaceInDiffView: "Toggle whitespace",
|
||||
ToggleWhitespaceInDiffViewTooltip: "Toggle whether or not whitespace changes are shown in the diff view.\n\nThe default can be changed in the config file with the key 'git.ignoreWhitespaceInDiffView'.",
|
||||
IgnoreWhitespaceDiffViewSubTitle: "(ignoring whitespace)",
|
||||
IgnoreWhitespaceNotSupportedHere: "Ignoring whitespace is not supported in this view",
|
||||
IncreaseContextInDiffView: "Increase diff context size",
|
||||
IncreaseContextInDiffViewTooltip: "Increase the amount of the context shown around changes in the diff view.\n\nThe default can be changed in the config file with the key 'git.diffContextSize'.",
|
||||
DecreaseContextInDiffView: "Decrease diff context size",
|
||||
@@ -2052,7 +2048,6 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
SortBasedOnReflog: "(based on reflog)",
|
||||
SortCommits: "Commit sort order",
|
||||
SortCommitsTooltip: "Change the sort order of the commits in the commit log.\n\nThe default can be changed in the config file with the key 'git.log.sortOrder'.",
|
||||
CantChangeContextSizeError: "Cannot change context while in patch building mode because we were too lazy to support it when releasing the feature. If you really want it, please let us know!",
|
||||
CantChangeRenameThresholdError: "Cannot change the rename similarity threshold while in patch building mode, because the custom patch can't cope with a rename turning into a delete and add underneath it.",
|
||||
OpenCommitInBrowser: "Open commit in browser",
|
||||
ViewBisectOptions: "View bisect options",
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gocui"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
@@ -60,6 +61,12 @@ func (self *TestDriver) click(x, y int) {
|
||||
self.Wait(self.inputDelay)
|
||||
}
|
||||
|
||||
func (self *TestDriver) clickWithModifier(x, y int, modifier gocui.Modifier, what string) {
|
||||
self.SetCaption(fmt.Sprintf("%s-clicking %d, %d", what, x, y))
|
||||
self.gui.ClickWithModifier(x, y, modifier)
|
||||
self.Wait(self.inputDelay)
|
||||
}
|
||||
|
||||
func (self *TestDriver) clickAndHold(x, y int) {
|
||||
self.SetCaption(fmt.Sprintf("Clicking and holding %d, %d", x, y))
|
||||
self.mouseX, self.mouseY = x, y
|
||||
|
||||
@@ -47,6 +47,10 @@ func (self *fakeGuiDriver) Click(x, y int) {
|
||||
self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y})
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) ClickWithModifier(x, y int, modifier gocui.Modifier) {
|
||||
self.clickedCoordinates = append(self.clickedCoordinates, coordinate{x: x, y: y})
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) ClickAndHold(x, y int) {
|
||||
self.heldCoordinates = append(self.heldCoordinates, coordinate{x: x, y: y})
|
||||
}
|
||||
@@ -200,6 +204,8 @@ func TestViewDriverPointerCoordinates(t *testing.T) {
|
||||
|
||||
viewDriver.
|
||||
Click(1, 2).
|
||||
AltClick(2, 3).
|
||||
ShiftClick(4, 5).
|
||||
FocusInAndClick(3, 4).
|
||||
ClickAndHold(5, 6).
|
||||
MouseMove(7, 8).
|
||||
@@ -207,11 +213,13 @@ func TestViewDriverPointerCoordinates(t *testing.T) {
|
||||
MouseMoveToView(targetViewDriver, 10, 11).
|
||||
ScrollWheelDown()
|
||||
|
||||
assert.Equal(t, []coordinate{{12, 23}, {14, 25}}, guiDriver.clickedCoordinates)
|
||||
assert.Equal(t,
|
||||
[]coordinate{{12, 23}, {13, 24}, {15, 26}, {14, 25}},
|
||||
guiDriver.clickedCoordinates)
|
||||
assert.Equal(t, []coordinate{{16, 27}}, guiDriver.heldCoordinates)
|
||||
assert.Equal(t, []coordinate{{18, 29}, {20, 30}, {51, 62}}, guiDriver.movedCoordinates)
|
||||
assert.Equal(t, []coordinate{{11, 21}}, guiDriver.scrolledCoordinates)
|
||||
assert.Equal(t, 7, guiDriver.onUIThreadCallCount)
|
||||
assert.Equal(t, 9, guiDriver.onUIThreadCallCount)
|
||||
}
|
||||
|
||||
func TestFailingFixture(t *testing.T) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user