mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-29 02:35:39 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d0db51caf | ||
|
|
980a8ca53e | ||
|
|
aa7d26be59 | ||
|
|
4b69d93660 | ||
|
|
82e283429a | ||
|
|
6edf29b0dc | ||
|
|
c9a883b273 | ||
|
|
7eb561921b | ||
|
|
48f2e1dfc1 | ||
|
|
2f575a0b37 | ||
|
|
641740fdec | ||
|
|
bdc780a890 | ||
|
|
443452ad6a | ||
|
|
d97531aeef | ||
|
|
819212b758 | ||
|
|
48a8a71d31 | ||
|
|
be703f3d27 | ||
|
|
06a9ff12ab | ||
|
|
4c9cc1ab7f | ||
|
|
02997d30d9 | ||
|
|
ce202a5612 | ||
|
|
f317a97ac1 | ||
|
|
6f6545e44f | ||
|
|
99e26be3d9 | ||
|
|
de98258e39 | ||
|
|
3bf833aea7 | ||
|
|
39cd4746d5 | ||
|
|
1ede4e6eb9 | ||
|
|
4c731abedd | ||
|
|
c10dfcefcf | ||
|
|
50b5c17d93 | ||
|
|
8d36c8b3c1 | ||
|
|
9f44caa568 | ||
|
|
39477172b8 | ||
|
|
7545fc48e5 | ||
|
|
3d78263e0c | ||
|
|
11e877cbad | ||
|
|
268d1934f8 | ||
|
|
4bcf3e181c | ||
|
|
df1e5512da | ||
|
|
2ba3c1dc3f | ||
|
|
b41bb8f845 | ||
|
|
0339b414d9 | ||
|
|
cb719c11c5 | ||
|
|
19605ad476 | ||
|
|
e0c1d1c752 | ||
|
|
4486c86271 | ||
|
|
b44a369295 | ||
|
|
498bc457fc | ||
|
|
7f957f059b | ||
|
|
1a4af359ec | ||
|
|
f16bec2ce3 | ||
|
|
798fe4a1ef | ||
|
|
bb2e5fa886 | ||
|
|
3067ff1cac | ||
|
|
312feff130 | ||
|
|
ee60102fdc | ||
|
|
7b969a03ad | ||
|
|
1f7e5d7767 | ||
|
|
80dd695d7a | ||
|
|
65edd99fd0 | ||
|
|
98bf8f86d1 |
@@ -1,7 +1,7 @@
|
||||
# adapted from https://github.com/devcontainers/images/blob/main/src/go/.devcontainer/Dockerfile
|
||||
|
||||
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
|
||||
ARG VARIANT=1-bullseye
|
||||
ARG VARIANT=1-trixie
|
||||
FROM golang:${VARIANT}
|
||||
|
||||
RUN go install mvdan.cc/gofumpt@latest
|
||||
|
||||
@@ -26,7 +26,9 @@ If applicable, add screenshots to help explain your problem.
|
||||
### Version info:
|
||||
|
||||
* _Run `lazygit --version` and paste the result here_
|
||||
* _Run `git --version` and paste the result here_
|
||||
|
||||
### Terminal info:
|
||||
What terminal are you using, and which version? For some types of bugs this information can be relevant.
|
||||
|
||||
### Additional context
|
||||
Add any other context about the problem here.
|
||||
|
||||
+17
-17
@@ -28,9 +28,9 @@ jobs:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Test code
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
mkdir -p /tmp/code_coverage
|
||||
go test ./... -short -cover -args "-test.gocoverdir=/tmp/code_coverage"
|
||||
- name: Upload code coverage artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-unit-${{ matrix.os }}-${{ github.run_id }}
|
||||
path: /tmp/code_coverage
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Restore Git cache
|
||||
if: matrix.git-version != 'latest'
|
||||
id: cache-git-restore
|
||||
@@ -85,7 +85,7 @@ jobs:
|
||||
path: ~/git-${{matrix.git-version}}
|
||||
key: ${{runner.os}}-git-${{matrix.git-version}}
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Print git version
|
||||
@@ -98,7 +98,7 @@ jobs:
|
||||
mkdir -p /tmp/code_coverage
|
||||
./scripts/run_integration_tests.sh
|
||||
- name: Upload code coverage artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: coverage-integration-${{ matrix.git-version }}-${{ github.run_id }}
|
||||
path: /tmp/code_coverage
|
||||
@@ -109,9 +109,9 @@ jobs:
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Build linux binary
|
||||
@@ -136,9 +136,9 @@ jobs:
|
||||
GOARCH: amd64
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Check Vendor Directory
|
||||
@@ -162,13 +162,13 @@ jobs:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
- name: Lint
|
||||
uses: golangci/golangci-lint-action@v8
|
||||
uses: golangci/golangci-lint-action@v9
|
||||
with:
|
||||
# If you change this, make sure to also update scripts/golangci-lint-shim.sh
|
||||
version: v2.4.0
|
||||
@@ -182,15 +182,15 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
- name: Download all coverage artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
path: /tmp/code_coverage
|
||||
|
||||
@@ -219,7 +219,7 @@ jobs:
|
||||
run: echo "PR_FETCH_DEPTH=$(( ${{ github.event.pull_request.commits }} ))" >> "${GITHUB_ENV}"
|
||||
|
||||
- name: "Checkout PR branch and all PR commits"
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.issue.pull_request == null && startsWith(github.event.comment.body, '/close') }}
|
||||
steps:
|
||||
- uses: actions/github-script@v7
|
||||
- uses: actions/github-script@v8
|
||||
with:
|
||||
script: |
|
||||
const trustedUsers = ['ChrisMcD1', 'jesseduffield', 'stefanhaller']
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Annotate locations with typos
|
||||
uses: codespell-project/codespell-problem-matcher@v1
|
||||
- name: Codespell
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs at 8:00 AM UTC on every Saturday
|
||||
# We'll check below if it's the first Saturday of the month, and fail if not
|
||||
- cron: '0 8 * * 6'
|
||||
# schedule:
|
||||
# # Runs at 8:00 AM UTC on every Saturday
|
||||
# # We'll check below if it's the first Saturday of the month, and fail if not
|
||||
# - cron: '0 8 * * 6'
|
||||
|
||||
# Allow manual triggering of the workflow
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -45,7 +46,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: jesseduffield/lazygit
|
||||
token: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
@@ -146,7 +147,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.LAZYGIT_RELEASE_PAT }}
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: 1.25.x
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
if: ${{ github.repository == 'jesseduffield/lazygit' }}
|
||||
steps:
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Generate Sponsors 💖
|
||||
uses: JamesIves/github-sponsors-readme-action@v1.2.2
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
file: "README.md"
|
||||
|
||||
- name: Create Pull Request 🚀
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
uses: peter-evans/create-pull-request@v8
|
||||
with:
|
||||
commit-message: "README.md: Update Sponsors"
|
||||
title: "README.md: Update Sponsors"
|
||||
|
||||
@@ -713,6 +713,7 @@ keybinding:
|
||||
renameCommitWithEditor: R
|
||||
viewResetOptions: g
|
||||
markCommitAsFixup: f
|
||||
setFixupMessage: c
|
||||
createFixupCommit: F
|
||||
squashAboveCommits: S
|
||||
moveDownCommit: <c-j>
|
||||
|
||||
@@ -89,6 +89,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Reword | Reword the selected commit's message. |
|
||||
| `` R `` | Reword with editor | |
|
||||
| `` d `` | Drop | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -69,6 +69,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` b `` | bisectオプションを表示 | |
|
||||
| `` s `` | スカッシュ | 選択したコミットをその下のコミットにスカッシュします。スカッシュとは複数のコミットを1つにまとめる操作です。選択したコミットのメッセージが下のコミットに追加されます。 |
|
||||
| `` f `` | フィックスアップ | 選択したコミットをその下のコミットにマージします。フィックスアップはスカッシュと似ていますが、選択したコミットのメッセージは破棄され、下のコミットのメッセージのみが保持されます。 |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | メッセージ変更 | 選択したコミットのメッセージを変更します。 |
|
||||
| `` R `` | エディタでメッセージ変更 | |
|
||||
| `` d `` | 削除 | 選択したコミットを削除します。これはリベースを通じてブランチからコミットを削除します。コミットが後続のコミットが依存する変更を行っている場合、マージコンフリクトを解決する必要があるかもしれません。 |
|
||||
@@ -114,7 +115,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` <space> `` | パッチに含めるファイルを切り替え | ファイルがカスタムパッチに含まれるかどうかを切り替えます。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` a `` | すべてのファイルを切り替え | コミットのすべてのファイルをカスタムパッチに追加/削除します。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` <enter> `` | ファイルに入る / ディレクトリの折りたたみを切り替える | ファイルが選択されている場合、そのファイルに入ってカスタムパッチに個々の行を追加/削除できます。ディレクトリが選択されている場合、ディレクトリを切り替えます。 |
|
||||
| `` ` `` | ファイルツリービューを切り替え | 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'. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
|
||||
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
@@ -234,7 +235,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` d `` | 破棄 | 選択したファイルの変更を破棄するオプションを表示します。 |
|
||||
| `` g `` | アップストリームへのリセットオプションを表示 | |
|
||||
| `` D `` | リセット | 作業ツリーのリセットオプション(例:作業ツリーの完全破棄)を表示します。 |
|
||||
| `` ` `` | ファイルツリービューを切り替え | 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'. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` <c-t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` f `` | フェッチ | リモートから変更をフェッチします。 |
|
||||
|
||||
@@ -298,6 +298,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | Bisect 옵션 보기 | |
|
||||
| `` s `` | 스쿼시 | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 커밋메시지 변경 | Reword the selected commit's message. |
|
||||
| `` R `` | 에디터에서 커밋메시지 수정 | |
|
||||
| `` d `` | 커밋 삭제 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -160,6 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Hernoem commit | Reword the selected commit's message. |
|
||||
| `` R `` | Hernoem commit met editor | |
|
||||
| `` d `` | Verwijder commit | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -62,6 +62,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
||||
| `` b `` | Zobacz opcje bisect | |
|
||||
| `` s `` | Scal | Scal wybrany commit z commitami poniżej. Wiadomość wybranego commita zostanie dołączona do commita poniżej. |
|
||||
| `` f `` | Poprawka | Włącz wybrany commit do commita poniżej. Podobnie do fixup, ale wiadomość wybranego commita zostanie odrzucona. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Przeformułuj | Przeformułuj wiadomość wybranego commita. |
|
||||
| `` R `` | Przeformułuj za pomocą edytora | |
|
||||
| `` d `` | Usuń | Usuń wybrany commit. To usunie commit z gałęzi za pomocą rebazowania. Jeśli commit wprowadza zmiany, od których zależą późniejsze commity, być może będziesz musiał rozwiązać konflikty scalania. |
|
||||
|
||||
@@ -164,6 +164,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash o commit selecionado no commit abaixo dele. A mensagem do commit selecionado será anexada ao commit abaixo dele. |
|
||||
| `` f `` | Fixup | Faça o commit selecionado no commit abaixo dele. Semelhante para o squash, mas a mensagem do commit selecionado será descartada. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Reword | Repetir a mensagem de submissão selecionada. |
|
||||
| `` R `` | Republicar com o editor | |
|
||||
| `` d `` | Descartar | Solte o commit selecionado. Isso irá remover o commit do branch através de uma rebase. Se o commit faz com que as alterações em commits posteriores dependem, você pode precisar resolver conflitos de merge. |
|
||||
|
||||
@@ -170,6 +170,7 @@ _Связки клавиш_
|
||||
| `` b `` | Просмотреть параметры бинарного поиска | |
|
||||
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Перефразировать коммит | Reword the selected commit's message. |
|
||||
| `` R `` | Переписать коммит с помощью редактора | |
|
||||
| `` d `` | Удалить коммит | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -126,6 +126,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` b `` | 查看二分查找选项 | |
|
||||
| `` s `` | 压缩(Squash) | 将已选提交压缩到该提交之下。这些选定的提交的消息会附加到该提交的消息之下。 |
|
||||
| `` f `` | 修正 (fixup) | 将选定的提交合并到其下面的提交中。与压缩类似,但所选提交的消息将被丢弃。 |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 改写提交 | 重写所选提交的消息。 |
|
||||
| `` R `` | 使用编辑器重命名提交 | |
|
||||
| `` d `` | 删除提交 | 删除选中的提交。这将通过变基从分支中删除该提交,如果该提交修改的内容依赖于后续的提交,则需要解决合并冲突。 |
|
||||
@@ -234,7 +235,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` F `` | 强制检出 | 强制检出所选分支。这将在检出所选分支之前放弃工作目录中的所有本地更改。 |
|
||||
| `` d `` | 删除 | 查看本地/远程分支的删除选项 |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` f `` | 从上游快进此分支 | 将当前分支直接移动到远程追踪分支的最新提交 |
|
||||
| `` T `` | 创建标签 | |
|
||||
| `` s `` | 排序 | |
|
||||
@@ -401,7 +402,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` <c-o> `` | 复制分支名称到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` d `` | 删除 | 从远程删除远程分支。 |
|
||||
| `` u `` | 设置为上游 | 设置为检出分支的上游 |
|
||||
|
||||
@@ -184,6 +184,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||
| `` b `` | 查看二分選項 | |
|
||||
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 改寫提交 | 改寫選中的提交訊息 |
|
||||
| `` R `` | 使用編輯器改寫提交 | |
|
||||
| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -713,6 +713,7 @@ keybinding:
|
||||
renameCommitWithEditor: R
|
||||
viewResetOptions: g
|
||||
markCommitAsFixup: f
|
||||
setFixupMessage: c
|
||||
createFixupCommit: F
|
||||
squashAboveCommits: S
|
||||
moveDownCommit: <c-j>
|
||||
|
||||
@@ -89,6 +89,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Reword | Reword the selected commit's message. |
|
||||
| `` R `` | Reword with editor | |
|
||||
| `` d `` | Drop | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -69,6 +69,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` b `` | bisectオプションを表示 | |
|
||||
| `` s `` | スカッシュ | 選択したコミットをその下のコミットにスカッシュします。スカッシュとは複数のコミットを1つにまとめる操作です。選択したコミットのメッセージが下のコミットに追加されます。 |
|
||||
| `` f `` | フィックスアップ | 選択したコミットをその下のコミットにマージします。フィックスアップはスカッシュと似ていますが、選択したコミットのメッセージは破棄され、下のコミットのメッセージのみが保持されます。 |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | メッセージ変更 | 選択したコミットのメッセージを変更します。 |
|
||||
| `` R `` | エディタでメッセージ変更 | |
|
||||
| `` d `` | 削除 | 選択したコミットを削除します。これはリベースを通じてブランチからコミットを削除します。コミットが後続のコミットが依存する変更を行っている場合、マージコンフリクトを解決する必要があるかもしれません。 |
|
||||
@@ -114,7 +115,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` <space> `` | パッチに含めるファイルを切り替え | ファイルがカスタムパッチに含まれるかどうかを切り替えます。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` a `` | すべてのファイルを切り替え | コミットのすべてのファイルをカスタムパッチに追加/削除します。https://github.com/jesseduffield/lazygit#rebase-magic-custom-patchesを参照してください。 |
|
||||
| `` <enter> `` | ファイルに入る / ディレクトリの折りたたみを切り替える | ファイルが選択されている場合、そのファイルに入ってカスタムパッチに個々の行を追加/削除できます。ディレクトリが選択されている場合、ディレクトリを切り替えます。 |
|
||||
| `` ` `` | ファイルツリービューを切り替え | 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'. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` - `` | すべてのファイルを折りたたむ | ファイルツリー内のすべてのディレクトリを折りたたみます |
|
||||
| `` = `` | すべてのファイルを展開 | ファイルツリー内のすべてのディレクトリを展開します |
|
||||
| `` 0 `` | メインビューにフォーカス | |
|
||||
@@ -234,7 +235,7 @@ _凡例:`<c-b>` はctrl+b、`<a-b>` はalt+b、`B` はshift+bを意味
|
||||
| `` d `` | 破棄 | 選択したファイルの変更を破棄するオプションを表示します。 |
|
||||
| `` g `` | アップストリームへのリセットオプションを表示 | |
|
||||
| `` D `` | リセット | 作業ツリーのリセットオプション(例:作業ツリーの完全破棄)を表示します。 |
|
||||
| `` ` `` | ファイルツリービューを切り替え | 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'. |
|
||||
| `` ` `` | ファイルツリービューを切り替え | ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。<br><br>デフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。 |
|
||||
| `` <c-t> `` | 外部差分ツールを開く(git difftool) | |
|
||||
| `` M `` | View merge conflict options | View options for resolving merge conflicts. |
|
||||
| `` f `` | フェッチ | リモートから変更をフェッチします。 |
|
||||
|
||||
@@ -298,6 +298,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | Bisect 옵션 보기 | |
|
||||
| `` s `` | 스쿼시 | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 커밋메시지 변경 | Reword the selected commit's message. |
|
||||
| `` R `` | 에디터에서 커밋메시지 수정 | |
|
||||
| `` d `` | 커밋 삭제 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -160,6 +160,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Fixup | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Hernoem commit | Reword the selected commit's message. |
|
||||
| `` R `` | Hernoem commit met editor | |
|
||||
| `` d `` | Verwijder commit | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -62,6 +62,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
|
||||
| `` b `` | Zobacz opcje bisect | |
|
||||
| `` s `` | Scal | Scal wybrany commit z commitami poniżej. Wiadomość wybranego commita zostanie dołączona do commita poniżej. |
|
||||
| `` f `` | Poprawka | Włącz wybrany commit do commita poniżej. Podobnie do fixup, ale wiadomość wybranego commita zostanie odrzucona. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Przeformułuj | Przeformułuj wiadomość wybranego commita. |
|
||||
| `` R `` | Przeformułuj za pomocą edytora | |
|
||||
| `` d `` | Usuń | Usuń wybrany commit. To usunie commit z gałęzi za pomocą rebazowania. Jeśli commit wprowadza zmiany, od których zależą późniejsze commity, być może będziesz musiał rozwiązać konflikty scalania. |
|
||||
|
||||
@@ -164,6 +164,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
|
||||
| `` b `` | View bisect options | |
|
||||
| `` s `` | Squash | Squash o commit selecionado no commit abaixo dele. A mensagem do commit selecionado será anexada ao commit abaixo dele. |
|
||||
| `` f `` | Fixup | Faça o commit selecionado no commit abaixo dele. Semelhante para o squash, mas a mensagem do commit selecionado será descartada. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Reword | Repetir a mensagem de submissão selecionada. |
|
||||
| `` R `` | Republicar com o editor | |
|
||||
| `` d `` | Descartar | Solte o commit selecionado. Isso irá remover o commit do branch através de uma rebase. Se o commit faz com que as alterações em commits posteriores dependem, você pode precisar resolver conflitos de merge. |
|
||||
|
||||
@@ -170,6 +170,7 @@ _Связки клавиш_
|
||||
| `` b `` | Просмотреть параметры бинарного поиска | |
|
||||
| `` s `` | Объединить коммиты (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | Перефразировать коммит | Reword the selected commit's message. |
|
||||
| `` R `` | Переписать коммит с помощью редактора | |
|
||||
| `` d `` | Удалить коммит | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -126,6 +126,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` b `` | 查看二分查找选项 | |
|
||||
| `` s `` | 压缩(Squash) | 将已选提交压缩到该提交之下。这些选定的提交的消息会附加到该提交的消息之下。 |
|
||||
| `` f `` | 修正 (fixup) | 将选定的提交合并到其下面的提交中。与压缩类似,但所选提交的消息将被丢弃。 |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 改写提交 | 重写所选提交的消息。 |
|
||||
| `` R `` | 使用编辑器重命名提交 | |
|
||||
| `` d `` | 删除提交 | 删除选中的提交。这将通过变基从分支中删除该提交,如果该提交修改的内容依赖于后续的提交,则需要解决合并冲突。 |
|
||||
@@ -234,7 +235,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` F `` | 强制检出 | 强制检出所选分支。这将在检出所选分支之前放弃工作目录中的所有本地更改。 |
|
||||
| `` d `` | 删除 | 查看本地/远程分支的删除选项 |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` f `` | 从上游快进此分支 | 将当前分支直接移动到远程追踪分支的最新提交 |
|
||||
| `` T `` | 创建标签 | |
|
||||
| `` s `` | 排序 | |
|
||||
@@ -401,7 +402,7 @@ _图例:`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
|
||||
| `` <c-o> `` | 复制分支名称到剪贴板 | |
|
||||
| `` <space> `` | 检出 | 基于当前选中的远程分支检出一个新的本地分支,或者将远程分支作分离的HEAD。 |
|
||||
| `` n `` | 新分支 | |
|
||||
| `` M `` | 合并到当前检出的分支 | Merge selected branch into currently checked out branch. |
|
||||
| `` M `` | 合并到当前检出的分支 | 查看将选中项合并到当前分支的选项(正常合并,压缩合并) |
|
||||
| `` r `` | 变基 | 将检出的分支变基到所选的分支上。 |
|
||||
| `` d `` | 删除 | 从远程删除远程分支。 |
|
||||
| `` u `` | 设置为上游 | 设置为检出分支的上游 |
|
||||
|
||||
@@ -184,6 +184,7 @@ _說明:`<c-b>` 表示 Ctrl+B、`<a-b>` 表示 Alt+B,`B`表示 Shift+B
|
||||
| `` b `` | 查看二分選項 | |
|
||||
| `` s `` | 壓縮 (Squash) | Squash the selected commit into the commit below it. The selected commit's message will be appended to the commit below it. |
|
||||
| `` f `` | 修復 (Fixup) | Meld the selected commit into the commit below it. Similar to squash, but the selected commit's message will be discarded. |
|
||||
| `` c `` | Set fixup message | Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message. |
|
||||
| `` r `` | 改寫提交 | 改寫選中的提交訊息 |
|
||||
| `` R `` | 使用編輯器改寫提交 | |
|
||||
| `` d `` | 刪除提交 | Drop the selected commit. This will remove the commit from the branch via a rebase. If the commit makes changes that later commits depend on, you may need to resolve merge conflicts. |
|
||||
|
||||
@@ -12,13 +12,13 @@ require (
|
||||
github.com/aybabtme/humanlog v0.4.1
|
||||
github.com/cloudfoundry/jibber_jabber v0.0.0-20151120183258-bcc4c8345a21
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/gdamore/tcell/v2 v2.13.5
|
||||
github.com/gdamore/tcell/v2 v2.13.8
|
||||
github.com/go-errors/errors v1.5.1
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/integrii/flaggy v1.4.0
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
|
||||
github.com/karimkhaleel/jsonschema v0.0.0-20231001195015-d933f0d94ea3
|
||||
@@ -39,7 +39,7 @@ require (
|
||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
||||
golang.org/x/sync v0.19.0
|
||||
golang.org/x/sys v0.39.0
|
||||
golang.org/x/sys v0.40.0
|
||||
gopkg.in/ozeidan/fuzzy-patricia.v3 v3.0.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
@@ -78,8 +78,8 @@ require (
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
golang.org/x/term v0.39.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
gopkg.in/fsnotify.v1 v1.4.7 // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
|
||||
@@ -95,8 +95,8 @@ github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||
github.com/gdamore/tcell/v2 v2.13.5 h1:YvWYCSr6gr2Ovs84dXbZLjDuOfQchhj8buOEqY52rpA=
|
||||
github.com/gdamore/tcell/v2 v2.13.5/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/gdamore/tcell/v2 v2.13.8 h1:Mys/Kl5wfC/GcC5Cx4C2BIQH9dbnhnkPgS9/wF3RlfU=
|
||||
github.com/gdamore/tcell/v2 v2.13.8/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
|
||||
@@ -189,8 +189,8 @@ github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c h1:tC2Paiis
|
||||
github.com/jesseduffield/generics v0.0.0-20250517122708-b0b4a53a6f5c/go.mod h1:F2fEBk0ddf6ixrBrJjY7phfQ3hL9rXG0uSjvwYe50bE=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd h1:ViKj6qth8FgcIWizn9KiACWwPemWSymx62OPN0tHT+Q=
|
||||
github.com/jesseduffield/go-git/v5 v5.14.1-0.20250407170251-e1a013310ccd/go.mod h1:lRhCiBr6XjQrvcQVa+UYsy/99d3wMXn/a0nSQlhnhlA=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985 h1:qdjGSiNnlGtoi+nzyERQJvee50JpJjeQ6sEhP7jCfMo=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18 h1:+Q17GqvNaGzuvIR1JCdIS0khVjMzdwrhXBBDxnsdN8Y=
|
||||
github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18/go.mod h1:lQCd2TvvNXVKFBowy4A7xxZbUp+1KEiGs4j0Q5Zt9gQ=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=
|
||||
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5/go.mod h1:qxN4mHOAyeIDLP7IK7defgPClM/z1Kze8VVQiaEjzsQ=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
@@ -458,13 +458,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
|
||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -475,8 +475,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -199,6 +199,7 @@ func (self *ChangeTodoActionsInstruction) run(common *common.Common) error {
|
||||
return utils.TodoChange{
|
||||
Hash: c.Hash,
|
||||
NewAction: c.NewAction,
|
||||
Flag: c.Flag,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
type ChangeTodoAction struct {
|
||||
Hash string
|
||||
NewAction todo.TodoCommand
|
||||
Flag string
|
||||
}
|
||||
|
||||
func handleInteractiveRebase(common *common.Common, f func(path string) error) error {
|
||||
|
||||
@@ -154,7 +154,7 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
|
||||
candidates := []string{}
|
||||
|
||||
cmdArgs := NewGitCmd("rev-list").Arg(newHash).ToArgv()
|
||||
err := self.cmd.New(cmdArgs).RunAndProcessLines(func(line string) (bool, error) {
|
||||
err := self.cmd.New(cmdArgs).DontLog().RunAndProcessLines(func(line string) (bool, error) {
|
||||
hash := strings.TrimSpace(line)
|
||||
|
||||
if status, ok := info.statusMap[hash]; ok {
|
||||
|
||||
@@ -325,6 +325,7 @@ func (self *CommitLoader) getHydratedTodoCommits(hashPool *utils.StringPool, tod
|
||||
hydratedCommits = append(hydratedCommits, rebasingCommit)
|
||||
} else if commit := findFullCommit(rebasingCommit.Hash()); commit != nil {
|
||||
commit.Action = rebasingCommit.Action
|
||||
commit.ActionFlag = rebasingCommit.ActionFlag
|
||||
commit.Status = rebasingCommit.Status
|
||||
hydratedCommits = append(hydratedCommits, commit)
|
||||
}
|
||||
@@ -371,10 +372,11 @@ func (self *CommitLoader) getRebasingCommits(hashPool *utils.StringPool, addConf
|
||||
continue
|
||||
}
|
||||
commits = utils.Prepend(commits, models.NewCommit(hashPool, models.NewCommitOpts{
|
||||
Hash: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
Hash: t.Commit,
|
||||
Name: t.Msg,
|
||||
Status: models.StatusRebasing,
|
||||
Action: t.Command,
|
||||
ActionFlag: t.Flag,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ func (self *RebaseCommands) MoveCommitsUp(commits []*models.Commit, startIdx int
|
||||
}).Run()
|
||||
}
|
||||
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx int, endIdx int, action todo.TodoCommand) error {
|
||||
func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx int, endIdx int, action todo.TodoCommand, flag string) error {
|
||||
baseIndex := endIdx + 1
|
||||
if action == todo.Squash || action == todo.Fixup {
|
||||
baseIndex++
|
||||
@@ -151,6 +151,7 @@ func (self *RebaseCommands) InteractiveRebase(commits []*models.Commit, startIdx
|
||||
return daemon.ChangeTodoAction{
|
||||
Hash: commit.Hash(),
|
||||
NewAction: action,
|
||||
Flag: flag,
|
||||
}, !commit.IsMerge()
|
||||
})
|
||||
|
||||
@@ -334,11 +335,12 @@ func todoFromCommit(commit *models.Commit) utils.Todo {
|
||||
}
|
||||
|
||||
// Sets the action for the given commits in the git-rebase-todo file
|
||||
func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo.TodoCommand) error {
|
||||
func (self *RebaseCommands) EditRebaseTodo(commits []*models.Commit, action todo.TodoCommand, flag string) error {
|
||||
commitsWithAction := lo.Map(commits, func(commit *models.Commit, _ int) utils.TodoChange {
|
||||
return utils.TodoChange{
|
||||
Hash: commit.Hash(),
|
||||
NewAction: action,
|
||||
Flag: flag,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -85,6 +85,6 @@ func (self *RemoteCommands) GetRemoteURL(remoteName string) (string, error) {
|
||||
Arg("--get-url", remoteName).
|
||||
ToArgv()
|
||||
|
||||
url, err := self.cmd.New(cmdArgs).RunWithOutput()
|
||||
url, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(url), err
|
||||
}
|
||||
|
||||
@@ -233,10 +233,10 @@ func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) e
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
}
|
||||
|
||||
// Escapes special characters in a filename for gitignore and exclude files
|
||||
// Escapes special characters in a filename for gitignore and exclude files, and prepends `/`
|
||||
func escapeFilename(filename string) string {
|
||||
re := regexp.MustCompile(`^[!#]|[\[\]*]`)
|
||||
return re.ReplaceAllString(filename, `\${0}`)
|
||||
return "/" + re.ReplaceAllString(filename, `\${0}`)
|
||||
}
|
||||
|
||||
// Ignore adds a file to the gitignore for the repo
|
||||
|
||||
@@ -60,6 +60,7 @@ type Commit struct {
|
||||
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
ActionFlag string // e.g. "-C" for fixup -C
|
||||
Divergence Divergence // set to DivergenceNone unless we are showing the divergence view
|
||||
}
|
||||
|
||||
@@ -68,6 +69,7 @@ type NewCommitOpts struct {
|
||||
Name string
|
||||
Status CommitStatus
|
||||
Action todo.TodoCommand
|
||||
ActionFlag string
|
||||
Tags []string
|
||||
ExtraInfo string
|
||||
AuthorName string
|
||||
@@ -83,6 +85,7 @@ func NewCommit(hashPool *utils.StringPool, opts NewCommitOpts) *Commit {
|
||||
Name: opts.Name,
|
||||
Status: opts.Status,
|
||||
Action: opts.Action,
|
||||
ActionFlag: opts.ActionFlag,
|
||||
Tags: opts.Tags,
|
||||
ExtraInfo: opts.ExtraInfo,
|
||||
AuthorName: opts.AuthorName,
|
||||
|
||||
@@ -60,6 +60,10 @@ func getPreset(shell string, osConfig *OSConfig, guessDefaultEditor func() strin
|
||||
nvimRemoteEditTemplate = `begin; if test -z "$NVIM"; nvim -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; end; end`
|
||||
nvimRemoteEditAtLineTemplate = `begin; if test -z "$NVIM"; nvim +{{line}} -- {{filename}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{filename}}; nvim --server "$NVIM" --remote-send ":{{line}}<CR>"; end; end`
|
||||
nvimRemoteOpenDirInEditorTemplate = `begin; if test -z "$NVIM"; nvim -- {{dir}}; else; nvim --server "$NVIM" --remote-send "q"; nvim --server "$NVIM" --remote-tab {{dir}}; end; end`
|
||||
} else if strings.HasSuffix(shell, "nu") || strings.HasSuffix(shell, "nushell") || os.Getenv("NU_VERSION") != "" {
|
||||
nvimRemoteEditTemplate = `if ($env | get -i NVIM | is-empty) { nvim -- {{filename}} } else { nvim --server $env.NVIM --remote-send "q"; nvim --server $env.NVIM --remote-tab {{filename}} }`
|
||||
nvimRemoteEditAtLineTemplate = `if ($env | get -i NVIM | is-empty) { nvim +{{line}} -- {{filename}} } else { nvim --server $env.NVIM --remote-send "q"; nvim --server $env.NVIM --remote-tab {{filename}}; nvim --server $env.NVIM --remote-send ":{{line}}<CR>" }`
|
||||
nvimRemoteOpenDirInEditorTemplate = `if ($env | get -i NVIM | is-empty) { nvim -- {{dir}} } else { nvim --server $env.NVIM --remote-send "q"; nvim --server $env.NVIM --remote-tab {{dir}} }`
|
||||
} else {
|
||||
nvimRemoteEditTemplate = `[ -z "$NVIM" ] && (nvim -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}})`
|
||||
nvimRemoteEditAtLineTemplate = `[ -z "$NVIM" ] && (nvim +{{line}} -- {{filename}}) || (nvim --server "$NVIM" --remote-send "q" && nvim --server "$NVIM" --remote-tab {{filename}} && nvim --server "$NVIM" --remote-send ":{{line}}<CR>")`
|
||||
|
||||
@@ -551,6 +551,7 @@ type KeybindingCommitsConfig struct {
|
||||
RenameCommitWithEditor string `yaml:"renameCommitWithEditor"`
|
||||
ViewResetOptions string `yaml:"viewResetOptions"`
|
||||
MarkCommitAsFixup string `yaml:"markCommitAsFixup"`
|
||||
SetFixupMessage string `yaml:"setFixupMessage"`
|
||||
CreateFixupCommit string `yaml:"createFixupCommit"`
|
||||
SquashAboveCommits string `yaml:"squashAboveCommits"`
|
||||
MoveDownCommit string `yaml:"moveDownCommit"`
|
||||
@@ -1008,6 +1009,7 @@ func GetDefaultConfig() *UserConfig {
|
||||
RenameCommitWithEditor: "R",
|
||||
ViewResetOptions: "g",
|
||||
MarkCommitAsFixup: "f",
|
||||
SetFixupMessage: "c",
|
||||
CreateFixupCommit: "F",
|
||||
SquashAboveCommits: "S",
|
||||
MoveDownCommit: "<c-j>",
|
||||
|
||||
@@ -68,7 +68,8 @@ func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -24,6 +24,10 @@ type ListContextTrait struct {
|
||||
// If renderOnlyVisibleLines is true, needRerenderVisibleLines indicates whether we need to
|
||||
// rerender the visible lines e.g. because the scroll position changed
|
||||
needRerenderVisibleLines bool
|
||||
|
||||
// true if we're inside the OnSearchSelect call; in that case we don't want to update the search
|
||||
// result index.
|
||||
inOnSearchSelect bool
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IsListContext() {}
|
||||
@@ -31,6 +35,10 @@ func (self *ListContextTrait) IsListContext() {}
|
||||
func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
||||
self.Context.FocusLine(scrollIntoView)
|
||||
|
||||
// Need to capture this in a local variable because by the time the AfterLayout function runs,
|
||||
// the field will have been reset to false already
|
||||
inOnSearchSelect := self.inOnSearchSelect
|
||||
|
||||
// Doing this at the end of the layout function because we need the view to be
|
||||
// resized before we focus the line, otherwise if we're in accordion mode
|
||||
// the view could be squashed and won't know how to adjust the cursor/origin.
|
||||
@@ -40,6 +48,9 @@ func (self *ListContextTrait) FocusLine(scrollIntoView bool) {
|
||||
|
||||
self.GetViewTrait().FocusPoint(
|
||||
self.ModelIndexToViewIndex(self.list.GetSelectedLineIdx()), scrollIntoView)
|
||||
if !inOnSearchSelect {
|
||||
self.GetView().SetNearestSearchPosition()
|
||||
}
|
||||
|
||||
selectRangeIndex, isSelectingRange := self.list.GetRangeStartIdx()
|
||||
if isSelectingRange {
|
||||
@@ -117,10 +128,11 @@ func (self *ListContextTrait) HandleRender() {
|
||||
self.setFooter()
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) error {
|
||||
func (self *ListContextTrait) OnSearchSelect(selectedLineIdx int) {
|
||||
self.GetList().SetSelection(self.ViewIndexToModelIndex(selectedLineIdx))
|
||||
self.inOnSearchSelect = true
|
||||
self.HandleFocus(types.OnFocusOpts{})
|
||||
return nil
|
||||
self.inOnSearchSelect = false
|
||||
}
|
||||
|
||||
func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
|
||||
|
||||
@@ -134,7 +134,8 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ func NewMainContext(
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(func(int) error { return nil }))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(func(int) {})
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ type PatchExplorerContext struct {
|
||||
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 (
|
||||
@@ -49,14 +53,14 @@ func NewPatchExplorerContext(
|
||||
SearchTrait: NewSearchTrait(c),
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(
|
||||
func(selectedLineIdx int) error {
|
||||
ctx.GetMutex().Lock()
|
||||
defer ctx.GetMutex().Unlock()
|
||||
ctx.NavigateTo(selectedLineIdx)
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(func(selectedLineIdx int) {
|
||||
ctx.GetMutex().Lock()
|
||||
defer ctx.GetMutex().Unlock()
|
||||
ctx.inOnSelectItemCallback = true
|
||||
ctx.NavigateTo(selectedLineIdx)
|
||||
ctx.inOnSelectItemCallback = false
|
||||
})
|
||||
|
||||
ctx.SetHandleRenderFunc(ctx.OnViewWidthChanged)
|
||||
|
||||
@@ -113,6 +117,10 @@ func (self *PatchExplorerContext) FocusSelection() {
|
||||
// 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 {
|
||||
|
||||
@@ -36,20 +36,6 @@ func (self *SearchTrait) ClearSearchString() {
|
||||
// used for type switch
|
||||
func (self *SearchTrait) IsSearchableContext() {}
|
||||
|
||||
func (self *SearchTrait) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
|
||||
return func(selectedLineIdx int, index int, total int) error {
|
||||
self.RenderSearchStatus(index, total)
|
||||
|
||||
if total != 0 {
|
||||
if err := innerFunc(selectedLineIdx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (self *SearchTrait) RenderSearchStatus(index int, total int) {
|
||||
keybindingConfig := self.c.UserConfig().Keybinding
|
||||
|
||||
|
||||
@@ -134,7 +134,8 @@ func NewSubCommitsContext(
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -56,7 +56,8 @@ func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext {
|
||||
},
|
||||
}
|
||||
|
||||
ctx.GetView().SetOnSelectItem(ctx.SearchTrait.onSelectItemWrapper(ctx.OnSearchSelect))
|
||||
ctx.GetView().SetRenderSearchStatus(ctx.SearchTrait.RenderSearchStatus)
|
||||
ctx.GetView().SetOnSelectItem(ctx.OnSearchSelect)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -130,14 +129,6 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
GetDisabledReason: self.require(self.canSelectCommitsOfCurrentBranch),
|
||||
Description: self.c.Tr.SelectCommitsOfCurrentBranch,
|
||||
},
|
||||
// Putting this at the bottom of the list so that it has the lowest priority,
|
||||
// meaning that if the user has configured another keybinding to the same key
|
||||
// then that will take precedence.
|
||||
{
|
||||
// Hardcoding this key because it's not configurable
|
||||
Key: opts.GetKey("c"),
|
||||
Handler: self.handleOldCherryPickKey,
|
||||
},
|
||||
}
|
||||
|
||||
return bindings
|
||||
@@ -382,16 +373,6 @@ func (self *BasicCommitsController) canCopyCommits(selectedCommits []*models.Com
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) handleOldCherryPickKey() error {
|
||||
msg := utils.ResolvePlaceholderString(self.c.Tr.OldCherryPickKeyWarning,
|
||||
map[string]string{
|
||||
"copy": keybindings.Label(self.c.UserConfig().Keybinding.Commits.CherryPickCopy),
|
||||
"paste": keybindings.Label(self.c.UserConfig().Keybinding.Commits.PasteCommits),
|
||||
})
|
||||
|
||||
return errors.New(msg)
|
||||
}
|
||||
|
||||
func (self *BasicCommitsController) openDiffTool(commit *models.Commit) error {
|
||||
to := commit.RefName()
|
||||
from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(commit.ParentRefName())
|
||||
|
||||
@@ -125,10 +125,10 @@ func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panel
|
||||
height/2 + panelHeight/2
|
||||
}
|
||||
|
||||
func (self *ConfirmationHelper) getPopupPanelWidth() int {
|
||||
func (self *ConfirmationHelper) getPopupPanelWidth(maxWidth int) int {
|
||||
width, _ := self.c.GocuiGui().Size()
|
||||
// we want a minimum width up to a point, then we do it based on ratio.
|
||||
panelWidth := 4 * width / 7
|
||||
// we want a minimum width up to a point, then we do it based on ratio, but only up to the given max width
|
||||
panelWidth := min(4*width/7, maxWidth)
|
||||
minWidth := 80
|
||||
if panelWidth < minWidth {
|
||||
panelWidth = min(width-2, minWidth)
|
||||
@@ -328,7 +328,7 @@ func (self *ConfirmationHelper) resizeMenu(parentPopupContext types.Context) {
|
||||
// resize the window
|
||||
itemCount := self.c.Contexts().Menu.UnfilteredLen()
|
||||
offset := 3
|
||||
panelWidth := self.getPopupPanelWidth()
|
||||
panelWidth := self.getPopupPanelWidth(90)
|
||||
contentWidth := panelWidth - 2 // minus 2 for the frame
|
||||
promptLinesCount := self.layoutMenuPrompt(contentWidth)
|
||||
x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset+promptLinesCount, parentPopupContext)
|
||||
@@ -375,7 +375,7 @@ func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int {
|
||||
}
|
||||
|
||||
func (self *ConfirmationHelper) resizeConfirmationPanel(parentPopupContext types.Context) {
|
||||
panelWidth := self.getPopupPanelWidth()
|
||||
panelWidth := self.getPopupPanelWidth(80)
|
||||
contentWidth := panelWidth - 2 // minus 2 for the frame
|
||||
confirmationView := self.c.Views().Confirmation
|
||||
prompt := confirmationView.Buffer()
|
||||
@@ -389,7 +389,7 @@ func (self *ConfirmationHelper) resizePromptPanel(parentPopupContext types.Conte
|
||||
if self.c.Views().Suggestions.Visible {
|
||||
suggestionsViewHeight = 11
|
||||
}
|
||||
panelWidth := self.getPopupPanelWidth()
|
||||
panelWidth := self.getPopupPanelWidth(80)
|
||||
contentWidth := panelWidth - 2 // minus 2 for the frame
|
||||
promptView := self.c.Views().Prompt
|
||||
prompt := promptView.TextArea.GetContent()
|
||||
@@ -403,7 +403,13 @@ func (self *ConfirmationHelper) resizePromptPanel(parentPopupContext types.Conte
|
||||
}
|
||||
|
||||
func (self *ConfirmationHelper) ResizeCommitMessagePanels(parentPopupContext types.Context) {
|
||||
panelWidth := self.getPopupPanelWidth()
|
||||
maxWidth := 100
|
||||
if self.c.UserConfig().Git.Commit.AutoWrapCommitMessage {
|
||||
// Adding some extra space to make it very obvious that we're wrapping the commit message
|
||||
// for real, not just soft-wrapping for display at the window width.
|
||||
maxWidth = self.c.UserConfig().Git.Commit.AutoWrapWidth + 25
|
||||
}
|
||||
panelWidth := self.getPopupPanelWidth(maxWidth)
|
||||
content := self.c.Views().CommitDescription.TextArea.GetContent()
|
||||
summaryViewHeight := 3
|
||||
panelHeight := getMessageHeight(false, true, content, panelWidth, self.c.Views().CommitDescription.TabWidth)
|
||||
|
||||
@@ -110,20 +110,24 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
return errors.New(self.c.Tr.BaseCommitIsAlreadyOnMainBranch)
|
||||
}
|
||||
|
||||
if len(hashGroups[NOT_MERGED]) > 1 {
|
||||
// If there are multiple commits that could be the base commit, list
|
||||
foundCommits := getCommitsForHashes(commits, hashGroups[NOT_MERGED])
|
||||
// If there are multiple commits that could be the base commit, remove all
|
||||
// those that are fixups for the last one.
|
||||
foundCommits = removeFixupCommits(foundCommits)
|
||||
|
||||
if len(foundCommits) > 1 {
|
||||
// If there are still multiple commits that could be the base commit, list
|
||||
// them in the error message. But only the candidates from the current
|
||||
// branch, not including any that are already merged.
|
||||
subjects := self.getHashesAndSubjects(commits, hashGroups[NOT_MERGED])
|
||||
subjects := getHashesAndSubjects(foundCommits)
|
||||
message := lo.Ternary(hasStagedChanges,
|
||||
self.c.Tr.MultipleBaseCommitsFoundStaged,
|
||||
self.c.Tr.MultipleBaseCommitsFoundUnstaged)
|
||||
return fmt.Errorf("%s\n\n%s", message, subjects)
|
||||
}
|
||||
|
||||
// At this point we know that the NOT_MERGED bucket has exactly one commit,
|
||||
// and that's the one we want to select.
|
||||
_, index, _ := self.findCommit(commits, hashGroups[NOT_MERGED][0])
|
||||
// Now we know that foundCommits has exactly one commit, so find its index
|
||||
_, index, _ := self.findCommit(commits, foundCommits[0].Hash())
|
||||
|
||||
return self.c.ConfirmIf(warnAboutAddedLines, types.ConfirmOpts{
|
||||
Title: self.c.Tr.FindBaseCommitForFixup,
|
||||
@@ -137,29 +141,59 @@ func (self *FixupHelper) HandleFindBaseCommitForFixupPress() error {
|
||||
}
|
||||
|
||||
self.c.Contexts().LocalCommits.SetSelection(index)
|
||||
self.c.Contexts().LocalCommits.FocusLine(true)
|
||||
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *FixupHelper) getHashesAndSubjects(commits []*models.Commit, hashes []string) string {
|
||||
func getCommitsForHashes(commits []*models.Commit, hashes []string) []*models.Commit {
|
||||
// This is called only for the NOT_MERGED commits, and we know that all of them are contained in
|
||||
// the commits slice.
|
||||
commitsSet := set.NewFromSlice(hashes)
|
||||
subjects := make([]string, 0, len(hashes))
|
||||
result := make([]*models.Commit, 0, len(hashes))
|
||||
for _, c := range commits {
|
||||
if commitsSet.Includes(c.Hash()) {
|
||||
subjects = append(subjects, fmt.Sprintf("%s %s", c.ShortRefName(), c.Name))
|
||||
result = append(result, c)
|
||||
commitsSet.Remove(c.Hash())
|
||||
if commitsSet.Len() == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func getHashesAndSubjects(commits []*models.Commit) string {
|
||||
subjects := lo.Map(commits, func(c *models.Commit, _ int) string {
|
||||
return fmt.Sprintf("%s %s", c.ShortRefName(), c.Name)
|
||||
})
|
||||
return strings.Join(subjects, "\n")
|
||||
}
|
||||
|
||||
func removeFixupCommits(commits []*models.Commit) []*models.Commit {
|
||||
if len(commits) <= 1 {
|
||||
return commits
|
||||
}
|
||||
|
||||
// If the last found commit is itself a fixup, don't eliminate anything.
|
||||
baseSubject, lastIsFixup := IsFixupCommit(commits[len(commits)-1].Name)
|
||||
if lastIsFixup {
|
||||
return commits
|
||||
}
|
||||
|
||||
// need to go backwards because we are mutating the slice as we go
|
||||
for i := len(commits) - 2; i >= 0; i-- {
|
||||
subject, isFixup := IsFixupCommit(commits[i].Name)
|
||||
if isFixup && subject == baseSubject {
|
||||
commits = utils.Remove(commits, i)
|
||||
}
|
||||
}
|
||||
|
||||
return commits
|
||||
}
|
||||
|
||||
func (self *FixupHelper) getDiff() (string, bool, error) {
|
||||
args := []string{"-U0", "--ignore-submodules=all", "HEAD", "--"}
|
||||
|
||||
@@ -349,3 +383,30 @@ func (self *FixupHelper) findCommit(commits []*models.Commit, hash string) (*mod
|
||||
return commit.Hash() == hash
|
||||
})
|
||||
}
|
||||
|
||||
// Check whether the given subject line is the subject of a fixup commit, and
|
||||
// returns (trimmedSubject, true) if so (where trimmedSubject is the subject
|
||||
// with all fixup prefixes removed), or (subject, false) if not.
|
||||
func IsFixupCommit(subject string) (string, bool) {
|
||||
prefixes := []string{"fixup! ", "squash! ", "amend! "}
|
||||
trimPrefix := func(s string) (string, bool) {
|
||||
for _, prefix := range prefixes {
|
||||
if trimmedSubject, ok := strings.CutPrefix(s, prefix); ok {
|
||||
return trimmedSubject, true
|
||||
}
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
if subject, wasTrimmed := trimPrefix(subject); wasTrimmed {
|
||||
for {
|
||||
// handle repeated prefixes like "fixup! amend! fixup! Subject"
|
||||
if subject, wasTrimmed = trimPrefix(subject); !wasTrimmed {
|
||||
break
|
||||
}
|
||||
}
|
||||
return subject, true
|
||||
}
|
||||
|
||||
return subject, false
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package helpers
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -155,3 +158,184 @@ index 9ce8efb33..0632e41b0 100644
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixupHelper_IsFixupCommit(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
subject string
|
||||
expectedTrimmedSubject string
|
||||
expectedIsFixup bool
|
||||
}{
|
||||
{
|
||||
subject: "Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
{
|
||||
subject: "fixup Bla",
|
||||
expectedTrimmedSubject: "fixup Bla",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
{
|
||||
subject: "fixup! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "fixup! fixup! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "amend! squash! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "fixup!",
|
||||
expectedTrimmedSubject: "fixup!",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.subject, func(t *testing.T) {
|
||||
trimmedSubject, isFixupCommit := IsFixupCommit(s.subject)
|
||||
assert.Equal(t, s.expectedTrimmedSubject, trimmedSubject)
|
||||
assert.Equal(t, s.expectedIsFixup, isFixupCommit)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixupHelper_removeFixupCommits(t *testing.T) {
|
||||
hashPool := &utils.StringPool{}
|
||||
|
||||
type commitDesc struct {
|
||||
Hash string
|
||||
Name string
|
||||
}
|
||||
|
||||
scenarios := []struct {
|
||||
name string
|
||||
commits []commitDesc
|
||||
expectedResult []commitDesc
|
||||
}{
|
||||
{
|
||||
name: "empty list",
|
||||
commits: []commitDesc{},
|
||||
expectedResult: []commitDesc{},
|
||||
},
|
||||
{
|
||||
name: "single commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"abc123", "Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two unrelated commits",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "First feature"},
|
||||
{"def456", "Second feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"abc123", "First feature"},
|
||||
{"def456", "Second feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fixup commit for last commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! Some feature"},
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "amend and squash commits for last commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "squash! Some feature"},
|
||||
{"def456", "amend! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fixup commit for different commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! Other feature"},
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"abc123", "fixup! Other feature"},
|
||||
{"def456", "Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "last commit is a fixup itself",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! Some feature"},
|
||||
{"def456", "fixup! Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"abc123", "fixup! Some feature"},
|
||||
{"def456", "fixup! Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nested fixup commit",
|
||||
commits: []commitDesc{
|
||||
{"abc123", "fixup! fixup! Some feature"},
|
||||
{"def456", "amend! squash! fixup! Some feature"},
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{"ghi789", "Some feature"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "fixup commits mixed with unrelated commits",
|
||||
commits: []commitDesc{
|
||||
{Hash: "abc123", Name: "fixup! Base commit"},
|
||||
{Hash: "def456", Name: "Unrelated commit"},
|
||||
{Hash: "ghi789", Name: "fixup! Base commit"},
|
||||
{Hash: "jkl012", Name: "Base commit"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{Hash: "def456", Name: "Unrelated commit"},
|
||||
{Hash: "jkl012", Name: "Base commit"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only fixup commits for last commit removed, others preserved",
|
||||
commits: []commitDesc{
|
||||
{Hash: "abc123", Name: "fixup! First feature"},
|
||||
{Hash: "def456", Name: "fixup! Second feature"},
|
||||
{Hash: "ghi789", Name: "Second feature"},
|
||||
{Hash: "jkl012", Name: "First feature"},
|
||||
},
|
||||
expectedResult: []commitDesc{
|
||||
{Hash: "def456", Name: "fixup! Second feature"},
|
||||
{Hash: "ghi789", Name: "Second feature"},
|
||||
{Hash: "jkl012", Name: "First feature"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
makeCommitFromDesc := func(desc commitDesc, _ int) *models.Commit {
|
||||
return models.NewCommit(hashPool, models.NewCommitOpts{Hash: desc.Hash, Name: desc.Name})
|
||||
}
|
||||
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.name, func(t *testing.T) {
|
||||
commits := lo.Map(s.commits, makeCommitFromDesc)
|
||||
result := removeFixupCommits(commits)
|
||||
expectedCommits := lo.Map(s.expectedResult, makeCommitFromDesc)
|
||||
assert.Equal(t, expectedCommits, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,20 +108,15 @@ func (self *SearchHelper) Confirm() error {
|
||||
return self.CancelPrompt()
|
||||
}
|
||||
|
||||
var err error
|
||||
switch state.SearchType() {
|
||||
case types.SearchTypeFilter:
|
||||
self.ConfirmFilter()
|
||||
case types.SearchTypeSearch:
|
||||
err = self.ConfirmSearch()
|
||||
self.ConfirmSearch()
|
||||
case types.SearchTypeNone:
|
||||
self.c.Context().Pop()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return self.c.ResetKeybindings()
|
||||
}
|
||||
|
||||
@@ -144,13 +139,13 @@ func (self *SearchHelper) ConfirmFilter() {
|
||||
self.c.Context().Pop()
|
||||
}
|
||||
|
||||
func (self *SearchHelper) ConfirmSearch() error {
|
||||
func (self *SearchHelper) ConfirmSearch() {
|
||||
state := self.searchState()
|
||||
|
||||
context, ok := state.Context.(types.ISearchableContext)
|
||||
if !ok {
|
||||
self.c.Log.Warnf("Context %s is searchable", state.Context.GetKey())
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
searchString := self.promptContent()
|
||||
@@ -161,7 +156,7 @@ func (self *SearchHelper) ConfirmSearch() error {
|
||||
|
||||
self.c.Context().Pop()
|
||||
|
||||
return context.GetView().Search(searchString, modelSearchResults(context))
|
||||
context.GetView().Search(searchString, modelSearchResults(context))
|
||||
}
|
||||
|
||||
func modelSearchResults(context types.ISearchableContext) []gocui.SearchPosition {
|
||||
|
||||
@@ -104,6 +104,7 @@ func (self *ListController) handleLineChangeAux(f func(int), change int) error {
|
||||
// doing this check so that if we're holding the up key at the start of the list
|
||||
// we're not constantly re-rendering the main view.
|
||||
cursorMoved := before != after
|
||||
originYBefore := self.context.GetView().OriginY()
|
||||
if cursorMoved {
|
||||
switch change {
|
||||
case -1:
|
||||
@@ -116,6 +117,15 @@ func (self *ListController) handleLineChangeAux(f func(int), change int) error {
|
||||
}
|
||||
|
||||
if cursorMoved || rangeBefore != rangeAfter {
|
||||
if originYBefore != self.context.GetView().OriginY() {
|
||||
// Since we already scrolled the view above, the normal mechanism that
|
||||
// ListContextTrait.FocusLine uses for deciding whether rerendering is needed won't
|
||||
// work. It is based on checking whether the origin was changed by the call to
|
||||
// FocusPoint in that function, but since we scrolled the view directly above, the
|
||||
// origin has already been updated. So we must tell it explicitly to rerender.
|
||||
self.context.SetNeedRerenderVisibleLines()
|
||||
}
|
||||
|
||||
self.context.HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
|
||||
} else {
|
||||
// If the selection did not change (because, for example, we are at the top of the list and
|
||||
|
||||
@@ -82,6 +82,15 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [
|
||||
Tooltip: self.c.Tr.FixupTooltip,
|
||||
DisplayOnScreen: true,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.SetFixupMessage),
|
||||
Handler: self.withItem(self.setFixupMessage),
|
||||
GetDisabledReason: self.require(
|
||||
self.singleItemSelected(self.canSetFixupMessage),
|
||||
),
|
||||
Description: self.c.Tr.SetFixupMessage,
|
||||
Tooltip: self.c.Tr.SetFixupMessageTooltip,
|
||||
},
|
||||
{
|
||||
Key: opts.GetKey(opts.Config.Commits.RenameCommit),
|
||||
Handler: self.withItem(self.reword),
|
||||
@@ -321,20 +330,71 @@ func (self *LocalCommitsController) fixup(selectedCommits []*models.Commit, star
|
||||
return self.updateTodos(todo.Fixup, selectedCommits)
|
||||
}
|
||||
|
||||
self.c.Confirm(types.ConfirmOpts{
|
||||
Title: self.c.Tr.Fixup,
|
||||
Prompt: self.c.Tr.SureFixupThisCommit,
|
||||
HandleConfirm: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
|
||||
return self.interactiveRebase(todo.Fixup, startIdx, endIdx)
|
||||
})
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.Fixup,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.Fixup,
|
||||
Key: 'f',
|
||||
OnPress: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.FixupCommit)
|
||||
return self.interactiveRebase(todo.Fixup, startIdx, endIdx)
|
||||
})
|
||||
},
|
||||
Tooltip: self.c.Tr.FixupTooltip,
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.FixupKeepMessage,
|
||||
Key: 'c',
|
||||
OnPress: func() error {
|
||||
return self.c.WithWaitingStatus(self.c.Tr.FixingStatus, func(gocui.Task) error {
|
||||
self.c.LogAction(self.c.Tr.Actions.FixupCommitKeepMessage)
|
||||
return self.interactiveRebaseWithFlag(todo.Fixup, startIdx, endIdx, "-C")
|
||||
})
|
||||
},
|
||||
Tooltip: self.c.Tr.FixupKeepMessageTooltip,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) canSetFixupMessage(commit *models.Commit) *types.DisabledReason {
|
||||
if !self.isRebasing() {
|
||||
return &types.DisabledReason{Text: self.c.Tr.NotMidRebase}
|
||||
}
|
||||
|
||||
if commit.Action != todo.Fixup {
|
||||
return &types.DisabledReason{Text: self.c.Tr.MustSelectFixupCommit}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) setFixupMessage(commit *models.Commit) error {
|
||||
return self.c.Menu(types.CreateMenuOptions{
|
||||
Title: self.c.Tr.SetFixupMessage,
|
||||
Items: []*types.MenuItem{
|
||||
{
|
||||
Label: self.c.Tr.FixupDiscardMessage,
|
||||
Key: 'f',
|
||||
OnPress: func() error {
|
||||
return self.updateTodosWithFlag(todo.Fixup, []*models.Commit{commit}, "")
|
||||
},
|
||||
Tooltip: self.c.Tr.FixupDiscardMessageTooltip,
|
||||
},
|
||||
{
|
||||
Label: self.c.Tr.FixupKeepMessage,
|
||||
Key: 'c',
|
||||
OnPress: func() error {
|
||||
return self.updateTodosWithFlag(todo.Fixup, []*models.Commit{commit}, "-C")
|
||||
},
|
||||
Tooltip: self.c.Tr.FixupKeepMessageTooltip,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) reword(commit *models.Commit) error {
|
||||
commitIdx := self.context().GetSelectedLineIdx()
|
||||
if self.c.Git().Config.NeedsGpgSubprocessForCommit() && !self.isHeadCommit(commitIdx) {
|
||||
@@ -505,7 +565,7 @@ func (self *LocalCommitsController) edit(selectedCommits []*models.Commit, start
|
||||
commits := self.c.Model().Commits
|
||||
if !commits[endIdx].IsMerge() {
|
||||
selectionRangeAndMode := self.getSelectionRangeAndMode()
|
||||
err := self.c.Git().Rebase.InteractiveRebase(commits, startIdx, endIdx, todo.Edit)
|
||||
err := self.c.Git().Rebase.InteractiveRebase(commits, startIdx, endIdx, todo.Edit, "")
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
|
||||
err,
|
||||
types.RefreshOptions{
|
||||
@@ -611,13 +671,17 @@ func (self *LocalCommitsController) pick(selectedCommits []*models.Commit) error
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, startIdx int, endIdx int) error {
|
||||
return self.interactiveRebaseWithFlag(action, startIdx, endIdx, "")
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) interactiveRebaseWithFlag(action todo.TodoCommand, startIdx int, endIdx int, flag string) error {
|
||||
// When performing an action that will remove the selected commits, we need to select the
|
||||
// next commit down (which will end up at the start index after the action is performed)
|
||||
if action == todo.Drop || action == todo.Fixup || action == todo.Squash {
|
||||
self.context().SetSelection(startIdx)
|
||||
}
|
||||
|
||||
err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, startIdx, endIdx, action)
|
||||
err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, startIdx, endIdx, action, flag)
|
||||
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
|
||||
}
|
||||
@@ -626,7 +690,11 @@ func (self *LocalCommitsController) interactiveRebase(action todo.TodoCommand, s
|
||||
// commit meaning you are trying to edit the todo file rather than actually
|
||||
// begin a rebase. It then updates the todo file with that action
|
||||
func (self *LocalCommitsController) updateTodos(action todo.TodoCommand, selectedCommits []*models.Commit) error {
|
||||
if err := self.c.Git().Rebase.EditRebaseTodo(selectedCommits, action); err != nil {
|
||||
return self.updateTodosWithFlag(action, selectedCommits, "")
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) updateTodosWithFlag(action todo.TodoCommand, selectedCommits []*models.Commit, flag string) error {
|
||||
if err := self.c.Git().Rebase.EditRebaseTodo(selectedCommits, action, flag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -670,6 +738,7 @@ func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, s
|
||||
return err
|
||||
}
|
||||
self.context().MoveSelection(1)
|
||||
self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
@@ -682,6 +751,7 @@ func (self *LocalCommitsController) moveDown(selectedCommits []*models.Commit, s
|
||||
err := self.c.Git().Rebase.MoveCommitsDown(self.c.Model().Commits, startIdx, endIdx)
|
||||
if err == nil {
|
||||
self.context().MoveSelection(1)
|
||||
self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
|
||||
}
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
|
||||
err, types.RefreshOptions{Mode: types.SYNC})
|
||||
@@ -694,6 +764,7 @@ func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, sta
|
||||
return err
|
||||
}
|
||||
self.context().MoveSelection(-1)
|
||||
self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
|
||||
|
||||
self.c.Refresh(types.RefreshOptions{
|
||||
Mode: types.SYNC, Scope: []types.RefreshableView{types.REBASE_COMMITS},
|
||||
@@ -706,6 +777,7 @@ func (self *LocalCommitsController) moveUp(selectedCommits []*models.Commit, sta
|
||||
err := self.c.Git().Rebase.MoveCommitsUp(self.c.Model().Commits, startIdx, endIdx)
|
||||
if err == nil {
|
||||
self.context().MoveSelection(-1)
|
||||
self.context().HandleFocus(types.OnFocusOpts{ScrollSelectionIntoView: true})
|
||||
}
|
||||
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
|
||||
err, types.RefreshOptions{Mode: types.SYNC})
|
||||
@@ -1096,7 +1168,7 @@ func countSquashableCommitsAbove(commits []*models.Commit, selectedIdx int, reba
|
||||
// For each commit _above_ the selection, ...
|
||||
for i, commit := range commits[0:selectedIdx] {
|
||||
// ... see if it is a fixup commit, and get the base subject it applies to
|
||||
if baseSubject, isFixup := isFixupCommit(commit.Name); isFixup {
|
||||
if baseSubject, isFixup := helpers.IsFixupCommit(commit.Name); isFixup {
|
||||
// Then, for each commit after the fixup, up to and including the
|
||||
// rebase start commit, see if we find the base commit
|
||||
for _, baseCommit := range commits[i+1 : rebaseStartIdx+1] {
|
||||
@@ -1109,33 +1181,6 @@ func countSquashableCommitsAbove(commits []*models.Commit, selectedIdx int, reba
|
||||
return result
|
||||
}
|
||||
|
||||
// Check whether the given subject line is the subject of a fixup commit, and
|
||||
// returns (trimmedSubject, true) if so (where trimmedSubject is the subject
|
||||
// with all fixup prefixes removed), or (subject, false) if not.
|
||||
func isFixupCommit(subject string) (string, bool) {
|
||||
prefixes := []string{"fixup! ", "squash! ", "amend! "}
|
||||
trimPrefix := func(s string) (string, bool) {
|
||||
for _, prefix := range prefixes {
|
||||
if trimmedSubject, ok := strings.CutPrefix(s, prefix); ok {
|
||||
return trimmedSubject, true
|
||||
}
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
if subject, wasTrimmed := trimPrefix(subject); wasTrimmed {
|
||||
for {
|
||||
// handle repeated prefixes like "fixup! amend! fixup! Subject"
|
||||
if subject, wasTrimmed = trimPrefix(subject); !wasTrimmed {
|
||||
break
|
||||
}
|
||||
}
|
||||
return subject, true
|
||||
}
|
||||
|
||||
return subject, false
|
||||
}
|
||||
|
||||
func (self *LocalCommitsController) createTag(commit *models.Commit) error {
|
||||
return self.c.Helpers().Tags.OpenCreateTagPrompt(commit.Hash(), func() {})
|
||||
}
|
||||
|
||||
@@ -93,49 +93,3 @@ func Test_countSquashableCommitsAbove(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_isFixupCommit(t *testing.T) {
|
||||
scenarios := []struct {
|
||||
subject string
|
||||
expectedTrimmedSubject string
|
||||
expectedIsFixup bool
|
||||
}{
|
||||
{
|
||||
subject: "Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
{
|
||||
subject: "fixup Bla",
|
||||
expectedTrimmedSubject: "fixup Bla",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
{
|
||||
subject: "fixup! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "fixup! fixup! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "amend! squash! Bla",
|
||||
expectedTrimmedSubject: "Bla",
|
||||
expectedIsFixup: true,
|
||||
},
|
||||
{
|
||||
subject: "fixup!",
|
||||
expectedTrimmedSubject: "fixup!",
|
||||
expectedIsFixup: false,
|
||||
},
|
||||
}
|
||||
for _, s := range scenarios {
|
||||
t.Run(s.subject, func(t *testing.T) {
|
||||
trimmedSubject, isFixupCommit := isFixupCommit(s.subject)
|
||||
assert.Equal(t, s.expectedTrimmedSubject, trimmedSubject)
|
||||
assert.Equal(t, s.expectedIsFixup, isFixupCommit)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error {
|
||||
gui.State.Contexts.Menu.SetKeybindingsTakePrecedence(!opts.KeepConflictingKeybindings)
|
||||
gui.State.Contexts.Menu.SetSelection(0)
|
||||
|
||||
gui.Views.Menu.SetOriginY(0)
|
||||
|
||||
gui.Views.Menu.Title = opts.Title
|
||||
gui.Views.Menu.FgColor = theme.GocuiDefaultTextColor
|
||||
|
||||
|
||||
@@ -385,7 +385,12 @@ func displayCommit(
|
||||
|
||||
actionString := ""
|
||||
if commit.Action != models.ActionNone {
|
||||
actionString = actionColorMap(commit.Action, commit.Status).Sprint(commit.Action.String())
|
||||
actionStr := commit.Action.String()
|
||||
// Only show the flag for fixup commands (where -C changes the meaning)
|
||||
if commit.ActionFlag != "" && commit.Action == todo.Fixup {
|
||||
actionStr += " " + commit.ActionFlag
|
||||
}
|
||||
actionString = actionColorMap(commit.Action, commit.Status).Sprint(actionStr)
|
||||
}
|
||||
|
||||
tagString := ""
|
||||
|
||||
@@ -102,7 +102,6 @@ func (gui *Gui) createAllViews() error {
|
||||
|
||||
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} {
|
||||
view.Wrap = true
|
||||
view.IgnoreCarriageReturns = true
|
||||
view.UnderlineHyperLinksOnlyOnHover = true
|
||||
view.AutoRenderHyperLinks = true
|
||||
}
|
||||
|
||||
+18
-4
@@ -164,7 +164,12 @@ type TranslationSet struct {
|
||||
CannotSquashOrFixupMergeCommit string
|
||||
Fixup string
|
||||
FixupTooltip string
|
||||
SureFixupThisCommit string
|
||||
FixupKeepMessage string
|
||||
FixupKeepMessageTooltip string
|
||||
SetFixupMessage string
|
||||
SetFixupMessageTooltip string
|
||||
FixupDiscardMessage string
|
||||
FixupDiscardMessageTooltip string
|
||||
SureSquashThisCommit string
|
||||
Squash string
|
||||
PickCommitTooltip string
|
||||
@@ -316,6 +321,8 @@ type TranslationSet struct {
|
||||
ViewRevertOptions string
|
||||
NotMergingOrRebasing string
|
||||
AlreadyRebasing string
|
||||
NotMidRebase string
|
||||
MustSelectFixupCommit string
|
||||
RecentRepos string
|
||||
MergeOptionsTitle string
|
||||
RebaseOptionsTitle string
|
||||
@@ -897,7 +904,6 @@ type TranslationSet struct {
|
||||
SelectedItemIsNotABranch string
|
||||
SelectedItemDoesNotHaveFiles string
|
||||
MultiSelectNotSupportedForSubmodules string
|
||||
OldCherryPickKeyWarning string
|
||||
CommandDoesNotSupportOpeningInEditor string
|
||||
CustomCommands string
|
||||
NoApplicableCommandsInThisContext string
|
||||
@@ -966,6 +972,7 @@ type Actions struct {
|
||||
CheckoutFile string
|
||||
SquashCommitDown string
|
||||
FixupCommit string
|
||||
FixupCommitKeepMessage string
|
||||
RewordCommit string
|
||||
DropCommit string
|
||||
EditCommit string
|
||||
@@ -1260,7 +1267,12 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CannotSquashOrFixupFirstCommit: "There's no commit below to squash into",
|
||||
CannotSquashOrFixupMergeCommit: "Cannot squash or fixup a merge commit",
|
||||
Fixup: "Fixup",
|
||||
SureFixupThisCommit: "Are you sure you want to 'fixup' the selected commit(s) into the commit below?",
|
||||
FixupKeepMessage: "Fixup and use this commit's message",
|
||||
FixupKeepMessageTooltip: "Squash the selected commit into the commit below, using this commit's message, discarding the message of the commit below.",
|
||||
SetFixupMessage: "Set fixup message",
|
||||
SetFixupMessageTooltip: "Set the message option for the fixup commit. The -C option means to use this commit's message instead of the target commit's message.",
|
||||
FixupDiscardMessage: "Fixup and discard this commit's message",
|
||||
FixupDiscardMessageTooltip: "Squash the selected commit into the commit below, discarding this commit's message.",
|
||||
SureSquashThisCommit: "Are you sure you want to squash the selected commit(s) into the commit below?",
|
||||
Squash: "Squash",
|
||||
PickCommitTooltip: "Mark the selected commit to be picked (when mid-rebase). This means that the commit will be retained upon continuing the rebase.",
|
||||
@@ -1412,6 +1424,8 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
ViewRevertOptions: "View revert options",
|
||||
NotMergingOrRebasing: "You are currently neither rebasing nor merging",
|
||||
AlreadyRebasing: "Can't perform this action during a rebase",
|
||||
NotMidRebase: "This action only works during an interactive rebase",
|
||||
MustSelectFixupCommit: "This action only works on fixup commits",
|
||||
RecentRepos: "Recent repositories",
|
||||
MergeOptionsTitle: "Merge options",
|
||||
RebaseOptionsTitle: "Rebase options",
|
||||
@@ -1993,7 +2007,6 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
SelectedItemIsNotABranch: "Selected item is not a branch",
|
||||
SelectedItemDoesNotHaveFiles: "Selected item does not have files to view",
|
||||
MultiSelectNotSupportedForSubmodules: "Multiselection not supported for submodules",
|
||||
OldCherryPickKeyWarning: "The 'c' key is no longer the default key for copying commits to cherry pick. Please use `{{.copy}}` instead (and `{{.paste}}` to paste). The reason for this change is that the 'v' key for selecting a range of lines when staging is now also used for selecting a range of lines in any list view, meaning that we needed to find a new key for pasting commits, and if we're going to now use `{{.paste}}` for pasting commits, we may as well use `{{.copy}}` for copying them. If you want to configure the keybindings to get the old behaviour, set the following in your config:\n\nkeybinding:\n universal:\n toggleRangeSelect: <something other than v>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'",
|
||||
CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor",
|
||||
CustomCommands: "Custom commands",
|
||||
NoApplicableCommandsInThisContext: "(No applicable commands in this context)",
|
||||
@@ -2025,6 +2038,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
CheckoutFile: "Checkout file",
|
||||
SquashCommitDown: "Squash commit down",
|
||||
FixupCommit: "Fixup commit",
|
||||
FixupCommitKeepMessage: "Fixup commit (keep message)",
|
||||
RewordCommit: "Reword commit",
|
||||
DropCommit: "Drop commit",
|
||||
EditCommit: "Edit commit",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"ToggleStagedAll": "すべてステージ",
|
||||
"ToggleStagedAllTooltip": "ワーキングツリー内のすべてのファイルのステージ/アンステージを切り替えます。",
|
||||
"ToggleTreeView": "ファイルツリービューを切り替え",
|
||||
"ToggleTreeViewTooltip": "ファイル表示をフラット表示とツリー表示で切り替えます。フラット表示はすべてのファイルパスを一覧で表示し、ツリー表示はディレクトリごとにファイルをグループ化します。\n\nデフォルトは設定ファイル内の 'gui.showFileTree' キーで変更できます。",
|
||||
"OpenDiffTool": "外部差分ツールを開く(git difftool)",
|
||||
"OpenMergeTool": "外部マージツールを開く",
|
||||
"Refresh": "更新",
|
||||
@@ -144,7 +145,6 @@
|
||||
"CannotSquashOrFixupMergeCommit": "マージコミットをスカッシュまたはフィックスアップすることはできません",
|
||||
"Fixup": "フィックスアップ",
|
||||
"FixupTooltip": "選択したコミットをその下のコミットにマージします。フィックスアップはスカッシュと似ていますが、選択したコミットのメッセージは破棄され、下のコミットのメッセージのみが保持されます。",
|
||||
"SureFixupThisCommit": "選択したコミットを下のコミットに'フィックスアップ'してもよろしいですか?",
|
||||
"SureSquashThisCommit": "選択したコミットを下のコミットにスカッシュしてもよろしいですか?",
|
||||
"Squash": "スカッシュ",
|
||||
"PickCommitTooltip": "選択したコミットをピックするようにマークします(リベース中)。これは、リベースを続行すると、コミットが保持されることを意味します。",
|
||||
@@ -814,7 +814,6 @@
|
||||
"SelectedItemIsNotABranch": "選択された項目はブランチではありません",
|
||||
"SelectedItemDoesNotHaveFiles": "選択された項目には表示するファイルがありません",
|
||||
"MultiSelectNotSupportedForSubmodules": "サブモジュールでは複数選択はサポートされていません",
|
||||
"OldCherryPickKeyWarning": "'c'キーは現在、コミットのコピー(チェリーピック)のデフォルトキーではなくなりました。代わりに`{{.copy}}`キーを使用してコピーし、`{{.paste}}`キーでペーストしてください。\n\nこの変更理由は以下の通りです:\n- 以前は'v'キーがステージング時の行選択にのみ使用されていましたが、現在はすべてのリストビューで範囲選択に使われています\n- そのため、コミットペースト用の新しいキーが必要になり、`{{.paste}}`をペーストに使うなら、`{{.copy}}`をコピーに使う方が直感的です\n\n以前の動作に戻したい場合は、設定ファイルに以下を追加してください:\n\nkeybinding:\n universal:\n toggleRangeSelect: <v以外の何か>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'",
|
||||
"CommandDoesNotSupportOpeningInEditor": "このコマンドはエディタへの切り替えをサポートしていません",
|
||||
"CustomCommands": "カスタムコマンド",
|
||||
"NoApplicableCommandsInThisContext": "(このコンテキストでは適用可能なコマンドはありません)",
|
||||
|
||||
@@ -77,7 +77,6 @@
|
||||
"CloseCancel": "닫기/취소",
|
||||
"Confirm": "확인",
|
||||
"Quit": "종료",
|
||||
"SureFixupThisCommit": "Are you sure you want to 'fixup' this commit? It will be merged into the commit below",
|
||||
"SureSquashThisCommit": "Are you sure you want to squash this commit into the commit below?",
|
||||
"Squash": "스쿼시",
|
||||
"PickCommitTooltip": "Pick commit (when mid-rebase)",
|
||||
|
||||
@@ -49,7 +49,6 @@
|
||||
"Close": "Sluiten",
|
||||
"CloseCancel": "Sluiten",
|
||||
"Confirm": "Bevestig",
|
||||
"SureFixupThisCommit": "Weet je zeker dat je fixup wil uitvoeren op deze commit? De commit hieronder zol worden squashed in deze",
|
||||
"SureSquashThisCommit": "Weet je zeker dat je deze commit wil samenvoegen met de commit hieronder?",
|
||||
"PickCommitTooltip": "Kies commit (wanneer midden in rebase)",
|
||||
"Reword": "Hernoem commit",
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
"CannotSquashOrFixupFirstCommit": "Nie ma commita poniżej do scalenia",
|
||||
"Fixup": "Poprawka",
|
||||
"FixupTooltip": "Włącz wybrany commit do commita poniżej. Podobnie do fixup, ale wiadomość wybranego commita zostanie odrzucona.",
|
||||
"SureFixupThisCommit": "Czy na pewno chcesz 'poprawić' wybrane commit(y) do commita poniżej?",
|
||||
"SureSquashThisCommit": "Czy na pewno chcesz scalić wybrane commit(y) do commita poniżej?",
|
||||
"Squash": "Scal",
|
||||
"PickCommitTooltip": "Oznacz wybrany commit do wybrania (podczas rebazowania). Oznacza to, że commit zostanie zachowany po kontynuacji rebazowania.",
|
||||
@@ -668,7 +667,6 @@
|
||||
"NoItemSelected": "Nie wybrano elementu",
|
||||
"SelectedItemIsNotABranch": "Wybrany element nie jest branch'em",
|
||||
"SelectedItemDoesNotHaveFiles": "Wybrany element nie ma plików do wyświetlenia",
|
||||
"OldCherryPickKeyWarning": "Klawisz 'c' nie jest już domyślnym klawiszem do kopiowania commitów do cherry pick. Proszę użyj `{{.copy}}` zamiast tego (i `{{.paste}}` aby wkleić). Powodem tej zmiany jest to, że klawisz 'v' do wybierania zakresu linii podczas stagingu jest teraz używany również do wybierania zakresu linii w każdym widoku listy, co oznacza, że musieliśmy znaleźć nowy klawisz do wklejania commitów, i jeśli zamierzamy teraz używać `{{.paste}}` do wklejania commitów, możemy równie dobrze użyć `{{.copy}}` do ich kopiowania. Jeśli chcesz skonfigurować klawisze, aby uzyskać stare zachowanie, ustaw następujące w swojej konfiguracji:\n\nkeybinding:\n universal:\n toggleRangeSelect: <coś innego niż v>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'",
|
||||
"Actions": {
|
||||
"CheckoutCommit": "Przełącz commit",
|
||||
"CheckoutTag": "Przełącz tag",
|
||||
|
||||
@@ -137,7 +137,6 @@
|
||||
"CannotSquashOrFixupMergeCommit": "Não é possível squash ou corrigir um commit de merge",
|
||||
"Fixup": "Fixup",
|
||||
"FixupTooltip": "Faça o commit selecionado no commit abaixo dele. Semelhante para o squash, mas a mensagem do commit selecionado será descartada.",
|
||||
"SureFixupThisCommit": "Tem certeza que deseja 'corrigir' o(s) commit(s) selecionado(s) no commit abaixo?",
|
||||
"SureSquashThisCommit": "Tem certeza que deseja esmagar o(s) commit(s) selecionado(s) no commit abaixo?",
|
||||
"Squash": "Squash",
|
||||
"PickCommitTooltip": "Marque o commit selecionado para ser escolhido (quando meados da base). Isso significa que o commit será mantido ao continuar o rebase.",
|
||||
|
||||
@@ -63,7 +63,6 @@
|
||||
"Quit": "Выйти",
|
||||
"CannotSquashOrFixupFirstCommit": "Ниже нет коммита, который можно было бы объединить",
|
||||
"Fixup": "Объединить несколько коммитов в один отбросив сообщение коммита (Fixup) ",
|
||||
"SureFixupThisCommit": "Вы уверены, что хотите объединить несколько коммитов, отбросив сообщение коммита? Он будет объединён с коммитом ниже",
|
||||
"SureSquashThisCommit": "Вы уверены, что хотите объединить несколько коммитов в нижний коммит?",
|
||||
"Squash": "Объединить коммиты (Squash)",
|
||||
"PickCommitTooltip": "Выбрать коммит (в середине перебазирования)",
|
||||
|
||||
@@ -152,7 +152,6 @@
|
||||
"CannotSquashOrFixupMergeCommit": "无法对合并提交进行压缩或修正",
|
||||
"Fixup": "修正 (fixup)",
|
||||
"FixupTooltip": "将选定的提交合并到其下面的提交中。与压缩类似,但所选提交的消息将被丢弃。",
|
||||
"SureFixupThisCommit": "您确定要“修正”此提交吗?它将合并到下面的提交中",
|
||||
"SureSquashThisCommit": "您确定要将这个提交压缩到下面的提交中吗?",
|
||||
"Squash": "压缩(Squash)",
|
||||
"PickCommitTooltip": "标记选中的提交为 picked(变基过程中)。这意味该提交将在后续的变基中保留。",
|
||||
@@ -249,7 +248,7 @@
|
||||
"RefreshFiles": "刷新文件",
|
||||
"FocusMainView": "聚焦主视图",
|
||||
"Merge": "合并到当前检出的分支",
|
||||
"MergeBranchTooltip": "Merge selected branch into currently checked out branch.",
|
||||
"MergeBranchTooltip": "查看将选中项合并到当前分支的选项(正常合并,压缩合并)",
|
||||
"RegularMergeFastForward": "常规合并(快进)",
|
||||
"RegularMergeFastForwardTooltip": "将 '{{.checkedOutBranch}}' 快进到 '{{.selectedBranch}}',不创建合并提交。",
|
||||
"CannotFastForwardMerge": "无法将 '{{.checkedOutBranch}}' 快进到 '{{.selectedBranch}}'",
|
||||
@@ -413,6 +412,7 @@
|
||||
"ViewItemFiles": "查看提交的文件",
|
||||
"CommitFilesTitle": "提交文件",
|
||||
"CheckoutCommitFileTooltip": "检出文件",
|
||||
"CannotCheckoutWithModifiedFilesErr": "您已有对您试图签出的文件作出的本地修改。您需要先保存或丢弃这些文件。",
|
||||
"CanOnlyDiscardFromLocalCommits": "只能从本地提交中丢弃更改",
|
||||
"Remove": "删除",
|
||||
"DiscardOldFileChangeTooltip": "放弃对此文件的提交变更",
|
||||
@@ -482,7 +482,7 @@
|
||||
"StashTooltip": "贮藏所有变更.若要使用其他贮藏变体,请使用查看贮藏选项快捷键",
|
||||
"StashAllChanges": "将所有变更加入贮藏",
|
||||
"StashStagedChanges": "贮藏已暂存变更",
|
||||
"StashAllChangesKeepIndex": "将已暂存的变更加入贮藏",
|
||||
"StashAllChangesKeepIndex": "贮藏所有更改并保留暂存区",
|
||||
"StashUnstagedChanges": "贮藏未暂存变更",
|
||||
"StashIncludeUntrackedChanges": "贮藏所有变更,包括未跟踪的文件",
|
||||
"StashOptions": "贮藏选项",
|
||||
@@ -882,7 +882,6 @@
|
||||
"SelectedItemIsNotABranch": "选中的条目不是一个分支",
|
||||
"SelectedItemDoesNotHaveFiles": "选中的条目中没有",
|
||||
"MultiSelectNotSupportedForSubmodules": "子模块不支持多选操作",
|
||||
"OldCherryPickKeyWarning": "The 'c' key is no longer the default key for copying commits to cherry pick. Please use `{{.copy}}` instead (and `{{.paste}}` to paste). The reason for this change is that the 'v' key for selecting a range of lines when staging is now also used for selecting a range of lines in any list view, meaning that we needed to find a new key for pasting commits, and if we're going to now use `{{.paste}}` for pasting commits, we may as well use `{{.copy}}` for copying them. If you want to configure the keybindings to get the old behaviour, set the following in your config:\n\nkeybinding:\n universal:\n toggleRangeSelect: <something other than v>\n commits:\n cherryPickCopy: 'c'\n pasteCommits: 'v'",
|
||||
"CommandDoesNotSupportOpeningInEditor": "该命令不支持切换到编辑器",
|
||||
"CustomCommands": "自定义命令",
|
||||
"NoApplicableCommandsInThisContext": "(当前上下文无可用命令)",
|
||||
@@ -951,7 +950,7 @@
|
||||
"Pull": "拉取(Pull)",
|
||||
"OpenFile": "打开文件",
|
||||
"StashAllChanges": "贮藏所有变更",
|
||||
"StashAllChangesKeepIndex": "贮藏所有更改并保持索引",
|
||||
"StashAllChangesKeepIndex": "贮藏所有更改并保留暂存区",
|
||||
"StashStagedChanges": "贮藏暂存的变更",
|
||||
"StashUnstagedChanges": "贮藏所有未暂存更改",
|
||||
"StashIncludeUntrackedChanges": "贮藏所有更改包括未跟踪的文件",
|
||||
|
||||
@@ -83,7 +83,6 @@
|
||||
"Quit": "結束",
|
||||
"CannotSquashOrFixupFirstCommit": "沒有可以壓縮的提交",
|
||||
"Fixup": "修復 (Fixup)",
|
||||
"SureFixupThisCommit": "是否對此提交進行 '修復' ? 其將被合併於以下之提交中",
|
||||
"SureSquashThisCommit": "是否要把這個提交壓縮到下面的提交中?",
|
||||
"Squash": "壓縮 (Squash)",
|
||||
"PickCommitTooltip": "挑選提交 (於變基過程中)",
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FindBaseCommitForFixupDisregardFixupsForSameBaseCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Finds the base commit to create a fixup for, disregarding fixup commits for the same base commit",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
EmptyCommit("1st commit").
|
||||
NewBranch("mybranch").
|
||||
CreateFileAndAdd("file1", "1\n2\n3\n").
|
||||
Commit("2nd commit").
|
||||
UpdateFileAndAdd("file1", "1\n2\n3a\n").
|
||||
Commit("fixup! 2nd commit").
|
||||
EmptyCommit("3rd commit").
|
||||
UpdateFileAndAdd("file1", "1a\n2\n3b\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("3rd commit").IsSelected(),
|
||||
Contains("fixup! 2nd commit"),
|
||||
Contains("2nd commit"),
|
||||
Contains("1st commit"),
|
||||
)
|
||||
|
||||
t.Views().Files().
|
||||
Focus().
|
||||
Press(keys.Files.FindBaseCommitForFixup)
|
||||
|
||||
t.Views().Commits().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains("3rd commit"),
|
||||
Contains("fixup! 2nd commit"),
|
||||
Contains("2nd commit").IsSelected(),
|
||||
Contains("1st commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
@@ -11,6 +11,10 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// Creating a branch avoids that searching for 't' will unexpectedly match the first commit
|
||||
// (since it finds it in the extra info line, which is "HEAD -> master")
|
||||
shell.NewBranch("branch")
|
||||
|
||||
shell.EmptyCommit("one")
|
||||
shell.EmptyCommit("two")
|
||||
shell.EmptyCommit("three")
|
||||
@@ -103,6 +107,54 @@ var Search = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one").IsSelected(),
|
||||
).
|
||||
NavigateToLine(Contains("three")).
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
|
||||
}).
|
||||
Press("N").
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 'o' (1 of 3)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four").IsSelected(),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
Press(keys.Universal.StartSearch).
|
||||
Tap(func() {
|
||||
t.ExpectSearch().
|
||||
Type("t").
|
||||
Confirm()
|
||||
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four"),
|
||||
Contains("three").IsSelected(),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
SelectPreviousItem().
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four").IsSelected(),
|
||||
Contains("three"),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
).
|
||||
Press("n").
|
||||
Tap(func() {
|
||||
t.Views().Search().IsVisible().Content(Contains("matches for 't' (1 of 2)"))
|
||||
}).
|
||||
Lines(
|
||||
Contains("four"),
|
||||
Contains("three").IsSelected(),
|
||||
Contains("two"),
|
||||
Contains("one"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -50,7 +50,7 @@ var Gitignore = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).Select(Contains("Add to .git/info/exclude")).Confirm()
|
||||
|
||||
t.FileSystem().FileContent(".gitignore", Equals(""))
|
||||
t.FileSystem().FileContent(".git/info/exclude", Contains("toExclude"))
|
||||
t.FileSystem().FileContent(".git/info/exclude", Contains("/toExclude"))
|
||||
}).
|
||||
SelectNextItem().
|
||||
Press(keys.Files.IgnoreFile).
|
||||
@@ -58,8 +58,8 @@ var Gitignore = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().Title(Equals("Ignore or exclude file")).Select(Contains("Add to .gitignore")).Confirm()
|
||||
|
||||
t.FileSystem().FileContent(".gitignore", Equals("toIgnore\n"))
|
||||
t.FileSystem().FileContent(".git/info/exclude", Contains("toExclude"))
|
||||
t.FileSystem().FileContent(".gitignore", Equals("/toIgnore\n"))
|
||||
t.FileSystem().FileContent(".git/info/exclude", Contains("/toExclude"))
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +61,6 @@ var GitignoreSpecialCharacters = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Equals(" ?? abc_def"),
|
||||
)
|
||||
|
||||
t.FileSystem().FileContent(".gitignore", Equals("\\#file\nfile#abc\n\\!file\nfile!abc\nabc\\*def\nfile\\[x\\]\n"))
|
||||
t.FileSystem().FileContent(".gitignore", Equals("/\\#file\n/file#abc\n/\\!file\n/file!abc\n/abc\\*def\n/file\\[x\\]\n"))
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FixupKeepMessage = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Fixup a commit, keeping its commit message",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
CreateFileAndAdd("file1.txt", "File1 Content\n").Commit("First Commit").
|
||||
CreateFileAndAdd("file2.txt", "File2 Content\n").Commit("Second Commit").
|
||||
CreateFileAndAdd("file3.txt", "File3 Content\n").Commit("Third Commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("Third Commit"),
|
||||
Contains("Second Commit"),
|
||||
Contains("First Commit"),
|
||||
).
|
||||
NavigateToLine(Contains("Second Commit")).
|
||||
Press(keys.Commits.MarkCommitAsFixup).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Fixup")).
|
||||
Select(Contains("use this commit's message")).
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
Contains("Third Commit"),
|
||||
Contains("Second Commit").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
// The resulting commit should have the message from the fixup commit,
|
||||
// not the target commit
|
||||
Content(Contains("Second Commit")).
|
||||
Content(DoesNotContain("First Commit")).
|
||||
Content(Contains("+File1 Content")).
|
||||
Content(Contains("+File2 Content"))
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,78 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var FixupKeepMessageRebase = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Set fixup -C flag on a fixup commit during interactive rebase",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.
|
||||
CreateFileAndAdd("file1.txt", "File1 Content\n").Commit("First Commit").
|
||||
CreateFileAndAdd("file2.txt", "File2 Content\n").Commit("Second Commit").
|
||||
CreateFileAndAdd("file3.txt", "File3 Content\n").Commit("Third Commit")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("Third Commit"),
|
||||
Contains("Second Commit"),
|
||||
Contains("First Commit"),
|
||||
).
|
||||
// Start interactive rebase from the first commit
|
||||
NavigateToLine(Contains("First Commit")).
|
||||
Press(keys.Universal.Edit).
|
||||
Lines(
|
||||
Contains("--- Pending rebase todos ---"),
|
||||
Contains("pick CI Third Commit"),
|
||||
Contains("pick CI Second Commit"),
|
||||
Contains("--- Commits ---"),
|
||||
Contains("First Commit").IsSelected(),
|
||||
).
|
||||
// Mark second commit as fixup
|
||||
NavigateToLine(Contains("Second Commit")).
|
||||
Press(keys.Commits.MarkCommitAsFixup).
|
||||
Lines(
|
||||
Contains("--- Pending rebase todos ---"),
|
||||
Contains("pick CI Third Commit"),
|
||||
Contains("fixup CI Second Commit").IsSelected(),
|
||||
Contains("--- Commits ---"),
|
||||
Contains("First Commit"),
|
||||
).
|
||||
// Now set the -C flag using the SetFixupMessage keybinding
|
||||
Press(keys.Commits.SetFixupMessage).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Set fixup message")).
|
||||
Select(Contains("use this commit's message")).
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
Contains("--- Pending rebase todos ---"),
|
||||
Contains("pick CI Third Commit"),
|
||||
Contains("fixup -C CI Second Commit").IsSelected(),
|
||||
Contains("--- Commits ---"),
|
||||
Contains("First Commit"),
|
||||
).
|
||||
// Continue the rebase
|
||||
Tap(func() {
|
||||
t.Common().ContinueRebase()
|
||||
}).
|
||||
Lines(
|
||||
Contains("Third Commit"),
|
||||
Contains("Second Commit").IsSelected(),
|
||||
)
|
||||
|
||||
t.Views().Main().
|
||||
// The resulting commit should have the message from the fixup commit
|
||||
Content(Contains("Second Commit")).
|
||||
Content(DoesNotContain("First Commit")).
|
||||
Content(Contains("+File1 Content")).
|
||||
Content(Contains("+File2 Content"))
|
||||
},
|
||||
})
|
||||
@@ -27,9 +27,8 @@ var FixupSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
NavigateToLine(Contains("Fixup Commit Message")).
|
||||
Press(keys.Commits.MarkCommitAsFixup).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Confirmation().
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Fixup")).
|
||||
Content(Equals("Are you sure you want to 'fixup' the selected commit(s) into the commit below?")).
|
||||
Confirm()
|
||||
}).
|
||||
Lines(
|
||||
|
||||
@@ -77,9 +77,8 @@ var OutsideRebaseRangeSelect = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
).
|
||||
Press(keys.Commits.MarkCommitAsFixup).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Confirmation().
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Fixup")).
|
||||
Content(Contains("Are you sure you want to 'fixup' the selected commit(s) into the commit below?")).
|
||||
Confirm()
|
||||
}).
|
||||
TopLines(
|
||||
|
||||
@@ -130,6 +130,7 @@ var tests = []*components.IntegrationTest{
|
||||
commit.DoNotShowBranchMarkerForHeadCommit,
|
||||
commit.FailHooksThenCommitNoHooks,
|
||||
commit.FindBaseCommitForFixup,
|
||||
commit.FindBaseCommitForFixupDisregardFixupsForSameBaseCommit,
|
||||
commit.FindBaseCommitForFixupDisregardMainBranch,
|
||||
commit.FindBaseCommitForFixupOnlyAddedLines,
|
||||
commit.FindBaseCommitForFixupWarningForAddedLines,
|
||||
@@ -278,6 +279,8 @@ var tests = []*components.IntegrationTest{
|
||||
interactive_rebase.EditRangeSelectOutsideRebase,
|
||||
interactive_rebase.EditTheConflCommit,
|
||||
interactive_rebase.FixupFirstCommit,
|
||||
interactive_rebase.FixupKeepMessage,
|
||||
interactive_rebase.FixupKeepMessageRebase,
|
||||
interactive_rebase.FixupSecondCommit,
|
||||
interactive_rebase.InteractiveRebaseOfCopiedBranch,
|
||||
interactive_rebase.InteractiveRebaseWithConflictForEditCommand,
|
||||
|
||||
@@ -19,6 +19,7 @@ type Todo struct {
|
||||
type TodoChange struct {
|
||||
Hash string
|
||||
NewAction todo.TodoCommand
|
||||
Flag string
|
||||
}
|
||||
|
||||
// Read a git-rebase-todo file, change the actions for the given commits,
|
||||
@@ -37,6 +38,7 @@ func EditRebaseTodo(filePath string, changes []TodoChange, commentChar byte) err
|
||||
if equalHash(t.Commit, change.Hash) {
|
||||
matchCount++
|
||||
t.Command = change.NewAction
|
||||
t.Flag = change.Flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,6 +946,10 @@
|
||||
"type": "string",
|
||||
"default": "f"
|
||||
},
|
||||
"setFixupMessage": {
|
||||
"type": "string",
|
||||
"default": "c"
|
||||
},
|
||||
"createFixupCommit": {
|
||||
"type": "string",
|
||||
"default": "F"
|
||||
|
||||
@@ -946,6 +946,10 @@
|
||||
"type": "string",
|
||||
"default": "f"
|
||||
},
|
||||
"setFixupMessage": {
|
||||
"type": "string",
|
||||
"default": "c"
|
||||
},
|
||||
"createFixupCommit": {
|
||||
"type": "string",
|
||||
"default": "F"
|
||||
|
||||
@@ -18,6 +18,8 @@ fi
|
||||
|
||||
download_dir="$1"
|
||||
|
||||
echo "Updating translations..."
|
||||
|
||||
# The Portuguese translation is named pt-PT, but we want to use pt instead (it
|
||||
# is used both for Brasilian and European Portuguese). I couldn't figure out how
|
||||
# to change this in Crowdin, so we'll do it here.
|
||||
@@ -30,3 +32,7 @@ do
|
||||
# exporting, but unfortunately it doesn't work for json files.
|
||||
jq 'del(..|select(. == ""))' < "$d/en.json" > pkg/i18n/translations/$(basename "$d").json
|
||||
done
|
||||
|
||||
# Update cheatsheets, which might be affected by changes in the translations.
|
||||
echo "Regenerating auto-generated files..."
|
||||
go generate ./...
|
||||
|
||||
+83
-41
@@ -84,6 +84,7 @@ type inputProcessor struct {
|
||||
evch chan<- Event
|
||||
rows int // used for clipping mouse coordinates
|
||||
cols int // used for clipping mouse coordinates
|
||||
surrogate rune
|
||||
nested *inputProcessor
|
||||
}
|
||||
|
||||
@@ -135,8 +136,9 @@ type csiParamMode struct {
|
||||
}
|
||||
|
||||
type keyMap struct {
|
||||
Key Key
|
||||
Mod ModMask
|
||||
Key Key
|
||||
Mod ModMask
|
||||
Rune rune
|
||||
}
|
||||
|
||||
var csiAllKeys = map[csiParamMode]keyMap{
|
||||
@@ -275,41 +277,70 @@ var csiAllKeys = map[csiParamMode]keyMap{
|
||||
}
|
||||
|
||||
// keys reported using Kitty csi-u protocol
|
||||
var csiUKeys = map[int]Key{
|
||||
27: KeyESC,
|
||||
9: KeyTAB,
|
||||
13: KeyEnter,
|
||||
127: KeyBS,
|
||||
57358: KeyCapsLock,
|
||||
57359: KeyScrollLock,
|
||||
57360: KeyNumLock,
|
||||
57361: KeyPrint,
|
||||
57362: KeyPause,
|
||||
57363: KeyMenu,
|
||||
57376: KeyF13,
|
||||
57377: KeyF14,
|
||||
57378: KeyF15,
|
||||
57379: KeyF16,
|
||||
57380: KeyF17,
|
||||
57381: KeyF18,
|
||||
57382: KeyF19,
|
||||
57383: KeyF20,
|
||||
57384: KeyF21,
|
||||
57385: KeyF22,
|
||||
57386: KeyF23,
|
||||
57387: KeyF24,
|
||||
57388: KeyF25,
|
||||
57389: KeyF26,
|
||||
57390: KeyF27,
|
||||
57391: KeyF28,
|
||||
57392: KeyF29,
|
||||
57393: KeyF30,
|
||||
57394: KeyF31,
|
||||
57395: KeyF32,
|
||||
57396: KeyF33,
|
||||
57397: KeyF34,
|
||||
57398: KeyF35,
|
||||
// TODO: KP keys
|
||||
var csiUKeys = map[int]keyMap{
|
||||
27: {Key: KeyESC},
|
||||
9: {Key: KeyTAB},
|
||||
13: {Key: KeyEnter},
|
||||
127: {Key: KeyBS},
|
||||
57358: {Key: KeyCapsLock},
|
||||
57359: {Key: KeyScrollLock},
|
||||
57360: {Key: KeyNumLock},
|
||||
57361: {Key: KeyPrint},
|
||||
57362: {Key: KeyPause},
|
||||
57363: {Key: KeyMenu},
|
||||
57376: {Key: KeyF13},
|
||||
57377: {Key: KeyF14},
|
||||
57378: {Key: KeyF15},
|
||||
57379: {Key: KeyF16},
|
||||
57380: {Key: KeyF17},
|
||||
57381: {Key: KeyF18},
|
||||
57382: {Key: KeyF19},
|
||||
57383: {Key: KeyF20},
|
||||
57384: {Key: KeyF21},
|
||||
57385: {Key: KeyF22},
|
||||
57386: {Key: KeyF23},
|
||||
57387: {Key: KeyF24},
|
||||
57388: {Key: KeyF25},
|
||||
57389: {Key: KeyF26},
|
||||
57390: {Key: KeyF27},
|
||||
57391: {Key: KeyF28},
|
||||
57392: {Key: KeyF29},
|
||||
57393: {Key: KeyF30},
|
||||
57394: {Key: KeyF31},
|
||||
57395: {Key: KeyF32},
|
||||
57396: {Key: KeyF33},
|
||||
57397: {Key: KeyF34},
|
||||
57398: {Key: KeyF35},
|
||||
57399: {Key: KeyRune, Rune: '0'}, // KP 0
|
||||
57400: {Key: KeyRune, Rune: '1'}, // KP 1
|
||||
57401: {Key: KeyRune, Rune: '2'}, // KP 2
|
||||
57402: {Key: KeyRune, Rune: '3'}, // KP 3
|
||||
57403: {Key: KeyRune, Rune: '4'}, // KP 4
|
||||
57404: {Key: KeyRune, Rune: '5'}, // KP 5
|
||||
57405: {Key: KeyRune, Rune: '6'}, // KP 6
|
||||
57406: {Key: KeyRune, Rune: '7'}, // KP 7
|
||||
57407: {Key: KeyRune, Rune: '8'}, // KP 8
|
||||
57408: {Key: KeyRune, Rune: '9'}, // KP 9
|
||||
57409: {Key: KeyRune, Rune: '.'}, // KP_DECIMAL
|
||||
57410: {Key: KeyRune, Rune: '/'}, // KP_DIVIDE
|
||||
57411: {Key: KeyRune, Rune: '*'}, // KP_MULTIPLY
|
||||
57412: {Key: KeyRune, Rune: '-'}, // KP_SUBTRACT
|
||||
57413: {Key: KeyRune, Rune: '+'}, // KP_ADD
|
||||
57414: {Key: KeyEnter}, // KP_ENTER
|
||||
57415: {Key: KeyRune, Rune: '='}, // KP_EQUAL
|
||||
57416: {Key: KeyClear}, // KP_SEPARATOR
|
||||
57417: {Key: KeyLeft}, // KP_LEFT
|
||||
57418: {Key: KeyRight}, // KP_RIGHT
|
||||
57419: {Key: KeyUp}, // KP_UP
|
||||
57420: {Key: KeyDown}, // KP_DOWN
|
||||
57421: {Key: KeyPgUp}, // KP_PG_UP
|
||||
57422: {Key: KeyPgDn}, // KP_PG_DN
|
||||
57423: {Key: KeyHome}, // KP_HOME
|
||||
57424: {Key: KeyEnd}, // KP_END
|
||||
57425: {Key: KeyInsert}, // KP_INSERT
|
||||
57426: {Key: KeyDelete}, // KP_DELETE
|
||||
// 57427: {Key: KeyBegin}, // KP_BEGIN
|
||||
|
||||
// TODO: Media keys
|
||||
}
|
||||
|
||||
@@ -318,8 +349,8 @@ var winKeys = map[int]Key{
|
||||
0x03: KeyCancel, // vkCancel
|
||||
0x08: KeyBackspace, // vkBackspace
|
||||
0x09: KeyTab, // vkTab
|
||||
0x0c: KeyClear, // vClear
|
||||
0x0d: KeyEnter, // vkReturn
|
||||
0x12: KeyClear, // vClear
|
||||
0x13: KeyPause, // vkPause
|
||||
0x1b: KeyEscape, // vkEscape
|
||||
0x21: KeyPgUp, // vkPrior
|
||||
@@ -701,11 +732,22 @@ func (ip *inputProcessor) handleWinKey(P []int) {
|
||||
} else if chr < ' ' && P[0] >= 0x41 && P[0] <= 0x5a {
|
||||
key = Key(P[0])
|
||||
chr = 0
|
||||
} else if key == 0x11 || key == 0x13 || key == 0x14 {
|
||||
|
||||
} else if chr >= 0xD800 && chr <= 0xDBFF {
|
||||
// high surrogate pair
|
||||
ip.surrogate = chr
|
||||
return
|
||||
} else if chr >= 0xDC00 && chr <= 0xDFFF {
|
||||
// low surrogate pair
|
||||
chr = utf16.DecodeRune(ip.surrogate, chr)
|
||||
} else if P[0] == 0x10 || P[0] == 0x11 || P[0] == 0x12 || P[0] == 0x14 {
|
||||
// lone modifiers
|
||||
ip.surrogate = 0
|
||||
return
|
||||
}
|
||||
|
||||
ip.surrogate = 0
|
||||
|
||||
// Modifiers
|
||||
if P[4]&0x010 != 0 {
|
||||
mod |= ModShift
|
||||
@@ -791,8 +833,8 @@ func (ip *inputProcessor) handleCsi(mode rune, params []byte, intermediate []byt
|
||||
key := KeyRune
|
||||
chr := rune(0)
|
||||
if k1, ok := csiUKeys[P0]; ok {
|
||||
key = k1
|
||||
chr = 0
|
||||
key = k1.Key
|
||||
chr = k1.Rune
|
||||
} else {
|
||||
chr = rune(P0)
|
||||
}
|
||||
|
||||
+14
-1
@@ -622,6 +622,14 @@ func (t *tScreen) drawCell(x, y int) int {
|
||||
width = 1
|
||||
str = " "
|
||||
}
|
||||
if width > 1 && x+width < t.w {
|
||||
// Clobber over any content in the next cell.
|
||||
// This fixes a problem with some terminals where overwriting two
|
||||
// adjacent single cells with a wide rune would leave an image
|
||||
// of the second cell. This is a workaround for buggy terminals.
|
||||
t.writeString(" \b\b")
|
||||
}
|
||||
|
||||
t.writeString(str)
|
||||
t.cx += width
|
||||
t.cells.SetDirty(x, y, false)
|
||||
@@ -989,9 +997,14 @@ func (t *tScreen) buildAcsMap() {
|
||||
}
|
||||
|
||||
func (t *tScreen) scanInput(buf *bytes.Buffer) {
|
||||
// The end of the buffer isn't necessarily the end of the input, because
|
||||
// large inputs are chunked. Set atEOF to false so the UTF-8 validating decoder
|
||||
// returns ErrShortSrc instead of ErrInvalidUTF8 for incomplete multi-byte codepoints.
|
||||
const atEOF = false
|
||||
|
||||
for buf.Len() > 0 {
|
||||
utf := make([]byte, min(8, max(buf.Len()*2, 128)))
|
||||
nOut, nIn, e := t.decoder.Transform(utf, buf.Bytes(), true)
|
||||
nOut, nIn, e := t.decoder.Transform(utf, buf.Bytes(), atEOF)
|
||||
_ = buf.Next(nIn)
|
||||
t.input.ScanUTF8(utf[:nOut])
|
||||
if e == transform.ErrShortSrc {
|
||||
|
||||
+23
-11
@@ -1,4 +1,4 @@
|
||||
// Copyright 2025 The TCell Authors
|
||||
// Copyright 2026 The TCell Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use file except in compliance with the License.
|
||||
@@ -72,6 +72,7 @@ type winTty struct {
|
||||
oomode uint32 // original output mode
|
||||
oscreen consoleInfo
|
||||
wg sync.WaitGroup
|
||||
surrogate rune
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
@@ -126,7 +127,7 @@ func (w *winTty) Drain() error {
|
||||
|
||||
func (w *winTty) getConsoleInput() error {
|
||||
// cancelFlag comes first as WaitForMultipleObjects returns the lowest index
|
||||
// in the event that both events are signalled.
|
||||
// in the event that both events are signaled.
|
||||
waitObjects := []syscall.Handle{w.cancelFlag, w.in}
|
||||
|
||||
// As arrays are contiguous in memory, a pointer to the first object is the
|
||||
@@ -158,19 +159,30 @@ func (w *winTty) getConsoleInput() error {
|
||||
if rv == 0 {
|
||||
return er
|
||||
}
|
||||
loop:
|
||||
for i := range nrec {
|
||||
ir := rec[i]
|
||||
switch ir.typ {
|
||||
case keyEvent:
|
||||
chr := ir.data[10] // we only see ASCII, key down events in VT mode
|
||||
|
||||
// because we use win32-input-mode, we will only
|
||||
// see US-ASCII characters - (Q: will they be
|
||||
// 16-bit values with possible surrogate pairs?)
|
||||
select {
|
||||
case w.buf <- chr:
|
||||
case <-w.stopQ:
|
||||
break
|
||||
// we normally only expect to see ascii, but paste data may come in as UTF-16.
|
||||
wc := rune(binary.LittleEndian.Uint16(ir.data[10:]))
|
||||
if wc >= 0xD800 && wc <= 0xDBFF {
|
||||
// if it was a high surrogate, which happens for pasted UTF-16,
|
||||
// then save it until we get the low and can decode it.
|
||||
w.surrogate = wc
|
||||
continue
|
||||
} else if wc >= 0xDC00 && wc <= 0xDFFF {
|
||||
wc = utf16.DecodeRune(w.surrogate, wc)
|
||||
}
|
||||
w.surrogate = 0
|
||||
for _, chr := range []byte(string(wc)) {
|
||||
// We normally expect only to see ASCII (win32-input-mode),
|
||||
// but apparently pasted data can arrive in UTF-16 here.
|
||||
select {
|
||||
case w.buf <- chr:
|
||||
case <-w.stopQ:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
|
||||
case resizeEvent:
|
||||
|
||||
+11
@@ -39,6 +39,7 @@ func (self noInstruction) isInstruction() {}
|
||||
const (
|
||||
stateNone escapeState = iota
|
||||
stateEscape
|
||||
stateCharacterSetDesignation
|
||||
stateCSI
|
||||
stateParams
|
||||
stateOSC
|
||||
@@ -144,9 +145,19 @@ func (ei *escapeInterpreter) parseOne(ch []byte) (isEscape bool, err error) {
|
||||
case characterEquals(ch, ']'):
|
||||
ei.state = stateOSC
|
||||
return true, nil
|
||||
case characterEquals(ch, '('),
|
||||
characterEquals(ch, ')'),
|
||||
characterEquals(ch, '*'),
|
||||
characterEquals(ch, '+'):
|
||||
ei.state = stateCharacterSetDesignation
|
||||
return true, nil
|
||||
default:
|
||||
return false, errNotCSI
|
||||
}
|
||||
case stateCharacterSetDesignation:
|
||||
// Not supported, so just skip it
|
||||
ei.state = stateNone
|
||||
return true, nil
|
||||
case stateCSI:
|
||||
switch {
|
||||
case len(ch) == 1 && ch[0] >= '0' && ch[0] <= '9':
|
||||
|
||||
+2
-2
@@ -1313,9 +1313,9 @@ func (g *Gui) onKey(ev *GocuiEvent) error {
|
||||
// back to '\n' fixes pasting multi-line text from Ghostty, and doesn't
|
||||
// seem harmful for other terminal emulators.
|
||||
//
|
||||
// KeyCtrlJ (int value 10) is '\r', and KeyCtrlM (int value 13) is '\n'.
|
||||
// KeyCtrlJ (int value 10) is '\r'.
|
||||
if g.IsPasting && ev.Key == KeyCtrlJ {
|
||||
ev.Key = KeyCtrlM
|
||||
ev.Key = KeyEnter
|
||||
}
|
||||
|
||||
err := g.execKeybindings(g.currentView, ev)
|
||||
|
||||
+75
-1
@@ -71,6 +71,7 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
||||
currentLineWidth := 0
|
||||
indexOfLastWhitespace := -1
|
||||
var footNoteMatcher footNoteMatcher
|
||||
var trailerMatcher trailerMatcher
|
||||
|
||||
cells := stringToTextAreaCells(content)
|
||||
y := 0
|
||||
@@ -94,9 +95,10 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
||||
indexOfLastWhitespace = -1
|
||||
currentLineWidth = 0
|
||||
footNoteMatcher.reset()
|
||||
trailerMatcher.reset()
|
||||
} else {
|
||||
currentLineWidth += c.width
|
||||
if c.char == " " && !footNoteMatcher.isFootNote() {
|
||||
if c.char == " " && !footNoteMatcher.isFootNote() && !trailerMatcher.isTrailer() {
|
||||
indexOfLastWhitespace = currentPos + 1
|
||||
} else if autoWrapWidth > 0 && currentLineWidth > autoWrapWidth && indexOfLastWhitespace >= 0 {
|
||||
wrapAt := indexOfLastWhitespace
|
||||
@@ -112,9 +114,11 @@ func contentToCells(content string, autoWrapWidth int) ([]TextAreaCell, []int) {
|
||||
currentLineWidth += c1.width
|
||||
}
|
||||
footNoteMatcher.reset()
|
||||
trailerMatcher.reset()
|
||||
}
|
||||
|
||||
footNoteMatcher.addCharacter(c.char)
|
||||
trailerMatcher.addCharacter(c.char)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,6 +171,76 @@ func (self *footNoteMatcher) reset() {
|
||||
self.didFailToMatch = false
|
||||
}
|
||||
|
||||
var supportedTrailers = []string{
|
||||
"Signed-off-by:",
|
||||
"Co-authored-by:",
|
||||
}
|
||||
|
||||
type trailerMatcher struct {
|
||||
lineStr strings.Builder
|
||||
didFailToMatch bool
|
||||
didMatch bool
|
||||
}
|
||||
|
||||
func (self *trailerMatcher) addCharacter(chr string) {
|
||||
if self.didFailToMatch || self.didMatch {
|
||||
return
|
||||
}
|
||||
|
||||
if len(chr) != 1 {
|
||||
// Trailers are all ASCII, so if we get a non-ASCII UTF-8 character (or even a multi-rune
|
||||
// grapheme cluster), we can fail early.
|
||||
self.didFailToMatch = true
|
||||
return
|
||||
}
|
||||
|
||||
if self.lineStr.Len() == 0 {
|
||||
// If this is the first character, see if it could possibly match any supported trailer; if
|
||||
// not, we can fail early and stop tracking further characters for this line.
|
||||
if !anyOf(supportedTrailers, func(trailer string) bool { return trailer[0] == chr[0] }) {
|
||||
self.didFailToMatch = true
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.lineStr.WriteString(chr)
|
||||
}
|
||||
|
||||
func (self *trailerMatcher) isTrailer() bool {
|
||||
if self.didFailToMatch {
|
||||
return false
|
||||
}
|
||||
|
||||
if self.didMatch {
|
||||
return true
|
||||
}
|
||||
|
||||
line := self.lineStr.String()
|
||||
if anyOf(supportedTrailers, func(trailer string) bool { return line == trailer }) {
|
||||
self.didMatch = true
|
||||
return true
|
||||
}
|
||||
|
||||
self.didFailToMatch = true
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *trailerMatcher) reset() {
|
||||
self.lineStr.Reset()
|
||||
self.didFailToMatch = false
|
||||
self.didMatch = false
|
||||
}
|
||||
|
||||
func anyOf(strings []string, predicate func(s string) bool) bool {
|
||||
for _, s := range strings {
|
||||
if predicate(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (self *TextArea) updateCells() {
|
||||
width := self.AutoWrapWidth
|
||||
if !self.AutoWrap {
|
||||
|
||||
+129
-23
@@ -167,9 +167,6 @@ type View struct {
|
||||
// If HasLoader is true, the message will be appended with a spinning loader animation
|
||||
HasLoader bool
|
||||
|
||||
// IgnoreCarriageReturns tells us whether to ignore '\r' characters
|
||||
IgnoreCarriageReturns bool
|
||||
|
||||
// ParentView is the view which catches events bubbled up from the given view if there's no matching handler
|
||||
ParentView *View
|
||||
|
||||
@@ -217,29 +214,53 @@ type searcher struct {
|
||||
searchPositions []SearchPosition
|
||||
modelSearchResults []SearchPosition
|
||||
currentSearchIndex int
|
||||
onSelectItem func(int, int, int) error
|
||||
onSelectItem func(int)
|
||||
renderSearchStatus func(int, int)
|
||||
}
|
||||
|
||||
func (v *View) SetOnSelectItem(onSelectItem func(int, int, int) error) {
|
||||
func (v *View) SetRenderSearchStatus(renderSearchStatus func(int, int)) {
|
||||
v.searcher.renderSearchStatus = renderSearchStatus
|
||||
}
|
||||
|
||||
func (v *View) SetOnSelectItem(onSelectItem func(int)) {
|
||||
v.searcher.onSelectItem = onSelectItem
|
||||
}
|
||||
|
||||
func (v *View) renderSearchStatus(index int, itemCount int) {
|
||||
if v.searcher.renderSearchStatus != nil {
|
||||
v.searcher.renderSearchStatus(index, itemCount)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) gotoNextMatch() error {
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if v.Highlight && v.oy+v.cy < v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||
// If the selection is before the current match, just jump to the current match and return.
|
||||
// This can only happen if the user has moved the cursor to before the first match.
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
if v.searcher.currentSearchIndex >= len(v.searcher.searchPositions)-1 {
|
||||
v.searcher.currentSearchIndex = 0
|
||||
} else {
|
||||
v.searcher.currentSearchIndex++
|
||||
}
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *View) gotoPreviousMatch() error {
|
||||
if len(v.searcher.searchPositions) == 0 {
|
||||
return nil
|
||||
}
|
||||
if v.Highlight && v.oy+v.cy > v.searcher.searchPositions[v.searcher.currentSearchIndex].Y {
|
||||
// If the selection is after the current match, just jump to the current match and return.
|
||||
// This happens if the user has moved the cursor down from the current match.
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
if v.searcher.currentSearchIndex == 0 {
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
v.searcher.currentSearchIndex = len(v.searcher.searchPositions) - 1
|
||||
@@ -247,13 +268,14 @@ func (v *View) gotoPreviousMatch() error {
|
||||
} else {
|
||||
v.searcher.currentSearchIndex--
|
||||
}
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v *View) SelectSearchResult(index int) error {
|
||||
func (v *View) SelectSearchResult(index int) {
|
||||
itemCount := len(v.searcher.searchPositions)
|
||||
if itemCount == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
if index > itemCount-1 {
|
||||
index = itemCount - 1
|
||||
@@ -262,10 +284,10 @@ func (v *View) SelectSearchResult(index int) error {
|
||||
y := v.searcher.searchPositions[index].Y
|
||||
|
||||
v.FocusPoint(v.ox, y, true)
|
||||
v.renderSearchStatus(index, itemCount)
|
||||
if v.searcher.onSelectItem != nil {
|
||||
return v.searcher.onSelectItem(y, index, itemCount)
|
||||
v.searcher.onSelectItem(y)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns <current match index>, <total matches>
|
||||
@@ -294,26 +316,29 @@ func (v *View) UpdateSearchResults(str string, modelSearchResults []SearchPositi
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
// get the first result past the current cursor
|
||||
currentIndex := 0
|
||||
adjustedY := v.oy + v.cy
|
||||
adjustedX := v.ox + v.cx
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
||||
currentIndex = i
|
||||
break
|
||||
if v.Highlight {
|
||||
// ...but only if we're showing the highlighted line
|
||||
adjustedY := v.oy + v.cy
|
||||
adjustedX := v.ox + v.cx
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y > adjustedY || (pos.Y == adjustedY && pos.XStart > adjustedX) {
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
v.searcher.currentSearchIndex = currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) Search(str string, modelSearchResults []SearchPosition) error {
|
||||
func (v *View) Search(str string, modelSearchResults []SearchPosition) {
|
||||
v.UpdateSearchResults(str, modelSearchResults)
|
||||
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
return v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
v.SelectSearchResult(v.searcher.currentSearchIndex)
|
||||
} else {
|
||||
v.renderSearchStatus(0, 0)
|
||||
}
|
||||
|
||||
return v.searcher.onSelectItem(-1, -1, 0)
|
||||
}
|
||||
|
||||
func (v *View) ClearSearch() {
|
||||
@@ -324,8 +349,37 @@ func (v *View) IsSearching() bool {
|
||||
return v.searcher.searchString != ""
|
||||
}
|
||||
|
||||
func (v *View) nearestSearchPosition() int {
|
||||
currentLineIndex := v.cy + v.oy
|
||||
lastSearchPos := 0
|
||||
for i, pos := range v.searcher.searchPositions {
|
||||
if pos.Y == currentLineIndex {
|
||||
return i
|
||||
}
|
||||
if pos.Y > currentLineIndex {
|
||||
break
|
||||
}
|
||||
lastSearchPos = i
|
||||
}
|
||||
return lastSearchPos
|
||||
}
|
||||
|
||||
func (v *View) SetNearestSearchPosition() {
|
||||
if len(v.searcher.searchPositions) > 0 {
|
||||
newPos := v.nearestSearchPosition()
|
||||
if newPos != v.searcher.currentSearchIndex {
|
||||
v.searcher.currentSearchIndex = newPos
|
||||
v.renderSearchStatus(newPos, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) FocusPoint(cx int, cy int, scrollIntoView bool) {
|
||||
lineCount := len(v.lines)
|
||||
v.writeMutex.Lock()
|
||||
defer v.writeMutex.Unlock()
|
||||
|
||||
v.refreshViewLinesIfNeeded()
|
||||
lineCount := len(v.viewLines)
|
||||
if cy < 0 || cy > lineCount {
|
||||
return
|
||||
}
|
||||
@@ -408,6 +462,10 @@ func characterEquals(chr []byte, b byte) bool {
|
||||
return len(chr) == 1 && chr[0] == b
|
||||
}
|
||||
|
||||
func isCRLF(chr []byte) bool {
|
||||
return len(chr) == 2 && chr[0] == '\r' && chr[1] == '\n'
|
||||
}
|
||||
|
||||
// String returns a string from a given cell slice.
|
||||
func (l lineType) String() string {
|
||||
var str strings.Builder
|
||||
@@ -783,7 +841,7 @@ func (v *View) write(p []byte) {
|
||||
chr, remaining, width, state = uniseg.FirstGraphemeCluster(remaining, state)
|
||||
|
||||
switch {
|
||||
case characterEquals(chr, '\n'):
|
||||
case characterEquals(chr, '\n') || isCRLF(chr):
|
||||
finishLine()
|
||||
advanceToNextLine()
|
||||
case characterEquals(chr, '\r'):
|
||||
@@ -1757,6 +1815,52 @@ func (v *View) setContentLineCount(lineCount int) {
|
||||
v.lines = v.lines[:lineCount]
|
||||
}
|
||||
|
||||
// If the current search result is no longer visible after a scroll up, select the last search
|
||||
// result that is visible in the view, if any, or the first one that is below the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollUp() {
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
windowBottom := v.oy + v.InnerHeight()
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y >= windowBottom {
|
||||
newSearchIndex := v.searcher.currentSearchIndex
|
||||
for newSearchIndex > 0 &&
|
||||
v.searcher.searchPositions[newSearchIndex-1].Y >= v.oy {
|
||||
newSearchIndex--
|
||||
if v.searcher.searchPositions[newSearchIndex].Y < windowBottom {
|
||||
break
|
||||
}
|
||||
}
|
||||
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||
v.searcher.currentSearchIndex = newSearchIndex
|
||||
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the current search result is no longer visible after a scroll down, select the first search
|
||||
// result that is visible in the view, if any, or the last one that is above the view if none is
|
||||
// visible.
|
||||
func (v *View) selectVisibleSearchResultAfterScrollDown() {
|
||||
if !v.Highlight && len(v.searcher.searchPositions) != 0 {
|
||||
if v.searcher.searchPositions[v.searcher.currentSearchIndex].Y < v.oy {
|
||||
newSearchIndex := v.searcher.currentSearchIndex
|
||||
windowBottom := v.oy + v.InnerHeight()
|
||||
for newSearchIndex+1 < len(v.searcher.searchPositions) &&
|
||||
v.searcher.searchPositions[newSearchIndex+1].Y < windowBottom {
|
||||
newSearchIndex++
|
||||
if v.searcher.searchPositions[newSearchIndex].Y >= v.oy {
|
||||
break
|
||||
}
|
||||
}
|
||||
if v.searcher.currentSearchIndex != newSearchIndex {
|
||||
v.searcher.currentSearchIndex = newSearchIndex
|
||||
v.renderSearchStatus(newSearchIndex, len(v.searcher.searchPositions))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (v *View) ScrollUp(amount int) {
|
||||
if amount > v.oy {
|
||||
amount = v.oy
|
||||
@@ -1767,6 +1871,7 @@ func (v *View) ScrollUp(amount int) {
|
||||
v.cy += amount
|
||||
|
||||
v.clearHover()
|
||||
v.selectVisibleSearchResultAfterScrollUp()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1778,6 +1883,7 @@ func (v *View) ScrollDown(amount int) {
|
||||
v.cy -= adjustedAmount
|
||||
|
||||
v.clearHover()
|
||||
v.selectVisibleSearchResultAfterScrollDown()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+122
-48
@@ -64,6 +64,80 @@ func initOptions() {
|
||||
|
||||
func archInit() {
|
||||
|
||||
// From internal/cpu
|
||||
const (
|
||||
// eax bits
|
||||
cpuid_AVXVNNI = 1 << 4
|
||||
|
||||
// ecx bits
|
||||
cpuid_SSE3 = 1 << 0
|
||||
cpuid_PCLMULQDQ = 1 << 1
|
||||
cpuid_AVX512VBMI = 1 << 1
|
||||
cpuid_AVX512VBMI2 = 1 << 6
|
||||
cpuid_SSSE3 = 1 << 9
|
||||
cpuid_AVX512GFNI = 1 << 8
|
||||
cpuid_AVX512VAES = 1 << 9
|
||||
cpuid_AVX512VNNI = 1 << 11
|
||||
cpuid_AVX512BITALG = 1 << 12
|
||||
cpuid_FMA = 1 << 12
|
||||
cpuid_AVX512VPOPCNTDQ = 1 << 14
|
||||
cpuid_SSE41 = 1 << 19
|
||||
cpuid_SSE42 = 1 << 20
|
||||
cpuid_POPCNT = 1 << 23
|
||||
cpuid_AES = 1 << 25
|
||||
cpuid_OSXSAVE = 1 << 27
|
||||
cpuid_AVX = 1 << 28
|
||||
|
||||
// "Extended Feature Flag" bits returned in EBX for CPUID EAX=0x7 ECX=0x0
|
||||
cpuid_BMI1 = 1 << 3
|
||||
cpuid_AVX2 = 1 << 5
|
||||
cpuid_BMI2 = 1 << 8
|
||||
cpuid_ERMS = 1 << 9
|
||||
cpuid_AVX512F = 1 << 16
|
||||
cpuid_AVX512DQ = 1 << 17
|
||||
cpuid_ADX = 1 << 19
|
||||
cpuid_AVX512CD = 1 << 28
|
||||
cpuid_SHA = 1 << 29
|
||||
cpuid_AVX512BW = 1 << 30
|
||||
cpuid_AVX512VL = 1 << 31
|
||||
|
||||
// "Extended Feature Flag" bits returned in ECX for CPUID EAX=0x7 ECX=0x0
|
||||
cpuid_AVX512_VBMI = 1 << 1
|
||||
cpuid_AVX512_VBMI2 = 1 << 6
|
||||
cpuid_GFNI = 1 << 8
|
||||
cpuid_AVX512VPCLMULQDQ = 1 << 10
|
||||
cpuid_AVX512_BITALG = 1 << 12
|
||||
|
||||
// edx bits
|
||||
cpuid_FSRM = 1 << 4
|
||||
// edx bits for CPUID 0x80000001
|
||||
cpuid_RDTSCP = 1 << 27
|
||||
)
|
||||
// Additional constants not in internal/cpu
|
||||
const (
|
||||
// eax=1: edx
|
||||
cpuid_SSE2 = 1 << 26
|
||||
// eax=1: ecx
|
||||
cpuid_CX16 = 1 << 13
|
||||
cpuid_RDRAND = 1 << 30
|
||||
// eax=7,ecx=0: ebx
|
||||
cpuid_RDSEED = 1 << 18
|
||||
cpuid_AVX512IFMA = 1 << 21
|
||||
cpuid_AVX512PF = 1 << 26
|
||||
cpuid_AVX512ER = 1 << 27
|
||||
// eax=7,ecx=0: edx
|
||||
cpuid_AVX5124VNNIW = 1 << 2
|
||||
cpuid_AVX5124FMAPS = 1 << 3
|
||||
cpuid_AMXBF16 = 1 << 22
|
||||
cpuid_AMXTile = 1 << 24
|
||||
cpuid_AMXInt8 = 1 << 25
|
||||
// eax=7,ecx=1: eax
|
||||
cpuid_AVX512BF16 = 1 << 5
|
||||
cpuid_AVXIFMA = 1 << 23
|
||||
// eax=7,ecx=1: edx
|
||||
cpuid_AVXVNNIInt8 = 1 << 4
|
||||
)
|
||||
|
||||
Initialized = true
|
||||
|
||||
maxID, _, _, _ := cpuid(0, 0)
|
||||
@@ -73,90 +147,90 @@ func archInit() {
|
||||
}
|
||||
|
||||
_, _, ecx1, edx1 := cpuid(1, 0)
|
||||
X86.HasSSE2 = isSet(26, edx1)
|
||||
X86.HasSSE2 = isSet(edx1, cpuid_SSE2)
|
||||
|
||||
X86.HasSSE3 = isSet(0, ecx1)
|
||||
X86.HasPCLMULQDQ = isSet(1, ecx1)
|
||||
X86.HasSSSE3 = isSet(9, ecx1)
|
||||
X86.HasFMA = isSet(12, ecx1)
|
||||
X86.HasCX16 = isSet(13, ecx1)
|
||||
X86.HasSSE41 = isSet(19, ecx1)
|
||||
X86.HasSSE42 = isSet(20, ecx1)
|
||||
X86.HasPOPCNT = isSet(23, ecx1)
|
||||
X86.HasAES = isSet(25, ecx1)
|
||||
X86.HasOSXSAVE = isSet(27, ecx1)
|
||||
X86.HasRDRAND = isSet(30, ecx1)
|
||||
X86.HasSSE3 = isSet(ecx1, cpuid_SSE3)
|
||||
X86.HasPCLMULQDQ = isSet(ecx1, cpuid_PCLMULQDQ)
|
||||
X86.HasSSSE3 = isSet(ecx1, cpuid_SSSE3)
|
||||
X86.HasFMA = isSet(ecx1, cpuid_FMA)
|
||||
X86.HasCX16 = isSet(ecx1, cpuid_CX16)
|
||||
X86.HasSSE41 = isSet(ecx1, cpuid_SSE41)
|
||||
X86.HasSSE42 = isSet(ecx1, cpuid_SSE42)
|
||||
X86.HasPOPCNT = isSet(ecx1, cpuid_POPCNT)
|
||||
X86.HasAES = isSet(ecx1, cpuid_AES)
|
||||
X86.HasOSXSAVE = isSet(ecx1, cpuid_OSXSAVE)
|
||||
X86.HasRDRAND = isSet(ecx1, cpuid_RDRAND)
|
||||
|
||||
var osSupportsAVX, osSupportsAVX512 bool
|
||||
// For XGETBV, OSXSAVE bit is required and sufficient.
|
||||
if X86.HasOSXSAVE {
|
||||
eax, _ := xgetbv()
|
||||
// Check if XMM and YMM registers have OS support.
|
||||
osSupportsAVX = isSet(1, eax) && isSet(2, eax)
|
||||
osSupportsAVX = isSet(eax, 1<<1) && isSet(eax, 1<<2)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
// Darwin requires special AVX512 checks, see cpu_darwin_x86.go
|
||||
osSupportsAVX512 = osSupportsAVX && darwinSupportsAVX512()
|
||||
} else {
|
||||
// Check if OPMASK and ZMM registers have OS support.
|
||||
osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax)
|
||||
osSupportsAVX512 = osSupportsAVX && isSet(eax, 1<<5) && isSet(eax, 1<<6) && isSet(eax, 1<<7)
|
||||
}
|
||||
}
|
||||
|
||||
X86.HasAVX = isSet(28, ecx1) && osSupportsAVX
|
||||
X86.HasAVX = isSet(ecx1, cpuid_AVX) && osSupportsAVX
|
||||
|
||||
if maxID < 7 {
|
||||
return
|
||||
}
|
||||
|
||||
eax7, ebx7, ecx7, edx7 := cpuid(7, 0)
|
||||
X86.HasBMI1 = isSet(3, ebx7)
|
||||
X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX
|
||||
X86.HasBMI2 = isSet(8, ebx7)
|
||||
X86.HasERMS = isSet(9, ebx7)
|
||||
X86.HasRDSEED = isSet(18, ebx7)
|
||||
X86.HasADX = isSet(19, ebx7)
|
||||
X86.HasBMI1 = isSet(ebx7, cpuid_BMI1)
|
||||
X86.HasAVX2 = isSet(ebx7, cpuid_AVX2) && osSupportsAVX
|
||||
X86.HasBMI2 = isSet(ebx7, cpuid_BMI2)
|
||||
X86.HasERMS = isSet(ebx7, cpuid_ERMS)
|
||||
X86.HasRDSEED = isSet(ebx7, cpuid_RDSEED)
|
||||
X86.HasADX = isSet(ebx7, cpuid_ADX)
|
||||
|
||||
X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension
|
||||
X86.HasAVX512 = isSet(ebx7, cpuid_AVX512F) && osSupportsAVX512 // Because avx-512 foundation is the core required extension
|
||||
if X86.HasAVX512 {
|
||||
X86.HasAVX512F = true
|
||||
X86.HasAVX512CD = isSet(28, ebx7)
|
||||
X86.HasAVX512ER = isSet(27, ebx7)
|
||||
X86.HasAVX512PF = isSet(26, ebx7)
|
||||
X86.HasAVX512VL = isSet(31, ebx7)
|
||||
X86.HasAVX512BW = isSet(30, ebx7)
|
||||
X86.HasAVX512DQ = isSet(17, ebx7)
|
||||
X86.HasAVX512IFMA = isSet(21, ebx7)
|
||||
X86.HasAVX512VBMI = isSet(1, ecx7)
|
||||
X86.HasAVX5124VNNIW = isSet(2, edx7)
|
||||
X86.HasAVX5124FMAPS = isSet(3, edx7)
|
||||
X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7)
|
||||
X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7)
|
||||
X86.HasAVX512VNNI = isSet(11, ecx7)
|
||||
X86.HasAVX512GFNI = isSet(8, ecx7)
|
||||
X86.HasAVX512VAES = isSet(9, ecx7)
|
||||
X86.HasAVX512VBMI2 = isSet(6, ecx7)
|
||||
X86.HasAVX512BITALG = isSet(12, ecx7)
|
||||
X86.HasAVX512CD = isSet(ebx7, cpuid_AVX512CD)
|
||||
X86.HasAVX512ER = isSet(ebx7, cpuid_AVX512ER)
|
||||
X86.HasAVX512PF = isSet(ebx7, cpuid_AVX512PF)
|
||||
X86.HasAVX512VL = isSet(ebx7, cpuid_AVX512VL)
|
||||
X86.HasAVX512BW = isSet(ebx7, cpuid_AVX512BW)
|
||||
X86.HasAVX512DQ = isSet(ebx7, cpuid_AVX512DQ)
|
||||
X86.HasAVX512IFMA = isSet(ebx7, cpuid_AVX512IFMA)
|
||||
X86.HasAVX512VBMI = isSet(ecx7, cpuid_AVX512_VBMI)
|
||||
X86.HasAVX5124VNNIW = isSet(edx7, cpuid_AVX5124VNNIW)
|
||||
X86.HasAVX5124FMAPS = isSet(edx7, cpuid_AVX5124FMAPS)
|
||||
X86.HasAVX512VPOPCNTDQ = isSet(ecx7, cpuid_AVX512VPOPCNTDQ)
|
||||
X86.HasAVX512VPCLMULQDQ = isSet(ecx7, cpuid_AVX512VPCLMULQDQ)
|
||||
X86.HasAVX512VNNI = isSet(ecx7, cpuid_AVX512VNNI)
|
||||
X86.HasAVX512GFNI = isSet(ecx7, cpuid_AVX512GFNI)
|
||||
X86.HasAVX512VAES = isSet(ecx7, cpuid_AVX512VAES)
|
||||
X86.HasAVX512VBMI2 = isSet(ecx7, cpuid_AVX512VBMI2)
|
||||
X86.HasAVX512BITALG = isSet(ecx7, cpuid_AVX512BITALG)
|
||||
}
|
||||
|
||||
X86.HasAMXTile = isSet(24, edx7)
|
||||
X86.HasAMXInt8 = isSet(25, edx7)
|
||||
X86.HasAMXBF16 = isSet(22, edx7)
|
||||
X86.HasAMXTile = isSet(edx7, cpuid_AMXTile)
|
||||
X86.HasAMXInt8 = isSet(edx7, cpuid_AMXInt8)
|
||||
X86.HasAMXBF16 = isSet(edx7, cpuid_AMXBF16)
|
||||
|
||||
// These features depend on the second level of extended features.
|
||||
if eax7 >= 1 {
|
||||
eax71, _, _, edx71 := cpuid(7, 1)
|
||||
if X86.HasAVX512 {
|
||||
X86.HasAVX512BF16 = isSet(5, eax71)
|
||||
X86.HasAVX512BF16 = isSet(eax71, cpuid_AVX512BF16)
|
||||
}
|
||||
if X86.HasAVX {
|
||||
X86.HasAVXIFMA = isSet(23, eax71)
|
||||
X86.HasAVXVNNI = isSet(4, eax71)
|
||||
X86.HasAVXVNNIInt8 = isSet(4, edx71)
|
||||
X86.HasAVXIFMA = isSet(eax71, cpuid_AVXIFMA)
|
||||
X86.HasAVXVNNI = isSet(eax71, cpuid_AVXVNNI)
|
||||
X86.HasAVXVNNIInt8 = isSet(edx71, cpuid_AVXVNNIInt8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isSet(bitpos uint, value uint32) bool {
|
||||
return value&(1<<bitpos) != 0
|
||||
func isSet(hwc uint32, value uint32) bool {
|
||||
return hwc&value != 0
|
||||
}
|
||||
|
||||
+27
-1
@@ -160,7 +160,9 @@ const (
|
||||
keyEnd
|
||||
keyDeleteWord
|
||||
keyDeleteLine
|
||||
keyDelete
|
||||
keyClearScreen
|
||||
keyTranspose
|
||||
keyPasteStart
|
||||
keyPasteEnd
|
||||
)
|
||||
@@ -194,6 +196,8 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
|
||||
return keyDeleteLine, b[1:]
|
||||
case 12: // ^L
|
||||
return keyClearScreen, b[1:]
|
||||
case 20: // ^T
|
||||
return keyTranspose, b[1:]
|
||||
case 23: // ^W
|
||||
return keyDeleteWord, b[1:]
|
||||
case 14: // ^N
|
||||
@@ -228,6 +232,10 @@ func bytesToKey(b []byte, pasteActive bool) (rune, []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
if !pasteActive && len(b) >= 4 && b[0] == keyEscape && b[1] == '[' && b[2] == '3' && b[3] == '~' {
|
||||
return keyDelete, b[4:]
|
||||
}
|
||||
|
||||
if !pasteActive && len(b) >= 6 && b[0] == keyEscape && b[1] == '[' && b[2] == '1' && b[3] == ';' && b[4] == '3' {
|
||||
switch b[5] {
|
||||
case 'C':
|
||||
@@ -590,7 +598,7 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
|
||||
}
|
||||
t.line = t.line[:t.pos]
|
||||
t.moveCursorToPos(t.pos)
|
||||
case keyCtrlD:
|
||||
case keyCtrlD, keyDelete:
|
||||
// Erase the character under the current position.
|
||||
// The EOF case when the line is empty is handled in
|
||||
// readLine().
|
||||
@@ -600,6 +608,24 @@ func (t *Terminal) handleKey(key rune) (line string, ok bool) {
|
||||
}
|
||||
case keyCtrlU:
|
||||
t.eraseNPreviousChars(t.pos)
|
||||
case keyTranspose:
|
||||
// This transposes the two characters around the cursor and advances the cursor. Best-effort.
|
||||
if len(t.line) < 2 || t.pos < 1 {
|
||||
return
|
||||
}
|
||||
swap := t.pos
|
||||
if swap == len(t.line) {
|
||||
swap-- // special: at end of line, swap previous two chars
|
||||
}
|
||||
t.line[swap-1], t.line[swap] = t.line[swap], t.line[swap-1]
|
||||
if t.pos < len(t.line) {
|
||||
t.pos++
|
||||
}
|
||||
if t.echo {
|
||||
t.moveCursorToPos(swap - 1)
|
||||
t.writeLine(t.line[swap-1:])
|
||||
t.moveCursorToPos(t.pos)
|
||||
}
|
||||
case keyClearScreen:
|
||||
// Erases the screen and moves the cursor to the home position.
|
||||
t.queue([]rune("\x1b[2J\x1b[H"))
|
||||
|
||||
Vendored
+5
-5
@@ -88,7 +88,7 @@ github.com/fatih/color
|
||||
# github.com/gdamore/encoding v1.0.1
|
||||
## explicit; go 1.9
|
||||
github.com/gdamore/encoding
|
||||
# github.com/gdamore/tcell/v2 v2.13.5
|
||||
# github.com/gdamore/tcell/v2 v2.13.8
|
||||
## explicit; go 1.24.0
|
||||
github.com/gdamore/tcell/v2
|
||||
github.com/gdamore/tcell/v2/terminfo
|
||||
@@ -211,7 +211,7 @@ github.com/jesseduffield/go-git/v5/utils/merkletrie/internal/frame
|
||||
github.com/jesseduffield/go-git/v5/utils/merkletrie/noder
|
||||
github.com/jesseduffield/go-git/v5/utils/sync
|
||||
github.com/jesseduffield/go-git/v5/utils/trace
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20260103133810-b7e030324985
|
||||
# github.com/jesseduffield/gocui v0.3.1-0.20260128194906-9d8c3cdfac18
|
||||
## explicit; go 1.25
|
||||
github.com/jesseduffield/gocui
|
||||
# github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
|
||||
@@ -345,17 +345,17 @@ golang.org/x/net/proxy
|
||||
# golang.org/x/sync v0.19.0
|
||||
## explicit; go 1.24.0
|
||||
golang.org/x/sync/errgroup
|
||||
# golang.org/x/sys v0.39.0
|
||||
# golang.org/x/sys v0.40.0
|
||||
## explicit; go 1.24.0
|
||||
golang.org/x/sys/cpu
|
||||
golang.org/x/sys/execabs
|
||||
golang.org/x/sys/plan9
|
||||
golang.org/x/sys/unix
|
||||
golang.org/x/sys/windows
|
||||
# golang.org/x/term v0.38.0
|
||||
# golang.org/x/term v0.39.0
|
||||
## explicit; go 1.24.0
|
||||
golang.org/x/term
|
||||
# golang.org/x/text v0.32.0
|
||||
# golang.org/x/text v0.33.0
|
||||
## explicit; go 1.24.0
|
||||
golang.org/x/text/encoding
|
||||
golang.org/x/text/encoding/internal/identifier
|
||||
|
||||
Reference in New Issue
Block a user