diff --git a/docs/keybindings/Keybindings_en.md b/docs/keybindings/Keybindings_en.md index 4719d995e..a3134488e 100644 --- a/docs/keybindings/Keybindings_en.md +++ b/docs/keybindings/Keybindings_en.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: switch to a recent repo pgup: scroll up main panel (fn+up/shift+k) pgdown: scroll down main panel (fn+down/shift+j) - m: view merge/rebase options - ctrl+p: view custom patch options - R: refresh - ?: open menu - +: next screen mode (normal/half/fullscreen) - _: prev screen mode - ctrl+s: view filter-by-path options - W: open diff menu - ctrl+e: open diff menu @: open command log menu - ctrl+w: Toggle whether or not whitespace changes are shown in the diff view }: Increase the size of the context shown around changes in the diff view {: Decrease the size of the context shown around changes in the diff view :: execute custom command + ctrl+p: view custom patch options + m: view merge/rebase options + R: refresh + +: next screen mode (normal/half/fullscreen) + _: prev screen mode + ?: open menu + ctrl+s: view filter-by-path options + W: open diff menu + ctrl+e: open diff menu + ctrl+w: Toggle whether or not whitespace changes are shown in the diff view z: undo (via reflog) (experimental) ctrl+z: redo (via reflog) (experimental) P: push @@ -56,6 +56,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct `: toggle file tree view +## Commit Message + +
+ enter: confirm + esc: close ++ ## Commits
@@ -89,6 +96,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: view selected item's files+## Confirmation Panel + +
+ enter: confirm + esc: close/cancel ++ ## Files
@@ -197,6 +211,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct C: commit changes using git editor+## Menu + +
+ enter: execute + esc: close ++ ## Reflog
@@ -250,8 +271,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## Status- e: edit config file o: open config file + e: edit config file u: check for update enter: switch to a recent repo a: show all branch logs diff --git a/docs/keybindings/Keybindings_ja.md b/docs/keybindings/Keybindings_ja.md index 9b6b1b9f8..69aa0d4e7 100644 --- a/docs/keybindings/Keybindings_ja.md +++ b/docs/keybindings/Keybindings_ja.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: 最近使用したリポジトリに切り替え pgup: メインパネルを上にスクロール (fn+up/shift+k) pgdown: メインパネルを下にスクロール (fn+down/shift+j) - m: view merge/rebase options - ctrl+p: view custom patch options - R: リフレッシュ - ?: メニューを開く - +: 次のスクリーンモード (normal/half/fullscreen) - _: 前のスクリーンモード - ctrl+s: view filter-by-path options - W: 差分メニューを開く - ctrl+e: 差分メニューを開く @: コマンドログメニューを開く - ctrl+w: 空白文字の差分の表示有無を切り替え }: Increase the size of the context shown around changes in the diff view {: Decrease the size of the context shown around changes in the diff view :: カスタムコマンドを実行 + ctrl+p: view custom patch options + m: view merge/rebase options + R: リフレッシュ + +: 次のスクリーンモード (normal/half/fullscreen) + _: 前のスクリーンモード + ?: メニューを開く + ctrl+s: view filter-by-path options + W: 差分メニューを開く + ctrl+e: 差分メニューを開く + ctrl+w: 空白文字の差分の表示有無を切り替え z: アンドゥ (via reflog) (experimental) ctrl+z: リドゥ (via reflog) (experimental) P: push @@ -115,6 +115,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct `: ファイルツリーの表示を切り替え+## コミットメッセージ + ++ enter: 確認 + esc: 閉じる ++ ## サブモジュール@@ -131,8 +138,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## ステータス+## 메뉴 + +- e: 設定ファイルを編集 o: 設定ファイルを開く + e: 設定ファイルを編集 u: 更新を確認 enter: 最近使用したリポジトリに切り替え a: すべてのブランチログを表示 @@ -257,6 +264,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct C: gitエディタを使用して変更をコミット+## メニュー + ++ enter: 実行 + esc: 閉じる ++ ## リモート@@ -295,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: reset cherry-picked (copied) commits selection enter: コミットを閲覧+ +## 確認パネル + ++ enter: 確認 + esc: 閉じる/キャンセル +diff --git a/docs/keybindings/Keybindings_ko.md b/docs/keybindings/Keybindings_ko.md index 5d55a35de..d8b5bf1fd 100644 --- a/docs/keybindings/Keybindings_ko.md +++ b/docs/keybindings/Keybindings_ko.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: 최근에 사용한 저장소로 전환 pgup: 메인 패널을 위로 스크롤 (fn+up/shift+k) pgdown: 메인 패널을 아래로로 스크롤 (fn+down/shift+j) - m: view merge/rebase options - ctrl+p: 커스텀 Patch 옵션 보기 - R: 새로고침 - ?: 매뉴 열기 - +: 다음 스크린 모드 (normal/half/fullscreen) - _: 이전 스크린 모드 - ctrl+s: view filter-by-path options - W: Diff 메뉴 열기 - ctrl+e: Diff 메뉴 열기 @: 명령어 로그 메뉴 열기 - ctrl+w: 공백문자를 Diff 뷰에서 표시 여부 전환 }: diff 보기의 변경 사항 주위에 표시되는 컨텍스트의 크기를 늘리기 {: diff 보기의 변경 사항 주위에 표시되는 컨텍스트 크기 줄이기 :: execute custom command + ctrl+p: 커스텀 Patch 옵션 보기 + m: view merge/rebase options + R: 새로고침 + +: 다음 스크린 모드 (normal/half/fullscreen) + _: 이전 스크린 모드 + ?: 매뉴 열기 + ctrl+s: view filter-by-path options + W: Diff 메뉴 열기 + ctrl+e: Diff 메뉴 열기 + ctrl+w: 공백문자를 Diff 뷰에서 표시 여부 전환 z: 되돌리기 (reflog) (실험적) ctrl+z: 다시 실행 (reflog) (실험적) P: 푸시 @@ -83,6 +83,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: view selected item's files+ enter: 실행 + esc: 닫기 ++ ## 메인 패널 (Merging)@@ -168,8 +175,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## 상태+## Bevestigingspaneel + +- e: 설정 파일 수정 o: 설정 파일 열기 + e: 설정 파일 수정 u: 업데이트 확인 enter: 최근에 사용한 저장소로 전환 a: 모든 브랜치 로그 표시 @@ -259,6 +266,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct `: 파일 트리뷰로 전환+## 커밋메시지 + ++ enter: 확인 + esc: 닫기 ++ ## 태그@@ -295,3 +309,10 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct M: git mergetool를 열기 f: fetch+ +## 확인 패널 + ++ enter: 확인 + esc: 닫기/취소 +diff --git a/docs/keybindings/Keybindings_nl.md b/docs/keybindings/Keybindings_nl.md index 801c3e5a0..4cfd7d809 100644 --- a/docs/keybindings/Keybindings_nl.md +++ b/docs/keybindings/Keybindings_nl.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: wissel naar een recente repo pgup: scroll naar beneden vanaf hoofdpaneel (fn+up/shift+k) pgdown: scroll naar beneden vanaf hoofdpaneel (fn+down/shift+j) - m: bekijk merge/rebase opties - ctrl+p: bekijk aangepaste patch opties - R: verversen - ?: open menu - +: volgende scherm modus (normaal/half/groot) - _: vorige scherm modus - ctrl+s: bekijk scoping opties - W: open diff menu - ctrl+e: open diff menu @: open command log menu - ctrl+w: Toggle whether or not whitespace changes are shown in the diff view }: Increase the size of the context shown around changes in the diff view {: Decrease the size of the context shown around changes in the diff view :: voer aangepaste commando uit + ctrl+p: bekijk aangepaste patch opties + m: bekijk merge/rebase opties + R: verversen + +: volgende scherm modus (normaal/half/groot) + _: vorige scherm modus + ?: open menu + ctrl+s: bekijk scoping opties + W: open diff menu + ctrl+e: open diff menu + ctrl+w: Toggle whether or not whitespace changes are shown in the diff view z: ongedaan maken (via reflog) (experimenteel) ctrl+z: redo (via reflog) (experimenteel) P: push @@ -68,6 +68,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct f: fetch+ enter: bevestig + esc: sluiten ++ ## Branches@@ -91,6 +98,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: bekijk commits+## Commit Bericht + ++ enter: bevestig + esc: sluiten ++ ## Commit bestanden@@ -138,6 +152,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: bekijk gecommite bestanden+## Menu + ++ enter: uitvoeren + esc: sluiten ++ ## Mergen@@ -250,8 +271,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## Status- e: verander config bestand o: open config bestand + e: verander config bestand u: check voor updates enter: wissel naar een recente repo a: alle logs van de branch laten zien diff --git a/docs/keybindings/Keybindings_pl.md b/docs/keybindings/Keybindings_pl.md index d502c095d..307765014 100644 --- a/docs/keybindings/Keybindings_pl.md +++ b/docs/keybindings/Keybindings_pl.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: switch to a recent repo pgup: scroll up main panel (fn+up/shift+k) pgdown: scroll down main panel (fn+down/shift+j) - m: widok scalenia/opcje zmiany bazy - ctrl+p: view custom patch options - R: odśwież - ?: open menu - +: next screen mode (normal/half/fullscreen) - _: prev screen mode - ctrl+s: view filter-by-path options - W: open diff menu - ctrl+e: open diff menu @: open command log menu - ctrl+w: Toggle whether or not whitespace changes are shown in the diff view }: Increase the size of the context shown around changes in the diff view {: Decrease the size of the context shown around changes in the diff view :: wykonaj własną komendę + ctrl+p: view custom patch options + m: widok scalenia/opcje zmiany bazy + R: odśwież + +: next screen mode (normal/half/fullscreen) + _: prev screen mode + ?: open menu + ctrl+s: view filter-by-path options + W: open diff menu + ctrl+e: open diff menu + ctrl+w: Toggle whether or not whitespace changes are shown in the diff view z: undo (via reflog) (experimental) ctrl+z: redo (via reflog) (experimental) P: push @@ -42,6 +42,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct [: previous tab+## Commit Message + ++ enter: potwierdź + esc: zamknij ++ ## Commity@@ -75,6 +82,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct enter: przeglądaj pliki commita+## Confirmation Panel + ++ enter: potwierdź + esc: zamknij ++ ## Local Branches@@ -113,6 +127,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct esc: wyście z trybu "linia po linii"+## Menu + ++ enter: wykonaj + esc: zamknij ++ ## Pliki@@ -243,8 +264,8 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## Status- e: edytuj konfigurację o: otwórz konfigurację + e: edytuj konfigurację u: sprawdź aktualizacje enter: switch to a recent repo a: pokaż wszystkie logi gałęzi diff --git a/docs/keybindings/Keybindings_zh.md b/docs/keybindings/Keybindings_zh.md index 379561f03..0660a0d0c 100644 --- a/docs/keybindings/Keybindings_zh.md +++ b/docs/keybindings/Keybindings_zh.md @@ -8,20 +8,20 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ctrl+r: 切换到最近的仓库 pgup: 向上滚动主面板 (fn+up/shift+k) pgdown: 向下滚动主面板 (fn+down/shift+j) - m: 查看 合并/变基 选项 - ctrl+p: 查看自定义补丁选项 - R: 刷新 - ?: 打开菜单 - +: 下一屏模式(正常/半屏/全屏) - _: 上一屏模式 - ctrl+s: 查看按路径过滤选项 - W: 打开 diff 菜单 - ctrl+e: 打开 diff 菜单 @: 打开命令日志菜单 - ctrl+w: 切换是否在差异视图中显示空白字符差异 }: 扩大差异视图中显示的上下文范围 {: 缩小差异视图中显示的上下文范围 :: 执行自定义命令 + ctrl+p: 查看自定义补丁选项 + m: 查看 合并/变基 选项 + R: 刷新 + +: 下一屏模式(正常/半屏/全屏) + _: 上一屏模式 + ?: 打开菜单 + ctrl+s: 查看按路径过滤选项 + W: 打开 diff 菜单 + ctrl+e: 打开 diff 菜单 + ctrl+w: 切换是否在差异视图中显示空白字符差异 z: (通过 reflog)撤销「实验功能」 ctrl+z: (通过 reflog)重做「实验功能」 P: 推送 @@ -155,6 +155,13 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct `: 切换文件树视图+## 提交讯息 + ++ enter: 确认 + esc: 关闭 ++ ## 文件@@ -254,13 +261,27 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct ## 状态- e: 编辑配置文件 o: 打开配置文件 + e: 编辑配置文件 u: 检查更新 enter: 切换到最近的仓库 a: 显示所有分支的日志+## 确认面板 + ++ enter: 确认 + esc: 关闭 ++ +## 菜单 + ++ enter: 执行 + esc: 关闭 ++ ## 贮藏diff --git a/pkg/app/app.go b/pkg/app/app.go index 23030b37d..5d0359d17 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -33,7 +33,6 @@ type App struct { Config config.AppConfigurer OSCommand *oscommands.OSCommand Gui *gui.Gui - Updater *updates.Updater // may only need this on the Gui } func Run( @@ -87,8 +86,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { app.OSCommand = oscommands.NewOSCommand(common, config, oscommands.GetPlatform(), oscommands.NewNullGuiIO(app.Log)) - var err error - app.Updater, err = updates.NewUpdater(common, config, app.OSCommand) + updater, err := updates.NewUpdater(common, config, app.OSCommand) if err != nil { return app, err } @@ -108,7 +106,7 @@ func NewApp(config config.AppConfigurer, common *common.Common) (*App, error) { return app, err } - app.Gui, err = gui.NewGui(common, config, gitVersion, app.Updater, showRecentRepos, dirName) + app.Gui, err = gui.NewGui(common, config, gitVersion, updater, showRecentRepos, dirName) if err != nil { return app, err } diff --git a/pkg/gui/app_status_manager.go b/pkg/gui/app_status_manager.go deleted file mode 100644 index 097a19438..000000000 --- a/pkg/gui/app_status_manager.go +++ /dev/null @@ -1,131 +0,0 @@ -package gui - -import ( - "time" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/sasha-s/go-deadlock" -) - -// statusManager's job is to handle rendering of loading states and toast notifications -// that you see at the bottom left of the screen. -type statusManager struct { - statuses []appStatus - nextId int - mutex deadlock.Mutex -} - -type appStatus struct { - message string - statusType string - id int -} - -func (m *statusManager) removeStatus(id int) { - m.mutex.Lock() - defer m.mutex.Unlock() - - m.statuses = slices.Filter(m.statuses, func(status appStatus) bool { - return status.id != id - }) -} - -func (m *statusManager) addWaitingStatus(message string) int { - m.mutex.Lock() - defer m.mutex.Unlock() - - m.nextId += 1 - id := m.nextId - - newStatus := appStatus{ - message: message, - statusType: "waiting", - id: id, - } - m.statuses = append([]appStatus{newStatus}, m.statuses...) - - return id -} - -func (m *statusManager) addToastStatus(message string) int { - m.mutex.Lock() - defer m.mutex.Unlock() - - m.nextId++ - id := m.nextId - - newStatus := appStatus{ - message: message, - statusType: "toast", - id: id, - } - m.statuses = append([]appStatus{newStatus}, m.statuses...) - - go func() { - time.Sleep(time.Second * 2) - - m.removeStatus(id) - }() - - return id -} - -func (m *statusManager) getStatusString() string { - if len(m.statuses) == 0 { - return "" - } - topStatus := m.statuses[0] - if topStatus.statusType == "waiting" { - return topStatus.message + " " + utils.Loader() - } - return topStatus.message -} - -func (m *statusManager) showStatus() bool { - return len(m.statuses) > 0 -} - -func (gui *Gui) toast(message string) { - gui.statusManager.addToastStatus(message) - - gui.renderAppStatus() -} - -func (gui *Gui) renderAppStatus() { - go utils.Safe(func() { - ticker := time.NewTicker(time.Millisecond * 50) - defer ticker.Stop() - for range ticker.C { - appStatus := gui.statusManager.getStatusString() - gui.c.OnUIThread(func() error { - return gui.renderString(gui.Views.AppStatus, appStatus) - }) - - if appStatus == "" { - return - } - } - }) -} - -// withWaitingStatus wraps a function and shows a waiting status while the function is still executing -func (gui *Gui) withWaitingStatus(message string, f func() error) error { - go utils.Safe(func() { - id := gui.statusManager.addWaitingStatus(message) - - defer func() { - gui.statusManager.removeStatus(id) - }() - - gui.renderAppStatus() - - if err := f(); err != nil { - gui.c.OnUIThread(func() error { - return gui.c.Error(err) - }) - } - }) - - return nil -} diff --git a/pkg/gui/background.go b/pkg/gui/background.go index 6deac3c31..14ee70187 100644 --- a/pkg/gui/background.go +++ b/pkg/gui/background.go @@ -4,18 +4,33 @@ import ( "strings" "time" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) startBackgroundRoutines() { - userConfig := gui.UserConfig +type BackgroundRoutineMgr struct { + gui *Gui + + // if we've suspended the gui (e.g. because we've switched to a subprocess) + // we typically want to pause some things that are running like background + // file refreshes + pauseBackgroundThreads bool +} + +func (self *BackgroundRoutineMgr) PauseBackgroundThreads(pause bool) { + self.pauseBackgroundThreads = pause +} + +func (self *BackgroundRoutineMgr) startBackgroundRoutines() { + userConfig := self.gui.UserConfig if userConfig.Git.AutoFetch { fetchInterval := userConfig.Refresher.FetchInterval if fetchInterval > 0 { - go utils.Safe(gui.startBackgroundFetch) + go utils.Safe(self.startBackgroundFetch) } else { - gui.c.Log.Errorf( + self.gui.c.Log.Errorf( "Value of config option 'refresher.fetchInterval' (%d) is invalid, disabling auto-fetch", fetchInterval) } @@ -24,42 +39,44 @@ func (gui *Gui) startBackgroundRoutines() { if userConfig.Git.AutoRefresh { refreshInterval := userConfig.Refresher.RefreshInterval if refreshInterval > 0 { - gui.goEvery(time.Second*time.Duration(refreshInterval), gui.stopChan, gui.refreshFilesAndSubmodules) + self.goEvery(time.Second*time.Duration(refreshInterval), self.gui.stopChan, func() error { + return self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) + }) } else { - gui.c.Log.Errorf( + self.gui.c.Log.Errorf( "Value of config option 'refresher.refreshInterval' (%d) is invalid, disabling auto-refresh", refreshInterval) } } } -func (gui *Gui) startBackgroundFetch() { - gui.waitForIntro.Wait() - isNew := gui.IsNewRepo - userConfig := gui.UserConfig +func (self *BackgroundRoutineMgr) startBackgroundFetch() { + self.gui.waitForIntro.Wait() + isNew := self.gui.IsNewRepo + userConfig := self.gui.UserConfig if !isNew { time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second) } - err := gui.backgroundFetch() + err := self.backgroundFetch() if err != nil && strings.Contains(err.Error(), "exit status 128") && isNew { - _ = gui.c.Alert(gui.c.Tr.NoAutomaticGitFetchTitle, gui.c.Tr.NoAutomaticGitFetchBody) + _ = self.gui.c.Alert(self.gui.c.Tr.NoAutomaticGitFetchTitle, self.gui.c.Tr.NoAutomaticGitFetchBody) } else { - gui.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), gui.stopChan, func() error { - err := gui.backgroundFetch() - gui.render() + self.goEvery(time.Second*time.Duration(userConfig.Refresher.FetchInterval), self.gui.stopChan, func() error { + err := self.backgroundFetch() + self.gui.c.Render() return err }) } } -func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function func() error) { +func (self *BackgroundRoutineMgr) goEvery(interval time.Duration, stop chan struct{}, function func() error) { go utils.Safe(func() { ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ticker.C: - if gui.PauseBackgroundThreads { + if self.pauseBackgroundThreads { continue } _ = function() @@ -69,3 +86,11 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun } }) } + +func (self *BackgroundRoutineMgr) backgroundFetch() (err error) { + err = self.gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true}) + + _ = self.gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) + + return err +} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go deleted file mode 100644 index b9a25ea67..000000000 --- a/pkg/gui/branches_panel.go +++ /dev/null @@ -1,23 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) branchesRenderToMain() error { - var task types.UpdateTask - branch := gui.State.Contexts.Branches.GetSelected() - if branch == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoBranchesThisRepo) - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(branch.FullRefName()) - - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.LogTitle, - Task: task, - }, - }) -} diff --git a/pkg/gui/commit_files_panel.go b/pkg/gui/commit_files_panel.go deleted file mode 100644 index d88b95495..000000000 --- a/pkg/gui/commit_files_panel.go +++ /dev/null @@ -1,52 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/controllers" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) commitFilesRenderToMain() error { - node := gui.State.Contexts.CommitFiles.GetSelected() - if node == nil { - return nil - } - - ref := gui.State.Contexts.CommitFiles.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - - cmdObj := gui.git.WorkingTree.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false, - gui.IgnoreWhitespaceInDiffView) - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - pair := gui.c.MainViewPairs().Normal - if node.File != nil { - pair = gui.c.MainViewPairs().PatchBuilding - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: pair, - Main: &types.ViewUpdateOpts{ - Title: gui.Tr.Patch, - Task: task, - }, - Secondary: gui.secondaryPatchPanelUpdateOpts(), - }) -} - -func (gui *Gui) SwitchToCommitFilesContext(opts controllers.SwitchToCommitFilesContextOpts) error { - gui.State.Contexts.CommitFiles.SetSelectedLineIdx(0) - gui.State.Contexts.CommitFiles.SetRef(opts.Ref) - gui.State.Contexts.CommitFiles.SetTitleRef(opts.Ref.Description()) - gui.State.Contexts.CommitFiles.SetCanRebase(opts.CanRebase) - gui.State.Contexts.CommitFiles.SetParentContext(opts.Context) - gui.State.Contexts.CommitFiles.SetWindowName(opts.Context.GetWindowName()) - - if err := gui.c.Refresh(types.RefreshOptions{ - Scope: []types.RefreshableView{types.COMMIT_FILES}, - }); err != nil { - return err - } - - return gui.c.PushContext(gui.State.Contexts.CommitFiles) -} diff --git a/pkg/gui/commit_message_panel.go b/pkg/gui/commit_message_panel.go deleted file mode 100644 index 051c76573..000000000 --- a/pkg/gui/commit_message_panel.go +++ /dev/null @@ -1,36 +0,0 @@ -package gui - -import ( - "strconv" - "strings" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func (gui *Gui) handleCommitMessageFocused() error { - message := utils.ResolvePlaceholderString( - gui.c.Tr.CommitMessageConfirm, - map[string]string{ - "keyBindClose": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Return), - "keyBindConfirm": keybindings.Label(gui.c.UserConfig.Keybinding.Universal.Confirm), - }, - ) - - gui.RenderCommitLength() - - return gui.renderString(gui.Views.Options, message) -} - -func (gui *Gui) RenderCommitLength() { - if !gui.c.UserConfig.Gui.CommitLength.Show { - return - } - - gui.Views.CommitMessage.Subtitle = getBufferLength(gui.Views.CommitMessage) -} - -func getBufferLength(view *gocui.View) string { - return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " " -} diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go deleted file mode 100644 index 2b75be4d0..000000000 --- a/pkg/gui/commits_panel.go +++ /dev/null @@ -1,88 +0,0 @@ -package gui - -import ( - "github.com/fsmiamoto/git-todo-parser/todo" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -// after selecting the 200th commit, we'll load in all the rest -const COMMIT_THRESHOLD = 200 - -// list panel functions - -func (gui *Gui) getSelectedLocalCommit() *models.Commit { - return gui.State.Contexts.LocalCommits.GetSelected() -} - -func (gui *Gui) onCommitFocus() error { - context := gui.State.Contexts.LocalCommits - if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { - context.SetLimitCommits(false) - go utils.Safe(func() { - if err := gui.refreshCommitsWithLimit(); err != nil { - _ = gui.c.Error(err) - } - }) - } - - return nil -} - -func (gui *Gui) branchCommitsRenderToMain() error { - var task types.UpdateTask - commit := gui.State.Contexts.LocalCommits.GetSelected() - if commit == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoCommitsThisBranch) - } else if commit.Action == todo.UpdateRef { - task = types.NewRenderStringTask( - utils.ResolvePlaceholderString( - gui.c.Tr.UpdateRefHere, - map[string]string{ - "ref": commit.Name, - })) - } else { - cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(), - gui.IgnoreWhitespaceInDiffView) - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Patch", - Task: task, - }, - Secondary: gui.secondaryPatchPanelUpdateOpts(), - }) -} - -func (gui *Gui) secondaryPatchPanelUpdateOpts() *types.ViewUpdateOpts { - if gui.git.Patch.PatchBuilder.Active() { - patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(false) - - return &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(patch), - Title: gui.Tr.CustomPatch, - } - } - - return nil -} - -func (gui *Gui) refForLog() string { - bisectInfo := gui.git.Bisect.GetInfo() - gui.State.Model.BisectInfo = bisectInfo - - if !bisectInfo.Started() { - return "HEAD" - } - - // need to see if our bisect's current commit is reachable from our 'new' ref. - if bisectInfo.Bisecting() && !gui.git.Bisect.ReachableFromStart(bisectInfo) { - return bisectInfo.GetNewSha() - } - - return bisectInfo.GetStartSha() -} diff --git a/pkg/gui/confirmation_panel.go b/pkg/gui/confirmation_panel.go deleted file mode 100644 index bcdec70d1..000000000 --- a/pkg/gui/confirmation_panel.go +++ /dev/null @@ -1,311 +0,0 @@ -package gui - -import ( - "context" - "fmt" - "strings" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/theme" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/mattn/go-runewidth" -) - -// This file is for the rendering of confirmation panels along with setting and handling associated -// keybindings. - -func (gui *Gui) wrappedConfirmationFunction(cancel context.CancelFunc, function func() error) func() error { - return func() error { - cancel() - - if err := gui.c.PopContext(); err != nil { - return err - } - - if function != nil { - if err := function(); err != nil { - return gui.c.Error(err) - } - } - - return nil - } -} - -func (gui *Gui) wrappedPromptConfirmationFunction(cancel context.CancelFunc, function func(string) error, getResponse func() string) func() error { - return func() error { - cancel() - - if err := gui.c.PopContext(); err != nil { - return err - } - - if function != nil { - if err := function(getResponse()); err != nil { - return gui.c.Error(err) - } - } - - return nil - } -} - -func (gui *Gui) deactivateConfirmationPrompt() { - gui.Mutexes.PopupMutex.Lock() - gui.State.CurrentPopupOpts = nil - gui.Mutexes.PopupMutex.Unlock() - - gui.Views.Confirmation.Visible = false - gui.Views.Suggestions.Visible = false - - gui.clearConfirmationViewKeyBindings() -} - -func (gui *Gui) getMessageHeight(wrap bool, message string, width int) int { - lines := strings.Split(message, "\n") - lineCount := 0 - // if we need to wrap, calculate height to fit content within view's width - if wrap { - for _, line := range lines { - lineCount += runewidth.StringWidth(line)/width + 1 - } - } else { - lineCount = len(lines) - } - return lineCount -} - -func (gui *Gui) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { - return gui.getPopupPanelDimensionsAux(panelWidth, contentHeight) -} - -func (gui *Gui) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { - width, height := gui.g.Size() - if panelHeight > height*3/4 { - panelHeight = height * 3 / 4 - } - return width/2 - panelWidth/2, - height/2 - panelHeight/2 - panelHeight%2 - 1, - width/2 + panelWidth/2, - height/2 + panelHeight/2 -} - -func (gui *Gui) getConfirmationPanelWidth() int { - width, _ := gui.g.Size() - // we want a minimum width up to a point, then we do it based on ratio. - panelWidth := 4 * width / 7 - minWidth := 80 - if panelWidth < minWidth { - if width-2 < minWidth { - panelWidth = width - 2 - } else { - panelWidth = minWidth - } - } - - return panelWidth -} - -func (gui *Gui) prepareConfirmationPanel( - ctx context.Context, - opts types.ConfirmOpts, -) error { - gui.Views.Confirmation.HasLoader = opts.HasLoader - if opts.HasLoader { - gui.g.StartTicking(ctx) - } - gui.Views.Confirmation.Title = opts.Title - // for now we do not support wrapping in our editor - gui.Views.Confirmation.Wrap = !opts.Editable - gui.Views.Confirmation.FgColor = theme.GocuiDefaultTextColor - gui.Views.Confirmation.Mask = runeForMask(opts.Mask) - _ = gui.Views.Confirmation.SetOrigin(0, 0) - - gui.findSuggestions = opts.FindSuggestionsFunc - if opts.FindSuggestionsFunc != nil { - suggestionsView := gui.Views.Suggestions - suggestionsView.Wrap = false - suggestionsView.FgColor = theme.GocuiDefaultTextColor - gui.setSuggestions(opts.FindSuggestionsFunc("")) - suggestionsView.Visible = true - suggestionsView.Title = fmt.Sprintf(gui.c.Tr.SuggestionsTitle, gui.c.UserConfig.Keybinding.Universal.TogglePanel) - } - - gui.resizeConfirmationPanel() - return nil -} - -func runeForMask(mask bool) rune { - if mask { - return '*' - } - return 0 -} - -func (gui *Gui) createPopupPanel(ctx context.Context, opts types.CreatePopupPanelOpts) error { - gui.Mutexes.PopupMutex.Lock() - defer gui.Mutexes.PopupMutex.Unlock() - - ctx, cancel := context.WithCancel(ctx) - - // we don't allow interruptions of non-loader popups in case we get stuck somehow - // e.g. a credentials popup never gets its required user input so a process hangs - // forever. - // The proper solution is to have a queue of popup options - if gui.State.CurrentPopupOpts != nil && !gui.State.CurrentPopupOpts.HasLoader { - gui.Log.Error("ignoring create popup panel because a popup panel is already open") - cancel() - return nil - } - - // remove any previous keybindings - gui.clearConfirmationViewKeyBindings() - - err := gui.prepareConfirmationPanel( - ctx, - types.ConfirmOpts{ - Title: opts.Title, - Prompt: opts.Prompt, - HasLoader: opts.HasLoader, - FindSuggestionsFunc: opts.FindSuggestionsFunc, - Editable: opts.Editable, - Mask: opts.Mask, - }) - if err != nil { - cancel() - return err - } - confirmationView := gui.Views.Confirmation - confirmationView.Editable = opts.Editable - confirmationView.Editor = gocui.EditorFunc(gui.promptEditor) - - if opts.Editable { - textArea := confirmationView.TextArea - textArea.Clear() - textArea.TypeString(opts.Prompt) - gui.resizeConfirmationPanel() - confirmationView.RenderTextArea() - } else { - if err := gui.renderString(confirmationView, style.AttrBold.Sprint(opts.Prompt)); err != nil { - cancel() - return err - } - } - - if err := gui.setKeyBindings(cancel, opts); err != nil { - cancel() - return err - } - - gui.State.CurrentPopupOpts = &opts - - return gui.c.PushContext(gui.State.Contexts.Confirmation) -} - -func (gui *Gui) setKeyBindings(cancel context.CancelFunc, opts types.CreatePopupPanelOpts) error { - actions := utils.ResolvePlaceholderString( - gui.c.Tr.CloseConfirm, - map[string]string{ - "keyBindClose": "esc", - "keyBindConfirm": "enter", - }, - ) - - _ = gui.renderString(gui.Views.Options, actions) - var onConfirm func() error - if opts.HandleConfirmPrompt != nil { - onConfirm = gui.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return gui.Views.Confirmation.TextArea.GetContent() }) - } else { - onConfirm = gui.wrappedConfirmationFunction(cancel, opts.HandleConfirm) - } - - keybindingConfig := gui.c.UserConfig.Keybinding - onSuggestionConfirm := gui.wrappedPromptConfirmationFunction( - cancel, - opts.HandleConfirmPrompt, - gui.getSelectedSuggestionValue, - ) - - bindings := []*types.Binding{ - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.Confirm), - Handler: onConfirm, - }, - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.Return), - Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose), - }, - { - ViewName: "confirmation", - Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), - Handler: func() error { - if len(gui.State.Suggestions) > 0 { - return gui.replaceContext(gui.State.Contexts.Suggestions) - } - return nil - }, - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.Confirm), - Handler: onSuggestionConfirm, - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.Return), - Handler: gui.wrappedConfirmationFunction(cancel, opts.HandleClose), - }, - { - ViewName: "suggestions", - Key: keybindings.GetKey(keybindingConfig.Universal.TogglePanel), - Handler: func() error { return gui.replaceContext(gui.State.Contexts.Confirmation) }, - }, - } - - for _, binding := range bindings { - if err := gui.SetKeybinding(binding); err != nil { - return err - } - } - - return nil -} - -func (gui *Gui) clearConfirmationViewKeyBindings() { - keybindingConfig := gui.c.UserConfig.Keybinding - _ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone) - _ = gui.g.DeleteKeybinding("confirmation", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone) - _ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Confirm), gocui.ModNone) - _ = gui.g.DeleteKeybinding("suggestions", keybindings.GetKey(keybindingConfig.Universal.Return), gocui.ModNone) -} - -func (gui *Gui) refreshSuggestions() { - gui.suggestionsAsyncHandler.Do(func() func() { - findSuggestionsFn := gui.findSuggestions - if findSuggestionsFn != nil { - suggestions := gui.findSuggestions(gui.c.GetPromptInput()) - return func() { gui.setSuggestions(suggestions) } - } else { - return func() {} - } - }) -} - -func (gui *Gui) handleAskFocused() error { - keybindingConfig := gui.c.UserConfig.Keybinding - - message := utils.ResolvePlaceholderString( - gui.c.Tr.CloseConfirm, - map[string]string{ - "keyBindClose": keybindings.Label(keybindingConfig.Universal.Return), - "keyBindConfirm": keybindings.Label(keybindingConfig.Universal.Confirm), - }, - ) - - return gui.renderString(gui.Views.Options, message) -} diff --git a/pkg/gui/constants/constants.go b/pkg/gui/constants/constants.go new file mode 100644 index 000000000..b6c786b73 --- /dev/null +++ b/pkg/gui/constants/constants.go @@ -0,0 +1,3 @@ +package constants + +const SEARCH_PREFIX = "search: " diff --git a/pkg/gui/context.go b/pkg/gui/context.go index d69b245ff..252815e18 100644 --- a/pkg/gui/context.go +++ b/pkg/gui/context.go @@ -1,12 +1,10 @@ package gui import ( - "sort" - "strings" + "errors" + "sync" - "github.com/jesseduffield/generics/maps" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" @@ -17,46 +15,66 @@ import ( // you in the menu context. When contexts are activated/deactivated certain things need // to happen like showing/hiding views and rendering content. -func (gui *Gui) popupViewNames() []string { - popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { - return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP - }) +type ContextMgr struct { + ContextStack []types.Context + sync.RWMutex + gui *Gui - return slices.Map(popups, func(c types.Context) string { - return c.GetViewName() - }) + allContexts *context.ContextTree } -// use replaceContext when you don't want to return to the original context upon +func NewContextMgr( + gui *Gui, + allContexts *context.ContextTree, +) *ContextMgr { + return &ContextMgr{ + ContextStack: []types.Context{}, + RWMutex: sync.RWMutex{}, + gui: gui, + allContexts: allContexts, + } +} + +// use when you don't want to return to the original context upon // hitting escape: you want to go that context's parent instead. -func (gui *Gui) replaceContext(c types.Context) error { +func (self *ContextMgr) Replace(c types.Context) error { if !c.IsFocusable() { return nil } - gui.State.ContextManager.Lock() + self.Lock() - if len(gui.State.ContextManager.ContextStack) == 0 { - gui.State.ContextManager.ContextStack = []types.Context{c} + if len(self.ContextStack) == 0 { + self.ContextStack = []types.Context{c} } else { // replace the last item with the given item - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack[0:len(gui.State.ContextManager.ContextStack)-1], c) + self.ContextStack = append(self.ContextStack[0:len(self.ContextStack)-1], c) } - defer gui.State.ContextManager.Unlock() + defer self.Unlock() - return gui.activateContext(c, types.OnFocusOpts{}) + return self.activateContext(c, types.OnFocusOpts{}) } -func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error { +func (self *ContextMgr) Push(c types.Context, opts ...types.OnFocusOpts) error { + if len(opts) > 1 { + return errors.New("cannot pass multiple opts to Push") + } + + singleOpts := types.OnFocusOpts{} + if len(opts) > 0 { + // using triple dot but you should only ever pass one of these opt structs + singleOpts = opts[0] + } + if !c.IsFocusable() { return nil } - contextsToDeactivate, contextToActivate := gui.pushToContextStack(c) + contextsToDeactivate, contextToActivate := self.pushToContextStack(c) for _, contextToDeactivate := range contextsToDeactivate { - if err := gui.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil { + if err := self.deactivateContext(contextToDeactivate, types.OnFocusLostOpts{NewContextKey: c.GetKey()}); err != nil { return err } } @@ -65,43 +83,43 @@ func (gui *Gui) pushContext(c types.Context, opts types.OnFocusOpts) error { return nil } - return gui.activateContext(contextToActivate, opts) + return self.activateContext(contextToActivate, singleOpts) } // Adjusts the context stack based on the context that's being pushed and // returns (contexts to deactivate, context to activate) -func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Context) { +func (self *ContextMgr) pushToContextStack(c types.Context) ([]types.Context, types.Context) { contextsToDeactivate := []types.Context{} - gui.State.ContextManager.Lock() - defer gui.State.ContextManager.Unlock() + self.Lock() + defer self.Unlock() - if len(gui.State.ContextManager.ContextStack) > 0 && - c == gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] { + if len(self.ContextStack) > 0 && + c == self.ContextStack[len(self.ContextStack)-1] { // Context being pushed is already on top of the stack: nothing to // deactivate or activate return contextsToDeactivate, nil } - if len(gui.State.ContextManager.ContextStack) == 0 { - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) + if len(self.ContextStack) == 0 { + self.ContextStack = append(self.ContextStack, c) } else if c.GetKind() == types.SIDE_CONTEXT { // if we are switching to a side context, remove all other contexts in the stack - contextsToDeactivate = gui.State.ContextManager.ContextStack - gui.State.ContextManager.ContextStack = []types.Context{c} + contextsToDeactivate = self.ContextStack + self.ContextStack = []types.Context{c} } else if c.GetKind() == types.MAIN_CONTEXT { // if we're switching to a main context, remove all other main contexts in the stack contextsToKeep := []types.Context{} - for _, stackContext := range gui.State.ContextManager.ContextStack { + for _, stackContext := range self.ContextStack { if stackContext.GetKind() == types.MAIN_CONTEXT { contextsToDeactivate = append(contextsToDeactivate, stackContext) } else { contextsToKeep = append(contextsToKeep, stackContext) } } - gui.State.ContextManager.ContextStack = append(contextsToKeep, c) + self.ContextStack = append(contextsToKeep, c) } else { - topContext := gui.currentContextWithoutLock() + topContext := self.currentContextWithoutLock() // if we're pushing the same context on, we do nothing. if topContext.GetKey() != c.GetKey() { @@ -114,48 +132,48 @@ func (gui *Gui) pushToContextStack(c types.Context) ([]types.Context, types.Cont (topContext.GetKind() == types.MAIN_CONTEXT && c.GetKind() == types.MAIN_CONTEXT) { contextsToDeactivate = append(contextsToDeactivate, topContext) - _, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) + _, self.ContextStack = slices.Pop(self.ContextStack) } - gui.State.ContextManager.ContextStack = append(gui.State.ContextManager.ContextStack, c) + self.ContextStack = append(self.ContextStack, c) } } return contextsToDeactivate, c } -func (gui *Gui) popContext() error { - gui.State.ContextManager.Lock() +func (self *ContextMgr) Pop() error { + self.Lock() - if len(gui.State.ContextManager.ContextStack) == 1 { + if len(self.ContextStack) == 1 { // cannot escape from bottommost context - gui.State.ContextManager.Unlock() + self.Unlock() return nil } var currentContext types.Context - currentContext, gui.State.ContextManager.ContextStack = slices.Pop(gui.State.ContextManager.ContextStack) + currentContext, self.ContextStack = slices.Pop(self.ContextStack) - newContext := gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] + newContext := self.ContextStack[len(self.ContextStack)-1] - gui.State.ContextManager.Unlock() + self.Unlock() - if err := gui.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil { + if err := self.deactivateContext(currentContext, types.OnFocusLostOpts{NewContextKey: newContext.GetKey()}); err != nil { return err } - return gui.activateContext(newContext, types.OnFocusOpts{}) + return self.activateContext(newContext, types.OnFocusOpts{}) } -func (gui *Gui) removeContexts(contextsToRemove []types.Context) error { - gui.State.ContextManager.Lock() +func (self *ContextMgr) RemoveContexts(contextsToRemove []types.Context) error { + self.Lock() - if len(gui.State.ContextManager.ContextStack) == 1 { - gui.State.ContextManager.Unlock() + if len(self.ContextStack) == 1 { + self.Unlock() return nil } - rest := lo.Filter(gui.State.ContextManager.ContextStack, func(context types.Context, _ int) bool { + rest := lo.Filter(self.ContextStack, func(context types.Context, _ int) bool { for _, contextToRemove := range contextsToRemove { if context.GetKey() == contextToRemove.GetKey() { return false @@ -163,25 +181,25 @@ func (gui *Gui) removeContexts(contextsToRemove []types.Context) error { } return true }) - gui.State.ContextManager.ContextStack = rest + self.ContextStack = rest contextToActivate := rest[len(rest)-1] - gui.State.ContextManager.Unlock() + self.Unlock() for _, context := range contextsToRemove { - if err := gui.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil { + if err := self.deactivateContext(context, types.OnFocusLostOpts{NewContextKey: contextToActivate.GetKey()}); err != nil { return err } } // activate the item at the top of the stack - return gui.activateContext(contextToActivate, types.OnFocusOpts{}) + return self.activateContext(contextToActivate, types.OnFocusOpts{}) } -func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { - view, _ := gui.g.View(c.GetViewName()) +func (self *ContextMgr) deactivateContext(c types.Context, opts types.OnFocusLostOpts) error { + view, _ := self.gui.c.GocuiGui().View(c.GetViewName()) if view != nil && view.IsSearching() { - if err := gui.onSearchEscape(); err != nil { + if err := self.gui.onSearchEscape(); err != nil { return err } } @@ -200,34 +218,17 @@ func (gui *Gui) deactivateContext(c types.Context, opts types.OnFocusLostOpts) e return nil } -// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed -// if the context's view is set to another context we do nothing. -// if the context's view is the current view we trigger a focus; re-selecting the current item. -func (gui *Gui) postRefreshUpdate(c types.Context) error { - if err := c.HandleRender(); err != nil { - return err - } - - if gui.currentViewName() == c.GetViewName() { - if err := c.HandleFocus(types.OnFocusOpts{}); err != nil { - return err - } - } - - return nil -} - -func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { +func (self *ContextMgr) activateContext(c types.Context, opts types.OnFocusOpts) error { viewName := c.GetViewName() - v, err := gui.g.View(viewName) + v, err := self.gui.c.GocuiGui().View(viewName) if err != nil { return err } - gui.setWindowContext(c) + self.gui.helpers.Window.SetWindowContext(c) - gui.moveToTopOfWindow(c) - if _, err := gui.g.SetCurrentView(viewName); err != nil { + self.gui.helpers.Window.MoveToTopOfWindow(c) + if _, err := self.gui.c.GocuiGui().SetCurrentView(viewName); err != nil { return err } @@ -238,14 +239,9 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { v.Visible = true - gui.g.Cursor = v.Editable + self.gui.c.GocuiGui().Cursor = v.Editable - // render the options available for the current context at the bottom of the screen - optionsMap := c.GetOptionsMap() - if optionsMap == nil { - optionsMap = gui.globalOptionsMap() - } - gui.renderOptionsMap(optionsMap) + self.gui.renderContextOptionsMap(c) if err := c.HandleFocus(opts); err != nil { return err @@ -254,63 +250,27 @@ func (gui *Gui) activateContext(c types.Context, opts types.OnFocusOpts) error { return nil } -func (gui *Gui) optionsMapToString(optionsMap map[string]string) string { - options := maps.MapToSlice(optionsMap, func(key string, description string) string { - return key + ": " + description - }) - sort.Strings(options) - return strings.Join(options, ", ") +func (self *ContextMgr) Current() types.Context { + self.RLock() + defer self.RUnlock() + + return self.currentContextWithoutLock() } -func (gui *Gui) renderOptionsMap(optionsMap map[string]string) { - _ = gui.renderString(gui.Views.Options, gui.optionsMapToString(optionsMap)) -} - -// // currently unused -// func (gui *Gui) renderContextStack() string { -// result := "" -// for _, context := range gui.State.ContextManager.ContextStack { -// result += string(context.GetKey()) + "\n" -// } -// return result -// } - -func (gui *Gui) currentContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - return gui.currentContextWithoutLock() -} - -func (gui *Gui) currentContextWithoutLock() types.Context { - if len(gui.State.ContextManager.ContextStack) == 0 { - return gui.defaultSideContext() +func (self *ContextMgr) currentContextWithoutLock() types.Context { + if len(self.ContextStack) == 0 { + return self.gui.defaultSideContext() } - return gui.State.ContextManager.ContextStack[len(gui.State.ContextManager.ContextStack)-1] + return self.ContextStack[len(self.ContextStack)-1] } -// the status panel is not yet a list context (and may never be), so this method is not -// quite the same as currentSideContext() -func (gui *Gui) currentSideListContext() types.IListContext { - context := gui.currentSideContext() - listContext, ok := context.(types.IListContext) - if !ok { - return nil - } - return listContext -} +// Note that this could return the 'status' context which is not itself a list context. +func (self *ContextMgr) CurrentSide() types.Context { + self.RLock() + defer self.RUnlock() -func (gui *Gui) currentSideContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - stack := gui.State.ContextManager.ContextStack - - // on startup the stack can be empty so we'll return an empty string in that case - if len(stack) == 0 { - return gui.defaultSideContext() - } + stack := self.ContextStack // find the first context in the stack with the type of types.SIDE_CONTEXT for i := range stack { @@ -321,22 +281,22 @@ func (gui *Gui) currentSideContext() types.Context { } } - return gui.defaultSideContext() + return self.gui.defaultSideContext() } // static as opposed to popup -func (gui *Gui) currentStaticContext() types.Context { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() +func (self *ContextMgr) CurrentStatic() types.Context { + self.RLock() + defer self.RUnlock() - return gui.currentStaticContextWithoutLock() + return self.currentStaticContextWithoutLock() } -func (gui *Gui) currentStaticContextWithoutLock() types.Context { - stack := gui.State.ContextManager.ContextStack +func (self *ContextMgr) currentStaticContextWithoutLock() types.Context { + stack := self.ContextStack if len(stack) == 0 { - return gui.defaultSideContext() + return self.gui.defaultSideContext() } // find the first context in the stack without a popup type @@ -348,88 +308,43 @@ func (gui *Gui) currentStaticContextWithoutLock() types.Context { } } - return gui.defaultSideContext() + return self.gui.defaultSideContext() } -func (gui *Gui) defaultSideContext() types.Context { - if gui.State.Modes.Filtering.Active() { - return gui.State.Contexts.LocalCommits - } else { - return gui.State.Contexts.Files +func (self *ContextMgr) ForEach(f func(types.Context)) { + self.RLock() + defer self.RUnlock() + + for _, context := range self.gui.State.ContextMgr.ContextStack { + f(context) } } -// getFocusLayout returns a manager function for when view gain and lose focus -func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { - var previousView *gocui.View - return func(g *gocui.Gui) error { - newView := gui.g.CurrentView() - // for now we don't consider losing focus to a popup panel as actually losing focus - if newView != previousView && !gui.isPopupPanel(newView.Name()) { - if err := gui.onViewFocusLost(previousView); err != nil { - return err - } +func (self *ContextMgr) IsCurrent(c types.Context) bool { + return self.Current().GetKey() == c.GetKey() +} - previousView = newView +// all list contexts +func (self *ContextMgr) AllList() []types.IListContext { + var listContexts []types.IListContext + + for _, context := range self.allContexts.Flatten() { + if listContext, ok := context.(types.IListContext); ok { + listContexts = append(listContexts, listContext) } - return nil - } -} - -func (gui *Gui) onViewFocusLost(oldView *gocui.View) error { - if oldView == nil { - return nil } - oldView.Highlight = false - - _ = oldView.SetOriginX(0) - - return nil + return listContexts } -func (gui *Gui) TransientContexts() []types.Context { - return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool { - return context.IsTransient() - }) -} +func (self *ContextMgr) AllPatchExplorer() []types.IPatchExplorerContext { + var listContexts []types.IPatchExplorerContext -func (gui *Gui) rerenderView(view *gocui.View) error { - context, ok := gui.contextForView(view.Name()) - if !ok { - gui.Log.Errorf("no context found for view %s", view.Name()) - return nil + for _, context := range self.allContexts.Flatten() { + if listContext, ok := context.(types.IPatchExplorerContext); ok { + listContexts = append(listContexts, listContext) + } } - return context.HandleRender() + return listContexts } - -func (gui *Gui) getSideContextSelectedItemId() string { - currentSideContext := gui.currentSideListContext() - if currentSideContext == nil { - return "" - } - - return currentSideContext.GetSelectedItemId() -} - -// currently unused -// func (gui *Gui) getCurrentSideView() *gocui.View { -// currentSideContext := gui.currentSideContext() -// if currentSideContext == nil { -// return nil -// } - -// view, _ := gui.g.View(currentSideContext.GetViewName()) - -// return view -// } - -// currently unused -// func (gui *Gui) renderContextStack() string { -// result := "" -// for _, context := range gui.State.ContextManager.ContextStack { -// result += context.GetViewName() + "\n" -// } -// return result -// } diff --git a/pkg/gui/context/base_context.go b/pkg/gui/context/base_context.go index 86d15c79a..421476368 100644 --- a/pkg/gui/context/base_context.go +++ b/pkg/gui/context/base_context.go @@ -16,6 +16,9 @@ type BaseContext struct { keybindingsFns []types.KeybindingsFn mouseKeybindingsFns []types.MouseKeybindingsFn onClickFn func() error + onRenderToMainFn func() error + onFocusFn onFocusFn + onFocusLostFn onFocusLostFn focusable bool transient bool @@ -25,6 +28,11 @@ type BaseContext struct { *ParentContextMgr } +type ( + onFocusFn = func(types.OnFocusOpts) error + onFocusLostFn = func(types.OnFocusLostOpts) error +) + var _ types.IBaseContext = &BaseContext{} type NewBaseContextOpts struct { @@ -129,6 +137,36 @@ func (self *BaseContext) GetOnClick() func() error { return self.onClickFn } +func (self *BaseContext) AddOnRenderToMainFn(fn func() error) { + if fn != nil { + self.onRenderToMainFn = fn + } +} + +func (self *BaseContext) GetOnRenderToMain() func() error { + return self.onRenderToMainFn +} + +func (self *BaseContext) AddOnFocusFn(fn onFocusFn) { + if fn != nil { + self.onFocusFn = fn + } +} + +func (self *BaseContext) GetOnFocus() onFocusFn { + return self.onFocusFn +} + +func (self *BaseContext) AddOnFocusLostFn(fn onFocusLostFn) { + if fn != nil { + self.onFocusLostFn = fn + } +} + +func (self *BaseContext) GetOnFocusLost() onFocusLostFn { + return self.onFocusLostFn +} + func (self *BaseContext) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { bindings := []*gocui.ViewMouseBinding{} for i := range self.mouseKeybindingsFns { diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index a3e404fdb..1d6453c13 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,40 +11,40 @@ type BranchesContext struct { *ListContextTrait } -var _ types.IListContext = (*BranchesContext)(nil) +var ( + _ types.IListContext = (*BranchesContext)(nil) + _ types.DiffableContext = (*BranchesContext)(nil) +) -func NewBranchesContext( - getModel func() []*models.Branch, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewBranchesContext(c *ContextCommon) *BranchesContext { + viewModel := NewBasicViewModel(func() []*models.Branch { return c.Model().Branches }) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetBranchListDisplayStrings( + c.Model().Branches, + c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, + c.Modes().Diffing.Ref, + c.Tr, + ) + } - c *types.HelperCommon, -) *BranchesContext { - viewModel := NewBasicViewModel(getModel) - - return &BranchesContext{ + self := &BranchesContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Branches, WindowName: "branches", Key: LOCAL_BRANCHES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, }, } + + return self } func (self *BranchesContext) GetSelectedItemId() string { @@ -63,3 +63,16 @@ func (self *BranchesContext) GetSelectedRef() types.Ref { } return branch } + +func (self *BranchesContext) GetDiffTerminals() []string { + // for our local branches we want to include both the branch and its upstream + branch := self.GetSelected() + if branch != nil { + names := []string{branch.ID()} + if branch.IsTrackingRemote() { + names = append(names, branch.ID()+"@{u}") + } + return names + } + return nil +} diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index 4a28ac4c5..96b6f2fcf 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -1,9 +1,11 @@ package context import ( - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -13,20 +15,28 @@ type CommitFilesContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*CommitFilesContext)(nil) +var ( + _ types.IListContext = (*CommitFilesContext)(nil) + _ types.DiffableContext = (*CommitFilesContext)(nil) +) -func NewCommitFilesContext( - getModel func() []*models.CommitFile, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext { + viewModel := filetree.NewCommitFileTreeViewModel( + func() []*models.CommitFile { return c.Model().CommitFiles }, + c.Log, + c.UserConfig.Gui.ShowFileTree, + ) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, + getDisplayStrings := func(startIdx int, length int) [][]string { + if viewModel.Len() == 0 { + return [][]string{{style.FgRed.Sprint("(none)")}} + } - c *types.HelperCommon, -) *CommitFilesContext { - viewModel := filetree.NewCommitFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) + lines := presentation.RenderCommitFileTree(viewModel, c.Modes().Diffing.Ref, c.Git().Patch.PatchBuilder) + return slices.Map(lines, func(line string) []string { + return []string{line} + }) + } return &CommitFilesContext{ CommitFileTreeViewModel: viewModel, @@ -34,18 +44,14 @@ func NewCommitFilesContext( ListContextTrait: &ListContextTrait{ Context: NewSimpleContext( NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().CommitFiles, WindowName: "commits", Key: COMMIT_FILES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, Transient: true, }), - ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + ), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -61,3 +67,7 @@ func (self *CommitFilesContext) GetSelectedItemId() string { return item.ID() } + +func (self *CommitFilesContext) GetDiffTerminals() []string { + return []string{self.GetRef().RefName()} +} diff --git a/pkg/gui/context/commit_message_context.go b/pkg/gui/context/commit_message_context.go index 6be4763e5..4241f859f 100644 --- a/pkg/gui/context/commit_message_context.go +++ b/pkg/gui/context/commit_message_context.go @@ -1,15 +1,21 @@ package context import ( + "strconv" + "strings" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type CommitMessageContext struct { + c *ContextCommon types.Context viewModel *CommitMessageViewModel } +var _ types.Context = (*CommitMessageContext)(nil) + // when selectedIndex (see below) is set to this value, it means that we're not // currently viewing a commit message of an existing commit: instead we're making our own // new commit message @@ -35,22 +41,21 @@ type CommitMessageViewModel struct { } func NewCommitMessageContext( - view *gocui.View, - opts ContextCallbackOpts, + c *ContextCommon, ) *CommitMessageContext { viewModel := &CommitMessageViewModel{} return &CommitMessageContext{ + c: c, viewModel: viewModel, Context: NewSimpleContext( NewBaseContext(NewBaseContextOpts{ Kind: types.PERSISTENT_POPUP, - View: view, + View: c.Views().CommitMessage, WindowName: "commitMessage", Key: COMMIT_MESSAGE_CONTEXT_KEY, Focusable: true, HasUncontrolledBounds: true, }), - opts, ), } } @@ -93,3 +98,15 @@ func (self *CommitMessageContext) SetPanelState(index int, title string, preserv self.viewModel.onConfirm = onConfirm self.GetView().Title = title } + +func (self *CommitMessageContext) RenderCommitLength() { + if !self.c.UserConfig.Gui.CommitLength.Show { + return + } + + self.c.Views().CommitMessage.Subtitle = getBufferLength(self.c.Views().CommitMessage) +} + +func getBufferLength(view *gocui.View) string { + return " " + strconv.Itoa(strings.Count(view.TextArea.GetContent(), "")-1) + " " +} diff --git a/pkg/gui/context/confirmation_context.go b/pkg/gui/context/confirmation_context.go new file mode 100644 index 000000000..69c498e49 --- /dev/null +++ b/pkg/gui/context/confirmation_context.go @@ -0,0 +1,35 @@ +package context + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ConfirmationContext struct { + *SimpleContext + c *ContextCommon + + State ConfirmationContextState +} + +type ConfirmationContextState struct { + OnConfirm func() error + OnClose func() error +} + +var _ types.Context = (*ConfirmationContext)(nil) + +func NewConfirmationContext( + c *ContextCommon, +) *ConfirmationContext { + return &ConfirmationContext{ + c: c, + SimpleContext: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ + View: c.Views().Confirmation, + WindowName: "confirmation", + Key: CONFIRMATION_CONTEXT_KEY, + Kind: types.TEMPORARY_POPUP, + Focusable: true, + HasUncontrolledBounds: true, + })), + } +} diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go index 7fc0d52f6..ab188d761 100644 --- a/pkg/gui/context/context.go +++ b/pkg/gui/context/context.go @@ -97,7 +97,7 @@ type ContextTree struct { CustomPatchBuilder *PatchExplorerContext CustomPatchBuilderSecondary types.Context MergeConflicts *MergeConflictsContext - Confirmation types.Context + Confirmation *ConfirmationContext CommitMessage *CommitMessageContext CommitDescription types.Context CommandLog types.Context diff --git a/pkg/gui/context/context_common.go b/pkg/gui/context/context_common.go new file mode 100644 index 000000000..9425280a4 --- /dev/null +++ b/pkg/gui/context/context_common.go @@ -0,0 +1,11 @@ +package context + +import ( + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ContextCommon struct { + *common.Common + types.IGuiCommon +} diff --git a/pkg/gui/context/list_context_trait.go b/pkg/gui/context/list_context_trait.go index df5bbc0af..74ff2f388 100644 --- a/pkg/gui/context/list_context_trait.go +++ b/pkg/gui/context/list_context_trait.go @@ -10,11 +10,13 @@ import ( type ListContextTrait struct { types.Context - c *types.HelperCommon + c *ContextCommon list types.IList getDisplayStrings func(startIdx int, length int) [][]string } +func (self *ListContextTrait) IsListContext() {} + func (self *ListContextTrait) GetList() types.IList { return self.list } diff --git a/pkg/gui/context/local_commits_context.go b/pkg/gui/context/local_commits_context.go index 462a85d59..e8ae0b6c5 100644 --- a/pkg/gui/context/local_commits_context.go +++ b/pkg/gui/context/local_commits_context.go @@ -1,8 +1,11 @@ package context import ( - "github.com/jesseduffield/gocui" + "log" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,36 +14,57 @@ type LocalCommitsContext struct { *ViewportListContextTrait } -var _ types.IListContext = (*LocalCommitsContext)(nil) +var ( + _ types.IListContext = (*LocalCommitsContext)(nil) + _ types.DiffableContext = (*LocalCommitsContext)(nil) +) -func NewLocalCommitsContext( - getModel func() []*models.Commit, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext { + viewModel := NewLocalCommitsViewModel( + func() []*models.Commit { return c.Model().Commits }, + c, + ) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, + getDisplayStrings := func(startIdx int, length int) [][]string { + selectedCommitSha := "" - c *types.HelperCommon, -) *LocalCommitsContext { - viewModel := NewLocalCommitsViewModel(getModel, c) + if c.CurrentContext().GetKey() == LOCAL_COMMITS_CONTEXT_KEY { + selectedCommit := viewModel.GetSelected() + if selectedCommit != nil { + selectedCommitSha = selectedCommit.Sha + } + } + + showYouAreHereLabel := c.Model().WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING + + return presentation.GetCommitListDisplayStrings( + c.Common, + c.Model().Commits, + c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, + c.Modes().CherryPicking.SelectedShaSet(), + c.Modes().Diffing.Ref, + c.UserConfig.Gui.TimeFormat, + c.UserConfig.Git.ParseEmoji, + selectedCommitSha, + startIdx, + length, + shouldShowGraph(c), + c.Model().BisectInfo, + showYouAreHereLabel, + ) + } return &LocalCommitsContext{ LocalCommitsViewModel: viewModel, ViewportListContextTrait: &ViewportListContextTrait{ ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Commits, WindowName: "commits", Key: LOCAL_COMMITS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -69,7 +93,7 @@ type LocalCommitsViewModel struct { showWholeGitGraph bool } -func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *types.HelperCommon) *LocalCommitsViewModel { +func NewLocalCommitsViewModel(getModel func() []*models.Commit, c *ContextCommon) *LocalCommitsViewModel { self := &LocalCommitsViewModel{ BasicViewModel: NewBasicViewModel(getModel), limitCommits: true, @@ -91,6 +115,12 @@ func (self *LocalCommitsContext) GetSelectedRef() types.Ref { return commit } +func (self *LocalCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} + func (self *LocalCommitsViewModel) SetLimitCommits(value bool) { self.limitCommits = value } @@ -110,3 +140,22 @@ func (self *LocalCommitsViewModel) GetShowWholeGitGraph() bool { func (self *LocalCommitsViewModel) GetCommits() []*models.Commit { return self.getModel() } + +func shouldShowGraph(c *ContextCommon) bool { + if c.Modes().Filtering.Active() { + return false + } + + value := c.UserConfig.Git.Log.ShowGraph + switch value { + case "always": + return true + case "never": + return false + case "when-maximised": + return c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL + } + + log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value) + return false +} diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index 780c35660..b2685b2e4 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -2,7 +2,6 @@ package context import ( "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -16,34 +15,21 @@ type MenuContext struct { var _ types.IListContext = (*MenuContext)(nil) func NewMenuContext( - view *gocui.View, - - c *types.HelperCommon, - getOptionsMap func() map[string]string, - renderToDescriptionView func(string), + c *ContextCommon, ) *MenuContext { viewModel := NewMenuViewModel() - onFocus := func(types.OnFocusOpts) error { - selectedMenuItem := viewModel.GetSelected() - renderToDescriptionView(selectedMenuItem.Tooltip) - return nil - } - return &MenuContext{ MenuViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Menu, WindowName: "menu", Key: "menu", Kind: types.TEMPORARY_POPUP, - OnGetOptionsMap: getOptionsMap, Focusable: true, HasUncontrolledBounds: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - }), + })), getDisplayStrings: viewModel.GetDisplayStrings, list: viewModel, c: c, diff --git a/pkg/gui/context/merge_conflicts_context.go b/pkg/gui/context/merge_conflicts_context.go index 1e765ffe7..60aac6e3a 100644 --- a/pkg/gui/context/merge_conflicts_context.go +++ b/pkg/gui/context/merge_conflicts_context.go @@ -3,7 +3,6 @@ package context import ( "math" - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/sasha-s/go-deadlock" @@ -12,7 +11,7 @@ import ( type MergeConflictsContext struct { types.Context viewModel *ConflictsViewModel - c *types.HelperCommon + c *ContextCommon mutex *deadlock.Mutex } @@ -25,12 +24,7 @@ type ConflictsViewModel struct { } func NewMergeConflictsContext( - view *gocui.View, - - opts ContextCallbackOpts, - - c *types.HelperCommon, - getOptionsMap func() map[string]string, + c *ContextCommon, ) *MergeConflictsContext { viewModel := &ConflictsViewModel{ state: mergeconflicts.NewState(), @@ -43,14 +37,12 @@ func NewMergeConflictsContext( Context: NewSimpleContext( NewBaseContext(NewBaseContextOpts{ Kind: types.MAIN_CONTEXT, - View: view, + View: c.Views().MergeConflicts, WindowName: "main", Key: MERGE_CONFLICTS_CONTEXT_KEY, - OnGetOptionsMap: getOptionsMap, Focusable: true, HighlightOnFocus: true, }), - opts, ), c: c, } diff --git a/pkg/gui/context/patch_explorer_context.go b/pkg/gui/context/patch_explorer_context.go index 5a3375f33..1c986ee1d 100644 --- a/pkg/gui/context/patch_explorer_context.go +++ b/pkg/gui/context/patch_explorer_context.go @@ -13,7 +13,7 @@ type PatchExplorerContext struct { state *patch_exploring.State viewTrait *ViewTrait getIncludedLineIndices func() []int - c *types.HelperCommon + c *ContextCommon mutex *deadlock.Mutex } @@ -24,11 +24,9 @@ func NewPatchExplorerContext( windowName string, key types.ContextKey, - onFocus func(types.OnFocusOpts) error, - onFocusLost func(opts types.OnFocusLostOpts) error, getIncludedLineIndices func() []int, - c *types.HelperCommon, + c *ContextCommon, ) *PatchExplorerContext { return &PatchExplorerContext{ state: nil, @@ -43,13 +41,12 @@ func NewPatchExplorerContext( Kind: types.MAIN_CONTEXT, Focusable: true, HighlightOnFocus: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - }), + })), } } +func (self *PatchExplorerContext) IsPatchExplorerContext() {} + func (self *PatchExplorerContext) GetState() *patch_exploring.State { return self.state } diff --git a/pkg/gui/context/reflog_commits_context.go b/pkg/gui/context/reflog_commits_context.go index e197a50bd..40111beea 100644 --- a/pkg/gui/context/reflog_commits_context.go +++ b/pkg/gui/context/reflog_commits_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,35 +11,35 @@ type ReflogCommitsContext struct { *ListContextTrait } -var _ types.IListContext = (*ReflogCommitsContext)(nil) +var ( + _ types.IListContext = (*ReflogCommitsContext)(nil) + _ types.DiffableContext = (*ReflogCommitsContext)(nil) +) -func NewReflogCommitsContext( - getModel func() []*models.Commit, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext { + viewModel := NewBasicViewModel(func() []*models.Commit { return c.Model().FilteredReflogCommits }) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, -) *ReflogCommitsContext { - viewModel := NewBasicViewModel(getModel) + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetReflogCommitListDisplayStrings( + c.Model().FilteredReflogCommits, + c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, + c.Modes().CherryPicking.SelectedShaSet(), + c.Modes().Diffing.Ref, + c.UserConfig.Gui.TimeFormat, + c.UserConfig.Git.ParseEmoji, + ) + } return &ReflogCommitsContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().ReflogCommits, WindowName: "commits", Key: REFLOG_COMMITS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -71,3 +71,9 @@ func (self *ReflogCommitsContext) GetSelectedRef() types.Ref { func (self *ReflogCommitsContext) GetCommits() []*models.Commit { return self.getModel() } + +func (self *ReflogCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/remote_branches_context.go b/pkg/gui/context/remote_branches_context.go index 44dc06848..a085c18cc 100644 --- a/pkg/gui/context/remote_branches_context.go +++ b/pkg/gui/context/remote_branches_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -12,37 +12,32 @@ type RemoteBranchesContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*RemoteBranchesContext)(nil) +var ( + _ types.IListContext = (*RemoteBranchesContext)(nil) + _ types.DiffableContext = (*RemoteBranchesContext)(nil) +) func NewRemoteBranchesContext( - getModel func() []*models.RemoteBranch, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, + c *ContextCommon, ) *RemoteBranchesContext { - viewModel := NewBasicViewModel(getModel) + viewModel := NewBasicViewModel(func() []*models.RemoteBranch { return c.Model().RemoteBranches }) + + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetRemoteBranchListDisplayStrings(c.Model().RemoteBranches, c.Modes().Diffing.Ref) + } return &RemoteBranchesContext{ BasicViewModel: viewModel, DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle), ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().RemoteBranches, WindowName: "branches", Key: REMOTE_BRANCHES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, Transient: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -66,3 +61,9 @@ func (self *RemoteBranchesContext) GetSelectedRef() types.Ref { } return remoteBranch } + +func (self *RemoteBranchesContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/remotes_context.go b/pkg/gui/context/remotes_context.go index 0f11908dd..d1082ab52 100644 --- a/pkg/gui/context/remotes_context.go +++ b/pkg/gui/context/remotes_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,35 +11,28 @@ type RemotesContext struct { *ListContextTrait } -var _ types.IListContext = (*RemotesContext)(nil) +var ( + _ types.IListContext = (*RemotesContext)(nil) + _ types.DiffableContext = (*RemotesContext)(nil) +) -func NewRemotesContext( - getModel func() []*models.Remote, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewRemotesContext(c *ContextCommon) *RemotesContext { + viewModel := NewBasicViewModel(func() []*models.Remote { return c.Model().Remotes }) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, -) *RemotesContext { - viewModel := NewBasicViewModel(getModel) + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetRemoteListDisplayStrings(c.Model().Remotes, c.Modes().Diffing.Ref) + } return &RemotesContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Remotes, WindowName: "branches", Key: REMOTES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -55,3 +48,9 @@ func (self *RemotesContext) GetSelectedItemId() string { return item.ID() } + +func (self *RemotesContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/setup.go b/pkg/gui/context/setup.go new file mode 100644 index 000000000..775803884 --- /dev/null +++ b/pkg/gui/context/setup.go @@ -0,0 +1,146 @@ +package context + +import "github.com/jesseduffield/lazygit/pkg/gui/types" + +func NewContextTree(c *ContextCommon) *ContextTree { + commitFilesContext := NewCommitFilesContext(c) + + return &ContextTree{ + Global: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.GLOBAL_CONTEXT, + View: nil, // TODO: see if this breaks anything + WindowName: "", + Key: GLOBAL_CONTEXT_KEY, + Focusable: false, + HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view + }), + ), + Status: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.SIDE_CONTEXT, + View: c.Views().Status, + WindowName: "status", + Key: STATUS_CONTEXT_KEY, + Focusable: true, + }), + ), + Files: NewWorkingTreeContext(c), + Submodules: NewSubmodulesContext(c), + Menu: NewMenuContext(c), + Remotes: NewRemotesContext(c), + RemoteBranches: NewRemoteBranchesContext(c), + LocalCommits: NewLocalCommitsContext(c), + CommitFiles: commitFilesContext, + ReflogCommits: NewReflogCommitsContext(c), + SubCommits: NewSubCommitsContext(c), + Branches: NewBranchesContext(c), + Tags: NewTagsContext(c), + Stash: NewStashContext(c), + Suggestions: NewSuggestionsContext(c), + Normal: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.MAIN_CONTEXT, + View: c.Views().Main, + WindowName: "main", + Key: NORMAL_MAIN_CONTEXT_KEY, + Focusable: false, + }), + ), + NormalSecondary: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.MAIN_CONTEXT, + View: c.Views().Secondary, + WindowName: "secondary", + Key: NORMAL_SECONDARY_CONTEXT_KEY, + Focusable: false, + }), + ), + Staging: NewPatchExplorerContext( + c.Views().Staging, + "main", + STAGING_MAIN_CONTEXT_KEY, + func() []int { return nil }, + c, + ), + StagingSecondary: NewPatchExplorerContext( + c.Views().StagingSecondary, + "secondary", + STAGING_SECONDARY_CONTEXT_KEY, + func() []int { return nil }, + c, + ), + CustomPatchBuilder: NewPatchExplorerContext( + c.Views().PatchBuilding, + "main", + PATCH_BUILDING_MAIN_CONTEXT_KEY, + func() []int { + filename := commitFilesContext.GetSelectedPath() + includedLineIndices, err := c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename) + if err != nil { + c.Log.Error(err) + return nil + } + + return includedLineIndices + }, + c, + ), + CustomPatchBuilderSecondary: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.MAIN_CONTEXT, + View: c.Views().PatchBuildingSecondary, + WindowName: "secondary", + Key: PATCH_BUILDING_SECONDARY_CONTEXT_KEY, + Focusable: false, + }), + ), + MergeConflicts: NewMergeConflictsContext( + c, + ), + Confirmation: NewConfirmationContext(c), + CommitMessage: NewCommitMessageContext(c), + CommitDescription: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.PERSISTENT_POPUP, + View: c.Views().CommitDescription, + WindowName: "commitDescription", + Key: COMMIT_DESCRIPTION_CONTEXT_KEY, + Focusable: true, + HasUncontrolledBounds: true, + }), + ), + Search: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.PERSISTENT_POPUP, + View: c.Views().Search, + WindowName: "search", + Key: SEARCH_CONTEXT_KEY, + Focusable: true, + }), + ), + CommandLog: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.EXTRAS_CONTEXT, + View: c.Views().Extras, + WindowName: "extras", + Key: COMMAND_LOG_CONTEXT_KEY, + Focusable: true, + }), + ), + Snake: NewSimpleContext( + NewBaseContext(NewBaseContextOpts{ + Kind: types.SIDE_CONTEXT, + View: c.Views().Snake, + WindowName: "files", + Key: SNAKE_CONTEXT_KEY, + Focusable: true, + }), + ), + Options: NewDisplayContext(OPTIONS_CONTEXT_KEY, c.Views().Options, "options"), + AppStatus: NewDisplayContext(APP_STATUS_CONTEXT_KEY, c.Views().AppStatus, "appStatus"), + SearchPrefix: NewDisplayContext(SEARCH_PREFIX_CONTEXT_KEY, c.Views().SearchPrefix, "searchPrefix"), + Information: NewDisplayContext(INFORMATION_CONTEXT_KEY, c.Views().Information, "information"), + Limit: NewDisplayContext(LIMIT_CONTEXT_KEY, c.Views().Limit, "limit"), + } +} diff --git a/pkg/gui/context/simple_context.go b/pkg/gui/context/simple_context.go index 38fe4ee78..7c00e09f7 100644 --- a/pkg/gui/context/simple_context.go +++ b/pkg/gui/context/simple_context.go @@ -6,29 +6,12 @@ import ( ) type SimpleContext struct { - OnFocus func(opts types.OnFocusOpts) error - OnFocusLost func(opts types.OnFocusLostOpts) error - OnRender func() error - // this is for pushing some content to the main view - OnRenderToMain func() error - *BaseContext } -type ContextCallbackOpts struct { - OnFocus func(opts types.OnFocusOpts) error - OnFocusLost func(opts types.OnFocusLostOpts) error - OnRender func() error - OnRenderToMain func() error -} - -func NewSimpleContext(baseContext *BaseContext, opts ContextCallbackOpts) *SimpleContext { +func NewSimpleContext(baseContext *BaseContext) *SimpleContext { return &SimpleContext{ - OnFocus: opts.OnFocus, - OnFocusLost: opts.OnFocusLost, - OnRender: opts.OnRender, - OnRenderToMain: opts.OnRenderToMain, - BaseContext: baseContext, + BaseContext: baseContext, } } @@ -45,7 +28,6 @@ func NewDisplayContext(key types.ContextKey, view *gocui.View, windowName string Focusable: false, Transient: false, }), - ContextCallbackOpts{}, ) } @@ -54,14 +36,14 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error { self.GetViewTrait().SetHighlight(true) } - if self.OnFocus != nil { - if err := self.OnFocus(opts); err != nil { + if self.onFocusFn != nil { + if err := self.onFocusFn(opts); err != nil { return err } } - if self.OnRenderToMain != nil { - if err := self.OnRenderToMain(); err != nil { + if self.onRenderToMainFn != nil { + if err := self.onRenderToMainFn(); err != nil { return err } } @@ -70,22 +52,19 @@ func (self *SimpleContext) HandleFocus(opts types.OnFocusOpts) error { } func (self *SimpleContext) HandleFocusLost(opts types.OnFocusLostOpts) error { - if self.OnFocusLost != nil { - return self.OnFocusLost(opts) + if self.onFocusLostFn != nil { + return self.onFocusLostFn(opts) } return nil } func (self *SimpleContext) HandleRender() error { - if self.OnRender != nil { - return self.OnRender() - } return nil } func (self *SimpleContext) HandleRenderToMain() error { - if self.OnRenderToMain != nil { - return self.OnRenderToMain() + if self.onRenderToMainFn != nil { + return self.onRenderToMainFn() } return nil diff --git a/pkg/gui/context/stash_context.go b/pkg/gui/context/stash_context.go index 19eb5030a..386292c00 100644 --- a/pkg/gui/context/stash_context.go +++ b/pkg/gui/context/stash_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,35 +11,30 @@ type StashContext struct { *ListContextTrait } -var _ types.IListContext = (*StashContext)(nil) +var ( + _ types.IListContext = (*StashContext)(nil) + _ types.DiffableContext = (*StashContext)(nil) +) func NewStashContext( - getModel func() []*models.StashEntry, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, + c *ContextCommon, ) *StashContext { - viewModel := NewBasicViewModel(getModel) + viewModel := NewBasicViewModel(func() []*models.StashEntry { return c.Model().StashEntries }) + + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetStashEntryListDisplayStrings(c.Model().StashEntries, c.Modes().Diffing.Ref) + } return &StashContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Stash, WindowName: "stash", Key: STASH_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -67,3 +62,9 @@ func (self *StashContext) GetSelectedRef() types.Ref { } return stash } + +func (self *StashContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index 5fdea8e6d..16712f982 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -3,8 +3,9 @@ package context import ( "fmt" - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -15,23 +16,45 @@ type SubCommitsContext struct { *DynamicTitleBuilder } -var _ types.IListContext = (*SubCommitsContext)(nil) +var ( + _ types.IListContext = (*SubCommitsContext)(nil) + _ types.DiffableContext = (*SubCommitsContext)(nil) +) func NewSubCommitsContext( - getModel func() []*models.Commit, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, + c *ContextCommon, ) *SubCommitsContext { viewModel := &SubCommitsViewModel{ - BasicViewModel: NewBasicViewModel(getModel), - ref: nil, - limitCommits: true, + BasicViewModel: NewBasicViewModel( + func() []*models.Commit { return c.Model().SubCommits }, + ), + ref: nil, + limitCommits: true, + } + + getDisplayStrings := func(startIdx int, length int) [][]string { + selectedCommitSha := "" + if c.CurrentContext().GetKey() == SUB_COMMITS_CONTEXT_KEY { + selectedCommit := viewModel.GetSelected() + if selectedCommit != nil { + selectedCommitSha = selectedCommit.Sha + } + } + return presentation.GetCommitListDisplayStrings( + c.Common, + c.Model().SubCommits, + c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, + c.Modes().CherryPicking.SelectedShaSet(), + c.Modes().Diffing.Ref, + c.UserConfig.Gui.TimeFormat, + c.UserConfig.Git.ParseEmoji, + selectedCommitSha, + startIdx, + length, + shouldShowGraph(c), + git_commands.NewNullBisectInfo(), + false, + ) } return &SubCommitsContext{ @@ -40,17 +63,13 @@ func NewSubCommitsContext( ViewportListContextTrait: &ViewportListContextTrait{ ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().SubCommits, WindowName: "branches", Key: SUB_COMMITS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, Transient: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -111,3 +130,9 @@ func (self *SubCommitsContext) SetLimitCommits(value bool) { func (self *SubCommitsContext) GetLimitCommits() bool { return self.limitCommits } + +func (self *SubCommitsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/submodules_context.go b/pkg/gui/context/submodules_context.go index 5491cd137..675e01cd1 100644 --- a/pkg/gui/context/submodules_context.go +++ b/pkg/gui/context/submodules_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -13,33 +13,23 @@ type SubmodulesContext struct { var _ types.IListContext = (*SubmodulesContext)(nil) -func NewSubmodulesContext( - getModel func() []*models.SubmoduleConfig, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewSubmodulesContext(c *ContextCommon) *SubmodulesContext { + viewModel := NewBasicViewModel(func() []*models.SubmoduleConfig { return c.Model().Submodules }) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, -) *SubmodulesContext { - viewModel := NewBasicViewModel(getModel) + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetSubmoduleListDisplayStrings(c.Model().Submodules) + } return &SubmodulesContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Submodules, WindowName: "files", Key: SUBMODULES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, diff --git a/pkg/gui/context/suggestions_context.go b/pkg/gui/context/suggestions_context.go index 4be86244a..022e96daf 100644 --- a/pkg/gui/context/suggestions_context.go +++ b/pkg/gui/context/suggestions_context.go @@ -1,45 +1,59 @@ package context import ( - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/tasks" ) type SuggestionsContext struct { *BasicViewModel[*types.Suggestion] *ListContextTrait + + State *SuggestionsContextState +} + +type SuggestionsContextState struct { + Suggestions []*types.Suggestion + OnConfirm func() error + OnClose func() error + AsyncHandler *tasks.AsyncHandler + + // FindSuggestions will take a string that the user has typed into a prompt + // and return a slice of suggestions which match that string. + FindSuggestions func(string) []*types.Suggestion } var _ types.IListContext = (*SuggestionsContext)(nil) func NewSuggestionsContext( - getModel func() []*types.Suggestion, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, + c *ContextCommon, ) *SuggestionsContext { + state := &SuggestionsContextState{ + AsyncHandler: tasks.NewAsyncHandler(), + } + getModel := func() []*types.Suggestion { + return state.Suggestions + } + + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetSuggestionListDisplayStrings(state.Suggestions) + } + viewModel := NewBasicViewModel(getModel) return &SuggestionsContext{ + State: state, BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Suggestions, WindowName: "suggestions", Key: SUGGESTIONS_CONTEXT_KEY, Kind: types.PERSISTENT_POPUP, Focusable: true, HasUncontrolledBounds: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -55,3 +69,22 @@ func (self *SuggestionsContext) GetSelectedItemId() string { return item.Value } + +func (self *SuggestionsContext) SetSuggestions(suggestions []*types.Suggestion) { + self.State.Suggestions = suggestions + self.SetSelectedLineIdx(0) + self.c.ResetViewOrigin(self.GetView()) + _ = self.HandleRender() +} + +func (self *SuggestionsContext) RefreshSuggestions() { + self.State.AsyncHandler.Do(func() func() { + findSuggestionsFn := self.State.FindSuggestions + if findSuggestionsFn != nil { + suggestions := findSuggestionsFn(self.c.GetPromptInput()) + return func() { self.SetSuggestions(suggestions) } + } else { + return func() {} + } + }) +} diff --git a/pkg/gui/context/tags_context.go b/pkg/gui/context/tags_context.go index 6cb14e371..e49cdad9b 100644 --- a/pkg/gui/context/tags_context.go +++ b/pkg/gui/context/tags_context.go @@ -1,8 +1,8 @@ package context import ( - "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -11,35 +11,30 @@ type TagsContext struct { *ListContextTrait } -var _ types.IListContext = (*TagsContext)(nil) +var ( + _ types.IListContext = (*TagsContext)(nil) + _ types.DiffableContext = (*TagsContext)(nil) +) func NewTagsContext( - getModel func() []*models.Tag, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, - - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, + c *ContextCommon, ) *TagsContext { - viewModel := NewBasicViewModel(getModel) + viewModel := NewBasicViewModel(func() []*models.Tag { return c.Model().Tags }) + + getDisplayStrings := func(startIdx int, length int) [][]string { + return presentation.GetTagListDisplayStrings(c.Model().Tags, c.Modes().Diffing.Ref) + } return &TagsContext{ BasicViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Tags, WindowName: "branches", Key: TAGS_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, @@ -63,3 +58,9 @@ func (self *TagsContext) GetSelectedRef() types.Ref { } return tag } + +func (self *TagsContext) GetDiffTerminals() []string { + itemId := self.GetSelectedItemId() + + return []string{itemId} +} diff --git a/pkg/gui/context/working_tree_context.go b/pkg/gui/context/working_tree_context.go index f8da6a068..45502eb60 100644 --- a/pkg/gui/context/working_tree_context.go +++ b/pkg/gui/context/working_tree_context.go @@ -1,9 +1,10 @@ package context import ( - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -14,33 +15,30 @@ type WorkingTreeContext struct { var _ types.IListContext = (*WorkingTreeContext)(nil) -func NewWorkingTreeContext( - getModel func() []*models.File, - view *gocui.View, - getDisplayStrings func(startIdx int, length int) [][]string, +func NewWorkingTreeContext(c *ContextCommon) *WorkingTreeContext { + viewModel := filetree.NewFileTreeViewModel( + func() []*models.File { return c.Model().Files }, + c.Log, + c.UserConfig.Gui.ShowFileTree, + ) - onFocus func(types.OnFocusOpts) error, - onRenderToMain func() error, - onFocusLost func(opts types.OnFocusLostOpts) error, - - c *types.HelperCommon, -) *WorkingTreeContext { - viewModel := filetree.NewFileTreeViewModel(getModel, c.Log, c.UserConfig.Gui.ShowFileTree) + getDisplayStrings := func(startIdx int, length int) [][]string { + lines := presentation.RenderFileTree(viewModel, c.Modes().Diffing.Ref, c.Model().Submodules) + return slices.Map(lines, func(line string) []string { + return []string{line} + }) + } return &WorkingTreeContext{ FileTreeViewModel: viewModel, ListContextTrait: &ListContextTrait{ Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{ - View: view, + View: c.Views().Files, WindowName: "files", Key: FILES_CONTEXT_KEY, Kind: types.SIDE_CONTEXT, Focusable: true, - }), ContextCallbackOpts{ - OnFocus: onFocus, - OnFocusLost: onFocusLost, - OnRenderToMain: onRenderToMain, - }), + })), list: viewModel, getDisplayStrings: getDisplayStrings, c: c, diff --git a/pkg/gui/context_config.go b/pkg/gui/context_config.go index aa056f5c5..748482a35 100644 --- a/pkg/gui/context_config.go +++ b/pkg/gui/context_config.go @@ -6,273 +6,11 @@ import ( ) func (gui *Gui) contextTree() *context.ContextTree { - return &context.ContextTree{ - Global: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.GLOBAL_CONTEXT, - View: nil, // TODO: see if this breaks anything - WindowName: "", - Key: context.GLOBAL_CONTEXT_KEY, - Focusable: false, - HasUncontrolledBounds: true, // setting to true because the global context doesn't even have a view - }), - context.ContextCallbackOpts{ - OnRenderToMain: gui.statusRenderToMain, - }, - ), - Status: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.SIDE_CONTEXT, - View: gui.Views.Status, - WindowName: "status", - Key: context.STATUS_CONTEXT_KEY, - Focusable: true, - }), - context.ContextCallbackOpts{ - OnRenderToMain: gui.statusRenderToMain, - }, - ), - Snake: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.SIDE_CONTEXT, - View: gui.Views.Snake, - WindowName: "files", - Key: context.SNAKE_CONTEXT_KEY, - Focusable: true, - }), - context.ContextCallbackOpts{ - OnFocus: func(opts types.OnFocusOpts) error { - gui.startSnake() - return nil - }, - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.snakeGame.Exit() - gui.moveToTopOfWindow(gui.State.Contexts.Submodules) - return nil - }, - }, - ), - Files: gui.filesListContext(), - Submodules: gui.submodulesListContext(), - Menu: gui.menuListContext(), - Remotes: gui.remotesListContext(), - RemoteBranches: gui.remoteBranchesListContext(), - LocalCommits: gui.branchCommitsListContext(), - CommitFiles: gui.commitFilesListContext(), - ReflogCommits: gui.reflogCommitsListContext(), - SubCommits: gui.subCommitsListContext(), - Branches: gui.branchesListContext(), - Tags: gui.tagsListContext(), - Stash: gui.stashListContext(), - Suggestions: gui.suggestionsListContext(), - Normal: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.MAIN_CONTEXT, - View: gui.Views.Main, - WindowName: "main", - Key: context.NORMAL_MAIN_CONTEXT_KEY, - Focusable: false, - }), - context.ContextCallbackOpts{ - OnFocus: func(opts types.OnFocusOpts) error { - return nil // TODO: should we do something here? We should allow for scrolling the panel - }, - }, - ), - NormalSecondary: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.MAIN_CONTEXT, - View: gui.Views.Secondary, - WindowName: "secondary", - Key: context.NORMAL_SECONDARY_CONTEXT_KEY, - Focusable: false, - }), - context.ContextCallbackOpts{}, - ), - Staging: context.NewPatchExplorerContext( - gui.Views.Staging, - "main", - context.STAGING_MAIN_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - gui.Views.Staging.Wrap = false - gui.Views.StagingSecondary.Wrap = false - - return gui.refreshStagingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.Staging.SetState(nil) - - if opts.NewContextKey != context.STAGING_SECONDARY_CONTEXT_KEY { - gui.Views.Staging.Wrap = true - gui.Views.StagingSecondary.Wrap = true - _ = gui.State.Contexts.Staging.Render(false) - _ = gui.State.Contexts.StagingSecondary.Render(false) - } - return nil - }, - func() []int { return nil }, - gui.c, - ), - StagingSecondary: context.NewPatchExplorerContext( - gui.Views.StagingSecondary, - "secondary", - context.STAGING_SECONDARY_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - gui.Views.Staging.Wrap = false - gui.Views.StagingSecondary.Wrap = false - - return gui.refreshStagingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.StagingSecondary.SetState(nil) - - if opts.NewContextKey != context.STAGING_MAIN_CONTEXT_KEY { - gui.Views.Staging.Wrap = true - gui.Views.StagingSecondary.Wrap = true - _ = gui.State.Contexts.Staging.Render(false) - _ = gui.State.Contexts.StagingSecondary.Render(false) - } - return nil - }, - func() []int { return nil }, - gui.c, - ), - CustomPatchBuilder: context.NewPatchExplorerContext( - gui.Views.PatchBuilding, - "main", - context.PATCH_BUILDING_MAIN_CONTEXT_KEY, - func(opts types.OnFocusOpts) error { - // no need to change wrap on the secondary view because it can't be interacted with - gui.Views.PatchBuilding.Wrap = false - - return gui.refreshPatchBuildingPanel(opts) - }, - func(opts types.OnFocusLostOpts) error { - gui.Views.PatchBuilding.Wrap = true - - if gui.git.Patch.PatchBuilder.IsEmpty() { - gui.git.Patch.PatchBuilder.Reset() - } - - return nil - }, - func() []int { - filename := gui.State.Contexts.CommitFiles.GetSelectedPath() - includedLineIndices, err := gui.git.Patch.PatchBuilder.GetFileIncLineIndices(filename) - if err != nil { - gui.Log.Error(err) - return nil - } - - return includedLineIndices - }, - gui.c, - ), - CustomPatchBuilderSecondary: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.MAIN_CONTEXT, - View: gui.Views.PatchBuildingSecondary, - WindowName: "secondary", - Key: context.PATCH_BUILDING_SECONDARY_CONTEXT_KEY, - Focusable: false, - }), - context.ContextCallbackOpts{}, - ), - MergeConflicts: context.NewMergeConflictsContext( - gui.Views.MergeConflicts, - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(func() error { - gui.Views.MergeConflicts.Wrap = false - - return gui.refreshMergePanel(true) - }), - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.State.Contexts.MergeConflicts.SetUserScrolling(false) - gui.State.Contexts.MergeConflicts.GetState().ResetConflictSelection() - gui.Views.MergeConflicts.Wrap = true - - return nil - }, - }, - gui.c, - func() map[string]string { - // wrapping in a function because contexts are initialized before helpers - return gui.helpers.MergeConflicts.GetMergingOptions() - }, - ), - Confirmation: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.TEMPORARY_POPUP, - View: gui.Views.Confirmation, - WindowName: "confirmation", - Key: context.CONFIRMATION_CONTEXT_KEY, - Focusable: true, - HasUncontrolledBounds: true, - }), - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(gui.handleAskFocused), - OnFocusLost: func(types.OnFocusLostOpts) error { - gui.deactivateConfirmationPrompt() - return nil - }, - }, - ), - CommitMessage: context.NewCommitMessageContext( - gui.Views.CommitMessage, - context.ContextCallbackOpts{ - OnFocus: OnFocusWrapper(gui.handleCommitMessageFocused), - }, - ), - CommitDescription: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.PERSISTENT_POPUP, - View: gui.Views.CommitDescription, - WindowName: "commitDescription", - Key: context.COMMIT_DESCRIPTION_CONTEXT_KEY, - Focusable: true, - HasUncontrolledBounds: true, - }), - context.ContextCallbackOpts{ - OnFocus: func(opts types.OnFocusOpts) error { - _, err := gui.g.SetViewBeneath("commitDescription", "commitMessage", 10) - if err != nil { - return err - } - return nil - }, - }, - ), - Search: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.PERSISTENT_POPUP, - View: gui.Views.Search, - WindowName: "search", - Key: context.SEARCH_CONTEXT_KEY, - Focusable: true, - }), - context.ContextCallbackOpts{}, - ), - CommandLog: context.NewSimpleContext( - context.NewBaseContext(context.NewBaseContextOpts{ - Kind: types.EXTRAS_CONTEXT, - View: gui.Views.Extras, - WindowName: "extras", - Key: context.COMMAND_LOG_CONTEXT_KEY, - Focusable: true, - }), - context.ContextCallbackOpts{ - OnFocusLost: func(opts types.OnFocusLostOpts) error { - gui.Views.Extras.Autoscroll = true - return nil - }, - }, - ), - Options: context.NewDisplayContext(context.OPTIONS_CONTEXT_KEY, gui.Views.Options, "options"), - AppStatus: context.NewDisplayContext(context.APP_STATUS_CONTEXT_KEY, gui.Views.AppStatus, "appStatus"), - SearchPrefix: context.NewDisplayContext(context.SEARCH_PREFIX_CONTEXT_KEY, gui.Views.SearchPrefix, "searchPrefix"), - Information: context.NewDisplayContext(context.INFORMATION_CONTEXT_KEY, gui.Views.Information, "information"), - Limit: context.NewDisplayContext(context.LIMIT_CONTEXT_KEY, gui.Views.Limit, "limit"), + contextCommon := &context.ContextCommon{ + IGuiCommon: gui.c.IGuiCommon, + Common: gui.c.Common, } + return context.NewContextTree(contextCommon) } // using this wrapper for when an onFocus function doesn't care about any potential @@ -283,10 +21,10 @@ func OnFocusWrapper(f func() error) func(opts types.OnFocusOpts) error { } } -func (gui *Gui) getPatchExplorerContexts() []types.IPatchExplorerContext { - return []types.IPatchExplorerContext{ - gui.State.Contexts.Staging, - gui.State.Contexts.StagingSecondary, - gui.State.Contexts.CustomPatchBuilder, +func (gui *Gui) defaultSideContext() types.Context { + if gui.State.Modes.Filtering.Active() { + return gui.State.Contexts.LocalCommits + } else { + return gui.State.Contexts.Files } } diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 4b13eaa20..78943e798 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -7,24 +7,22 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/controllers" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" - "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" - "github.com/jesseduffield/lazygit/pkg/snake" + "github.com/jesseduffield/lazygit/pkg/gui/status" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) -func (gui *Gui) resetControllers() { - helperCommon := gui.c - osCommand := gui.os - model := gui.State.Model - refsHelper := helpers.NewRefsHelper( - helperCommon, - gui.git, - gui.State.Contexts, - model, - ) +func (gui *Gui) Helpers() *helpers.Helpers { + return gui.helpers +} + +func (gui *Gui) resetHelpersAndControllers() { + helperCommon := gui.c + refsHelper := helpers.NewRefsHelper(helperCommon) + + rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper) + suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon) - rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, gui.State.Contexts, gui.git, refsHelper) - suggestionsHelper := helpers.NewSuggestionsHelper(helperCommon, model, gui.refreshSuggestions) setCommitSummary := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitMessage }) setCommitDescription := gui.getCommitMessageSetTextareaTextFn(func() *gocui.View { return gui.Views.CommitDescription }) getCommitSummary := func() string { @@ -35,66 +33,86 @@ func (gui *Gui) resetControllers() { return strings.TrimSpace(gui.Views.CommitDescription.TextArea.GetContent()) } commitsHelper := helpers.NewCommitsHelper(helperCommon, - gui.State.Model, - gui.State.Contexts, getCommitSummary, setCommitSummary, getCommitDescription, setCommitDescription, - gui.RenderCommitLength, ) - gpgHelper := helpers.NewGpgHelper(helperCommon, gui.os, gui.git) + + gpgHelper := helpers.NewGpgHelper(helperCommon) + viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) + recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon) + patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon) + stagingHelper := helpers.NewStagingHelper(helperCommon) + mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon) + refreshHelper := helpers.NewRefreshHelper(helperCommon, refsHelper, rebaseHelper, patchBuildingHelper, stagingHelper, mergeConflictsHelper, gui.fileWatcher) + diffHelper := helpers.NewDiffHelper(helperCommon) + cherryPickHelper := helpers.NewCherryPickHelper( + helperCommon, + rebaseHelper, + ) + bisectHelper := helpers.NewBisectHelper(helperCommon) + windowHelper := helpers.NewWindowHelper(helperCommon, viewHelper) + modeHelper := helpers.NewModeHelper( + helperCommon, + diffHelper, + patchBuildingHelper, + cherryPickHelper, + rebaseHelper, + bisectHelper, + ) + appStatusHelper := helpers.NewAppStatusHelper( + helperCommon, + func() *status.StatusManager { return gui.statusManager }, + ) gui.helpers = &helpers.Helpers{ - Refs: refsHelper, - Host: helpers.NewHostHelper(helperCommon, gui.git), - PatchBuilding: helpers.NewPatchBuildingHelper(helperCommon, gui.git, gui.State.Contexts), - Bisect: helpers.NewBisectHelper(helperCommon, gui.git), - Suggestions: suggestionsHelper, - Files: helpers.NewFilesHelper(helperCommon, gui.git, osCommand), - WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, gui.git, gui.State.Contexts, refsHelper, model, setCommitSummary, commitsHelper, gpgHelper), - Tags: helpers.NewTagsHelper(helperCommon, gui.git), - GPG: gpgHelper, - MergeAndRebase: rebaseHelper, - MergeConflicts: helpers.NewMergeConflictsHelper(helperCommon, gui.State.Contexts, gui.git), - CherryPick: helpers.NewCherryPickHelper( - helperCommon, - gui.git, - gui.State.Contexts, - func() *cherrypicking.CherryPicking { return gui.State.Modes.CherryPicking }, - rebaseHelper, + Refs: refsHelper, + Host: helpers.NewHostHelper(helperCommon), + PatchBuilding: patchBuildingHelper, + Staging: stagingHelper, + Bisect: bisectHelper, + Suggestions: suggestionsHelper, + Files: helpers.NewFilesHelper(helperCommon), + WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper), + Tags: helpers.NewTagsHelper(helperCommon), + GPG: helpers.NewGpgHelper(helperCommon), + MergeAndRebase: rebaseHelper, + MergeConflicts: mergeConflictsHelper, + CherryPick: cherryPickHelper, + Upstream: helpers.NewUpstreamHelper(helperCommon, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), + AmendHelper: helpers.NewAmendHelper(helperCommon, gpgHelper), + Commits: commitsHelper, + Snake: helpers.NewSnakeHelper(helperCommon), + Diff: diffHelper, + Repos: helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo), + RecordDirectory: recordDirectoryHelper, + Update: helpers.NewUpdateHelper(helperCommon, gui.Updater), + Window: windowHelper, + View: viewHelper, + Refresh: refreshHelper, + Confirmation: helpers.NewConfirmationHelper(helperCommon), + Mode: modeHelper, + AppStatus: appStatusHelper, + WindowArrangement: helpers.NewWindowArrangementHelper( + gui.c, + windowHelper, + modeHelper, + appStatusHelper, ), - Upstream: helpers.NewUpstreamHelper(helperCommon, model, suggestionsHelper.GetRemoteBranchesSuggestionsFunc), - AmendHelper: helpers.NewAmendHelper(helperCommon, gui.git, gpgHelper), - Commits: commitsHelper, } gui.CustomCommandsClient = custom_commands.NewClient( helperCommon, - gui.os, - gui.git, - gui.State.Contexts, gui.helpers, ) - common := controllers.NewControllerCommon( - helperCommon, - osCommand, - gui.git, - gui.helpers, - model, - gui.State.Contexts, - gui.State.Modes, - &gui.Mutexes, - ) + common := controllers.NewControllerCommon(helperCommon, gui) syncController := controllers.NewSyncController( common, ) - submodulesController := controllers.NewSubmodulesController( - common, - gui.enterSubmodule, - ) + submodulesController := controllers.NewSubmodulesController(common) bisectController := controllers.NewBisectController(common) @@ -113,8 +131,6 @@ func (gui *Gui) resetControllers() { tagsController := controllers.NewTagsController(common) filesController := controllers.NewFilesController( common, - gui.enterSubmodule, - setCommitSummary, ) mergeConflictsController := controllers.NewMergeConflictsController(common) remotesController := controllers.NewRemotesController( @@ -135,7 +151,34 @@ func (gui *Gui) resetControllers() { stagingController := controllers.NewStagingController(common, gui.State.Contexts.Staging, gui.State.Contexts.StagingSecondary, false) stagingSecondaryController := controllers.NewStagingController(common, gui.State.Contexts.StagingSecondary, gui.State.Contexts.Staging, true) patchBuildingController := controllers.NewPatchBuildingController(common) - snakeController := controllers.NewSnakeController(common, func() *snake.Game { return gui.snakeGame }) + snakeController := controllers.NewSnakeController(common) + reflogCommitsController := controllers.NewReflogCommitsController(common) + subCommitsController := controllers.NewSubCommitsController(common) + statusController := controllers.NewStatusController(common) + commandLogController := controllers.NewCommandLogController(common) + confirmationController := controllers.NewConfirmationController(common) + suggestionsController := controllers.NewSuggestionsController(common) + jumpToSideWindowController := controllers.NewJumpToSideWindowController(common) + + sideWindowControllerFactory := controllers.NewSideWindowControllerFactory(common) + + // allow for navigating between side window contexts + for _, context := range []types.Context{ + gui.State.Contexts.Status, + gui.State.Contexts.Remotes, + gui.State.Contexts.Tags, + gui.State.Contexts.Branches, + gui.State.Contexts.RemoteBranches, + gui.State.Contexts.Files, + gui.State.Contexts.Submodules, + gui.State.Contexts.ReflogCommits, + gui.State.Contexts.LocalCommits, + gui.State.Contexts.CommitFiles, + gui.State.Contexts.SubCommits, + gui.State.Contexts.Stash, + } { + controllers.AttachControllers(context, sideWindowControllerFactory.Create(context)) + } setSubCommits := func(commits []*models.Commit) { gui.Mutexes.SubCommitsMutex.Lock() @@ -161,7 +204,7 @@ func (gui *Gui) resetControllers() { gui.State.Contexts.Stash, } { controllers.AttachControllers(context, controllers.NewSwitchToDiffFilesController( - common, gui.SwitchToCommitFilesContext, context, + common, context, gui.State.Contexts.CommitFiles, )) } @@ -173,6 +216,14 @@ func (gui *Gui) resetControllers() { controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context)) } + controllers.AttachControllers(gui.State.Contexts.ReflogCommits, + reflogCommitsController, + ) + + controllers.AttachControllers(gui.State.Contexts.SubCommits, + subCommitsController, + ) + // TODO: add scroll controllers for main panels (need to bring some more functionality across for that e.g. reading more from the currently displayed git command) controllers.AttachControllers(gui.State.Contexts.Staging, stagingController, @@ -256,11 +307,28 @@ func (gui *Gui) resetControllers() { remoteBranchesController, ) + controllers.AttachControllers(gui.State.Contexts.Status, + statusController, + ) + + controllers.AttachControllers(gui.State.Contexts.CommandLog, + commandLogController, + ) + + controllers.AttachControllers(gui.State.Contexts.Confirmation, + confirmationController, + ) + + controllers.AttachControllers(gui.State.Contexts.Suggestions, + suggestionsController, + ) + controllers.AttachControllers(gui.State.Contexts.Global, syncController, undoController, globalController, contextLinesController, + jumpToSideWindowController, ) controllers.AttachControllers(gui.State.Contexts.Snake, @@ -268,8 +336,19 @@ func (gui *Gui) resetControllers() { ) // this must come last so that we've got our click handlers defined against the context - listControllerFactory := controllers.NewListControllerFactory(gui.c) - for _, context := range gui.getListContexts() { + listControllerFactory := controllers.NewListControllerFactory(common) + for _, context := range gui.c.Context().AllList() { controllers.AttachControllers(context, listControllerFactory.Create(context)) } } + +func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) { + return func(text string) { + // using a getView function so that we don't need to worry about when the view is created + view := getView() + view.ClearTextArea() + view.TextArea.TypeString(text) + gui.helpers.Confirmation.ResizeCommitMessagePanels() + view.RenderTextArea() + } +} diff --git a/pkg/gui/controllers/attach.go b/pkg/gui/controllers/attach.go index 3e621c54c..6a24fd7d3 100644 --- a/pkg/gui/controllers/attach.go +++ b/pkg/gui/controllers/attach.go @@ -7,5 +7,8 @@ func AttachControllers(context types.Context, controllers ...types.IController) context.AddKeybindingsFn(controller.GetKeybindings) context.AddMouseKeybindingsFn(controller.GetMouseKeybindings) context.AddOnClickFn(controller.GetOnClick()) + context.AddOnRenderToMainFn(controller.GetOnRenderToMain()) + context.AddOnFocusFn(controller.GetOnFocus()) + context.AddOnFocusLostFn(controller.GetOnFocusLost()) } } diff --git a/pkg/gui/controllers/base_controller.go b/pkg/gui/controllers/base_controller.go index db7ad7a40..100acfd2a 100644 --- a/pkg/gui/controllers/base_controller.go +++ b/pkg/gui/controllers/base_controller.go @@ -18,3 +18,15 @@ func (self *baseController) GetMouseKeybindings(opts types.KeybindingsOpts) []*g func (self *baseController) GetOnClick() func() error { return nil } + +func (self *baseController) GetOnRenderToMain() func() error { + return nil +} + +func (self *baseController) GetOnFocus() func(types.OnFocusOpts) error { + return nil +} + +func (self *baseController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return nil +} diff --git a/pkg/gui/controllers/basic_commits_controller.go b/pkg/gui/controllers/basic_commits_controller.go index cecba00fe..f2c794abc 100644 --- a/pkg/gui/controllers/basic_commits_controller.go +++ b/pkg/gui/controllers/basic_commits_controller.go @@ -20,15 +20,15 @@ type ContainsCommits interface { type BasicCommitsController struct { baseController - *controllerCommon + c *ControllerCommon context ContainsCommits } -func NewBasicCommitsController(controllerCommon *controllerCommon, context ContainsCommits) *BasicCommitsController { +func NewBasicCommitsController(controllerCommon *ControllerCommon, context ContainsCommits) *BasicCommitsController { return &BasicCommitsController{ - baseController: baseController{}, - controllerCommon: controllerCommon, - context: context, + baseController: baseController{}, + c: controllerCommon, + context: context, } } @@ -73,7 +73,7 @@ func (self *BasicCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ }, { Key: opts.GetKey(opts.Config.Commits.ResetCherryPick), - Handler: self.helpers.CherryPick.Reset, + Handler: self.c.Helpers().CherryPick.Reset, Description: self.c.Tr.LcResetCherryPick, }, } @@ -141,7 +141,7 @@ func (self *BasicCommitsController) copyCommitAttribute(commit *models.Commit) e func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Commit) error { self.c.LogAction(self.c.Tr.Actions.CopyCommitSHAToClipboard) - if err := self.os.CopyToClipboard(commit.Sha); err != nil { + if err := self.c.OS().CopyToClipboard(commit.Sha); err != nil { return self.c.Error(err) } @@ -150,13 +150,13 @@ func (self *BasicCommitsController) copyCommitSHAToClipboard(commit *models.Comm } func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Commit) error { - url, err := self.helpers.Host.GetCommitURL(commit.Sha) + url, err := self.c.Helpers().Host.GetCommitURL(commit.Sha) if err != nil { return err } self.c.LogAction(self.c.Tr.Actions.CopyCommitURLToClipboard) - if err := self.os.CopyToClipboard(url); err != nil { + if err := self.c.OS().CopyToClipboard(url); err != nil { return self.c.Error(err) } @@ -165,13 +165,13 @@ func (self *BasicCommitsController) copyCommitURLToClipboard(commit *models.Comm } func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Commit) error { - diff, err := self.git.Commit.GetCommitDiff(commit.Sha) + diff, err := self.c.Git().Commit.GetCommitDiff(commit.Sha) if err != nil { return self.c.Error(err) } self.c.LogAction(self.c.Tr.Actions.CopyCommitDiffToClipboard) - if err := self.os.CopyToClipboard(diff); err != nil { + if err := self.c.OS().CopyToClipboard(diff); err != nil { return self.c.Error(err) } @@ -180,7 +180,7 @@ func (self *BasicCommitsController) copyCommitDiffToClipboard(commit *models.Com } func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) error { - author, err := self.git.Commit.GetCommitAuthor(commit.Sha) + author, err := self.c.Git().Commit.GetCommitAuthor(commit.Sha) if err != nil { return self.c.Error(err) } @@ -188,7 +188,7 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) formattedAuthor := fmt.Sprintf("%s <%s>", author.Name, author.Email) self.c.LogAction(self.c.Tr.Actions.CopyCommitAuthorToClipboard) - if err := self.os.CopyToClipboard(formattedAuthor); err != nil { + if err := self.c.OS().CopyToClipboard(formattedAuthor); err != nil { return self.c.Error(err) } @@ -197,13 +197,13 @@ func (self *BasicCommitsController) copyAuthorToClipboard(commit *models.Commit) } func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models.Commit) error { - message, err := self.git.Commit.GetCommitMessage(commit.Sha) + message, err := self.c.Git().Commit.GetCommitMessage(commit.Sha) if err != nil { return self.c.Error(err) } self.c.LogAction(self.c.Tr.Actions.CopyCommitMessageToClipboard) - if err := self.os.CopyToClipboard(message); err != nil { + if err := self.c.OS().CopyToClipboard(message); err != nil { return self.c.Error(err) } @@ -212,13 +212,13 @@ func (self *BasicCommitsController) copyCommitMessageToClipboard(commit *models. } func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error { - url, err := self.helpers.Host.GetCommitURL(commit.Sha) + url, err := self.c.Helpers().Host.GetCommitURL(commit.Sha) if err != nil { return self.c.Error(err) } self.c.LogAction(self.c.Tr.Actions.OpenCommitInBrowser) - if err := self.os.OpenLink(url); err != nil { + if err := self.c.OS().OpenLink(url); err != nil { return self.c.Error(err) } @@ -226,11 +226,11 @@ func (self *BasicCommitsController) openInBrowser(commit *models.Commit) error { } func (self *BasicCommitsController) newBranch(commit *models.Commit) error { - return self.helpers.Refs.NewBranch(commit.RefName(), commit.Description(), "") + return self.c.Helpers().Refs.NewBranch(commit.RefName(), commit.Description(), "") } func (self *BasicCommitsController) createResetMenu(commit *models.Commit) error { - return self.helpers.Refs.CreateGitResetMenu(commit.Sha) + return self.c.Helpers().Refs.CreateGitResetMenu(commit.Sha) } func (self *BasicCommitsController) checkout(commit *models.Commit) error { @@ -239,15 +239,15 @@ func (self *BasicCommitsController) checkout(commit *models.Commit) error { Prompt: self.c.Tr.SureCheckoutThisCommit, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.CheckoutCommit) - return self.helpers.Refs.CheckoutRef(commit.Sha, types.CheckoutRefOptions{}) + return self.c.Helpers().Refs.CheckoutRef(commit.Sha, types.CheckoutRefOptions{}) }, }) } func (self *BasicCommitsController) copy(commit *models.Commit) error { - return self.helpers.CherryPick.Copy(commit, self.context.GetCommits(), self.context) + return self.c.Helpers().CherryPick.Copy(commit, self.context.GetCommits(), self.context) } func (self *BasicCommitsController) copyRange(*models.Commit) error { - return self.helpers.CherryPick.CopyRange(self.context.GetSelectedLineIdx(), self.context.GetCommits(), self.context) + return self.c.Helpers().CherryPick.CopyRange(self.context.GetSelectedLineIdx(), self.context.GetCommits(), self.context) } diff --git a/pkg/gui/controllers/bisect_controller.go b/pkg/gui/controllers/bisect_controller.go index 083b6dce8..9aab43ee7 100644 --- a/pkg/gui/controllers/bisect_controller.go +++ b/pkg/gui/controllers/bisect_controller.go @@ -12,17 +12,17 @@ import ( type BisectController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &BisectController{} func NewBisectController( - common *controllerCommon, + common *ControllerCommon, ) *BisectController { return &BisectController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -42,7 +42,7 @@ func (self *BisectController) GetKeybindings(opts types.KeybindingsOpts) []*type func (self *BisectController) openMenu(commit *models.Commit) error { // no shame in getting this directly rather than using the cached value // given how cheap it is to obtain - info := self.git.Bisect.GetInfo() + info := self.c.Git().Bisect.GetInfo() if info.Started() { return self.openMidBisectMenu(info, commit) } else { @@ -63,14 +63,14 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c selectCurrentAfter := info.GetCurrentSha() == "" || info.GetCurrentSha() == commit.Sha // we need to wait to reselect if our bisect commits aren't ancestors of our 'start' // ref, because we'll be reloading our commits in that case. - waitToReselect := selectCurrentAfter && !self.git.Bisect.ReachableFromStart(info) + waitToReselect := selectCurrentAfter && !self.c.Git().Bisect.ReachableFromStart(info) menuItems := []*types.MenuItem{ { Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.NewTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectMark) - if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { + if err := self.c.Git().Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { return self.c.Error(err) } @@ -82,7 +82,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c Label: fmt.Sprintf(self.c.Tr.Bisect.Mark, commit.ShortSha(), info.OldTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectMark) - if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { + if err := self.c.Git().Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { return self.c.Error(err) } @@ -94,7 +94,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c Label: fmt.Sprintf(self.c.Tr.Bisect.Skip, commit.ShortSha()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.BisectSkip) - if err := self.git.Bisect.Skip(commit.Sha); err != nil { + if err := self.c.Git().Bisect.Skip(commit.Sha); err != nil { return self.c.Error(err) } @@ -105,7 +105,7 @@ func (self *BisectController) openMidBisectMenu(info *git_commands.BisectInfo, c { Label: self.c.Tr.Bisect.ResetOption, OnPress: func() error { - return self.helpers.Bisect.Reset() + return self.c.Helpers().Bisect.Reset() }, Key: 'r', }, @@ -125,15 +125,15 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.NewTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.StartBisect) - if err := self.git.Bisect.Start(); err != nil { + if err := self.c.Git().Bisect.Start(); err != nil { return self.c.Error(err) } - if err := self.git.Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { + if err := self.c.Git().Bisect.Mark(commit.Sha, info.NewTerm()); err != nil { return self.c.Error(err) } - return self.helpers.Bisect.PostBisectCommandRefresh() + return self.c.Helpers().Bisect.PostBisectCommandRefresh() }, Key: 'b', }, @@ -141,15 +141,15 @@ func (self *BisectController) openStartBisectMenu(info *git_commands.BisectInfo, Label: fmt.Sprintf(self.c.Tr.Bisect.MarkStart, commit.ShortSha(), info.OldTerm()), OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.StartBisect) - if err := self.git.Bisect.Start(); err != nil { + if err := self.c.Git().Bisect.Start(); err != nil { return self.c.Error(err) } - if err := self.git.Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { + if err := self.c.Git().Bisect.Mark(commit.Sha, info.OldTerm()); err != nil { return self.c.Error(err) } - return self.helpers.Bisect.PostBisectCommandRefresh() + return self.c.Helpers().Bisect.PostBisectCommandRefresh() }, Key: 'g', }, @@ -163,7 +163,7 @@ func (self *BisectController) showBisectCompleteMessage(candidateShas []string) prompt = self.c.Tr.Bisect.CompletePromptIndeterminate } - formattedCommits, err := self.git.Commit.GetCommitsOneline(candidateShas) + formattedCommits, err := self.c.Git().Commit.GetCommitsOneline(candidateShas) if err != nil { return self.c.Error(err) } @@ -173,17 +173,17 @@ func (self *BisectController) showBisectCompleteMessage(candidateShas []string) Prompt: fmt.Sprintf(prompt, strings.TrimSpace(formattedCommits)), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.ResetBisect) - if err := self.git.Bisect.Reset(); err != nil { + if err := self.c.Git().Bisect.Reset(); err != nil { return self.c.Error(err) } - return self.helpers.Bisect.PostBisectCommandRefresh() + return self.c.Helpers().Bisect.PostBisectCommandRefresh() }, }) } func (self *BisectController) afterMark(selectCurrent bool, waitToReselect bool) error { - done, candidateShas, err := self.git.Bisect.IsDone() + done, candidateShas, err := self.c.Git().Bisect.IsDone() if err != nil { return self.c.Error(err) } @@ -211,15 +211,15 @@ func (self *BisectController) afterBisectMarkRefresh(selectCurrent bool, waitToR } else { selectFn() - return self.helpers.Bisect.PostBisectCommandRefresh() + return self.c.Helpers().Bisect.PostBisectCommandRefresh() } } func (self *BisectController) selectCurrentBisectCommit() { - info := self.git.Bisect.GetInfo() + info := self.c.Git().Bisect.GetInfo() if info.GetCurrentSha() != "" { // find index of commit with that sha, move cursor to that. - for i, commit := range self.model.Commits { + for i, commit := range self.c.Model().Commits { if commit.Sha == info.GetCurrentSha() { self.context().SetSelectedLineIdx(i) _ = self.context().HandleFocus(types.OnFocusOpts{}) @@ -245,5 +245,5 @@ func (self *BisectController) Context() types.Context { } func (self *BisectController) context() *context.LocalCommitsContext { - return self.contexts.LocalCommits + return self.c.Contexts().LocalCommits } diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index d5564ec95..a39fe18e2 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -14,17 +14,17 @@ import ( type BranchesController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &BranchesController{} func NewBranchesController( - common *controllerCommon, + common *ControllerCommon, ) *BranchesController { return &BranchesController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -111,6 +111,30 @@ func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*ty } } +func (self *BranchesController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + branch := self.context().GetSelected() + if branch == nil { + task = types.NewRenderStringTask(self.c.Tr.NoBranchesThisRepo) + } else { + cmdObj := self.c.Git().Branch.GetGraphCmdObj(branch.FullRefName()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.LogTitle, + Task: task, + }, + }) + }) + } +} + func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.Actions.SetUnsetUpstream, @@ -118,7 +142,7 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { LabelColumns: []string{self.c.Tr.LcUnsetUpstream}, OnPress: func() error { - if err := self.git.Branch.UnsetUpstream(selectedBranch.Name); err != nil { + if err := self.c.Git().Branch.UnsetUpstream(selectedBranch.Name); err != nil { return self.c.Error(err) } if err := self.c.Refresh(types.RefreshOptions{ @@ -137,13 +161,13 @@ func (self *BranchesController) setUpstream(selectedBranch *models.Branch) error { LabelColumns: []string{self.c.Tr.LcSetUpstream}, OnPress: func() error { - return self.helpers.Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error { - upstreamRemote, upstreamBranch, err := self.helpers.Upstream.ParseUpstream(upstream) + return self.c.Helpers().Upstream.PromptForUpstreamWithoutInitialContent(selectedBranch, func(upstream string) error { + upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return self.c.Error(err) } - if err := self.git.Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil { + if err := self.c.Git().Branch.SetUpstream(upstreamRemote, upstreamBranch, selectedBranch.Name); err != nil { return self.c.Error(err) } if err := self.c.Refresh(types.RefreshOptions{ @@ -169,16 +193,16 @@ func (self *BranchesController) Context() types.Context { } func (self *BranchesController) context() *context.BranchesContext { - return self.contexts.Branches + return self.c.Contexts().Branches } func (self *BranchesController) press(selectedBranch *models.Branch) error { - if selectedBranch == self.helpers.Refs.GetCheckedOutRef() { + if selectedBranch == self.c.Helpers().Refs.GetCheckedOutRef() { return self.c.ErrorMsg(self.c.Tr.AlreadyCheckedOutBranch) } self.c.LogAction(self.c.Tr.Actions.CheckoutBranch) - return self.helpers.Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) + return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) } func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.Branch) error { @@ -186,7 +210,7 @@ func (self *BranchesController) handleCreatePullRequest(selectedBranch *models.B } func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *models.Branch) error { - checkedOutBranch := self.helpers.Refs.GetCheckedOutRef() + checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() return self.createPullRequestMenu(selectedBranch, checkedOutBranch) } @@ -194,18 +218,18 @@ func (self *BranchesController) handleCreatePullRequestMenu(selectedBranch *mode func (self *BranchesController) copyPullRequestURL() error { branch := self.context().GetSelected() - branchExistsOnRemote := self.git.Remote.CheckRemoteBranchExists(branch.Name) + branchExistsOnRemote := self.c.Git().Remote.CheckRemoteBranchExists(branch.Name) if !branchExistsOnRemote { return self.c.Error(errors.New(self.c.Tr.NoBranchOnRemote)) } - url, err := self.helpers.Host.GetPullRequestURL(branch.Name, "") + url, err := self.c.Helpers().Host.GetPullRequestURL(branch.Name, "") if err != nil { return self.c.Error(err) } self.c.LogAction(self.c.Tr.Actions.CopyPullRequestURL) - if err := self.os.CopyToClipboard(url); err != nil { + if err := self.c.OS().CopyToClipboard(url); err != nil { return self.c.Error(err) } @@ -224,7 +248,7 @@ func (self *BranchesController) forceCheckout() error { Prompt: message, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.ForceCheckoutBranch) - if err := self.git.Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { + if err := self.c.Git().Branch.Checkout(branch.Name, git_commands.CheckoutOptions{Force: true}); err != nil { _ = self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) @@ -235,10 +259,10 @@ func (self *BranchesController) forceCheckout() error { func (self *BranchesController) checkoutByName() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.BranchName + ":", - FindSuggestionsFunc: self.helpers.Suggestions.GetRefsSuggestionsFunc(), + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(), HandleConfirm: func(response string) error { self.c.LogAction("Checkout branch") - return self.helpers.Refs.CheckoutRef(response, types.CheckoutRefOptions{ + return self.c.Helpers().Refs.CheckoutRef(response, types.CheckoutRefOptions{ OnRefNotFound: func(ref string) error { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.BranchNotFoundTitle, @@ -260,7 +284,7 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er return nil } - if err := self.git.Branch.New(newBranchName, branch.FullRefName()); err != nil { + if err := self.c.Git().Branch.New(newBranchName, branch.FullRefName()); err != nil { return self.c.Error(err) } @@ -269,7 +293,7 @@ func (self *BranchesController) createNewBranchWithName(newBranchName string) er } func (self *BranchesController) delete(branch *models.Branch) error { - checkedOutBranch := self.helpers.Refs.GetCheckedOutRef() + checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() if checkedOutBranch.Name == branch.Name { return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch) } @@ -296,7 +320,7 @@ func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, f Prompt: message, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.DeleteBranch) - if err := self.git.Branch.Delete(selectedBranch.Name, force); err != nil { + if err := self.c.Git().Branch.Delete(selectedBranch.Name, force); err != nil { errMessage := err.Error() if !force && strings.Contains(errMessage, "git branch -D ") { return self.deleteWithForce(selectedBranch, true) @@ -310,12 +334,12 @@ func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, f func (self *BranchesController) merge() error { selectedBranchName := self.context().GetSelected().Name - return self.helpers.MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) + return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranchName) } func (self *BranchesController) rebase() error { selectedBranchName := self.context().GetSelected().Name - return self.helpers.MergeAndRebase.RebaseOntoRef(selectedBranchName) + return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranchName) } func (self *BranchesController) fastForward(branch *models.Branch) error { @@ -340,10 +364,10 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { ) return self.c.WithLoaderPanel(message, func() error { - if branch == self.helpers.Refs.GetCheckedOutRef() { + if branch == self.c.Helpers().Refs.GetCheckedOutRef() { self.c.LogAction(action) - err := self.git.Sync.Pull( + err := self.c.Git().Sync.Pull( git_commands.PullOptions{ RemoteName: branch.UpstreamRemote, BranchName: branch.UpstreamBranch, @@ -357,7 +381,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } else { self.c.LogAction(action) - err := self.git.Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) + err := self.c.Git().Sync.FastForward(branch.Name, branch.UpstreamRemote, branch.UpstreamBranch) if err != nil { _ = self.c.Error(err) } @@ -369,11 +393,11 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { } func (self *BranchesController) createTag(branch *models.Branch) error { - return self.helpers.Tags.CreateTagMenu(branch.FullRefName(), func() {}) + return self.c.Helpers().Tags.CreateTagMenu(branch.FullRefName(), func() {}) } func (self *BranchesController) createResetMenu(selectedBranch *models.Branch) error { - return self.helpers.Refs.CreateGitResetMenu(selectedBranch.Name) + return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.Name) } func (self *BranchesController) rename(branch *models.Branch) error { @@ -383,7 +407,7 @@ func (self *BranchesController) rename(branch *models.Branch) error { InitialContent: branch.Name, HandleConfirm: func(newBranchName string) error { self.c.LogAction(self.c.Tr.Actions.RenameBranch) - if err := self.git.Branch.Rename(branch.Name, newBranchName); err != nil { + if err := self.c.Git().Branch.Rename(branch.Name, newBranchName); err != nil { return self.c.Error(err) } @@ -391,7 +415,7 @@ func (self *BranchesController) rename(branch *models.Branch) error { _ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES}}) // now that we've got our stuff again we need to find that branch and reselect it. - for i, newBranch := range self.model.Branches { + for i, newBranch := range self.c.Model().Branches { if newBranch.Name == newBranchName { self.context().SetSelectedLineIdx(i) if err := self.context().HandleRender(); err != nil { @@ -420,7 +444,7 @@ func (self *BranchesController) rename(branch *models.Branch) error { } func (self *BranchesController) newBranch(selectedBranch *models.Branch) error { - return self.helpers.Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "") + return self.c.Helpers().Refs.NewBranch(selectedBranch.FullRefName(), selectedBranch.RefName(), "") } func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Branch, checkedOutBranch *models.Branch) error { @@ -443,7 +467,7 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra OnPress: func() error { return self.c.Prompt(types.PromptOpts{ Title: branch.Name + " →", - FindSuggestionsFunc: self.helpers.Suggestions.GetBranchNameSuggestionsFunc(), + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetBranchNameSuggestionsFunc(), HandleConfirm: func(targetBranchName string) error { return self.createPullRequest(branch.Name, targetBranchName) }, @@ -471,14 +495,14 @@ func (self *BranchesController) createPullRequestMenu(selectedBranch *models.Bra } func (self *BranchesController) createPullRequest(from string, to string) error { - url, err := self.helpers.Host.GetPullRequestURL(from, to) + url, err := self.c.Helpers().Host.GetPullRequestURL(from, to) if err != nil { return self.c.Error(err) } self.c.LogAction(self.c.Tr.Actions.OpenPullRequest) - if err := self.os.OpenLink(url); err != nil { + if err := self.c.OS().OpenLink(url); err != nil { return self.c.Error(err) } diff --git a/pkg/gui/controllers/command_log_controller.go b/pkg/gui/controllers/command_log_controller.go new file mode 100644 index 000000000..0c3479914 --- /dev/null +++ b/pkg/gui/controllers/command_log_controller.go @@ -0,0 +1,42 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CommandLogController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &CommandLogController{} + +func NewCommandLogController( + common *ControllerCommon, +) *CommandLogController { + return &CommandLogController{ + baseController: baseController{}, + c: common, + } +} + +func (self *CommandLogController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{} + + return bindings +} + +func (self *CommandLogController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.c.Views().Extras.Autoscroll = true + return nil + } +} + +func (self *CommandLogController) Context() types.Context { + return self.context() +} + +func (self *CommandLogController) context() types.Context { + return self.c.Contexts().CommandLog +} diff --git a/pkg/gui/controllers/commit_description_controller.go b/pkg/gui/controllers/commit_description_controller.go index 5624fa448..78d275184 100644 --- a/pkg/gui/controllers/commit_description_controller.go +++ b/pkg/gui/controllers/commit_description_controller.go @@ -6,17 +6,17 @@ import ( type CommitDescriptionController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &CommitMessageController{} func NewCommitDescriptionController( - common *controllerCommon, + common *ControllerCommon, ) *CommitDescriptionController { return &CommitDescriptionController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -44,17 +44,17 @@ func (self *CommitDescriptionController) Context() types.Context { } func (self *CommitDescriptionController) context() types.Context { - return self.contexts.CommitMessage + return self.c.Contexts().CommitMessage } func (self *CommitDescriptionController) switchToCommitMessage() error { - return self.c.PushContext(self.contexts.CommitMessage) + return self.c.PushContext(self.c.Contexts().CommitMessage) } func (self *CommitDescriptionController) close() error { - return self.helpers.Commits.CloseCommitMessagePanel() + return self.c.Helpers().Commits.CloseCommitMessagePanel() } func (self *CommitDescriptionController) confirm() error { - return self.helpers.Commits.HandleCommitConfirm() + return self.c.Helpers().Commits.HandleCommitConfirm() } diff --git a/pkg/gui/controllers/commit_message_controller.go b/pkg/gui/controllers/commit_message_controller.go index 0772cc16b..929e647f3 100644 --- a/pkg/gui/controllers/commit_message_controller.go +++ b/pkg/gui/controllers/commit_message_controller.go @@ -8,29 +8,33 @@ import ( type CommitMessageController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &CommitMessageController{} func NewCommitMessageController( - common *controllerCommon, + common *ControllerCommon, ) *CommitMessageController { return &CommitMessageController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } +// TODO: merge that commit panel PR because we're not currently showing how to add a newline as it's +// handled by the editor func rather than by the controller here. func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { bindings := []*types.Binding{ { - Key: opts.GetKey(opts.Config.Universal.SubmitEditorText), - Handler: self.confirm, + Key: opts.GetKey(opts.Config.Universal.SubmitEditorText), + Handler: self.confirm, + Description: self.c.Tr.LcConfirm, }, { - Key: opts.GetKey(opts.Config.Universal.Return), - Handler: self.close, + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: self.close, + Description: self.c.Tr.LcClose, }, { Key: opts.GetKey(opts.Config.Universal.PrevItem), @@ -49,12 +53,19 @@ func (self *CommitMessageController) GetKeybindings(opts types.KeybindingsOpts) return bindings } +func (self *CommitMessageController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.context().RenderCommitLength() + return nil + } +} + func (self *CommitMessageController) Context() types.Context { return self.context() } func (self *CommitMessageController) context() *context.CommitMessageContext { - return self.contexts.CommitMessage + return self.c.Contexts().CommitMessage } func (self *CommitMessageController) handlePreviousCommit() error { @@ -69,7 +80,7 @@ func (self *CommitMessageController) handleNextCommit() error { } func (self *CommitMessageController) switchToCommitDescription() error { - if err := self.c.PushContext(self.contexts.CommitDescription); err != nil { + if err := self.c.PushContext(self.c.Contexts().CommitDescription); err != nil { return err } return nil @@ -80,10 +91,10 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error { newIndex := currentIndex + value if newIndex == context.NoCommitIndex { self.context().SetSelectedIndex(newIndex) - self.helpers.Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage()) + self.c.Helpers().Commits.SetMessageAndDescriptionInView(self.context().GetHistoryMessage()) return nil } else if currentIndex == context.NoCommitIndex { - self.context().SetHistoryMessage(self.helpers.Commits.JoinCommitMessageAndDescription()) + self.context().SetHistoryMessage(self.c.Helpers().Commits.JoinCommitMessageAndDescription()) } validCommit, err := self.setCommitMessageAtIndex(newIndex) @@ -95,21 +106,21 @@ func (self *CommitMessageController) handleCommitIndexChange(value int) error { // returns true if the given index is for a valid commit func (self *CommitMessageController) setCommitMessageAtIndex(index int) (bool, error) { - commitMessage, err := self.git.Commit.GetCommitMessageFromHistory(index) + commitMessage, err := self.c.Git().Commit.GetCommitMessageFromHistory(index) if err != nil { if err == git_commands.ErrInvalidCommitIndex { return false, nil } return false, self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) } - self.helpers.Commits.UpdateCommitPanelView(commitMessage) + self.c.Helpers().Commits.UpdateCommitPanelView(commitMessage) return true, nil } func (self *CommitMessageController) confirm() error { - return self.helpers.Commits.HandleCommitConfirm() + return self.c.Helpers().Commits.HandleCommitConfirm() } func (self *CommitMessageController) close() error { - return self.helpers.Commits.CloseCommitMessagePanel() + return self.c.Helpers().Commits.CloseCommitMessagePanel() } diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index a5c5e667d..b7f53ec69 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -11,17 +11,17 @@ import ( type CommitFilesController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &CommitFilesController{} func NewCommitFilesController( - common *controllerCommon, + common *ControllerCommon, ) *CommitFilesController { return &CommitFilesController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -99,7 +99,39 @@ func (self *CommitFilesController) Context() types.Context { } func (self *CommitFilesController) context() *context.CommitFilesContext { - return self.contexts.CommitFiles + return self.c.Contexts().CommitFiles +} + +func (self *CommitFilesController) GetOnRenderToMain() func() error { + return func() error { + node := self.context().GetSelected() + if node == nil { + return nil + } + + ref := self.context().GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + + cmdObj := self.c.Git().WorkingTree.ShowFileDiffCmdObj( + from, to, reverse, node.GetPath(), false, self.c.State().GetIgnoreWhitespaceInDiffView(), + ) + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + pair := self.c.MainViewPairs().Normal + if node.File != nil { + pair = self.c.MainViewPairs().PatchBuilding + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: pair, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.Patch, + Task: task, + }, + Secondary: secondaryPatchPanelUpdateOpts(self.c), + }) + } } func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) error { @@ -112,7 +144,7 @@ func (self *CommitFilesController) onClickMain(opts gocui.ViewMouseBindingOpts) func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error { self.c.LogAction(self.c.Tr.Actions.CheckoutFile) - if err := self.git.WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil { + if err := self.c.Git().WorkingTree.CheckoutFile(self.context().GetRef().RefName(), node.GetPath()); err != nil { return self.c.Error(err) } @@ -120,7 +152,7 @@ func (self *CommitFilesController) checkout(node *filetree.CommitFileNode) error } func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error { - if ok, err := self.helpers.PatchBuilding.ValidateNormalWorkingTreeState(); !ok { + if ok, err := self.c.Helpers().PatchBuilding.ValidateNormalWorkingTreeState(); !ok { return err } @@ -130,8 +162,8 @@ func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.DiscardOldFileChange) - if err := self.git.Rebase.DiscardOldFileChanges(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), node.GetPath()); err != nil { - if err := self.helpers.MergeAndRebase.CheckMergeOrRebase(err); err != nil { + if err := self.c.Git().Rebase.DiscardOldFileChanges(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), node.GetPath()); err != nil { + if err := self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err); err != nil { return err } } @@ -143,7 +175,7 @@ func (self *CommitFilesController) discard(node *filetree.CommitFileNode) error } func (self *CommitFilesController) open(node *filetree.CommitFileNode) error { - return self.helpers.Files.OpenFile(node.GetPath()) + return self.c.Helpers().Files.OpenFile(node.GetPath()) } func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { @@ -151,13 +183,13 @@ func (self *CommitFilesController) edit(node *filetree.CommitFileNode) error { return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) } - return self.helpers.Files.EditFile(node.GetPath()) + return self.c.Helpers().Files.EditFile(node.GetPath()) } func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) error { toggle := func() error { return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingPatch, func() error { - if !self.git.Patch.PatchBuilder.Active() { + if !self.c.Git().Patch.PatchBuilder.Active() { if err := self.startPatchBuilder(); err != nil { return err } @@ -166,34 +198,34 @@ func (self *CommitFilesController) toggleForPatch(node *filetree.CommitFileNode) // if there is any file that hasn't been fully added we'll fully add everything, // otherwise we'll remove everything adding := node.SomeFile(func(file *models.CommitFile) bool { - return self.git.Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE + return self.c.Git().Patch.PatchBuilder.GetFileStatus(file.Name, self.context().GetRef().RefName()) != patch.WHOLE }) err := node.ForEachFile(func(file *models.CommitFile) error { if adding { - return self.git.Patch.PatchBuilder.AddFileWhole(file.Name) + return self.c.Git().Patch.PatchBuilder.AddFileWhole(file.Name) } else { - return self.git.Patch.PatchBuilder.RemoveFile(file.Name) + return self.c.Git().Patch.PatchBuilder.RemoveFile(file.Name) } }) if err != nil { return self.c.Error(err) } - if self.git.Patch.PatchBuilder.IsEmpty() { - self.git.Patch.PatchBuilder.Reset() + if self.c.Git().Patch.PatchBuilder.IsEmpty() { + self.c.Git().Patch.PatchBuilder.Reset() } return self.c.PostRefreshUpdate(self.context()) }) } - if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() { + if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.To != self.context().GetRef().RefName() { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.DiscardPatch, Prompt: self.c.Tr.DiscardPatchConfirm, HandleConfirm: func() error { - self.git.Patch.PatchBuilder.Reset() + self.c.Git().Patch.PatchBuilder.Reset() return toggle() }, }) @@ -213,9 +245,9 @@ func (self *CommitFilesController) startPatchBuilder() error { canRebase := commitFilesContext.GetCanRebase() ref := commitFilesContext.GetRef() to := ref.RefName() - from, reverse := self.modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - self.git.Patch.PatchBuilder.Start(from, to, reverse, canRebase) + self.c.Git().Patch.PatchBuilder.Start(from, to, reverse, canRebase) return nil } @@ -229,21 +261,21 @@ func (self *CommitFilesController) enterCommitFile(node *filetree.CommitFileNode } enterTheFile := func() error { - if !self.git.Patch.PatchBuilder.Active() { + if !self.c.Git().Patch.PatchBuilder.Active() { if err := self.startPatchBuilder(); err != nil { return err } } - return self.c.PushContext(self.contexts.CustomPatchBuilder, opts) + return self.c.PushContext(self.c.Contexts().CustomPatchBuilder, opts) } - if self.git.Patch.PatchBuilder.Active() && self.git.Patch.PatchBuilder.To != self.context().GetRef().RefName() { + if self.c.Git().Patch.PatchBuilder.Active() && self.c.Git().Patch.PatchBuilder.To != self.context().GetRef().RefName() { return self.c.Confirm(types.ConfirmOpts{ Title: self.c.Tr.DiscardPatch, Prompt: self.c.Tr.DiscardPatchConfirm, HandleConfirm: func() error { - self.git.Patch.PatchBuilder.Reset() + self.c.Git().Patch.PatchBuilder.Reset() return enterTheFile() }, }) diff --git a/pkg/gui/controllers/common.go b/pkg/gui/controllers/common.go index 12a3788fd..3498ad59d 100644 --- a/pkg/gui/controllers/common.go +++ b/pkg/gui/controllers/common.go @@ -1,42 +1,24 @@ package controllers import ( - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" - "github.com/jesseduffield/lazygit/pkg/gui/types" ) -type controllerCommon struct { - c *types.HelperCommon - os *oscommands.OSCommand - git *commands.GitCommand - helpers *helpers.Helpers - model *types.Model - contexts *context.ContextTree - modes *types.Modes - mutexes *types.Mutexes +type ControllerCommon struct { + *helpers.HelperCommon + IGetHelpers +} + +type IGetHelpers interface { + Helpers() *helpers.Helpers } func NewControllerCommon( - c *types.HelperCommon, - os *oscommands.OSCommand, - git *commands.GitCommand, - helpers *helpers.Helpers, - model *types.Model, - contexts *context.ContextTree, - modes *types.Modes, - mutexes *types.Mutexes, -) *controllerCommon { - return &controllerCommon{ - c: c, - os: os, - git: git, - helpers: helpers, - model: model, - contexts: contexts, - modes: modes, - mutexes: mutexes, + c *helpers.HelperCommon, + IGetHelpers IGetHelpers, +) *ControllerCommon { + return &ControllerCommon{ + HelperCommon: c, + IGetHelpers: IGetHelpers, } } diff --git a/pkg/gui/controllers/confirmation_controller.go b/pkg/gui/controllers/confirmation_controller.go new file mode 100644 index 000000000..09af35586 --- /dev/null +++ b/pkg/gui/controllers/confirmation_controller.go @@ -0,0 +1,65 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ConfirmationController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &ConfirmationController{} + +func NewConfirmationController( + common *ControllerCommon, +) *ConfirmationController { + return &ConfirmationController{ + baseController: baseController{}, + c: common, + } +} + +func (self *ConfirmationController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: func() error { return self.context().State.OnConfirm() }, + Description: self.c.Tr.LcConfirm, + Display: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: func() error { return self.context().State.OnClose() }, + Description: self.c.Tr.LcCloseCancel, + Display: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: func() error { + if len(self.c.Contexts().Suggestions.State.Suggestions) > 0 { + return self.c.ReplaceContext(self.c.Contexts().Suggestions) + } + return nil + }, + }, + } + + return bindings +} + +func (self *ConfirmationController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.c.Helpers().Confirmation.DeactivateConfirmationPrompt() + return nil + } +} + +func (self *ConfirmationController) Context() types.Context { + return self.context() +} + +func (self *ConfirmationController) context() *context.ConfirmationContext { + return self.c.Contexts().Confirmation +} diff --git a/pkg/gui/controllers/context_lines_controller.go b/pkg/gui/controllers/context_lines_controller.go index 5a4fb291b..5ec2d3167 100644 --- a/pkg/gui/controllers/context_lines_controller.go +++ b/pkg/gui/controllers/context_lines_controller.go @@ -24,17 +24,17 @@ var CONTEXT_KEYS_SHOWING_DIFFS = []types.ContextKey{ type ContextLinesController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &ContextLinesController{} func NewContextLinesController( - common *controllerCommon, + common *ControllerCommon, ) *ContextLinesController { return &ContextLinesController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -101,7 +101,7 @@ func (self *ContextLinesController) applyChange() error { } func (self *ContextLinesController) checkCanChangeContext() error { - if self.git.Patch.PatchBuilder.Active() { + if self.c.Git().Patch.PatchBuilder.Active() { return errors.New(self.c.Tr.CantChangeContextSizeError) } diff --git a/pkg/gui/controllers/custom_command_action.go b/pkg/gui/controllers/custom_command_action.go new file mode 100644 index 000000000..4b3aaa885 --- /dev/null +++ b/pkg/gui/controllers/custom_command_action.go @@ -0,0 +1,53 @@ +package controllers + +import ( + "strings" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +type CustomCommandAction struct { + c *ControllerCommon +} + +func (self *CustomCommandAction) Call() error { + return self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.CustomCommand, + FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(), + HandleConfirm: func(command string) error { + if self.shouldSaveCommand(command) { + self.c.GetAppState().CustomCommandsHistory = utils.Limit( + lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)), + 1000, + ) + } + + err := self.c.SaveAppState() + if err != nil { + self.c.Log.Error(err) + } + + self.c.LogAction(self.c.Tr.Actions.CustomCommand) + return self.c.RunSubprocessAndRefresh( + self.c.OS().Cmd.NewShell(command), + ) + }, + }) +} + +func (self *CustomCommandAction) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { + // reversing so that we display the latest command first + history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory) + + return helpers.FuzzySearchFunc(history) +} + +// this mimics the shell functionality `ignorespace` +// which doesn't save a command to history if it starts with a space +func (self *CustomCommandAction) shouldSaveCommand(command string) bool { + return !strings.HasPrefix(command, " ") +} diff --git a/pkg/gui/controllers/custom_patch_options_menu_action.go b/pkg/gui/controllers/custom_patch_options_menu_action.go new file mode 100644 index 000000000..ca566c88c --- /dev/null +++ b/pkg/gui/controllers/custom_patch_options_menu_action.go @@ -0,0 +1,219 @@ +package controllers + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type CustomPatchOptionsMenuAction struct { + c *ControllerCommon +} + +func (self *CustomPatchOptionsMenuAction) Call() error { + if !self.c.Git().Patch.PatchBuilder.Active() { + return self.c.ErrorMsg(self.c.Tr.NoPatchError) + } + + menuItems := []*types.MenuItem{ + { + Label: "reset patch", + OnPress: self.c.Helpers().PatchBuilding.Reset, + Key: 'c', + }, + { + Label: "apply patch", + OnPress: func() error { return self.handleApplyPatch(false) }, + Key: 'a', + }, + { + Label: "apply patch in reverse", + OnPress: func() error { return self.handleApplyPatch(true) }, + Key: 'r', + }, + } + + if self.c.Git().Patch.PatchBuilder.CanRebase && self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_NONE { + menuItems = append(menuItems, []*types.MenuItem{ + { + Label: fmt.Sprintf("remove patch from original commit (%s)", self.c.Git().Patch.PatchBuilder.To), + OnPress: self.handleDeletePatchFromCommit, + Key: 'd', + }, + { + Label: "move patch out into index", + OnPress: self.handleMovePatchIntoWorkingTree, + Key: 'i', + }, + { + Label: "move patch into new commit", + OnPress: self.handlePullPatchIntoNewCommit, + Key: 'n', + }, + }...) + + if self.c.CurrentContext().GetKey() == self.c.Contexts().LocalCommits.GetKey() { + selectedCommit := self.c.Contexts().LocalCommits.GetSelected() + if selectedCommit != nil && self.c.Git().Patch.PatchBuilder.To != selectedCommit.Sha { + // adding this option to index 1 + menuItems = append( + menuItems[:1], + append( + []*types.MenuItem{ + { + Label: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha), + OnPress: self.handleMovePatchToSelectedCommit, + Key: 'm', + }, + }, menuItems[1:]..., + )..., + ) + } + } + } + + menuItems = append(menuItems, []*types.MenuItem{ + { + Label: "copy patch to clipboard", + OnPress: func() error { return self.copyPatchToClipboard() }, + Key: 'y', + }, + }...) + + return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.PatchOptionsTitle, Items: menuItems}) +} + +func (self *CustomPatchOptionsMenuAction) getPatchCommitIndex() int { + for index, commit := range self.c.Model().Commits { + if commit.Sha == self.c.Git().Patch.PatchBuilder.To { + return index + } + } + return -1 +} + +func (self *CustomPatchOptionsMenuAction) validateNormalWorkingTreeState() (bool, error) { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + return false, self.c.ErrorMsg(self.c.Tr.CantPatchWhileRebasingError) + } + return true, nil +} + +func (self *CustomPatchOptionsMenuAction) returnFocusFromPatchExplorerIfNecessary() error { + if self.c.CurrentContext().GetKey() == self.c.Contexts().CustomPatchBuilder.GetKey() { + return self.c.Helpers().PatchBuilding.Escape() + } + return nil +} + +func (self *CustomPatchOptionsMenuAction) handleDeletePatchFromCommit() error { + if ok, err := self.validateNormalWorkingTreeState(); !ok { + return err + } + + if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil { + return err + } + + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + commitIndex := self.getPatchCommitIndex() + self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit) + err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) +} + +func (self *CustomPatchOptionsMenuAction) handleMovePatchToSelectedCommit() error { + if ok, err := self.validateNormalWorkingTreeState(); !ok { + return err + } + + if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil { + return err + } + + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + commitIndex := self.getPatchCommitIndex() + self.c.LogAction(self.c.Tr.Actions.MovePatchToSelectedCommit) + err := self.c.Git().Patch.MovePatchToSelectedCommit(self.c.Model().Commits, commitIndex, self.c.Contexts().LocalCommits.GetSelectedLineIdx()) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) +} + +func (self *CustomPatchOptionsMenuAction) handleMovePatchIntoWorkingTree() error { + if ok, err := self.validateNormalWorkingTreeState(); !ok { + return err + } + + if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil { + return err + } + + pull := func(stash bool) error { + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + commitIndex := self.getPatchCommitIndex() + self.c.LogAction(self.c.Tr.Actions.MovePatchIntoIndex) + err := self.c.Git().Patch.MovePatchIntoIndex(self.c.Model().Commits, commitIndex, stash) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) + } + + if self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { + return self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.MustStashTitle, + Prompt: self.c.Tr.MustStashWarning, + HandleConfirm: func() error { + return pull(true) + }, + }) + } else { + return pull(false) + } +} + +func (self *CustomPatchOptionsMenuAction) handlePullPatchIntoNewCommit() error { + if ok, err := self.validateNormalWorkingTreeState(); !ok { + return err + } + + if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil { + return err + } + + return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { + commitIndex := self.getPatchCommitIndex() + self.c.LogAction(self.c.Tr.Actions.MovePatchIntoNewCommit) + err := self.c.Git().Patch.PullPatchIntoNewCommit(self.c.Model().Commits, commitIndex) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) +} + +func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error { + if err := self.returnFocusFromPatchExplorerIfNecessary(); err != nil { + return err + } + + action := self.c.Tr.Actions.ApplyPatch + if reverse { + action = "Apply patch in reverse" + } + self.c.LogAction(action) + if err := self.c.Git().Patch.PatchBuilder.ApplyPatches(reverse); err != nil { + return self.c.Error(err) + } + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) +} + +func (self *CustomPatchOptionsMenuAction) copyPatchToClipboard() error { + patch := self.c.Git().Patch.PatchBuilder.RenderAggregatedPatch(true) + + self.c.LogAction(self.c.Tr.Actions.CopyPatchToClipboard) + if err := self.c.OS().CopyToClipboard(patch); err != nil { + return self.c.Error(err) + } + + self.c.Toast(self.c.Tr.PatchCopiedToClipboard) + + return nil +} diff --git a/pkg/gui/controllers/diffing_menu_action.go b/pkg/gui/controllers/diffing_menu_action.go new file mode 100644 index 000000000..951286ece --- /dev/null +++ b/pkg/gui/controllers/diffing_menu_action.go @@ -0,0 +1,69 @@ +package controllers + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type DiffingMenuAction struct { + c *ControllerCommon +} + +func (self *DiffingMenuAction) Call() error { + names := self.c.Helpers().Diff.CurrentDiffTerminals() + + menuItems := []*types.MenuItem{} + for _, name := range names { + name := name + menuItems = append(menuItems, []*types.MenuItem{ + { + Label: fmt.Sprintf("%s %s", self.c.Tr.LcDiff, name), + OnPress: func() error { + self.c.Modes().Diffing.Ref = name + // can scope this down based on current view but too lazy right now + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }, + }...) + } + + menuItems = append(menuItems, []*types.MenuItem{ + { + Label: self.c.Tr.LcEnterRefToDiff, + OnPress: func() error { + return self.c.Prompt(types.PromptOpts{ + Title: self.c.Tr.LcEnteRefName, + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRefsSuggestionsFunc(), + HandleConfirm: func(response string) error { + self.c.Modes().Diffing.Ref = strings.TrimSpace(response) + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }) + }, + }, + }...) + + if self.c.Modes().Diffing.Active() { + menuItems = append(menuItems, []*types.MenuItem{ + { + Label: self.c.Tr.LcSwapDiff, + OnPress: func() error { + self.c.Modes().Diffing.Reverse = !self.c.Modes().Diffing.Reverse + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }, + { + Label: self.c.Tr.LcExitDiffMode, + OnPress: func() error { + self.c.Modes().Diffing = diffing.New() + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) + }, + }, + }...) + } + + return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.DiffingMenuTitle, Items: menuItems}) +} diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index dcbf05e76..65fd16891 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -13,23 +13,16 @@ import ( type FilesController struct { baseController // nolint: unused - *controllerCommon - - enterSubmodule func(submodule *models.SubmoduleConfig) error - setCommitMessage func(message string) + c *ControllerCommon } var _ types.IController = &FilesController{} func NewFilesController( - common *controllerCommon, - enterSubmodule func(submodule *models.SubmoduleConfig) error, - setCommitMessage func(message string), + common *ControllerCommon, ) *FilesController { return &FilesController{ - controllerCommon: common, - enterSubmodule: enterSubmodule, - setCommitMessage: setCommitMessage, + c: common, } } @@ -47,12 +40,12 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types }, { Key: opts.GetKey(opts.Config.Files.CommitChanges), - Handler: self.helpers.WorkingTree.HandleCommitPress, + Handler: self.c.Helpers().WorkingTree.HandleCommitPress, Description: self.c.Tr.CommitChanges, }, { Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook), - Handler: self.helpers.WorkingTree.HandleWIPCommitPress, + Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress, Description: self.c.Tr.LcCommitChangesWithoutHook, }, { @@ -62,7 +55,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types }, { Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor), - Handler: self.helpers.WorkingTree.HandleCommitEditorPress, + Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress, Description: self.c.Tr.CommitChangesWithEditor, }, { @@ -126,7 +119,7 @@ func (self *FilesController) GetKeybindings(opts types.KeybindingsOpts) []*types }, { Key: opts.GetKey(opts.Config.Files.OpenMergeTool), - Handler: self.helpers.WorkingTree.OpenMergeTool, + Handler: self.c.Helpers().WorkingTree.OpenMergeTool, Description: self.c.Tr.LcOpenMergeTool, }, { @@ -172,6 +165,74 @@ func (self *FilesController) GetMouseKeybindings(opts types.KeybindingsOpts) []* } } +func (self *FilesController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + node := self.context().GetSelected() + + if node == nil { + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.DiffTitle, + Task: types.NewRenderStringTask(self.c.Tr.NoChangedFiles), + }, + }) + } + + if node.File != nil && node.File.HasInlineMergeConflicts { + hasConflicts, err := self.c.Helpers().MergeConflicts.SetMergeState(node.GetPath()) + if err != nil { + return err + } + + if hasConflicts { + return self.c.Helpers().MergeConflicts.Render(false) + } + } + + self.c.Helpers().MergeConflicts.ResetMergeState() + + pair := self.c.MainViewPairs().Normal + if node.File != nil { + pair = self.c.MainViewPairs().Staging + } + + split := self.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) + mainShowsStaged := !split && node.GetHasStagedChanges() + + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, self.c.State().GetIgnoreWhitespaceInDiffView()) + title := self.c.Tr.UnstagedChanges + if mainShowsStaged { + title = self.c.Tr.StagedChanges + } + refreshOpts := types.RefreshMainOpts{ + Pair: pair, + Main: &types.ViewUpdateOpts{ + Task: types.NewRunPtyTask(cmdObj.GetCmd()), + Title: title, + }, + } + + if split { + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(node, false, true, self.c.State().GetIgnoreWhitespaceInDiffView()) + + title := self.c.Tr.StagedChanges + if mainShowsStaged { + title = self.c.Tr.UnstagedChanges + } + + refreshOpts.Secondary = &types.ViewUpdateOpts{ + Title: title, + Task: types.NewRunPtyTask(cmdObj.GetCmd()), + } + } + + return self.c.RenderToMainViews(refreshOpts) + }) + } +} + func (self *FilesController) GetOnClick() func() error { return self.checkSelectedFileNode(self.press) } @@ -227,7 +288,7 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti rerender := false err := node.ForEachFile(func(f *models.File) error { // can't act on the file itself: we need to update the original model file - for _, modelFile := range self.model.Files { + for _, modelFile := range self.c.Model().Files { if modelFile.Name == f.Name { if optimisticChangeFn(modelFile) { rerender = true @@ -242,7 +303,7 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti return err } if rerender { - if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil { + if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil { return err } } @@ -253,8 +314,8 @@ func (self *FilesController) optimisticChange(node *filetree.FileNode, optimisti func (self *FilesController) pressWithLock(node *filetree.FileNode) error { // Obtaining this lock because optimistic rendering requires us to mutate // the files in our model. - self.mutexes.RefreshingFilesMutex.Lock() - defer self.mutexes.RefreshingFilesMutex.Unlock() + self.c.Mutexes().RefreshingFilesMutex.Lock() + defer self.c.Mutexes().RefreshingFilesMutex.Unlock() if node.IsFile() { file := node.File @@ -266,7 +327,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error { return err } - if err := self.git.WorkingTree.StageFile(file.Name); err != nil { + if err := self.c.Git().WorkingTree.StageFile(file.Name); err != nil { return self.c.Error(err) } } else { @@ -276,7 +337,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error { return err } - if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return self.c.Error(err) } } @@ -294,7 +355,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error { return err } - if err := self.git.WorkingTree.StageFile(node.Path); err != nil { + if err := self.c.Git().WorkingTree.StageFile(node.Path); err != nil { return self.c.Error(err) } } else { @@ -305,7 +366,7 @@ func (self *FilesController) pressWithLock(node *filetree.FileNode) error { } // pretty sure it doesn't matter that we're always passing true here - if err := self.git.WorkingTree.UnStageFile([]string{node.Path}, true); err != nil { + if err := self.c.Git().WorkingTree.UnStageFile([]string{node.Path}, true); err != nil { return self.c.Error(err) } } @@ -346,7 +407,7 @@ func (self *FilesController) Context() types.Context { } func (self *FilesController) context() *context.WorkingTreeContext { - return self.contexts.Files + return self.c.Contexts().Files } func (self *FilesController) getSelectedFile() *models.File { @@ -373,10 +434,10 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error { file := node.File - submoduleConfigs := self.model.Submodules + submoduleConfigs := self.c.Model().Submodules if file.IsSubmodule(submoduleConfigs) { submoduleConfig := file.SubmoduleConfig(submoduleConfigs) - return self.enterSubmodule(submoduleConfig) + return self.c.Helpers().Repos.EnterSubmodule(submoduleConfig) } if file.HasInlineMergeConflicts { @@ -386,7 +447,7 @@ func (self *FilesController) EnterFile(opts types.OnFocusOpts) error { return self.c.ErrorMsg(self.c.Tr.FileStagingRequirements) } - return self.c.PushContext(self.contexts.Staging, opts) + return self.c.PushContext(self.c.Contexts().Staging, opts) } func (self *FilesController) toggleStagedAll() error { @@ -402,8 +463,8 @@ func (self *FilesController) toggleStagedAll() error { } func (self *FilesController) toggleStagedAllWithLock() error { - self.mutexes.RefreshingFilesMutex.Lock() - defer self.mutexes.RefreshingFilesMutex.Unlock() + self.c.Mutexes().RefreshingFilesMutex.Lock() + defer self.c.Mutexes().RefreshingFilesMutex.Unlock() root := self.context().FileTreeViewModel.GetRoot() @@ -420,7 +481,7 @@ func (self *FilesController) toggleStagedAllWithLock() error { return err } - if err := self.git.WorkingTree.StageAll(); err != nil { + if err := self.c.Git().WorkingTree.StageAll(); err != nil { return self.c.Error(err) } } else { @@ -430,7 +491,7 @@ func (self *FilesController) toggleStagedAllWithLock() error { return err } - if err := self.git.WorkingTree.UnstageAll(); err != nil { + if err := self.c.Git().WorkingTree.UnstageAll(); err != nil { return self.c.Error(err) } } @@ -441,7 +502,7 @@ func (self *FilesController) toggleStagedAllWithLock() error { func (self *FilesController) unstageFiles(node *filetree.FileNode) error { return node.ForEachFile(func(file *models.File) error { if file.HasStagedChanges { - if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return err } } @@ -457,7 +518,7 @@ func (self *FilesController) ignoreOrExcludeTracked(node *filetree.FileNode, trA return err } - if err := self.git.WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { + if err := self.c.Git().WorkingTree.RemoveTrackedFiles(node.GetPath()); err != nil { return err } @@ -495,7 +556,7 @@ func (self *FilesController) ignore(node *filetree.FileNode) error { if node.GetPath() == ".gitignore" { return self.c.ErrorMsg(self.c.Tr.Actions.IgnoreFileErr) } - err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.LcIgnoreExcludeFile, self.git.WorkingTree.Ignore) + err := self.ignoreOrExcludeFile(node, self.c.Tr.IgnoreTracked, self.c.Tr.IgnoreTrackedPrompt, self.c.Tr.Actions.LcIgnoreExcludeFile, self.c.Git().WorkingTree.Ignore) if err != nil { return err } @@ -512,7 +573,7 @@ func (self *FilesController) exclude(node *filetree.FileNode) error { return self.c.ErrorMsg(self.c.Tr.Actions.ExcludeGitIgnoreErr) } - err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.git.WorkingTree.Exclude) + err := self.ignoreOrExcludeFile(node, self.c.Tr.ExcludeTracked, self.c.Tr.ExcludeTrackedPrompt, self.c.Tr.Actions.ExcludeFile, self.c.Git().WorkingTree.Exclude) if err != nil { return err } @@ -552,19 +613,19 @@ func (self *FilesController) refresh() error { } func (self *FilesController) handleAmendCommitPress() error { - if len(self.model.Files) == 0 { + if len(self.c.Model().Files) == 0 { return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) } - if !self.helpers.WorkingTree.AnyStagedFiles() { - return self.helpers.WorkingTree.PromptToStageAllAndRetry(self.handleAmendCommitPress) + if !self.c.Helpers().WorkingTree.AnyStagedFiles() { + return self.c.Helpers().WorkingTree.PromptToStageAllAndRetry(self.handleAmendCommitPress) } - if len(self.model.Commits) == 0 { + if len(self.c.Model().Commits) == 0 { return self.c.ErrorMsg(self.c.Tr.NoCommitToAmend) } - return self.helpers.AmendHelper.AmendHead() + return self.c.Helpers().AmendHelper.AmendHead() } func (self *FilesController) handleStatusFilterPressed() error { @@ -603,7 +664,7 @@ func (self *FilesController) edit(node *filetree.FileNode) error { return self.c.ErrorMsg(self.c.Tr.ErrCannotEditDirectory) } - return self.helpers.Files.EditFile(node.GetPath()) + return self.c.Helpers().Files.EditFile(node.GetPath()) } func (self *FilesController) Open() error { @@ -612,7 +673,7 @@ func (self *FilesController) Open() error { return nil } - return self.helpers.Files.OpenFile(node.GetPath()) + return self.c.Helpers().Files.OpenFile(node.GetPath()) } func (self *FilesController) switchToMerge() error { @@ -621,7 +682,7 @@ func (self *FilesController) switchToMerge() error { return nil } - return self.helpers.MergeConflicts.SwitchToMerge(file.Name) + return self.c.Helpers().MergeConflicts.SwitchToMerge(file.Name) } func (self *FilesController) createStashMenu() error { @@ -631,28 +692,28 @@ func (self *FilesController) createStashMenu() error { { Label: self.c.Tr.LcStashAllChanges, OnPress: func() error { - if !self.helpers.WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) } - return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashAllChanges) + return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashAllChanges) }, Key: 'a', }, { Label: self.c.Tr.LcStashAllChangesKeepIndex, OnPress: func() error { - if !self.helpers.WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) } // if there are no staged files it behaves the same as Stash.Save - return self.handleStashSave(self.git.Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex) + return self.handleStashSave(self.c.Git().Stash.StashAndKeepIndex, self.c.Tr.Actions.StashAllChangesKeepIndex) }, Key: 'i', }, { Label: self.c.Tr.LcStashIncludeUntrackedChanges, OnPress: func() error { - return self.handleStashSave(self.git.Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges) + return self.handleStashSave(self.c.Git().Stash.StashIncludeUntrackedChanges, self.c.Tr.Actions.StashIncludeUntrackedChanges) }, Key: 'U', }, @@ -660,24 +721,24 @@ func (self *FilesController) createStashMenu() error { Label: self.c.Tr.LcStashStagedChanges, OnPress: func() error { // there must be something in staging otherwise the current implementation mucks the stash up - if !self.helpers.WorkingTree.AnyStagedFiles() { + if !self.c.Helpers().WorkingTree.AnyStagedFiles() { return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash) } - return self.handleStashSave(self.git.Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges) + return self.handleStashSave(self.c.Git().Stash.SaveStagedChanges, self.c.Tr.Actions.StashStagedChanges) }, Key: 's', }, { Label: self.c.Tr.LcStashUnstagedChanges, OnPress: func() error { - if !self.helpers.WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { return self.c.ErrorMsg(self.c.Tr.NoFilesToStash) } - if self.helpers.WorkingTree.AnyStagedFiles() { - return self.handleStashSave(self.git.Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges) + if self.c.Helpers().WorkingTree.AnyStagedFiles() { + return self.handleStashSave(self.c.Git().Stash.StashUnstagedChanges, self.c.Tr.Actions.StashUnstagedChanges) } // ordinary stash - return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashUnstagedChanges) + return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashUnstagedChanges) }, Key: 'u', }, @@ -686,11 +747,11 @@ func (self *FilesController) createStashMenu() error { } func (self *FilesController) stash() error { - return self.handleStashSave(self.git.Stash.Save, self.c.Tr.Actions.StashAllChanges) + return self.handleStashSave(self.c.Git().Stash.Save, self.c.Tr.Actions.StashAllChanges) } func (self *FilesController) createResetToUpstreamMenu() error { - return self.helpers.Refs.CreateGitResetMenu("@{upstream}") + return self.c.Helpers().Refs.CreateGitResetMenu("@{upstream}") } func (self *FilesController) handleToggleDirCollapsed() error { @@ -701,7 +762,7 @@ func (self *FilesController) handleToggleDirCollapsed() error { self.context().FileTreeViewModel.ToggleCollapsed(node.GetPath()) - if err := self.c.PostRefreshUpdate(self.contexts.Files); err != nil { + if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil { self.c.Log.Error(err) } @@ -747,7 +808,7 @@ func (self *FilesController) fetch() error { func (self *FilesController) fetchAux() (err error) { self.c.LogAction("Fetch") - err = self.git.Sync.Fetch(git_commands.FetchOptions{}) + err = self.c.Git().Sync.Fetch(git_commands.FetchOptions{}) if err != nil && strings.Contains(err.Error(), "exit status 128") { _ = self.c.ErrorMsg(self.c.Tr.PassUnameWrong) diff --git a/pkg/gui/controllers/files_remove_controller.go b/pkg/gui/controllers/files_remove_controller.go index 646118550..35093273c 100644 --- a/pkg/gui/controllers/files_remove_controller.go +++ b/pkg/gui/controllers/files_remove_controller.go @@ -12,17 +12,17 @@ import ( type FilesRemoveController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &FilesRemoveController{} func NewFilesRemoveController( - common *controllerCommon, + common *ControllerCommon, ) *FilesRemoveController { return &FilesRemoveController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -47,7 +47,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { Label: self.c.Tr.LcDiscardAllChanges, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInDirectory) - if err := self.git.WorkingTree.DiscardAllDirChanges(node); err != nil { + if err := self.c.Git().WorkingTree.DiscardAllDirChanges(node); err != nil { return self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) @@ -67,7 +67,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { Label: self.c.Tr.LcDiscardUnstagedChanges, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedChangesInDirectory) - if err := self.git.WorkingTree.DiscardUnstagedDirChanges(node); err != nil { + if err := self.c.Git().WorkingTree.DiscardUnstagedDirChanges(node); err != nil { return self.c.Error(err) } @@ -85,7 +85,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { } else { file := node.File - submodules := self.model.Submodules + submodules := self.c.Model().Submodules if file.IsSubmodule(submodules) { submodule := file.SubmoduleConfig(submodules) @@ -103,7 +103,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { Label: self.c.Tr.LcDiscardAllChanges, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.DiscardAllChangesInFile) - if err := self.git.WorkingTree.DiscardAllFileChanges(file); err != nil { + if err := self.c.Git().WorkingTree.DiscardAllFileChanges(file); err != nil { return self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) @@ -123,7 +123,7 @@ func (self *FilesRemoveController) remove(node *filetree.FileNode) error { Label: self.c.Tr.LcDiscardUnstagedChanges, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.DiscardAllUnstagedChangesInFile) - if err := self.git.WorkingTree.DiscardUnstagedFileChanges(file); err != nil { + if err := self.c.Git().WorkingTree.DiscardUnstagedFileChanges(file); err != nil { return self.c.Error(err) } @@ -148,17 +148,17 @@ func (self *FilesRemoveController) ResetSubmodule(submodule *models.SubmoduleCon return self.c.WithWaitingStatus(self.c.Tr.LcResettingSubmoduleStatus, func() error { self.c.LogAction(self.c.Tr.Actions.ResetSubmodule) - file := self.helpers.WorkingTree.FileForSubmodule(submodule) + file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule) if file != nil { - if err := self.git.WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { + if err := self.c.Git().WorkingTree.UnStageFile(file.Names(), file.Tracked); err != nil { return self.c.Error(err) } } - if err := self.git.Submodule.Stash(submodule); err != nil { + if err := self.c.Git().Submodule.Stash(submodule); err != nil { return self.c.Error(err) } - if err := self.git.Submodule.Reset(submodule); err != nil { + if err := self.c.Git().Submodule.Reset(submodule); err != nil { return self.c.Error(err) } @@ -182,5 +182,5 @@ func (self *FilesRemoveController) Context() types.Context { } func (self *FilesRemoveController) context() *context.WorkingTreeContext { - return self.contexts.Files + return self.c.Contexts().Files } diff --git a/pkg/gui/controllers/filtering_menu_action.go b/pkg/gui/controllers/filtering_menu_action.go new file mode 100644 index 000000000..7c9de7973 --- /dev/null +++ b/pkg/gui/controllers/filtering_menu_action.go @@ -0,0 +1,78 @@ +package controllers + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type FilteringMenuAction struct { + c *ControllerCommon +} + +func (self *FilteringMenuAction) Call() error { + fileName := "" + switch self.c.CurrentSideContext() { + case self.c.Contexts().Files: + node := self.c.Contexts().Files.GetSelected() + if node != nil { + fileName = node.GetPath() + } + case self.c.Contexts().CommitFiles: + node := self.c.Contexts().CommitFiles.GetSelected() + if node != nil { + fileName = node.GetPath() + } + } + + menuItems := []*types.MenuItem{} + + if fileName != "" { + menuItems = append(menuItems, &types.MenuItem{ + Label: fmt.Sprintf("%s '%s'", self.c.Tr.LcFilterBy, fileName), + OnPress: func() error { + return self.setFiltering(fileName) + }, + }) + } + + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.LcFilterPathOption, + OnPress: func() error { + return self.c.Prompt(types.PromptOpts{ + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetFilePathSuggestionsFunc(), + Title: self.c.Tr.EnterFileName, + HandleConfirm: func(response string) error { + return self.setFiltering(strings.TrimSpace(response)) + }, + }) + }, + }) + + if self.c.Modes().Filtering.Active() { + menuItems = append(menuItems, &types.MenuItem{ + Label: self.c.Tr.LcExitFilterMode, + OnPress: self.c.Helpers().Mode.ClearFiltering, + }) + } + + return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.FilteringMenuTitle, Items: menuItems}) +} + +func (self *FilteringMenuAction) setFiltering(path string) error { + self.c.Modes().Filtering.SetPath(path) + + repoState := self.c.State().GetRepoState() + if repoState.GetScreenMode() == types.SCREEN_NORMAL { + repoState.SetScreenMode(types.SCREEN_HALF) + } + + if err := self.c.PushContext(self.c.Contexts().LocalCommits); err != nil { + return err + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() { + self.c.Contexts().LocalCommits.SetSelectedLineIdx(0) + }}) +} diff --git a/pkg/gui/controllers/git_flow_controller.go b/pkg/gui/controllers/git_flow_controller.go index 2504ad2dd..cba4c7ddc 100644 --- a/pkg/gui/controllers/git_flow_controller.go +++ b/pkg/gui/controllers/git_flow_controller.go @@ -11,17 +11,17 @@ import ( type GitFlowController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &GitFlowController{} func NewGitFlowController( - common *controllerCommon, + common *ControllerCommon, ) *GitFlowController { return &GitFlowController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -39,7 +39,7 @@ func (self *GitFlowController) GetKeybindings(opts types.KeybindingsOpts) []*typ } func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) error { - if !self.git.Flow.GitFlowEnabled() { + if !self.c.Git().Flow.GitFlowEnabled() { return self.c.ErrorMsg("You need to install git-flow and enable it in this repo to use git-flow features") } @@ -52,7 +52,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er HandleConfirm: func(name string) error { self.c.LogAction(self.c.Tr.Actions.GitFlowStart) return self.c.RunSubprocessAndRefresh( - self.git.Flow.StartCmdObj(branchType, name), + self.c.Git().Flow.StartCmdObj(branchType, name), ) }, }) @@ -94,7 +94,7 @@ func (self *GitFlowController) handleCreateGitFlowMenu(branch *models.Branch) er } func (self *GitFlowController) gitFlowFinishBranch(branchName string) error { - cmdObj, err := self.git.Flow.FinishCmdObj(branchName) + cmdObj, err := self.c.Git().Flow.FinishCmdObj(branchName) if err != nil { return self.c.Error(err) } @@ -119,5 +119,5 @@ func (self *GitFlowController) Context() types.Context { } func (self *GitFlowController) context() *context.BranchesContext { - return self.contexts.Branches + return self.c.Contexts().Branches } diff --git a/pkg/gui/controllers/global_controller.go b/pkg/gui/controllers/global_controller.go index dac1bbc08..6f948a630 100644 --- a/pkg/gui/controllers/global_controller.go +++ b/pkg/gui/controllers/global_controller.go @@ -1,26 +1,21 @@ package controllers import ( - "strings" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" + "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/samber/lo" ) type GlobalController struct { baseController - *controllerCommon + c *ControllerCommon } func NewGlobalController( - common *controllerCommon, + common *ControllerCommon, ) *GlobalController { return &GlobalController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -31,47 +26,143 @@ func (self *GlobalController) GetKeybindings(opts types.KeybindingsOpts) []*type Handler: self.customCommand, Description: self.c.Tr.LcExecuteCustomCommand, }, - } -} - -func (self *GlobalController) customCommand() error { - return self.c.Prompt(types.PromptOpts{ - Title: self.c.Tr.CustomCommand, - FindSuggestionsFunc: self.GetCustomCommandsHistorySuggestionsFunc(), - HandleConfirm: func(command string) error { - if self.shouldSaveCommand(command) { - self.c.GetAppState().CustomCommandsHistory = utils.Limit( - lo.Uniq(append(self.c.GetAppState().CustomCommandsHistory, command)), - 1000, - ) - } - - err := self.c.SaveAppState() - if err != nil { - self.c.Log.Error(err) - } - - self.c.LogAction(self.c.Tr.Actions.CustomCommand) - return self.c.RunSubprocessAndRefresh( - self.os.Cmd.NewShell(command), - ) + { + Key: opts.GetKey(opts.Config.Universal.CreatePatchOptionsMenu), + Handler: self.createCustomPatchOptionsMenu, + Description: self.c.Tr.ViewPatchOptions, + OpensMenu: true, }, - }) -} - -// this mimics the shell functionality `ignorespace` -// which doesn't save a command to history if it starts with a space -func (self *GlobalController) shouldSaveCommand(command string) bool { - return !strings.HasPrefix(command, " ") -} - -func (self *GlobalController) GetCustomCommandsHistorySuggestionsFunc() func(string) []*types.Suggestion { - // reversing so that we display the latest command first - history := slices.Reverse(self.c.GetAppState().CustomCommandsHistory) - - return helpers.FuzzySearchFunc(history) + { + Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu), + Handler: self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu, + Description: self.c.Tr.ViewMergeRebaseOptions, + OpensMenu: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.Refresh), + Handler: self.refresh, + Description: self.c.Tr.LcRefresh, + }, + { + Key: opts.GetKey(opts.Config.Universal.NextScreenMode), + Handler: self.nextScreenMode, + Description: self.c.Tr.LcNextScreenMode, + }, + { + Key: opts.GetKey(opts.Config.Universal.PrevScreenMode), + Handler: self.prevScreenMode, + Description: self.c.Tr.LcPrevScreenMode, + }, + { + ViewName: "", + Key: opts.GetKey(opts.Config.Universal.OptionMenu), + Handler: self.createOptionsMenu, + OpensMenu: true, + }, + { + ViewName: "", + Key: opts.GetKey(opts.Config.Universal.OptionMenuAlt1), + Modifier: gocui.ModNone, + // we have the description on the alt key and not the main key for legacy reasons + // (the original main key was 'x' but we've reassigned that to other purposes) + Description: self.c.Tr.LcOpenMenu, + Handler: self.createOptionsMenu, + }, + { + ViewName: "", + Key: opts.GetKey(opts.Config.Universal.FilteringMenu), + Handler: self.createFilteringMenu, + Description: self.c.Tr.LcOpenFilteringMenu, + OpensMenu: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.DiffingMenu), + Handler: self.createDiffingMenu, + Description: self.c.Tr.LcOpenDiffingMenu, + OpensMenu: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.DiffingMenuAlt), + Handler: self.createDiffingMenu, + Description: self.c.Tr.LcOpenDiffingMenu, + OpensMenu: true, + }, + { + Key: opts.GetKey(opts.Config.Universal.Quit), + Modifier: gocui.ModNone, + Handler: self.quit, + }, + { + Key: opts.GetKey(opts.Config.Universal.QuitAlt1), + Modifier: gocui.ModNone, + Handler: self.quit, + }, + { + Key: opts.GetKey(opts.Config.Universal.QuitWithoutChangingDirectory), + Modifier: gocui.ModNone, + Handler: self.quitWithoutChangingDirectory, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Modifier: gocui.ModNone, + Handler: self.escape, + }, + { + Key: opts.GetKey(opts.Config.Universal.ToggleWhitespaceInDiffView), + Handler: self.toggleWhitespace, + Description: self.c.Tr.ToggleWhitespaceInDiffView, + }, + } } func (self *GlobalController) Context() types.Context { return nil } + +func (self *GlobalController) customCommand() error { + return (&CustomCommandAction{c: self.c}).Call() +} + +func (self *GlobalController) createCustomPatchOptionsMenu() error { + return (&CustomPatchOptionsMenuAction{c: self.c}).Call() +} + +func (self *GlobalController) refresh() error { + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) +} + +func (self *GlobalController) nextScreenMode() error { + return (&ScreenModeActions{c: self.c}).Next() +} + +func (self *GlobalController) prevScreenMode() error { + return (&ScreenModeActions{c: self.c}).Prev() +} + +func (self *GlobalController) createOptionsMenu() error { + return (&OptionsMenuAction{c: self.c}).Call() +} + +func (self *GlobalController) createFilteringMenu() error { + return (&FilteringMenuAction{c: self.c}).Call() +} + +func (self *GlobalController) createDiffingMenu() error { + return (&DiffingMenuAction{c: self.c}).Call() +} + +func (self *GlobalController) quit() error { + return (&QuitActions{c: self.c}).Quit() +} + +func (self *GlobalController) quitWithoutChangingDirectory() error { + return (&QuitActions{c: self.c}).QuitWithoutChangingDirectory() +} + +func (self *GlobalController) escape() error { + return (&QuitActions{c: self.c}).Escape() +} + +func (self *GlobalController) toggleWhitespace() error { + return (&ToggleWhitespaceAction{c: self.c}).Call() +} diff --git a/pkg/gui/controllers/helpers/amend_helper.go b/pkg/gui/controllers/helpers/amend_helper.go index 29570d635..0e5216f83 100644 --- a/pkg/gui/controllers/helpers/amend_helper.go +++ b/pkg/gui/controllers/helpers/amend_helper.go @@ -1,24 +1,20 @@ package helpers import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type AmendHelper struct { - c *types.HelperCommon - git *commands.GitCommand + c *HelperCommon gpg *GpgHelper } func NewAmendHelper( - c *types.HelperCommon, - git *commands.GitCommand, + c *HelperCommon, gpg *GpgHelper, ) *AmendHelper { return &AmendHelper{ c: c, - git: git, gpg: gpg, } } @@ -28,7 +24,7 @@ func (self *AmendHelper) AmendHead() error { Title: self.c.Tr.AmendLastCommitTitle, Prompt: self.c.Tr.SureToAmend, HandleConfirm: func() error { - cmdObj := self.git.Commit.AmendHeadCmdObj() + cmdObj := self.c.Git().Commit.AmendHeadCmdObj() self.c.LogAction(self.c.Tr.Actions.AmendCommit) return self.gpg.WithGpgHandling(cmdObj, self.c.Tr.AmendingStatus, nil) }, diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go new file mode 100644 index 000000000..f125ebf7b --- /dev/null +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -0,0 +1,68 @@ +package helpers + +import ( + "time" + + "github.com/jesseduffield/lazygit/pkg/gui/status" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type AppStatusHelper struct { + c *HelperCommon + + statusMgr func() *status.StatusManager +} + +func NewAppStatusHelper(c *HelperCommon, statusMgr func() *status.StatusManager) *AppStatusHelper { + return &AppStatusHelper{ + c: c, + statusMgr: statusMgr, + } +} + +func (self *AppStatusHelper) Toast(message string) { + self.statusMgr().AddToastStatus(message) + + self.renderAppStatus() +} + +// withWaitingStatus wraps a function and shows a waiting status while the function is still executing +func (self *AppStatusHelper) WithWaitingStatus(message string, f func() error) { + go utils.Safe(func() { + self.statusMgr().WithWaitingStatus(message, func() { + self.renderAppStatus() + + if err := f(); err != nil { + self.c.OnUIThread(func() error { + return self.c.Error(err) + }) + } + }) + }) +} + +func (self *AppStatusHelper) HasStatus() bool { + return self.statusMgr().HasStatus() +} + +func (self *AppStatusHelper) GetStatusString() string { + return self.statusMgr().GetStatusString() +} + +func (self *AppStatusHelper) renderAppStatus() { + go utils.Safe(func() { + ticker := time.NewTicker(time.Millisecond * 50) + defer ticker.Stop() + for range ticker.C { + appStatus := self.statusMgr().GetStatusString() + self.c.OnUIThread(func() error { + self.c.SetViewContent(self.c.Views().AppStatus, appStatus) + return nil + }) + + if appStatus == "" { + return + } + } + }) +} diff --git a/pkg/gui/controllers/helpers/bisect_helper.go b/pkg/gui/controllers/helpers/bisect_helper.go index 65fb781d4..7d0fadf8a 100644 --- a/pkg/gui/controllers/helpers/bisect_helper.go +++ b/pkg/gui/controllers/helpers/bisect_helper.go @@ -1,23 +1,15 @@ package helpers import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type BisectHelper struct { - c *types.HelperCommon - git *commands.GitCommand + c *HelperCommon } -func NewBisectHelper( - c *types.HelperCommon, - git *commands.GitCommand, -) *BisectHelper { - return &BisectHelper{ - c: c, - git: git, - } +func NewBisectHelper(c *HelperCommon) *BisectHelper { + return &BisectHelper{c: c} } func (self *BisectHelper) Reset() error { @@ -26,7 +18,7 @@ func (self *BisectHelper) Reset() error { Prompt: self.c.Tr.Bisect.ResetPrompt, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.ResetBisect) - if err := self.git.Bisect.Reset(); err != nil { + if err := self.c.Git().Bisect.Reset(); err != nil { return self.c.Error(err) } diff --git a/pkg/gui/controllers/helpers/cherry_pick_helper.go b/pkg/gui/controllers/helpers/cherry_pick_helper.go index a5c4427a7..2c9b7c7c9 100644 --- a/pkg/gui/controllers/helpers/cherry_pick_helper.go +++ b/pkg/gui/controllers/helpers/cherry_pick_helper.go @@ -1,22 +1,13 @@ package helpers import ( - "github.com/jesseduffield/generics/set" - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/modes/cherrypicking" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type CherryPickHelper struct { - c *types.HelperCommon - - git *commands.GitCommand - - contexts *context.ContextTree - getData func() *cherrypicking.CherryPicking + c *HelperCommon rebaseHelper *MergeAndRebaseHelper } @@ -25,38 +16,31 @@ type CherryPickHelper struct { // even if in truth we're running git cherry-pick func NewCherryPickHelper( - c *types.HelperCommon, - git *commands.GitCommand, - contexts *context.ContextTree, - getData func() *cherrypicking.CherryPicking, + c *HelperCommon, rebaseHelper *MergeAndRebaseHelper, ) *CherryPickHelper { return &CherryPickHelper{ c: c, - git: git, - contexts: contexts, - getData: getData, rebaseHelper: rebaseHelper, } } +func (self *CherryPickHelper) getData() *cherrypicking.CherryPicking { + return self.c.Modes().CherryPicking +} + func (self *CherryPickHelper) Copy(commit *models.Commit, commitsList []*models.Commit, context types.Context) error { if err := self.resetIfNecessary(context); err != nil { return err } // we will un-copy it if it's already copied - for index, cherryPickedCommit := range self.getData().CherryPickedCommits { - if commit.Sha == cherryPickedCommit.Sha { - self.getData().CherryPickedCommits = append( - self.getData().CherryPickedCommits[0:index], - self.getData().CherryPickedCommits[index+1:]..., - ) - return self.rerender() - } + if self.getData().SelectedShaSet().Includes(commit.Sha) { + self.getData().Remove(commit, commitsList) + } else { + self.getData().Add(commit, commitsList) } - self.add(commit, commitsList) return self.rerender() } @@ -65,7 +49,7 @@ func (self *CherryPickHelper) CopyRange(selectedIndex int, commitsList []*models return err } - commitSet := self.CherryPickedCommitShaSet() + commitSet := self.getData().SelectedShaSet() // find the last commit that is copied that's above our position // if there are none, startIndex = 0 @@ -78,7 +62,7 @@ func (self *CherryPickHelper) CopyRange(selectedIndex int, commitsList []*models for index := startIndex; index <= selectedIndex; index++ { commit := commitsList[index] - self.add(commit, commitsList) + self.getData().Add(commit, commitsList) } return self.rerender() @@ -93,7 +77,7 @@ func (self *CherryPickHelper) Paste() error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.CherryPickingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.CherryPick) - err := self.git.Rebase.CherryPickCommits(self.getData().CherryPickedCommits) + err := self.c.Git().Rebase.CherryPickCommits(self.getData().CherryPickedCommits) return self.rebaseHelper.CheckMergeOrRebase(err) }) }, @@ -107,26 +91,6 @@ func (self *CherryPickHelper) Reset() error { return self.rerender() } -func (self *CherryPickHelper) CherryPickedCommitShaSet() *set.Set[string] { - shas := slices.Map(self.getData().CherryPickedCommits, func(commit *models.Commit) string { - return commit.Sha - }) - return set.NewFromSlice(shas) -} - -func (self *CherryPickHelper) add(selectedCommit *models.Commit, commitsList []*models.Commit) { - commitSet := self.CherryPickedCommitShaSet() - commitSet.Add(selectedCommit.Sha) - - cherryPickedCommits := slices.Filter(commitsList, func(commit *models.Commit) bool { - return commitSet.Includes(commit.Sha) - }) - - self.getData().CherryPickedCommits = slices.Map(cherryPickedCommits, func(commit *models.Commit) *models.Commit { - return &models.Commit{Name: commit.Name, Sha: commit.Sha} - }) -} - // you can only copy from one context at a time, because the order and position of commits matter func (self *CherryPickHelper) resetIfNecessary(context types.Context) error { oldContextKey := types.ContextKey(self.getData().ContextKey) @@ -142,9 +106,9 @@ func (self *CherryPickHelper) resetIfNecessary(context types.Context) error { func (self *CherryPickHelper) rerender() error { for _, context := range []types.Context{ - self.contexts.LocalCommits, - self.contexts.ReflogCommits, - self.contexts.SubCommits, + self.c.Contexts().LocalCommits, + self.c.Contexts().ReflogCommits, + self.c.Contexts().SubCommits, } { if err := self.c.PostRefreshUpdate(context); err != nil { return err diff --git a/pkg/gui/controllers/helpers/commits_helper.go b/pkg/gui/controllers/helpers/commits_helper.go index 4126d9ee5..262f970de 100644 --- a/pkg/gui/controllers/helpers/commits_helper.go +++ b/pkg/gui/controllers/helpers/commits_helper.go @@ -3,7 +3,6 @@ package helpers import ( "strings" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -12,38 +11,29 @@ type ICommitsHelper interface { } type CommitsHelper struct { - c *types.HelperCommon + c *HelperCommon - model *types.Model - contexts *context.ContextTree getCommitSummary func() string setCommitSummary func(string) getCommitDescription func() string setCommitDescription func(string) - renderCommitLength func() } var _ ICommitsHelper = &CommitsHelper{} func NewCommitsHelper( - c *types.HelperCommon, - model *types.Model, - contexts *context.ContextTree, + c *HelperCommon, getCommitSummary func() string, setCommitSummary func(string), getCommitDescription func() string, setCommitDescription func(string), - renderCommitLength func(), ) *CommitsHelper { return &CommitsHelper{ c: c, - model: model, - contexts: contexts, getCommitSummary: getCommitSummary, setCommitSummary: setCommitSummary, getCommitDescription: getCommitDescription, setCommitDescription: setCommitDescription, - renderCommitLength: renderCommitLength, } } @@ -62,7 +52,7 @@ func (self *CommitsHelper) SetMessageAndDescriptionInView(message string) { self.setCommitSummary(summary) self.setCommitDescription(description) - self.renderCommitLength() + self.c.Contexts().CommitMessage.RenderCommitLength() } func (self *CommitsHelper) JoinCommitMessageAndDescription() string { @@ -78,7 +68,7 @@ func (self *CommitsHelper) UpdateCommitPanelView(message string) { self.SetMessageAndDescriptionInView(message) return } - message = self.contexts.CommitMessage.GetPreservedMessage() + message = self.c.Contexts().CommitMessage.GetPreservedMessage() if message != "" { self.SetMessageAndDescriptionInView(message) } else { @@ -95,7 +85,7 @@ type OpenCommitMessagePanelOpts struct { } func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOpts) error { - self.contexts.CommitMessage.SetPanelState( + self.c.Contexts().CommitMessage.SetPanelState( opts.CommitIndex, opts.Title, opts.PreserveMessage, @@ -109,8 +99,8 @@ func (self *CommitsHelper) OpenCommitMessagePanel(opts *OpenCommitMessagePanelOp func (self *CommitsHelper) OnCommitSuccess() { // if we have a preserved message we want to clear it on success - if self.contexts.CommitMessage.GetPreserveMessage() { - self.contexts.CommitMessage.SetPreservedMessage("") + if self.c.Contexts().CommitMessage.GetPreserveMessage() { + self.c.Contexts().CommitMessage.SetPreservedMessage("") } self.SetMessageAndDescriptionInView("") } @@ -122,7 +112,7 @@ func (self *CommitsHelper) HandleCommitConfirm() error { return self.c.ErrorMsg(self.c.Tr.CommitWithoutMessageErr) } - err := self.contexts.CommitMessage.OnConfirm(fullMessage) + err := self.c.Contexts().CommitMessage.OnConfirm(fullMessage) if err != nil { return err } @@ -131,15 +121,15 @@ func (self *CommitsHelper) HandleCommitConfirm() error { } func (self *CommitsHelper) CloseCommitMessagePanel() error { - if self.contexts.CommitMessage.GetPreserveMessage() { + if self.c.Contexts().CommitMessage.GetPreserveMessage() { message := self.JoinCommitMessageAndDescription() - self.contexts.CommitMessage.SetPreservedMessage(message) + self.c.Contexts().CommitMessage.SetPreservedMessage(message) } else { self.SetMessageAndDescriptionInView("") } - self.contexts.CommitMessage.SetHistoryMessage("") + self.c.Contexts().CommitMessage.SetHistoryMessage("") return self.PopCommitMessageContexts() } @@ -160,7 +150,7 @@ func (self *CommitsHelper) pushCommitMessageContexts() error { func (self *CommitsHelper) commitMessageContexts() []types.Context { return []types.Context{ - self.contexts.CommitDescription, - self.contexts.CommitMessage, + self.c.Contexts().CommitDescription, + self.c.Contexts().CommitMessage, } } diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go new file mode 100644 index 000000000..7968933fc --- /dev/null +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -0,0 +1,349 @@ +package helpers + +import ( + goContext "context" + "fmt" + "strings" + + "github.com/jesseduffield/gocui" + + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/theme" + "github.com/mattn/go-runewidth" +) + +type ConfirmationHelper struct { + c *HelperCommon +} + +func NewConfirmationHelper(c *HelperCommon) *ConfirmationHelper { + return &ConfirmationHelper{ + c: c, + } +} + +// This file is for the rendering of confirmation panels along with setting and handling associated +// keybindings. + +func (self *ConfirmationHelper) wrappedConfirmationFunction(cancel goContext.CancelFunc, function func() error) func() error { + return func() error { + cancel() + + if err := self.c.PopContext(); err != nil { + return err + } + + if function != nil { + if err := function(); err != nil { + return self.c.Error(err) + } + } + + return nil + } +} + +func (self *ConfirmationHelper) wrappedPromptConfirmationFunction(cancel goContext.CancelFunc, function func(string) error, getResponse func() string) func() error { + return self.wrappedConfirmationFunction(cancel, func() error { + return function(getResponse()) + }) +} + +func (self *ConfirmationHelper) DeactivateConfirmationPrompt() { + self.c.Mutexes().PopupMutex.Lock() + self.c.State().GetRepoState().SetCurrentPopupOpts(nil) + self.c.Mutexes().PopupMutex.Unlock() + + self.c.Views().Confirmation.Visible = false + self.c.Views().Suggestions.Visible = false + + self.clearConfirmationViewKeyBindings() +} + +func getMessageHeight(wrap bool, message string, width int) int { + lines := strings.Split(message, "\n") + lineCount := 0 + // if we need to wrap, calculate height to fit content within view's width + if wrap { + for _, line := range lines { + lineCount += runewidth.StringWidth(line)/width + 1 + } + } else { + lineCount = len(lines) + } + return lineCount +} + +func (self *ConfirmationHelper) getPopupPanelDimensions(wrap bool, prompt string) (int, int, int, int) { + panelWidth := self.getPopupPanelWidth() + panelHeight := getMessageHeight(wrap, prompt, panelWidth) + return self.getPopupPanelDimensionsAux(panelWidth, panelHeight) +} + +func (self *ConfirmationHelper) getPopupPanelDimensionsForContentHeight(panelWidth, contentHeight int) (int, int, int, int) { + return self.getPopupPanelDimensionsAux(panelWidth, contentHeight) +} + +func (self *ConfirmationHelper) getPopupPanelDimensionsAux(panelWidth int, panelHeight int) (int, int, int, int) { + width, height := self.c.GocuiGui().Size() + if panelHeight > height*3/4 { + panelHeight = height * 3 / 4 + } + return width/2 - panelWidth/2, + height/2 - panelHeight/2 - panelHeight%2 - 1, + width/2 + panelWidth/2, + height/2 + panelHeight/2 +} + +func (self *ConfirmationHelper) getPopupPanelWidth() 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 + minWidth := 80 + if panelWidth < minWidth { + if width-2 < minWidth { + panelWidth = width - 2 + } else { + panelWidth = minWidth + } + } + + return panelWidth +} + +func (self *ConfirmationHelper) prepareConfirmationPanel( + ctx goContext.Context, + opts types.ConfirmOpts, +) error { + self.c.Views().Confirmation.HasLoader = opts.HasLoader + if opts.HasLoader { + self.c.GocuiGui().StartTicking(ctx) + } + self.c.Views().Confirmation.Title = opts.Title + // for now we do not support wrapping in our editor + self.c.Views().Confirmation.Wrap = !opts.Editable + self.c.Views().Confirmation.FgColor = theme.GocuiDefaultTextColor + self.c.Views().Confirmation.Mask = runeForMask(opts.Mask) + _ = self.c.Views().Confirmation.SetOrigin(0, 0) + + suggestionsContext := self.c.Contexts().Suggestions + suggestionsContext.State.FindSuggestions = opts.FindSuggestionsFunc + if opts.FindSuggestionsFunc != nil { + suggestionsView := self.c.Views().Suggestions + suggestionsView.Wrap = false + suggestionsView.FgColor = theme.GocuiDefaultTextColor + suggestionsContext.SetSuggestions(opts.FindSuggestionsFunc("")) + suggestionsView.Visible = true + suggestionsView.Title = fmt.Sprintf(self.c.Tr.SuggestionsTitle, self.c.UserConfig.Keybinding.Universal.TogglePanel) + } + + self.ResizeConfirmationPanel() + return nil +} + +func runeForMask(mask bool) rune { + if mask { + return '*' + } + return 0 +} + +func (self *ConfirmationHelper) CreatePopupPanel(ctx goContext.Context, opts types.CreatePopupPanelOpts) error { + self.c.Mutexes().PopupMutex.Lock() + defer self.c.Mutexes().PopupMutex.Unlock() + + ctx, cancel := goContext.WithCancel(ctx) + + // we don't allow interruptions of non-loader popups in case we get stuck somehow + // e.g. a credentials popup never gets its required user input so a process hangs + // forever. + // The proper solution is to have a queue of popup options + currentPopupOpts := self.c.State().GetRepoState().GetCurrentPopupOpts() + if currentPopupOpts != nil && !currentPopupOpts.HasLoader { + self.c.Log.Error("ignoring create popup panel because a popup panel is already open") + cancel() + return nil + } + + // remove any previous keybindings + self.clearConfirmationViewKeyBindings() + + err := self.prepareConfirmationPanel( + ctx, + types.ConfirmOpts{ + Title: opts.Title, + Prompt: opts.Prompt, + HasLoader: opts.HasLoader, + FindSuggestionsFunc: opts.FindSuggestionsFunc, + Editable: opts.Editable, + Mask: opts.Mask, + }) + if err != nil { + cancel() + return err + } + confirmationView := self.c.Views().Confirmation + confirmationView.Editable = opts.Editable + + if opts.Editable { + textArea := confirmationView.TextArea + textArea.Clear() + textArea.TypeString(opts.Prompt) + self.ResizeConfirmationPanel() + confirmationView.RenderTextArea() + } else { + self.c.ResetViewOrigin(confirmationView) + self.c.SetViewContent(confirmationView, style.AttrBold.Sprint(opts.Prompt)) + } + + if err := self.setKeyBindings(cancel, opts); err != nil { + cancel() + return err + } + + self.c.State().GetRepoState().SetCurrentPopupOpts(&opts) + + return self.c.PushContext(self.c.Contexts().Confirmation) +} + +func (self *ConfirmationHelper) setKeyBindings(cancel goContext.CancelFunc, opts types.CreatePopupPanelOpts) error { + var onConfirm func() error + if opts.HandleConfirmPrompt != nil { + onConfirm = self.wrappedPromptConfirmationFunction(cancel, opts.HandleConfirmPrompt, func() string { return self.c.Views().Confirmation.TextArea.GetContent() }) + } else { + onConfirm = self.wrappedConfirmationFunction(cancel, opts.HandleConfirm) + } + + onSuggestionConfirm := self.wrappedPromptConfirmationFunction( + cancel, + opts.HandleConfirmPrompt, + self.getSelectedSuggestionValue, + ) + + onClose := self.wrappedConfirmationFunction(cancel, opts.HandleClose) + + self.c.Contexts().Confirmation.State.OnConfirm = onConfirm + self.c.Contexts().Confirmation.State.OnClose = onClose + self.c.Contexts().Suggestions.State.OnConfirm = onSuggestionConfirm + self.c.Contexts().Suggestions.State.OnClose = onClose + + return nil +} + +func (self *ConfirmationHelper) clearConfirmationViewKeyBindings() { + noop := func() error { return nil } + self.c.Contexts().Confirmation.State.OnConfirm = noop + self.c.Contexts().Confirmation.State.OnClose = noop + self.c.Contexts().Suggestions.State.OnConfirm = noop + self.c.Contexts().Suggestions.State.OnClose = noop +} + +func (self *ConfirmationHelper) getSelectedSuggestionValue() string { + selectedSuggestion := self.c.Contexts().Suggestions.GetSelected() + + if selectedSuggestion != nil { + return selectedSuggestion.Value + } + + return "" +} + +func (self *ConfirmationHelper) ResizeConfirmationPanel() { + suggestionsViewHeight := 0 + if self.c.Views().Suggestions.Visible { + suggestionsViewHeight = 11 + } + panelWidth := self.getPopupPanelWidth() + prompt := self.c.Views().Confirmation.Buffer() + wrap := true + if self.c.Views().Confirmation.Editable { + prompt = self.c.Views().Confirmation.TextArea.GetContent() + wrap = false + } + panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight + x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight) + confirmationViewBottom := y1 - suggestionsViewHeight + _, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0) + + suggestionsViewTop := confirmationViewBottom + 1 + _, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) +} + +func (self *ConfirmationHelper) ResizeCurrentPopupPanel() error { + c := self.c.CurrentContext() + + switch c { + case self.c.Contexts().Menu: + self.resizeMenu() + case self.c.Contexts().Confirmation, self.c.Contexts().Suggestions: + self.resizeConfirmationPanel() + case self.c.Contexts().CommitMessage, self.c.Contexts().CommitDescription: + self.ResizeCommitMessagePanels() + } + + return nil +} + +func (self *ConfirmationHelper) ResizePopupPanel(v *gocui.View, content string) error { + x0, y0, x1, y1 := self.getPopupPanelDimensions(v.Wrap, content) + _, err := self.c.GocuiGui().SetView(v.Name(), x0, y0, x1, y1, 0) + return err +} + +func (self *ConfirmationHelper) resizeMenu() { + itemCount := self.c.Contexts().Menu.GetList().Len() + offset := 3 + panelWidth := self.getPopupPanelWidth() + x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) + menuBottom := y1 - offset + _, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0) + + tooltipTop := menuBottom + 1 + tooltipHeight := getMessageHeight(true, self.c.Contexts().Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame + _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) +} + +func (self *ConfirmationHelper) resizeConfirmationPanel() { + suggestionsViewHeight := 0 + if self.c.Views().Suggestions.Visible { + suggestionsViewHeight = 11 + } + panelWidth := self.getPopupPanelWidth() + prompt := self.c.Views().Confirmation.Buffer() + wrap := true + if self.c.Views().Confirmation.Editable { + prompt = self.c.Views().Confirmation.TextArea.GetContent() + wrap = false + } + panelHeight := getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight + x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight) + confirmationViewBottom := y1 - suggestionsViewHeight + _, _ = self.c.GocuiGui().SetView(self.c.Views().Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0) + + suggestionsViewTop := confirmationViewBottom + 1 + _, _ = self.c.GocuiGui().SetView(self.c.Views().Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) +} + +func (self *ConfirmationHelper) ResizeCommitMessagePanels() { + panelWidth := self.getPopupPanelWidth() + content := self.c.Views().CommitDescription.TextArea.GetContent() + summaryViewHeight := 3 + panelHeight := getMessageHeight(false, content, panelWidth) + minHeight := 7 + if panelHeight < minHeight { + panelHeight = minHeight + } + x0, y0, x1, y1 := self.getPopupPanelDimensionsAux(panelWidth, panelHeight) + + _, _ = self.c.GocuiGui().SetView(self.c.Views().CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0) + _, _ = self.c.GocuiGui().SetView(self.c.Views().CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0) +} + +func (self *ConfirmationHelper) IsPopupPanel(viewName string) bool { + return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" +} + +func (self *ConfirmationHelper) IsPopupPanelFocused() bool { + return self.IsPopupPanel(self.c.CurrentContext().GetViewName()) +} diff --git a/pkg/gui/controllers/helpers/credentials_helper.go b/pkg/gui/controllers/helpers/credentials_helper.go index 10679d7a4..0aed34110 100644 --- a/pkg/gui/controllers/helpers/credentials_helper.go +++ b/pkg/gui/controllers/helpers/credentials_helper.go @@ -8,11 +8,11 @@ import ( ) type CredentialsHelper struct { - c *types.HelperCommon + c *HelperCommon } func NewCredentialsHelper( - c *types.HelperCommon, + c *HelperCommon, ) *CredentialsHelper { return &CredentialsHelper{ c: c, diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go new file mode 100644 index 000000000..701df93cd --- /dev/null +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -0,0 +1,114 @@ +package helpers + +import ( + "fmt" + + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +type DiffHelper struct { + c *HelperCommon +} + +func NewDiffHelper(c *HelperCommon) *DiffHelper { + return &DiffHelper{ + c: c, + } +} + +func (self *DiffHelper) DiffStr() string { + output := self.c.Modes().Diffing.Ref + + right := self.currentDiffTerminal() + if right != "" { + output += " " + right + } + + if self.c.Modes().Diffing.Reverse { + output += " -R" + } + + if self.c.State().GetIgnoreWhitespaceInDiffView() { + output += " --ignore-all-space" + } + + file := self.currentlySelectedFilename() + if file != "" { + output += " -- " + file + } else if self.c.Modes().Filtering.Active() { + output += " -- " + self.c.Modes().Filtering.GetPath() + } + + return output +} + +func (self *DiffHelper) ExitDiffMode() error { + self.c.Modes().Diffing = diffing.New() + return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) +} + +func (self *DiffHelper) RenderDiff() error { + cmdObj := self.c.OS().Cmd.New( + fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", self.DiffStr()), + ) + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Diff", + Task: task, + }, + }) +} + +// CurrentDiffTerminals returns the current diff terminals of the currently selected item. +// in the case of a branch it returns both the branch and it's upstream name, +// which becomes an option when you bring up the diff menu, but when you're just +// flicking through branches it will be using the local branch name. +func (self *DiffHelper) CurrentDiffTerminals() []string { + c := self.c.CurrentSideContext() + + if c.GetKey() == "" { + return nil + } + + switch v := c.(type) { + case types.DiffableContext: + return v.GetDiffTerminals() + } + + return nil +} + +func (self *DiffHelper) currentDiffTerminal() string { + names := self.CurrentDiffTerminals() + if len(names) == 0 { + return "" + } + return names[0] +} + +func (self *DiffHelper) currentlySelectedFilename() string { + currentContext := self.c.CurrentContext() + + switch currentContext := currentContext.(type) { + case types.IListContext: + if lo.Contains([]types.ContextKey{context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY}, currentContext.GetKey()) { + return currentContext.GetSelectedItemId() + } + } + + return "" +} + +func (self *DiffHelper) WithDiffModeCheck(f func() error) error { + if self.c.Modes().Diffing.Active() { + return self.RenderDiff() + } + + return f() +} diff --git a/pkg/gui/controllers/helpers/files_helper.go b/pkg/gui/controllers/helpers/files_helper.go index a0d2d4e8c..1baa0191b 100644 --- a/pkg/gui/controllers/helpers/files_helper.go +++ b/pkg/gui/controllers/helpers/files_helper.go @@ -1,11 +1,5 @@ package helpers -import ( - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - type IFilesHelper interface { EditFile(filename string) error EditFileAtLine(filename string, lineNumber int) error @@ -13,37 +7,29 @@ type IFilesHelper interface { } type FilesHelper struct { - c *types.HelperCommon - git *commands.GitCommand - os *oscommands.OSCommand + c *HelperCommon } -func NewFilesHelper( - c *types.HelperCommon, - git *commands.GitCommand, - os *oscommands.OSCommand, -) *FilesHelper { +func NewFilesHelper(c *HelperCommon) *FilesHelper { return &FilesHelper{ - c: c, - git: git, - os: os, + c: c, } } var _ IFilesHelper = &FilesHelper{} func (self *FilesHelper) EditFile(filename string) error { - cmdStr, editInTerminal := self.git.File.GetEditCmdStr(filename) + cmdStr, editInTerminal := self.c.Git().File.GetEditCmdStr(filename) return self.callEditor(cmdStr, editInTerminal) } func (self *FilesHelper) EditFileAtLine(filename string, lineNumber int) error { - cmdStr, editInTerminal := self.git.File.GetEditAtLineCmdStr(filename, lineNumber) + cmdStr, editInTerminal := self.c.Git().File.GetEditAtLineCmdStr(filename, lineNumber) return self.callEditor(cmdStr, editInTerminal) } func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) error { - cmdStr := self.git.File.GetEditAtLineAndWaitCmdStr(filename, lineNumber) + cmdStr := self.c.Git().File.GetEditAtLineAndWaitCmdStr(filename, lineNumber) // Always suspend, regardless of the value of the editInTerminal config, // since we want to prevent interacting with the UI until the editor @@ -54,16 +40,16 @@ func (self *FilesHelper) EditFileAtLineAndWait(filename string, lineNumber int) func (self *FilesHelper) callEditor(cmdStr string, editInTerminal bool) error { if editInTerminal { return self.c.RunSubprocessAndRefresh( - self.os.Cmd.NewShell(cmdStr), + self.c.OS().Cmd.NewShell(cmdStr), ) } - return self.os.Cmd.NewShell(cmdStr).Run() + return self.c.OS().Cmd.NewShell(cmdStr).Run() } func (self *FilesHelper) OpenFile(filename string) error { self.c.LogAction(self.c.Tr.Actions.OpenFile) - if err := self.os.OpenFile(filename); err != nil { + if err := self.c.OS().OpenFile(filename); err != nil { return self.c.Error(err) } return nil diff --git a/pkg/gui/controllers/helpers/gpg_helper.go b/pkg/gui/controllers/helpers/gpg_helper.go index 2e287c2b4..0cefc4208 100644 --- a/pkg/gui/controllers/helpers/gpg_helper.go +++ b/pkg/gui/controllers/helpers/gpg_helper.go @@ -3,26 +3,17 @@ package helpers import ( "fmt" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type GpgHelper struct { - c *types.HelperCommon - os *oscommands.OSCommand - git *commands.GitCommand + c *HelperCommon } -func NewGpgHelper( - c *types.HelperCommon, - os *oscommands.OSCommand, - git *commands.GitCommand, -) *GpgHelper { +func NewGpgHelper(c *HelperCommon) *GpgHelper { return &GpgHelper{ - c: c, - os: os, - git: git, + c: c, } } @@ -32,9 +23,9 @@ func NewGpgHelper( // we don't need to see a loading status if we're in a subprocess. // TODO: we shouldn't need to use a shell here, but looks like that NewShell function contains some windows specific quoting stuff. We should centralise that. func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { - useSubprocess := self.git.Config.UsingGpg() + useSubprocess := self.c.Git().Config.UsingGpg() if useSubprocess { - success, err := self.c.RunSubprocess(self.os.Cmd.NewShell(cmdObj.ToString())) + success, err := self.c.RunSubprocess(self.c.OS().Cmd.NewShell(cmdObj.ToString())) if success && onSuccess != nil { if err := onSuccess(); err != nil { return err @@ -51,7 +42,7 @@ func (self *GpgHelper) WithGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus } func (self *GpgHelper) runAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error { - cmdObj = self.os.Cmd.NewShell(cmdObj.ToString()) + cmdObj = self.c.OS().Cmd.NewShell(cmdObj.ToString()) return self.c.WithWaitingStatus(waitingStatus, func() error { if err := cmdObj.StreamOutput().Run(); err != nil { diff --git a/pkg/gui/controllers/helpers/helpers.go b/pkg/gui/controllers/helpers/helpers.go index 471e8a03a..faf342f0a 100644 --- a/pkg/gui/controllers/helpers/helpers.go +++ b/pkg/gui/controllers/helpers/helpers.go @@ -1,5 +1,21 @@ package helpers +import ( + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type HelperCommon struct { + *common.Common + types.IGuiCommon + IGetContexts +} + +type IGetContexts interface { + Contexts() *context.ContextTree +} + type Helpers struct { Refs *RefsHelper Bisect *BisectHelper @@ -12,28 +28,55 @@ type Helpers struct { CherryPick *CherryPickHelper Host *HostHelper PatchBuilding *PatchBuildingHelper + Staging *StagingHelper GPG *GpgHelper Upstream *UpstreamHelper AmendHelper *AmendHelper Commits *CommitsHelper + Snake *SnakeHelper + // lives in context package because our contexts need it to render to main + Diff *DiffHelper + Repos *ReposHelper + RecordDirectory *RecordDirectoryHelper + Update *UpdateHelper + Window *WindowHelper + View *ViewHelper + Refresh *RefreshHelper + Confirmation *ConfirmationHelper + Mode *ModeHelper + AppStatus *AppStatusHelper + WindowArrangement *WindowArrangementHelper } func NewStubHelpers() *Helpers { return &Helpers{ - Refs: &RefsHelper{}, - Bisect: &BisectHelper{}, - Suggestions: &SuggestionsHelper{}, - Files: &FilesHelper{}, - WorkingTree: &WorkingTreeHelper{}, - Tags: &TagsHelper{}, - MergeAndRebase: &MergeAndRebaseHelper{}, - MergeConflicts: &MergeConflictsHelper{}, - CherryPick: &CherryPickHelper{}, - Host: &HostHelper{}, - PatchBuilding: &PatchBuildingHelper{}, - GPG: &GpgHelper{}, - Upstream: &UpstreamHelper{}, - AmendHelper: &AmendHelper{}, - Commits: &CommitsHelper{}, + Refs: &RefsHelper{}, + Bisect: &BisectHelper{}, + Suggestions: &SuggestionsHelper{}, + Files: &FilesHelper{}, + WorkingTree: &WorkingTreeHelper{}, + Tags: &TagsHelper{}, + MergeAndRebase: &MergeAndRebaseHelper{}, + MergeConflicts: &MergeConflictsHelper{}, + CherryPick: &CherryPickHelper{}, + Host: &HostHelper{}, + PatchBuilding: &PatchBuildingHelper{}, + Staging: &StagingHelper{}, + GPG: &GpgHelper{}, + Upstream: &UpstreamHelper{}, + AmendHelper: &AmendHelper{}, + Commits: &CommitsHelper{}, + Snake: &SnakeHelper{}, + Diff: &DiffHelper{}, + Repos: &ReposHelper{}, + RecordDirectory: &RecordDirectoryHelper{}, + Update: &UpdateHelper{}, + Window: &WindowHelper{}, + View: &ViewHelper{}, + Refresh: &RefreshHelper{}, + Confirmation: &ConfirmationHelper{}, + Mode: &ModeHelper{}, + AppStatus: &AppStatusHelper{}, + WindowArrangement: &WindowArrangementHelper{}, } } diff --git a/pkg/gui/controllers/helpers/host_helper.go b/pkg/gui/controllers/helpers/host_helper.go index edc0bc7ba..06102cc13 100644 --- a/pkg/gui/controllers/helpers/host_helper.go +++ b/pkg/gui/controllers/helpers/host_helper.go @@ -1,9 +1,7 @@ package helpers import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/hosting_service" - "github.com/jesseduffield/lazygit/pkg/gui/types" ) // this helper just wraps our hosting_service package @@ -14,17 +12,14 @@ type IHostHelper interface { } type HostHelper struct { - c *types.HelperCommon - git *commands.GitCommand + c *HelperCommon } func NewHostHelper( - c *types.HelperCommon, - git *commands.GitCommand, + c *HelperCommon, ) *HostHelper { return &HostHelper{ - c: c, - git: git, + c: c, } } @@ -40,7 +35,7 @@ func (self *HostHelper) GetCommitURL(commitSha string) (string, error) { // from one invocation to the next. Note however that we're currently caching config // results so we might want to invalidate the cache here if it becomes a problem. func (self *HostHelper) getHostingServiceMgr() *hosting_service.HostingServiceMgr { - remoteUrl := self.git.Config.GetRemoteURL() + remoteUrl := self.c.Git().Config.GetRemoteURL() configServices := self.c.UserConfig.Services return hosting_service.NewHostingServiceMgr(self.c.Log, self.c.Tr, remoteUrl, configServices) } diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 981b85a0d..dfa40e434 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -5,31 +5,23 @@ import ( "strings" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type MergeAndRebaseHelper struct { - c *types.HelperCommon - contexts *context.ContextTree - git *commands.GitCommand + c *HelperCommon refsHelper *RefsHelper } func NewMergeAndRebaseHelper( - c *types.HelperCommon, - contexts *context.ContextTree, - git *commands.GitCommand, + c *HelperCommon, refsHelper *RefsHelper, ) *MergeAndRebaseHelper { return &MergeAndRebaseHelper{ c: c, - contexts: contexts, - git: git, refsHelper: refsHelper, } } @@ -53,7 +45,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error { {option: REBASE_OPTION_ABORT, key: 'a'}, } - if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { options = append(options, optionAndKey{ option: REBASE_OPTION_SKIP, key: 's', }) @@ -70,7 +62,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error { }) var title string - if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { + if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_MERGING { title = self.c.Tr.MergeOptionsTitle } else { title = self.c.Tr.RebaseOptionsTitle @@ -80,7 +72,7 @@ func (self *MergeAndRebaseHelper) CreateRebaseOptionsMenu() error { } func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error { - status := self.git.Status.WorkingTreeState() + status := self.c.Git().Status.WorkingTreeState() if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING { return self.c.ErrorMsg(self.c.Tr.NotMergingOrRebasing) @@ -104,10 +96,10 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error { if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && self.c.UserConfig.Git.Merging.ManualCommit { // TODO: see if we should be calling more of the code from self.Git.Rebase.GenericMergeOrRebaseAction return self.c.RunSubprocessAndRefresh( - self.git.Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), + self.c.Git().Rebase.GenericMergeOrRebaseActionCmdObj(commandType, command), ) } - result := self.git.Rebase.GenericMergeOrRebaseAction(commandType, command) + result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command) if err := self.CheckMergeOrRebase(result); err != nil { return err } @@ -150,7 +142,7 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebase(result error) error { Title: self.c.Tr.FoundConflictsTitle, Prompt: self.c.Tr.FoundConflicts, HandleConfirm: func() error { - return self.c.PushContext(self.contexts.Files) + return self.c.PushContext(self.c.Contexts().Files) }, HandleClose: func() error { return self.genericMergeCommand(REBASE_OPTION_ABORT) @@ -174,7 +166,7 @@ func (self *MergeAndRebaseHelper) AbortMergeOrRebaseWithConfirm() error { } func (self *MergeAndRebaseHelper) workingTreeStateNoun() string { - workingTreeState := self.git.Status.WorkingTreeState() + workingTreeState := self.c.Git().Status.WorkingTreeState() switch workingTreeState { case enums.REBASE_MODE_NONE: return "" @@ -207,7 +199,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { Key: 's', OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.RebaseBranch) - err := self.git.Rebase.RebaseBranch(ref) + err := self.c.Git().Rebase.RebaseBranch(ref) return self.CheckMergeOrRebase(err) }, }, @@ -217,11 +209,11 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { Tooltip: self.c.Tr.InteractiveRebaseTooltip, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.RebaseBranch) - err := self.git.Rebase.EditRebase(ref) + err := self.c.Git().Rebase.EditRebase(ref) if err = self.CheckMergeOrRebase(err); err != nil { return err } - return self.c.PushContext(self.contexts.LocalCommits) + return self.c.PushContext(self.c.Contexts().LocalCommits) }, }, } @@ -241,7 +233,7 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error { } func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) error { - if self.git.Branch.IsHeadDetached() { + if self.c.Git().Branch.IsHeadDetached() { return self.c.ErrorMsg("Cannot merge branch in detached head state. You might have checked out a commit directly or a remote branch, in which case you should checkout the local branch you want to be on") } checkedOutBranchName := self.refsHelper.GetCheckedOutRef().Name @@ -261,7 +253,7 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e Prompt: prompt, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.Merge) - err := self.git.Branch.Merge(refName, git_commands.MergeOpts{}) + err := self.c.Git().Branch.Merge(refName, git_commands.MergeOpts{}) return self.CheckMergeOrRebase(err) }, }) diff --git a/pkg/gui/controllers/helpers/merge_conflicts_helper.go b/pkg/gui/controllers/helpers/merge_conflicts_helper.go index d7f7aa747..3610e28c8 100644 --- a/pkg/gui/controllers/helpers/merge_conflicts_helper.go +++ b/pkg/gui/controllers/helpers/merge_conflicts_helper.go @@ -1,41 +1,19 @@ package helpers import ( - "fmt" - - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" ) type MergeConflictsHelper struct { - c *types.HelperCommon - contexts *context.ContextTree - git *commands.GitCommand + c *HelperCommon } func NewMergeConflictsHelper( - c *types.HelperCommon, - contexts *context.ContextTree, - git *commands.GitCommand, + c *HelperCommon, ) *MergeConflictsHelper { return &MergeConflictsHelper{ - c: c, - contexts: contexts, - git: git, - } -} - -func (self *MergeConflictsHelper) GetMergingOptions() map[string]string { - keybindingConfig := self.c.UserConfig.Keybinding - - return map[string]string{ - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): self.c.Tr.LcSelectHunk, - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock)): self.c.Tr.LcNavigateConflicts, - keybindings.Label(keybindingConfig.Universal.Select): self.c.Tr.LcPickHunk, - keybindings.Label(keybindingConfig.Main.PickBothHunks): self.c.Tr.LcPickAllHunks, - keybindings.Label(keybindingConfig.Universal.Undo): self.c.Tr.LcUndo, + c: c, } } @@ -47,7 +25,7 @@ func (self *MergeConflictsHelper) SetMergeState(path string) (bool, error) { } func (self *MergeConflictsHelper) setMergeStateWithoutLock(path string) (bool, error) { - content, err := self.git.File.Cat(path) + content, err := self.c.Git().File.Cat(path) if err != nil { return false, err } @@ -78,7 +56,7 @@ func (self *MergeConflictsHelper) EscapeMerge() error { // doing this in separate UI thread so that we're not still holding the lock by the time refresh the file self.c.OnUIThread(func() error { - return self.c.PushContext(self.contexts.Files) + return self.c.PushContext(self.c.Contexts().Files) }) return nil } @@ -107,9 +85,48 @@ func (self *MergeConflictsHelper) SwitchToMerge(path string) error { } } - return self.c.PushContext(self.contexts.MergeConflicts) + return self.c.PushContext(self.c.Contexts().MergeConflicts) } func (self *MergeConflictsHelper) context() *context.MergeConflictsContext { - return self.contexts.MergeConflicts + return self.c.Contexts().MergeConflicts +} + +func (self *MergeConflictsHelper) Render(isFocused bool) error { + content := self.context().GetContentToRender(isFocused) + + var task types.UpdateTask + if self.context().IsUserScrolling() { + task = types.NewRenderStringWithoutScrollTask(content) + } else { + originY := self.context().GetOriginY() + task = types.NewRenderStringWithScrollTask(content, 0, originY) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().MergeConflicts, + Main: &types.ViewUpdateOpts{ + Task: task, + }, + }) +} + +func (self *MergeConflictsHelper) RefreshMergeState() error { + self.c.Contexts().MergeConflicts.GetMutex().Lock() + defer self.c.Contexts().MergeConflicts.GetMutex().Unlock() + + if self.c.CurrentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY { + return nil + } + + hasConflicts, err := self.SetConflictsAndRender(self.c.Contexts().MergeConflicts.GetState().GetPath(), true) + if err != nil { + return self.c.Error(err) + } + + if !hasConflicts { + return self.EscapeMerge() + } + + return nil } diff --git a/pkg/gui/controllers/helpers/mode_helper.go b/pkg/gui/controllers/helpers/mode_helper.go new file mode 100644 index 000000000..a97625ac7 --- /dev/null +++ b/pkg/gui/controllers/helpers/mode_helper.go @@ -0,0 +1,159 @@ +package helpers + +import ( + "fmt" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ModeHelper struct { + c *HelperCommon + + diffHelper *DiffHelper + patchBuildingHelper *PatchBuildingHelper + cherryPickHelper *CherryPickHelper + mergeAndRebaseHelper *MergeAndRebaseHelper + bisectHelper *BisectHelper +} + +func NewModeHelper( + c *HelperCommon, + diffHelper *DiffHelper, + patchBuildingHelper *PatchBuildingHelper, + cherryPickHelper *CherryPickHelper, + mergeAndRebaseHelper *MergeAndRebaseHelper, + bisectHelper *BisectHelper, +) *ModeHelper { + return &ModeHelper{ + c: c, + diffHelper: diffHelper, + patchBuildingHelper: patchBuildingHelper, + cherryPickHelper: cherryPickHelper, + mergeAndRebaseHelper: mergeAndRebaseHelper, + bisectHelper: bisectHelper, + } +} + +type ModeStatus struct { + IsActive func() bool + Description func() string + Reset func() error +} + +func (self *ModeHelper) Statuses() []ModeStatus { + return []ModeStatus{ + { + IsActive: self.c.Modes().Diffing.Active, + Description: func() string { + return self.withResetButton( + fmt.Sprintf( + "%s %s", + self.c.Tr.LcShowingGitDiff, + "git diff "+self.diffHelper.DiffStr(), + ), + style.FgMagenta, + ) + }, + Reset: self.diffHelper.ExitDiffMode, + }, + { + IsActive: self.c.Git().Patch.PatchBuilder.Active, + Description: func() string { + return self.withResetButton(self.c.Tr.LcBuildingPatch, style.FgYellow.SetBold()) + }, + Reset: self.patchBuildingHelper.Reset, + }, + { + IsActive: self.c.Modes().Filtering.Active, + Description: func() string { + return self.withResetButton( + fmt.Sprintf( + "%s '%s'", + self.c.Tr.LcFilteringBy, + self.c.Modes().Filtering.GetPath(), + ), + style.FgRed, + ) + }, + Reset: self.ExitFilterMode, + }, + { + IsActive: self.c.Modes().CherryPicking.Active, + Description: func() string { + copiedCount := len(self.c.Modes().CherryPicking.CherryPickedCommits) + text := self.c.Tr.LcCommitsCopied + if copiedCount == 1 { + text = self.c.Tr.LcCommitCopied + } + + return self.withResetButton( + fmt.Sprintf( + "%d %s", + copiedCount, + text, + ), + style.FgCyan, + ) + }, + Reset: self.cherryPickHelper.Reset, + }, + { + IsActive: func() bool { + return self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE + }, + Description: func() string { + workingTreeState := self.c.Git().Status.WorkingTreeState() + return self.withResetButton( + presentation.FormatWorkingTreeState(workingTreeState), style.FgYellow, + ) + }, + Reset: self.mergeAndRebaseHelper.AbortMergeOrRebaseWithConfirm, + }, + { + IsActive: func() bool { + return self.c.Model().BisectInfo.Started() + }, + Description: func() string { + return self.withResetButton("bisecting", style.FgGreen) + }, + Reset: self.bisectHelper.Reset, + }, + } +} + +func (self *ModeHelper) withResetButton(content string, textStyle style.TextStyle) string { + return textStyle.Sprintf( + "%s %s", + content, + style.AttrUnderline.Sprint(self.c.Tr.ResetInParentheses), + ) +} + +func (self *ModeHelper) GetActiveMode() (ModeStatus, bool) { + return slices.Find(self.Statuses(), func(mode ModeStatus) bool { + return mode.IsActive() + }) +} + +func (self *ModeHelper) IsAnyModeActive() bool { + return slices.Some(self.Statuses(), func(mode ModeStatus) bool { + return mode.IsActive() + }) +} + +func (self *ModeHelper) ExitFilterMode() error { + return self.ClearFiltering() +} + +func (self *ModeHelper) ClearFiltering() error { + self.c.Modes().Filtering.Reset() + if self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_HALF { + self.c.State().GetRepoState().SetScreenMode(types.SCREEN_NORMAL) + } + + return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) +} diff --git a/pkg/gui/controllers/helpers/patch_building_helper.go b/pkg/gui/controllers/helpers/patch_building_helper.go index 59de50b99..38f850886 100644 --- a/pkg/gui/controllers/helpers/patch_building_helper.go +++ b/pkg/gui/controllers/helpers/patch_building_helper.go @@ -1,9 +1,8 @@ package helpers import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -12,25 +11,19 @@ type IPatchBuildingHelper interface { } type PatchBuildingHelper struct { - c *types.HelperCommon - git *commands.GitCommand - contexts *context.ContextTree + c *HelperCommon } func NewPatchBuildingHelper( - c *types.HelperCommon, - git *commands.GitCommand, - contexts *context.ContextTree, + c *HelperCommon, ) *PatchBuildingHelper { return &PatchBuildingHelper{ - c: c, - git: git, - contexts: contexts, + c: c, } } func (self *PatchBuildingHelper) ValidateNormalWorkingTreeState() (bool, error) { - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { return false, self.c.ErrorMsg(self.c.Tr.CantPatchWhileRebasingError) } return true, nil @@ -43,7 +36,7 @@ func (self *PatchBuildingHelper) Escape() error { // kills the custom patch and returns us back to the commit files panel if needed func (self *PatchBuildingHelper) Reset() error { - self.git.Patch.PatchBuilder.Reset() + self.c.Git().Patch.PatchBuilder.Reset() if self.c.CurrentStaticContext().GetKind() != types.SIDE_CONTEXT { if err := self.Escape(); err != nil { @@ -60,3 +53,59 @@ func (self *PatchBuildingHelper) Reset() error { // refreshing the current context so that the secondary panel is hidden if necessary. return self.c.PostRefreshUpdate(self.c.CurrentContext()) } + +func (self *PatchBuildingHelper) RefreshPatchBuildingPanel(opts types.OnFocusOpts) error { + selectedLineIdx := -1 + if opts.ClickedWindowName == "main" { + selectedLineIdx = opts.ClickedViewLineIdx + } + + if !self.c.Git().Patch.PatchBuilder.Active() { + return self.Escape() + } + + // get diff from commit file that's currently selected + path := self.c.Contexts().CommitFiles.GetSelectedPath() + if path == "" { + return nil + } + + ref := self.c.Contexts().CommitFiles.CommitFileTreeViewModel.GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + diff, err := self.c.Git().WorkingTree.ShowFileDiff(from, to, reverse, path, true, self.c.State().GetIgnoreWhitespaceInDiffView()) + if err != nil { + return err + } + + secondaryDiff := self.c.Git().Patch.PatchBuilder.RenderPatchForFile(path, false, false) + if err != nil { + return err + } + + context := self.c.Contexts().CustomPatchBuilder + + oldState := context.GetState() + + state := patch_exploring.NewState(diff, selectedLineIdx, oldState, self.c.Log) + context.SetState(state) + if state == nil { + return self.Escape() + } + + mainContent := context.GetContentToRender(true) + + self.c.Contexts().CustomPatchBuilder.FocusSelection() + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().PatchBuilding, + Main: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(mainContent), + Title: self.c.Tr.Patch, + }, + Secondary: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(secondaryDiff), + Title: self.c.Tr.CustomPatch, + }, + }) +} diff --git a/pkg/gui/controllers/helpers/record_directory_helper.go b/pkg/gui/controllers/helpers/record_directory_helper.go new file mode 100644 index 000000000..377e3b94a --- /dev/null +++ b/pkg/gui/controllers/helpers/record_directory_helper.go @@ -0,0 +1,36 @@ +package helpers + +import ( + "os" +) + +type RecordDirectoryHelper struct { + c *HelperCommon +} + +func NewRecordDirectoryHelper(c *HelperCommon) *RecordDirectoryHelper { + return &RecordDirectoryHelper{ + c: c, + } +} + +// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined +// we will write the current directory to that file on exit so that their +// shell can then change to that directory. That means you don't get kicked +// back to the directory that you started with. +func (self *RecordDirectoryHelper) RecordCurrentDirectory() error { + // determine current directory, set it in LAZYGIT_NEW_DIR_FILE + dirName, err := os.Getwd() + if err != nil { + return err + } + return self.RecordDirectory(dirName) +} + +func (self *RecordDirectoryHelper) RecordDirectory(dirName string) error { + newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE") + if newDirFilePath == "" { + return nil + } + return self.c.OS().CreateFileWithContent(newDirFilePath, dirName) +} diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go new file mode 100644 index 000000000..354a2115b --- /dev/null +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -0,0 +1,611 @@ +package helpers + +import ( + "fmt" + "strings" + "sync" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/filetree" + "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type RefreshHelper struct { + c *HelperCommon + refsHelper *RefsHelper + mergeAndRebaseHelper *MergeAndRebaseHelper + patchBuildingHelper *PatchBuildingHelper + stagingHelper *StagingHelper + mergeConflictsHelper *MergeConflictsHelper + fileWatcher types.IFileWatcher +} + +func NewRefreshHelper( + c *HelperCommon, + refsHelper *RefsHelper, + mergeAndRebaseHelper *MergeAndRebaseHelper, + patchBuildingHelper *PatchBuildingHelper, + stagingHelper *StagingHelper, + mergeConflictsHelper *MergeConflictsHelper, + fileWatcher types.IFileWatcher, +) *RefreshHelper { + return &RefreshHelper{ + c: c, + refsHelper: refsHelper, + mergeAndRebaseHelper: mergeAndRebaseHelper, + patchBuildingHelper: patchBuildingHelper, + stagingHelper: stagingHelper, + mergeConflictsHelper: mergeConflictsHelper, + fileWatcher: fileWatcher, + } +} + +func (self *RefreshHelper) Refresh(options types.RefreshOptions) error { + if options.Scope == nil { + self.c.Log.Infof( + "refreshing all scopes in %s mode", + getModeName(options.Mode), + ) + } else { + self.c.Log.Infof( + "refreshing the following scopes in %s mode: %s", + getModeName(options.Mode), + strings.Join(getScopeNames(options.Scope), ","), + ) + } + + wg := sync.WaitGroup{} + + f := func() { + var scopeSet *set.Set[types.RefreshableView] + if len(options.Scope) == 0 { + // not refreshing staging/patch-building unless explicitly requested because we only need + // to refresh those while focused. + scopeSet = set.NewFromSlice([]types.RefreshableView{ + types.COMMITS, + types.BRANCHES, + types.FILES, + types.STASH, + types.REFLOG, + types.TAGS, + types.REMOTES, + types.STATUS, + types.BISECT_INFO, + types.STAGING, + }) + } else { + scopeSet = set.NewFromSlice(options.Scope) + } + + refresh := func(f func()) { + wg.Add(1) + func() { + if options.Mode == types.ASYNC { + go utils.Safe(f) + } else { + f() + } + wg.Done() + }() + } + + if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { + refresh(self.refreshCommits) + } else if scopeSet.Includes(types.REBASE_COMMITS) { + // the above block handles rebase commits so we only need to call this one + // if we've asked specifically for rebase commits and not those other things + refresh(func() { _ = self.refreshRebaseCommits() }) + } + + if scopeSet.Includes(types.SUB_COMMITS) { + refresh(func() { _ = self.refreshSubCommitsWithLimit() }) + } + + // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway + if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { + refresh(func() { _ = self.refreshCommitFilesContext() }) + } + + if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) { + refresh(func() { _ = self.refreshFilesAndSubmodules() }) + } + + if scopeSet.Includes(types.STASH) { + refresh(func() { _ = self.refreshStashEntries() }) + } + + if scopeSet.Includes(types.TAGS) { + refresh(func() { _ = self.refreshTags() }) + } + + if scopeSet.Includes(types.REMOTES) { + refresh(func() { _ = self.refreshRemotes() }) + } + + if scopeSet.Includes(types.STAGING) { + refresh(func() { _ = self.stagingHelper.RefreshStagingPanel(types.OnFocusOpts{}) }) + } + + if scopeSet.Includes(types.PATCH_BUILDING) { + refresh(func() { _ = self.patchBuildingHelper.RefreshPatchBuildingPanel(types.OnFocusOpts{}) }) + } + + if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) { + refresh(func() { _ = self.mergeConflictsHelper.RefreshMergeState() }) + } + + wg.Wait() + + self.refreshStatus() + + if options.Then != nil { + options.Then() + } + } + + if options.Mode == types.BLOCK_UI { + self.c.OnUIThread(func() error { + f() + return nil + }) + } else { + f() + } + + return nil +} + +func getScopeNames(scopes []types.RefreshableView) []string { + scopeNameMap := map[types.RefreshableView]string{ + types.COMMITS: "commits", + types.BRANCHES: "branches", + types.FILES: "files", + types.SUBMODULES: "submodules", + types.SUB_COMMITS: "subCommits", + types.STASH: "stash", + types.REFLOG: "reflog", + types.TAGS: "tags", + types.REMOTES: "remotes", + types.STATUS: "status", + types.BISECT_INFO: "bisect", + types.STAGING: "staging", + types.MERGE_CONFLICTS: "mergeConflicts", + } + + return slices.Map(scopes, func(scope types.RefreshableView) string { + return scopeNameMap[scope] + }) +} + +func getModeName(mode types.RefreshMode) string { + switch mode { + case types.SYNC: + return "sync" + case types.ASYNC: + return "async" + case types.BLOCK_UI: + return "block-ui" + default: + return "unknown mode" + } +} + +// during startup, the bottleneck is fetching the reflog entries. We need these +// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE. +// In the initial phase we don't get any reflog commits, but we asynchronously get them +// and refresh the branches after that +func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() { + switch self.c.State().GetRepoState().GetStartupStage() { + case types.INITIAL: + go utils.Safe(func() { + _ = self.refreshReflogCommits() + self.refreshBranches() + self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) + }) + + case types.COMPLETE: + _ = self.refreshReflogCommits() + } +} + +// whenever we change commits, we should update branches because the upstream/downstream +// counts can change. Whenever we change branches we should probably also change commits +// e.g. in the case of switching branches. +func (self *RefreshHelper) refreshCommits() { + wg := sync.WaitGroup{} + wg.Add(2) + + go utils.Safe(func() { + self.refreshReflogCommitsConsideringStartup() + + self.refreshBranches() + wg.Done() + }) + + go utils.Safe(func() { + _ = self.refreshCommitsWithLimit() + ctx, ok := self.c.Contexts().CommitFiles.GetParentContext() + if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { + // This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position. + // However if we've just added a brand new commit, it pushes the list down by one and so we would end up + // showing the contents of a different commit than the one we initially entered. + // Ideally we would know when to refresh the commit files context and when not to, + // or perhaps we could just pop that context off the stack whenever cycling windows. + // For now the awkwardness remains. + commit := self.c.Contexts().LocalCommits.GetSelected() + if commit != nil { + self.c.Contexts().CommitFiles.SetRef(commit) + self.c.Contexts().CommitFiles.SetTitleRef(commit.RefName()) + _ = self.refreshCommitFilesContext() + } + } + wg.Done() + }) + + wg.Wait() +} + +func (self *RefreshHelper) refreshCommitsWithLimit() error { + self.c.Mutexes().LocalCommitsMutex.Lock() + defer self.c.Mutexes().LocalCommitsMutex.Unlock() + + commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( + git_commands.GetCommitsOptions{ + Limit: self.c.Contexts().LocalCommits.GetLimitCommits(), + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: true, + RefName: self.refForLog(), + All: self.c.Contexts().LocalCommits.GetShowWholeGitGraph(), + }, + ) + if err != nil { + return err + } + self.c.Model().Commits = commits + self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState() + + return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) +} + +func (self *RefreshHelper) refreshSubCommitsWithLimit() error { + self.c.Mutexes().SubCommitsMutex.Lock() + defer self.c.Mutexes().SubCommitsMutex.Unlock() + + commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( + git_commands.GetCommitsOptions{ + Limit: self.c.Contexts().SubCommits.GetLimitCommits(), + FilterPath: self.c.Modes().Filtering.GetPath(), + IncludeRebaseCommits: false, + RefName: self.c.Contexts().SubCommits.GetRef().FullRefName(), + }, + ) + if err != nil { + return err + } + self.c.Model().SubCommits = commits + + return self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) +} + +func (self *RefreshHelper) refreshCommitFilesContext() error { + ref := self.c.Contexts().CommitFiles.GetRef() + to := ref.RefName() + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) + + files, err := self.c.Git().Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse) + if err != nil { + return self.c.Error(err) + } + self.c.Model().CommitFiles = files + self.c.Contexts().CommitFiles.CommitFileTreeViewModel.SetTree() + + return self.c.PostRefreshUpdate(self.c.Contexts().CommitFiles) +} + +func (self *RefreshHelper) refreshRebaseCommits() error { + self.c.Mutexes().LocalCommitsMutex.Lock() + defer self.c.Mutexes().LocalCommitsMutex.Unlock() + + updatedCommits, err := self.c.Git().Loaders.CommitLoader.MergeRebasingCommits(self.c.Model().Commits) + if err != nil { + return err + } + self.c.Model().Commits = updatedCommits + self.c.Model().WorkingTreeStateAtLastCommitRefresh = self.c.Git().Status.WorkingTreeState() + + return self.c.PostRefreshUpdate(self.c.Contexts().LocalCommits) +} + +func (self *RefreshHelper) refreshTags() error { + tags, err := self.c.Git().Loaders.TagLoader.GetTags() + if err != nil { + return self.c.Error(err) + } + + self.c.Model().Tags = tags + + return self.c.PostRefreshUpdate(self.c.Contexts().Tags) +} + +func (self *RefreshHelper) refreshStateSubmoduleConfigs() error { + configs, err := self.c.Git().Submodule.GetConfigs() + if err != nil { + return err + } + + self.c.Model().Submodules = configs + + return nil +} + +// self.refreshStatus is called at the end of this because that's when we can +// be sure there is a State.Model.Branches array to pick the current branch from +func (self *RefreshHelper) refreshBranches() { + reflogCommits := self.c.Model().FilteredReflogCommits + if self.c.Modes().Filtering.Active() { + // in filter mode we filter our reflog commits to just those containing the path + // however we need all the reflog entries to populate the recencies of our branches + // which allows us to order them correctly. So if we're filtering we'll just + // manually load all the reflog commits here + var err error + reflogCommits, _, err = self.c.Git().Loaders.ReflogCommitLoader.GetReflogCommits(nil, "") + if err != nil { + self.c.Log.Error(err) + } + } + + branches, err := self.c.Git().Loaders.BranchLoader.Load(reflogCommits) + if err != nil { + _ = self.c.Error(err) + } + + self.c.Model().Branches = branches + + if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil { + self.c.Log.Error(err) + } + + self.refreshStatus() +} + +func (self *RefreshHelper) refreshFilesAndSubmodules() error { + self.c.Mutexes().RefreshingFilesMutex.Lock() + self.c.State().SetIsRefreshingFiles(true) + defer func() { + self.c.State().SetIsRefreshingFiles(false) + self.c.Mutexes().RefreshingFilesMutex.Unlock() + }() + + if err := self.refreshStateSubmoduleConfigs(); err != nil { + return err + } + + if err := self.refreshStateFiles(); err != nil { + return err + } + + self.c.OnUIThread(func() error { + if err := self.c.PostRefreshUpdate(self.c.Contexts().Submodules); err != nil { + self.c.Log.Error(err) + } + + if err := self.c.PostRefreshUpdate(self.c.Contexts().Files); err != nil { + self.c.Log.Error(err) + } + + return nil + }) + + return nil +} + +func (self *RefreshHelper) refreshStateFiles() error { + fileTreeViewModel := self.c.Contexts().Files.FileTreeViewModel + + // If git thinks any of our files have inline merge conflicts, but they actually don't, + // we stage them. + // Note that if files with merge conflicts have both arisen and have been resolved + // between refreshes, we won't stage them here. This is super unlikely though, + // and this approach spares us from having to call `git status` twice in a row. + // Although this also means that at startup we won't be staging anything until + // we call git status again. + pathsToStage := []string{} + prevConflictFileCount := 0 + for _, file := range self.c.Model().Files { + if file.HasMergeConflicts { + prevConflictFileCount++ + } + if file.HasInlineMergeConflicts { + hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name) + if err != nil { + self.c.Log.Error(err) + } else if !hasConflicts { + pathsToStage = append(pathsToStage, file.Name) + } + } + } + + if len(pathsToStage) > 0 { + self.c.LogAction(self.c.Tr.Actions.StageResolvedFiles) + if err := self.c.Git().WorkingTree.StageFiles(pathsToStage); err != nil { + return self.c.Error(err) + } + } + + files := self.c.Git().Loaders.FileLoader. + GetStatusFiles(git_commands.GetStatusFileOptions{}) + + conflictFileCount := 0 + for _, file := range files { + if file.HasMergeConflicts { + conflictFileCount++ + } + } + + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { + self.c.OnUIThread(func() error { return self.mergeAndRebaseHelper.PromptToContinueRebase() }) + } + + fileTreeViewModel.RWMutex.Lock() + + // only taking over the filter if it hasn't already been set by the user. + // Though this does make it impossible for the user to actually say they want to display all if + // conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some + // extra state here to see if the user's set the filter themselves we can do that, but + // I'd prefer to maintain as little state as possible. + if conflictFileCount > 0 { + if fileTreeViewModel.GetFilter() == filetree.DisplayAll { + fileTreeViewModel.SetFilter(filetree.DisplayConflicted) + } + } else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted { + fileTreeViewModel.SetFilter(filetree.DisplayAll) + } + + self.c.Model().Files = files + fileTreeViewModel.SetTree() + fileTreeViewModel.RWMutex.Unlock() + + if err := self.fileWatcher.AddFilesToFileWatcher(files); err != nil { + return err + } + + return nil +} + +// the reflogs panel is the only panel where we cache data, in that we only +// load entries that have been created since we last ran the call. This means +// we need to be more careful with how we use this, and to ensure we're emptying +// the reflogs array when changing contexts. +// This method also manages two things: ReflogCommits and FilteredReflogCommits. +// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits +// are used by the branches panel to obtain recency values for sorting. +func (self *RefreshHelper) refreshReflogCommits() error { + // pulling state into its own variable incase it gets swapped out for another state + // and we get an out of bounds exception + model := self.c.Model() + var lastReflogCommit *models.Commit + if len(model.ReflogCommits) > 0 { + lastReflogCommit = model.ReflogCommits[0] + } + + refresh := func(stateCommits *[]*models.Commit, filterPath string) error { + commits, onlyObtainedNewReflogCommits, err := self.c.Git().Loaders.ReflogCommitLoader. + GetReflogCommits(lastReflogCommit, filterPath) + if err != nil { + return self.c.Error(err) + } + + if onlyObtainedNewReflogCommits { + *stateCommits = append(commits, *stateCommits...) + } else { + *stateCommits = commits + } + return nil + } + + if err := refresh(&model.ReflogCommits, ""); err != nil { + return err + } + + if self.c.Modes().Filtering.Active() { + if err := refresh(&model.FilteredReflogCommits, self.c.Modes().Filtering.GetPath()); err != nil { + return err + } + } else { + model.FilteredReflogCommits = model.ReflogCommits + } + + return self.c.PostRefreshUpdate(self.c.Contexts().ReflogCommits) +} + +func (self *RefreshHelper) refreshRemotes() error { + prevSelectedRemote := self.c.Contexts().Remotes.GetSelected() + + remotes, err := self.c.Git().Loaders.RemoteLoader.GetRemotes() + if err != nil { + return self.c.Error(err) + } + + self.c.Model().Remotes = remotes + + // we need to ensure our selected remote branches aren't now outdated + if prevSelectedRemote != nil && self.c.Model().RemoteBranches != nil { + // find remote now + for _, remote := range remotes { + if remote.Name == prevSelectedRemote.Name { + self.c.Model().RemoteBranches = remote.Branches + break + } + } + } + + if err := self.c.PostRefreshUpdate(self.c.Contexts().Remotes); err != nil { + return err + } + + if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil { + return err + } + + return nil +} + +func (self *RefreshHelper) refreshStashEntries() error { + self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader. + GetStashEntries(self.c.Modes().Filtering.GetPath()) + + return self.c.PostRefreshUpdate(self.c.Contexts().Stash) +} + +// never call this on its own, it should only be called from within refreshCommits() +func (self *RefreshHelper) refreshStatus() { + self.c.Mutexes().RefreshingStatusMutex.Lock() + defer self.c.Mutexes().RefreshingStatusMutex.Unlock() + + currentBranch := self.refsHelper.GetCheckedOutRef() + if currentBranch == nil { + // need to wait for branches to refresh + return + } + status := "" + + if currentBranch.IsRealBranch() { + status += presentation.ColoredBranchStatus(currentBranch, self.c.Tr) + " " + } + + workingTreeState := self.c.Git().Status.WorkingTreeState() + if workingTreeState != enums.REBASE_MODE_NONE { + status += style.FgYellow.Sprintf("(%s) ", presentation.FormatWorkingTreeState(workingTreeState)) + } + + name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) + repoName := utils.GetCurrentRepoName() + status += fmt.Sprintf("%s → %s ", repoName, name) + + self.c.SetViewContent(self.c.Views().Status, status) +} + +func (self *RefreshHelper) refForLog() string { + bisectInfo := self.c.Git().Bisect.GetInfo() + self.c.Model().BisectInfo = bisectInfo + + if !bisectInfo.Started() { + return "HEAD" + } + + // need to see if our bisect's current commit is reachable from our 'new' ref. + if bisectInfo.Bisecting() && !self.c.Git().Bisect.ReachableFromStart(bisectInfo) { + return bisectInfo.GetNewSha() + } + + return bisectInfo.GetStartSha() +} diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index c03a2b24c..f40a2ad10 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -5,10 +5,8 @@ import ( "strings" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -23,23 +21,14 @@ type IRefsHelper interface { } type RefsHelper struct { - c *types.HelperCommon - git *commands.GitCommand - contexts *context.ContextTree - model *types.Model + c *HelperCommon } func NewRefsHelper( - c *types.HelperCommon, - git *commands.GitCommand, - contexts *context.ContextTree, - model *types.Model, + c *HelperCommon, ) *RefsHelper { return &RefsHelper{ - c: c, - git: git, - contexts: contexts, - model: model, + c: c, } } @@ -54,15 +43,15 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions cmdOptions := git_commands.CheckoutOptions{Force: false, EnvVars: options.EnvVars} onSuccess := func() { - self.contexts.Branches.SetSelectedLineIdx(0) - self.contexts.ReflogCommits.SetSelectedLineIdx(0) - self.contexts.LocalCommits.SetSelectedLineIdx(0) + self.c.Contexts().Branches.SetSelectedLineIdx(0) + self.c.Contexts().ReflogCommits.SetSelectedLineIdx(0) + self.c.Contexts().LocalCommits.SetSelectedLineIdx(0) // loading a heap of commits is slow so we limit them whenever doing a reset - self.contexts.LocalCommits.SetLimitCommits(true) + self.c.Contexts().LocalCommits.SetLimitCommits(true) } return self.c.WithWaitingStatus(waitingStatus, func() error { - if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil { + if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil { // note, this will only work for english-language git commands. If we force git to use english, and the error isn't this one, then the user will receive an english command they may not understand. I'm not sure what the best solution to this is. Running the command once in english and a second time in the native language is one option if options.OnRefNotFound != nil && strings.Contains(err.Error(), "did not match any file(s) known to git") { @@ -75,15 +64,15 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions Title: self.c.Tr.AutoStashTitle, Prompt: self.c.Tr.AutoStashPrompt, HandleConfirm: func() error { - if err := self.git.Stash.Save(self.c.Tr.StashPrefix + ref); err != nil { + if err := self.c.Git().Stash.Save(self.c.Tr.StashPrefix + ref); err != nil { return self.c.Error(err) } - if err := self.git.Branch.Checkout(ref, cmdOptions); err != nil { + if err := self.c.Git().Branch.Checkout(ref, cmdOptions); err != nil { return self.c.Error(err) } onSuccess() - if err := self.git.Stash.Pop(0); err != nil { + if err := self.c.Git().Stash.Pop(0); err != nil { if err := self.c.Refresh(types.RefreshOptions{Mode: types.BLOCK_UI}); err != nil { return err } @@ -105,22 +94,22 @@ func (self *RefsHelper) CheckoutRef(ref string, options types.CheckoutRefOptions } func (self *RefsHelper) GetCheckedOutRef() *models.Branch { - if len(self.model.Branches) == 0 { + if len(self.c.Model().Branches) == 0 { return nil } - return self.model.Branches[0] + return self.c.Model().Branches[0] } func (self *RefsHelper) ResetToRef(ref string, strength string, envVars []string) error { - if err := self.git.Commit.ResetToCommit(ref, strength, envVars); err != nil { + if err := self.c.Git().Commit.ResetToCommit(ref, strength, envVars); err != nil { return self.c.Error(err) } - self.contexts.LocalCommits.SetSelectedLineIdx(0) - self.contexts.ReflogCommits.SetSelectedLineIdx(0) + self.c.Contexts().LocalCommits.SetSelectedLineIdx(0) + self.c.Contexts().ReflogCommits.SetSelectedLineIdx(0) // loading a heap of commits is slow so we limit them whenever doing a reset - self.contexts.LocalCommits.SetLimitCommits(true) + self.c.Contexts().LocalCommits.SetLimitCommits(true) if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.BRANCHES, types.REFLOG, types.COMMITS}}); err != nil { return err @@ -173,18 +162,18 @@ func (self *RefsHelper) NewBranch(from string, fromFormattedName string, suggest InitialContent: suggestedBranchName, HandleConfirm: func(response string) error { self.c.LogAction(self.c.Tr.Actions.CreateBranch) - if err := self.git.Branch.New(sanitizedBranchName(response), from); err != nil { + if err := self.c.Git().Branch.New(sanitizedBranchName(response), from); err != nil { return err } - if self.c.CurrentContext() != self.contexts.Branches { - if err := self.c.PushContext(self.contexts.Branches); err != nil { + if self.c.CurrentContext() != self.c.Contexts().Branches { + if err := self.c.PushContext(self.c.Contexts().Branches); err != nil { return err } } - self.contexts.LocalCommits.SetSelectedLineIdx(0) - self.contexts.Branches.SetSelectedLineIdx(0) + self.c.Contexts().LocalCommits.SetSelectedLineIdx(0) + self.c.Contexts().Branches.SetSelectedLineIdx(0) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }, diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go new file mode 100644 index 000000000..060724a0a --- /dev/null +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -0,0 +1,175 @@ +package helpers + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" + + "github.com/jesseduffield/generics/slices" + appTypes "github.com/jesseduffield/lazygit/pkg/app/types" + "github.com/jesseduffield/lazygit/pkg/commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/env" + "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type onNewRepoFn func(startArgs appTypes.StartArgs, reuseState bool) error + +// helps switch back and forth between repos +type ReposHelper struct { + c *HelperCommon + recordDirectoryHelper *RecordDirectoryHelper + onNewRepo onNewRepoFn +} + +func NewRecentReposHelper( + c *HelperCommon, + recordDirectoryHelper *RecordDirectoryHelper, + onNewRepo onNewRepoFn, +) *ReposHelper { + return &ReposHelper{ + c: c, + recordDirectoryHelper: recordDirectoryHelper, + onNewRepo: onNewRepo, + } +} + +func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error { + wd, err := os.Getwd() + if err != nil { + return err + } + self.c.State().GetRepoPathStack().Push(wd) + + return self.DispatchSwitchToRepo(submodule.Path, true) +} + +func (self *ReposHelper) getCurrentBranch(path string) string { + readHeadFile := func(path string) (string, error) { + headFile, err := os.ReadFile(filepath.Join(path, "HEAD")) + if err == nil { + content := strings.TrimSpace(string(headFile)) + refsPrefix := "ref: refs/heads/" + var branchDisplay string + if strings.HasPrefix(content, refsPrefix) { + // is a branch + branchDisplay = strings.TrimPrefix(content, refsPrefix) + } else { + // detached HEAD state, displaying short SHA + branchDisplay = utils.ShortSha(content) + } + return branchDisplay, nil + } + return "", err + } + + gitDirPath := filepath.Join(path, ".git") + + if gitDir, err := os.Stat(gitDirPath); err == nil { + if gitDir.IsDir() { + // ordinary repo + if branch, err := readHeadFile(gitDirPath); err == nil { + return branch + } + } else { + // worktree + if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil { + content := strings.TrimSpace(string(worktreeGitDir)) + worktreePath := strings.TrimPrefix(content, "gitdir: ") + if branch, err := readHeadFile(worktreePath); err == nil { + return branch + } + } + } + } + + return self.c.Tr.LcBranchUnknown +} + +func (self *ReposHelper) CreateRecentReposMenu() error { + // we'll show an empty panel if there are no recent repos + recentRepoPaths := []string{} + if len(self.c.GetAppState().RecentRepos) > 0 { + // we skip the first one because we're currently in it + recentRepoPaths = self.c.GetAppState().RecentRepos[1:] + } + + currentBranches := sync.Map{} + + wg := sync.WaitGroup{} + wg.Add(len(recentRepoPaths)) + + for _, path := range recentRepoPaths { + go func(path string) { + defer wg.Done() + currentBranches.Store(path, self.getCurrentBranch(path)) + }(path) + } + + wg.Wait() + + menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem { + branchName, _ := currentBranches.Load(path) + if icons.IsIconEnabled() { + branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName) + } + + return &types.MenuItem{ + LabelColumns: []string{ + filepath.Base(path), + style.FgCyan.Sprint(branchName), + style.FgMagenta.Sprint(path), + }, + OnPress: func() error { + // if we were in a submodule, we want to forget about that stack of repos + // so that hitting escape in the new repo does nothing + self.c.State().GetRepoPathStack().Clear() + return self.DispatchSwitchToRepo(path, false) + }, + } + }) + + return self.c.Menu(types.CreateMenuOptions{Title: self.c.Tr.RecentRepos, Items: menuItems}) +} + +func (self *ReposHelper) DispatchSwitchToRepo(path string, reuse bool) error { + env.UnsetGitDirEnvs() + originalPath, err := os.Getwd() + if err != nil { + return nil + } + + if err := os.Chdir(path); err != nil { + if os.IsNotExist(err) { + return self.c.ErrorMsg(self.c.Tr.ErrRepositoryMovedOrDeleted) + } + return err + } + + if err := commands.VerifyInGitRepo(self.c.OS()); err != nil { + if err := os.Chdir(originalPath); err != nil { + return err + } + + return err + } + + if err := self.recordDirectoryHelper.RecordCurrentDirectory(); err != nil { + return err + } + + // these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to + // switch to a repo while one of these goroutines is in the process of updating something + self.c.Mutexes().SyncMutex.Lock() + defer self.c.Mutexes().SyncMutex.Unlock() + + self.c.Mutexes().RefreshingFilesMutex.Lock() + defer self.c.Mutexes().RefreshingFilesMutex.Unlock() + + return self.onNewRepo(appTypes.StartArgs{}, reuse) +} diff --git a/pkg/gui/controllers/helpers/snake_helper.go b/pkg/gui/controllers/helpers/snake_helper.go new file mode 100644 index 000000000..fb4d67bb4 --- /dev/null +++ b/pkg/gui/controllers/helpers/snake_helper.go @@ -0,0 +1,75 @@ +package helpers + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/snake" +) + +type SnakeHelper struct { + c *HelperCommon + game *snake.Game +} + +func NewSnakeHelper(c *HelperCommon) *SnakeHelper { + return &SnakeHelper{ + c: c, + } +} + +func (self *SnakeHelper) StartGame() { + view := self.c.Views().Snake + + game := snake.NewGame(view.Width(), view.Height(), self.renderSnakeGame, self.c.LogAction) + self.game = game + game.Start() +} + +func (self *SnakeHelper) ExitGame() { + self.game.Exit() +} + +func (self *SnakeHelper) SetDirection(direction snake.Direction) { + self.game.SetDirection(direction) +} + +func (self *SnakeHelper) renderSnakeGame(cells [][]snake.CellType, alive bool) { + view := self.c.Views().Snake + + if !alive { + _ = self.c.ErrorMsg(self.c.Tr.YouDied) + return + } + + output := self.drawSnakeGame(cells) + + view.Clear() + fmt.Fprint(view, output) + self.c.Render() +} + +func (self *SnakeHelper) drawSnakeGame(cells [][]snake.CellType) string { + writer := &strings.Builder{} + + for i, row := range cells { + for _, cell := range row { + switch cell { + case snake.None: + writer.WriteString(" ") + case snake.Snake: + writer.WriteString("█") + case snake.Food: + writer.WriteString(style.FgMagenta.Sprint("█")) + } + } + + if i < len(cells) { + writer.WriteString("\n") + } + } + + output := writer.String() + return output +} diff --git a/pkg/gui/controllers/helpers/staging_helper.go b/pkg/gui/controllers/helpers/staging_helper.go new file mode 100644 index 000000000..75280b985 --- /dev/null +++ b/pkg/gui/controllers/helpers/staging_helper.go @@ -0,0 +1,122 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type StagingHelper struct { + c *HelperCommon +} + +func NewStagingHelper( + c *HelperCommon, +) *StagingHelper { + return &StagingHelper{ + c: c, + } +} + +// NOTE: used from outside this file +func (self *StagingHelper) RefreshStagingPanel(focusOpts types.OnFocusOpts) error { + secondaryFocused := self.secondaryStagingFocused() + mainFocused := self.mainStagingFocused() + + // this method could be called when the staging panel is not being used, + // in which case we don't want to do anything. + if !mainFocused && !secondaryFocused { + return nil + } + + mainSelectedLineIdx := -1 + secondarySelectedLineIdx := -1 + if focusOpts.ClickedViewLineIdx > 0 { + if secondaryFocused { + secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx + } else { + mainSelectedLineIdx = focusOpts.ClickedViewLineIdx + } + } + + mainContext := self.c.Contexts().Staging + secondaryContext := self.c.Contexts().StagingSecondary + + var file *models.File + node := self.c.Contexts().Files.GetSelected() + if node != nil { + file = node.File + } + + if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) { + return self.handleStagingEscape() + } + + mainDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, false, false) + secondaryDiff := self.c.Git().WorkingTree.WorktreeFileDiff(file, true, true, false) + + // grabbing locks here and releasing before we finish the function + // because pushing say the secondary context could mean entering this function + // again, and we don't want to have a deadlock + mainContext.GetMutex().Lock() + secondaryContext.GetMutex().Lock() + + mainContext.SetState( + patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), self.c.Log), + ) + + secondaryContext.SetState( + patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), self.c.Log), + ) + + mainState := mainContext.GetState() + secondaryState := secondaryContext.GetState() + + mainContent := mainContext.GetContentToRender(!secondaryFocused) + secondaryContent := secondaryContext.GetContentToRender(secondaryFocused) + + mainContext.GetMutex().Unlock() + secondaryContext.GetMutex().Unlock() + + if mainState == nil && secondaryState == nil { + return self.handleStagingEscape() + } + + if mainState == nil && !secondaryFocused { + return self.c.PushContext(secondaryContext, focusOpts) + } + + if secondaryState == nil && secondaryFocused { + return self.c.PushContext(mainContext, focusOpts) + } + + if secondaryFocused { + self.c.Contexts().StagingSecondary.FocusSelection() + } else { + self.c.Contexts().Staging.FocusSelection() + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Staging, + Main: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(mainContent), + Title: self.c.Tr.UnstagedChanges, + }, + Secondary: &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(secondaryContent), + Title: self.c.Tr.StagedChanges, + }, + }) +} + +func (self *StagingHelper) handleStagingEscape() error { + return self.c.PushContext(self.c.Contexts().Files) +} + +func (self *StagingHelper) secondaryStagingFocused() bool { + return self.c.CurrentStaticContext().GetKey() == self.c.Contexts().StagingSecondary.GetKey() +} + +func (self *StagingHelper) mainStagingFocused() bool { + return self.c.CurrentStaticContext().GetKey() == self.c.Contexts().Staging.GetKey() +} diff --git a/pkg/gui/controllers/helpers/suggestions_helper.go b/pkg/gui/controllers/helpers/suggestions_helper.go index 0cc4a642b..2234e3c2b 100644 --- a/pkg/gui/controllers/helpers/suggestions_helper.go +++ b/pkg/gui/controllers/helpers/suggestions_helper.go @@ -33,28 +33,21 @@ type ISuggestionsHelper interface { } type SuggestionsHelper struct { - c *types.HelperCommon - - model *types.Model - refreshSuggestionsFn func() + c *HelperCommon } var _ ISuggestionsHelper = &SuggestionsHelper{} func NewSuggestionsHelper( - c *types.HelperCommon, - model *types.Model, - refreshSuggestionsFn func(), + c *HelperCommon, ) *SuggestionsHelper { return &SuggestionsHelper{ - c: c, - model: model, - refreshSuggestionsFn: refreshSuggestionsFn, + c: c, } } func (self *SuggestionsHelper) getRemoteNames() []string { - return slices.Map(self.model.Remotes, func(remote *models.Remote) string { + return slices.Map(self.c.Model().Remotes, func(remote *models.Remote) string { return remote.Name }) } @@ -75,7 +68,7 @@ func (self *SuggestionsHelper) GetRemoteSuggestionsFunc() func(string) []*types. } func (self *SuggestionsHelper) getBranchNames() []string { - return slices.Map(self.model.Branches, func(branch *models.Branch) string { + return slices.Map(self.c.Model().Branches, func(branch *models.Branch) string { return branch.Name }) } @@ -101,8 +94,8 @@ func (self *SuggestionsHelper) GetBranchNameSuggestionsFunc() func(string) []*ty } // here we asynchronously fetch the latest set of paths in the repo and store in -// self.model.FilesTrie. On the main thread we'll be doing a fuzzy search via -// self.model.FilesTrie. So if we've looked for a file previously, we'll start with +// self.c.Model().FilesTrie. On the main thread we'll be doing a fuzzy search via +// self.c.Model().FilesTrie. So if we've looked for a file previously, we'll start with // the old trie and eventually it'll be swapped out for the new one. // Notably, unlike other suggestion functions we're not showing all the options // if nothing has been typed because there'll be too much to display efficiently @@ -125,16 +118,16 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type }) // cache the trie for future use - self.model.FilesTrie = trie + self.c.Model().FilesTrie = trie - self.refreshSuggestionsFn() + self.c.Contexts().Suggestions.RefreshSuggestions() return err }) return func(input string) []*types.Suggestion { matchingNames := []string{} - _ = self.model.FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { + _ = self.c.Model().FilesTrie.VisitFuzzy(patricia.Prefix(input), true, func(prefix patricia.Prefix, item patricia.Item, skipped int) error { matchingNames = append(matchingNames, item.(string)) return nil }) @@ -147,7 +140,7 @@ func (self *SuggestionsHelper) GetFilePathSuggestionsFunc() func(string) []*type } func (self *SuggestionsHelper) getRemoteBranchNames(separator string) []string { - return slices.FlatMap(self.model.Remotes, func(remote *models.Remote) []string { + return slices.FlatMap(self.c.Model().Remotes, func(remote *models.Remote) []string { return slices.Map(remote.Branches, func(branch *models.RemoteBranch) string { return fmt.Sprintf("%s%s%s", remote.Name, separator, branch.Name) }) @@ -159,7 +152,7 @@ func (self *SuggestionsHelper) GetRemoteBranchesSuggestionsFunc(separator string } func (self *SuggestionsHelper) getTagNames() []string { - return slices.Map(self.model.Tags, func(tag *models.Tag) string { + return slices.Map(self.c.Model().Tags, func(tag *models.Tag) string { return tag.Name }) } @@ -176,7 +169,7 @@ func (self *SuggestionsHelper) GetRefsSuggestionsFunc() func(string) []*types.Su } func (self *SuggestionsHelper) GetAuthorsSuggestionsFunc() func(string) []*types.Suggestion { - authors := lo.Uniq(slices.Map(self.model.Commits, func(commit *models.Commit) string { + authors := lo.Uniq(slices.Map(self.c.Model().Commits, func(commit *models.Commit) string { return fmt.Sprintf("%s <%s>", commit.AuthorName, commit.AuthorEmail) })) diff --git a/pkg/gui/controllers/helpers/tags_helper.go b/pkg/gui/controllers/helpers/tags_helper.go index a2f2e9c77..d3cbf2331 100644 --- a/pkg/gui/controllers/helpers/tags_helper.go +++ b/pkg/gui/controllers/helpers/tags_helper.go @@ -1,7 +1,6 @@ package helpers import ( - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -10,14 +9,12 @@ import ( // and the commits context. type TagsHelper struct { - c *types.HelperCommon - git *commands.GitCommand + c *HelperCommon } -func NewTagsHelper(c *types.HelperCommon, git *commands.GitCommand) *TagsHelper { +func NewTagsHelper(c *HelperCommon) *TagsHelper { return &TagsHelper{ - c: c, - git: git, + c: c, } } @@ -56,7 +53,7 @@ func (self *TagsHelper) handleCreateAnnotatedTag(ref string, onCreate func()) er Title: self.c.Tr.TagMessageTitle, HandleConfirm: func(msg string) error { self.c.LogAction(self.c.Tr.Actions.CreateAnnotatedTag) - if err := self.git.Tag.CreateAnnotated(tagName, ref, msg); err != nil { + if err := self.c.Git().Tag.CreateAnnotated(tagName, ref, msg); err != nil { return self.c.Error(err) } return self.afterTagCreate(onCreate) @@ -71,7 +68,7 @@ func (self *TagsHelper) handleCreateLightweightTag(ref string, onCreate func()) Title: self.c.Tr.TagNameTitle, HandleConfirm: func(tagName string) error { self.c.LogAction(self.c.Tr.Actions.CreateLightweightTag) - if err := self.git.Tag.CreateLightweight(tagName, ref); err != nil { + if err := self.c.Git().Tag.CreateLightweight(tagName, ref); err != nil { return self.c.Error(err) } return self.afterTagCreate(onCreate) diff --git a/pkg/gui/controllers/helpers/update_helper.go b/pkg/gui/controllers/helpers/update_helper.go new file mode 100644 index 000000000..ea9be8f16 --- /dev/null +++ b/pkg/gui/controllers/helpers/update_helper.go @@ -0,0 +1,96 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/updates" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type UpdateHelper struct { + c *HelperCommon + updater *updates.Updater +} + +func NewUpdateHelper(c *HelperCommon, updater *updates.Updater) *UpdateHelper { + return &UpdateHelper{ + c: c, + updater: updater, + } +} + +func (self *UpdateHelper) CheckForUpdateInBackground() { + self.updater.CheckForNewUpdate(func(newVersion string, err error) error { + if err != nil { + // ignoring the error for now so that I'm not annoying users + self.c.Log.Error(err.Error()) + return nil + } + if newVersion == "" { + return nil + } + if self.c.UserConfig.Update.Method == "background" { + self.startUpdating(newVersion) + return nil + } + return self.showUpdatePrompt(newVersion) + }, false) +} + +func (self *UpdateHelper) CheckForUpdateInForeground() error { + return self.c.WithWaitingStatus(self.c.Tr.CheckingForUpdates, func() error { + self.updater.CheckForNewUpdate(func(newVersion string, err error) error { + if err != nil { + return self.c.Error(err) + } + if newVersion == "" { + return self.c.ErrorMsg(self.c.Tr.FailedToRetrieveLatestVersionErr) + } + return self.showUpdatePrompt(newVersion) + }, true) + + return nil + }) +} + +func (self *UpdateHelper) startUpdating(newVersion string) { + _ = self.c.WithWaitingStatus(self.c.Tr.UpdateInProgressWaitingStatus, func() error { + self.c.State().SetUpdating(true) + err := self.updater.Update(newVersion) + return self.onUpdateFinish(err) + }) +} + +func (self *UpdateHelper) onUpdateFinish(err error) error { + self.c.State().SetUpdating(false) + self.c.OnUIThread(func() error { + self.c.SetViewContent(self.c.Views().AppStatus, "") + if err != nil { + errMessage := utils.ResolvePlaceholderString( + self.c.Tr.UpdateFailedErr, map[string]string{ + "errMessage": err.Error(), + }, + ) + return self.c.ErrorMsg(errMessage) + } + return self.c.Alert(self.c.Tr.UpdateCompletedTitle, self.c.Tr.UpdateCompleted) + }) + + return nil +} + +func (self *UpdateHelper) showUpdatePrompt(newVersion string) error { + message := utils.ResolvePlaceholderString( + self.c.Tr.UpdateAvailable, map[string]string{ + "newVersion": newVersion, + }, + ) + + return self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.UpdateAvailableTitle, + Prompt: message, + HandleConfirm: func() error { + self.startUpdating(newVersion) + return nil + }, + }) +} diff --git a/pkg/gui/controllers/helpers/upstream_helper.go b/pkg/gui/controllers/helpers/upstream_helper.go index a2d8e8ae2..ea3187ed8 100644 --- a/pkg/gui/controllers/helpers/upstream_helper.go +++ b/pkg/gui/controllers/helpers/upstream_helper.go @@ -9,8 +9,7 @@ import ( ) type UpstreamHelper struct { - c *types.HelperCommon - model *types.Model + c *HelperCommon getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion } @@ -25,13 +24,11 @@ type IUpstreamHelper interface { var _ IUpstreamHelper = &UpstreamHelper{} func NewUpstreamHelper( - c *types.HelperCommon, - model *types.Model, + c *HelperCommon, getRemoteBranchesSuggestionsFunc func(string) func(string) []*types.Suggestion, ) *UpstreamHelper { return &UpstreamHelper{ c: c, - model: model, getRemoteBranchesSuggestionsFunc: getRemoteBranchesSuggestionsFunc, } } @@ -70,7 +67,7 @@ func (self *UpstreamHelper) PromptForUpstreamWithoutInitialContent(_ *models.Bra } func (self *UpstreamHelper) GetSuggestedRemote() string { - return getSuggestedRemote(self.model.Remotes) + return getSuggestedRemote(self.c.Model().Remotes) } func getSuggestedRemote(remotes []*models.Remote) string { diff --git a/pkg/gui/controllers/helpers/view_helper.go b/pkg/gui/controllers/helpers/view_helper.go new file mode 100644 index 000000000..c8d9ad94e --- /dev/null +++ b/pkg/gui/controllers/helpers/view_helper.go @@ -0,0 +1,31 @@ +package helpers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ViewHelper struct { + c *HelperCommon +} + +func NewViewHelper(c *HelperCommon, contexts *context.ContextTree) *ViewHelper { + return &ViewHelper{ + c: c, + } +} + +func (self *ViewHelper) ContextForView(viewName string) (types.Context, bool) { + view, err := self.c.GocuiGui().View(viewName) + if err != nil { + return nil, false + } + + for _, context := range self.c.Contexts().Flatten() { + if context.GetViewName() == view.Name() { + return context, true + } + } + + return nil, false +} diff --git a/pkg/gui/arrangement.go b/pkg/gui/controllers/helpers/window_arrangement_helper.go similarity index 64% rename from pkg/gui/arrangement.go rename to pkg/gui/controllers/helpers/window_arrangement_helper.go index ac6f2d9c1..8efec0a22 100644 --- a/pkg/gui/arrangement.go +++ b/pkg/gui/controllers/helpers/window_arrangement_helper.go @@ -1,7 +1,8 @@ -package gui +package helpers import ( "github.com/jesseduffield/lazycore/pkg/boxlayout" + "github.com/jesseduffield/lazygit/pkg/gui/constants" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -11,12 +12,33 @@ import ( // In this file we use the boxlayout package, along with knowledge about the app's state, // to arrange the windows (i.e. panels) on the screen. +type WindowArrangementHelper struct { + c *HelperCommon + windowHelper *WindowHelper + modeHelper *ModeHelper + appStatusHelper *AppStatusHelper +} + +func NewWindowArrangementHelper( + c *HelperCommon, + windowHelper *WindowHelper, + modeHelper *ModeHelper, + appStatusHelper *AppStatusHelper, +) *WindowArrangementHelper { + return &WindowArrangementHelper{ + c: c, + windowHelper: windowHelper, + modeHelper: modeHelper, + appStatusHelper: appStatusHelper, + } +} + const INFO_SECTION_PADDING = " " -func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { - width, height := gui.g.Size() +func (self *WindowArrangementHelper) GetWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + width, height := self.c.GocuiGui().Size() - sideSectionWeight, mainSectionWeight := gui.getMidSectionWeights() + sideSectionWeight, mainSectionWeight := self.getMidSectionWeights() sidePanelsDirection := boxlayout.COLUMN portraitMode := width <= 84 && height > 45 @@ -25,13 +47,18 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map } mainPanelsDirection := boxlayout.ROW - if gui.splitMainPanelSideBySide() { + if self.splitMainPanelSideBySide() { mainPanelsDirection = boxlayout.COLUMN } - extrasWindowSize := gui.getExtrasWindowSize(height) + extrasWindowSize := self.getExtrasWindowSize(height) - showInfoSection := gui.c.UserConfig.Gui.ShowBottomLine || gui.State.Searching.isSearching || gui.isAnyModeActive() || gui.statusManager.showStatus() + self.c.Modes().Filtering.Active() + + showInfoSection := self.c.UserConfig.Gui.ShowBottomLine || + self.c.State().GetRepoState().IsSearching() || + self.modeHelper.IsAnyModeActive() || + self.appStatusHelper.HasStatus() infoSectionSize := 0 if showInfoSection { infoSectionSize = 1 @@ -47,7 +74,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map { Direction: boxlayout.ROW, Weight: sideSectionWeight, - ConditionalChildren: gui.sidePanelChildren, + ConditionalChildren: self.sidePanelChildren, }, { Direction: boxlayout.ROW, @@ -55,7 +82,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map Children: []*boxlayout.Box{ { Direction: mainPanelsDirection, - Children: gui.mainSectionChildren(), + Children: self.mainSectionChildren(), Weight: 1, }, { @@ -69,7 +96,7 @@ func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map { Direction: boxlayout.COLUMN, Size: infoSectionSize, - Children: gui.infoSectionChildren(informationStr, appStatus), + Children: self.infoSectionChildren(informationStr, appStatus), }, }, } @@ -91,12 +118,12 @@ func MergeMaps[K comparable, V any](maps ...map[K]V) map[K]V { return result } -func (gui *Gui) mainSectionChildren() []*boxlayout.Box { - currentWindow := gui.currentWindow() +func (self *WindowArrangementHelper) mainSectionChildren() []*boxlayout.Box { + currentWindow := self.windowHelper.CurrentWindow() // if we're not in split mode we can just show the one main panel. Likewise if // the main panel is focused and we're in full-screen mode - if !gui.isMainPanelSplit() || (gui.State.ScreenMode == SCREEN_FULL && currentWindow == "main") { + if !self.c.State().GetRepoState().GetSplitMainPanel() || (self.c.State().GetRepoState().GetScreenMode() == types.SCREEN_FULL && currentWindow == "main") { return []*boxlayout.Box{ { Window: "main", @@ -117,27 +144,29 @@ func (gui *Gui) mainSectionChildren() []*boxlayout.Box { } } -func (gui *Gui) getMidSectionWeights() (int, int) { - currentWindow := gui.currentWindow() +func (self *WindowArrangementHelper) getMidSectionWeights() (int, int) { + currentWindow := self.windowHelper.CurrentWindow() // we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4 - sidePanelWidthRatio := gui.c.UserConfig.Gui.SidePanelWidth + sidePanelWidthRatio := self.c.UserConfig.Gui.SidePanelWidth // we could make this better by creating ratios like 2:3 rather than always 1:something mainSectionWeight := int(1/sidePanelWidthRatio) - 1 sideSectionWeight := 1 - if gui.splitMainPanelSideBySide() { + if self.splitMainPanelSideBySide() { mainSectionWeight = 5 // need to shrink side panel to make way for main panels if side-by-side } + screenMode := self.c.State().GetRepoState().GetScreenMode() + if currentWindow == "main" { - if gui.State.ScreenMode == SCREEN_HALF || gui.State.ScreenMode == SCREEN_FULL { + if screenMode == types.SCREEN_HALF || screenMode == types.SCREEN_FULL { sideSectionWeight = 0 } } else { - if gui.State.ScreenMode == SCREEN_HALF { + if screenMode == types.SCREEN_HALF { mainSectionWeight = 1 - } else if gui.State.ScreenMode == SCREEN_FULL { + } else if screenMode == types.SCREEN_FULL { mainSectionWeight = 0 } } @@ -145,12 +174,12 @@ func (gui *Gui) getMidSectionWeights() (int, int) { return sideSectionWeight, mainSectionWeight } -func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { - if gui.State.Searching.isSearching { +func (self *WindowArrangementHelper) infoSectionChildren(informationStr string, appStatus string) []*boxlayout.Box { + if self.c.State().GetRepoState().IsSearching() { return []*boxlayout.Box{ { Window: "searchPrefix", - Size: runewidth.StringWidth(SEARCH_PREFIX), + Size: runewidth.StringWidth(constants.SEARCH_PREFIX), }, { Window: "search", @@ -162,7 +191,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* appStatusBox := &boxlayout.Box{Window: "appStatus"} optionsBox := &boxlayout.Box{Window: "options"} - if !gui.c.UserConfig.Gui.ShowBottomLine { + if !self.c.UserConfig.Gui.ShowBottomLine { optionsBox.Weight = 0 appStatusBox.Weight = 1 } else { @@ -172,7 +201,7 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* result := []*boxlayout.Box{appStatusBox, optionsBox} - if gui.c.UserConfig.Gui.ShowBottomLine || gui.isAnyModeActive() { + if self.c.UserConfig.Gui.ShowBottomLine || self.modeHelper.IsAnyModeActive() { result = append(result, &boxlayout.Box{ Window: "information", // unlike appStatus, informationStr has various colors so we need to decolorise before taking the length @@ -183,13 +212,13 @@ func (gui *Gui) infoSectionChildren(informationStr string, appStatus string) []* return result } -func (gui *Gui) splitMainPanelSideBySide() bool { - if !gui.isMainPanelSplit() { +func (self *WindowArrangementHelper) splitMainPanelSideBySide() bool { + if !self.c.State().GetRepoState().GetSplitMainPanel() { return false } - mainPanelSplitMode := gui.c.UserConfig.Gui.MainPanelSplitMode - width, height := gui.g.Size() + mainPanelSplitMode := self.c.UserConfig.Gui.MainPanelSplitMode + width, height := self.c.GocuiGui().Size() switch mainPanelSplitMode { case "vertical": @@ -205,18 +234,18 @@ func (gui *Gui) splitMainPanelSideBySide() bool { } } -func (gui *Gui) getExtrasWindowSize(screenHeight int) int { - if !gui.ShowExtrasWindow { +func (self *WindowArrangementHelper) getExtrasWindowSize(screenHeight int) int { + if !self.c.State().GetShowExtrasWindow() { return 0 } var baseSize int - if gui.currentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { + if self.c.CurrentStaticContext().GetKey() == context.COMMAND_LOG_CONTEXT_KEY { baseSize = 1000 // my way of saying 'fill the available space' } else if screenHeight < 40 { baseSize = 1 } else { - baseSize = gui.c.UserConfig.Gui.CommandLogSize + baseSize = self.c.UserConfig.Gui.CommandLogSize } frameSize := 2 @@ -227,17 +256,15 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int { // too much space, but if you access it it should take up some space. This is // the default behaviour when accordion mode is NOT in effect. If it is in effect // then when it's accessed it will have weight 2, not 1. -func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - box := &boxlayout.Box{Window: "stash"} +func (self *WindowArrangementHelper) getDefaultStashWindowBox() *boxlayout.Box { stashWindowAccessed := false - for _, context := range gui.State.ContextManager.ContextStack { + self.c.Context().ForEach(func(context types.Context) { if context.GetWindowName() == "stash" { stashWindowAccessed = true } - } + }) + + box := &boxlayout.Box{Window: "stash"} // if the stash window is anywhere in our stack we should enlargen it if stashWindowAccessed { box.Weight = 1 @@ -248,10 +275,11 @@ func (gui *Gui) getDefaultStashWindowBox() *boxlayout.Box { return box } -func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { - currentWindow := gui.currentSideWindowName() +func (self *WindowArrangementHelper) sidePanelChildren(width int, height int) []*boxlayout.Box { + currentWindow := self.c.CurrentSideContext().GetWindowName() - if gui.State.ScreenMode == SCREEN_FULL || gui.State.ScreenMode == SCREEN_HALF { + screenMode := self.c.State().GetRepoState().GetScreenMode() + if screenMode == types.SCREEN_FULL || screenMode == types.SCREEN_HALF { fullHeightBox := func(window string) *boxlayout.Box { if window == currentWindow { return &boxlayout.Box{ @@ -274,7 +302,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { fullHeightBox("stash"), } } else if height >= 28 { - accordionMode := gui.c.UserConfig.Gui.ExpandFocusedSidePanel + accordionMode := self.c.UserConfig.Gui.ExpandFocusedSidePanel accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box { if accordionMode && defaultBox.Window == currentWindow { return &boxlayout.Box{ @@ -294,7 +322,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { accordionBox(&boxlayout.Box{Window: "files", Weight: 1}), accordionBox(&boxlayout.Box{Window: "branches", Weight: 1}), accordionBox(&boxlayout.Box{Window: "commits", Weight: 1}), - accordionBox(gui.getDefaultStashWindowBox()), + accordionBox(self.getDefaultStashWindowBox()), } } else { squashedHeight := 1 @@ -325,24 +353,3 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box { } } } - -func (gui *Gui) getCyclableWindows() []string { - return []string{"status", "files", "branches", "commits", "stash"} -} - -func (gui *Gui) currentSideWindowName() string { - // there is always one and only one cyclable context in the context stack. We'll look from top to bottom - gui.State.ContextManager.RLock() - defer gui.State.ContextManager.RUnlock() - - for idx := range gui.State.ContextManager.ContextStack { - reversedIdx := len(gui.State.ContextManager.ContextStack) - 1 - idx - context := gui.State.ContextManager.ContextStack[reversedIdx] - - if context.GetKind() == types.SIDE_CONTEXT { - return context.GetWindowName() - } - } - - return "files" // default -} diff --git a/pkg/gui/controllers/helpers/window_helper.go b/pkg/gui/controllers/helpers/window_helper.go new file mode 100644 index 000000000..efe84c398 --- /dev/null +++ b/pkg/gui/controllers/helpers/window_helper.go @@ -0,0 +1,139 @@ +package helpers + +import ( + "fmt" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/samber/lo" +) + +type WindowHelper struct { + c *HelperCommon + viewHelper *ViewHelper +} + +func NewWindowHelper(c *HelperCommon, viewHelper *ViewHelper) *WindowHelper { + return &WindowHelper{ + c: c, + viewHelper: viewHelper, + } +} + +// A window refers to a place on the screen which can hold one or more views. +// A view is a box that renders content, and within a window only one view will +// appear at a time. When a view appears within a window, it occupies the whole +// space. Right now most windows are 1:1 with views, except for commitFiles which +// is a view that moves between windows + +func (self *WindowHelper) GetViewNameForWindow(window string) string { + viewName, ok := self.windowViewNameMap().Get(window) + if !ok { + panic(fmt.Sprintf("Viewname not found for window: %s", window)) + } + + return viewName +} + +func (self *WindowHelper) GetContextForWindow(window string) types.Context { + viewName := self.GetViewNameForWindow(window) + + context, ok := self.viewHelper.ContextForView(viewName) + if !ok { + panic("TODO: fix this") + } + + return context +} + +// for now all we actually care about is the context's view so we're storing that +func (self *WindowHelper) SetWindowContext(c types.Context) { + if c.IsTransient() { + self.resetWindowContext(c) + } + + self.windowViewNameMap().Set(c.GetWindowName(), c.GetViewName()) +} + +func (self *WindowHelper) windowViewNameMap() *utils.ThreadSafeMap[string, string] { + return self.c.State().GetRepoState().GetWindowViewNameMap() +} + +func (self *WindowHelper) CurrentWindow() string { + return self.c.CurrentContext().GetWindowName() +} + +// assumes the context's windowName has been set to the new window if necessary +func (self *WindowHelper) resetWindowContext(c types.Context) { + for _, windowName := range self.windowViewNameMap().Keys() { + viewName, ok := self.windowViewNameMap().Get(windowName) + if !ok { + continue + } + if viewName == c.GetViewName() && windowName != c.GetWindowName() { + for _, context := range self.c.Contexts().Flatten() { + if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName { + self.windowViewNameMap().Set(windowName, context.GetViewName()) + } + } + } + } +} + +// moves given context's view to the top of the window +func (self *WindowHelper) MoveToTopOfWindow(context types.Context) { + view := context.GetView() + if view == nil { + return + } + + window := context.GetWindowName() + + topView := self.TopViewInWindow(window) + + if view.Name() != topView.Name() { + if err := self.c.GocuiGui().SetViewOnTopOf(view.Name(), topView.Name()); err != nil { + self.c.Log.Error(err) + } + } +} + +func (self *WindowHelper) TopViewInWindow(windowName string) *gocui.View { + // now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list. + viewNamesInWindow := self.viewNamesInWindow(windowName) + + // The views list is ordered highest-last, so we're grabbing the last view of the window + var topView *gocui.View + for _, currentView := range self.c.GocuiGui().Views() { + if lo.Contains(viewNamesInWindow, currentView.Name()) { + topView = currentView + } + } + + return topView +} + +func (self *WindowHelper) viewNamesInWindow(windowName string) []string { + result := []string{} + for _, context := range self.c.Contexts().Flatten() { + if context.GetWindowName() == windowName { + result = append(result, context.GetViewName()) + } + } + + return result +} + +func (self *WindowHelper) WindowForView(viewName string) string { + context, ok := self.viewHelper.ContextForView(viewName) + if !ok { + panic("todo: deal with this") + } + + return context.GetWindowName() +} + +func (self *WindowHelper) SideWindows() []string { + return []string{"status", "files", "branches", "commits", "stash"} +} diff --git a/pkg/gui/controllers/helpers/working_tree_helper.go b/pkg/gui/controllers/helpers/working_tree_helper.go index 6fa85bc35..2679310f9 100644 --- a/pkg/gui/controllers/helpers/working_tree_helper.go +++ b/pkg/gui/controllers/helpers/working_tree_helper.go @@ -4,7 +4,6 @@ import ( "fmt" "regexp" - "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -20,40 +19,28 @@ type IWorkingTreeHelper interface { } type WorkingTreeHelper struct { - c *types.HelperCommon - git *commands.GitCommand - contexts *context.ContextTree - refHelper *RefsHelper - model *types.Model - setCommitMessage func(message string) - commitsHelper *CommitsHelper - gpgHelper *GpgHelper + c *HelperCommon + refHelper *RefsHelper + commitsHelper *CommitsHelper + gpgHelper *GpgHelper } func NewWorkingTreeHelper( - c *types.HelperCommon, - git *commands.GitCommand, - contexts *context.ContextTree, + c *HelperCommon, refHelper *RefsHelper, - model *types.Model, - setCommitMessage func(message string), commitsHelper *CommitsHelper, gpgHelper *GpgHelper, ) *WorkingTreeHelper { return &WorkingTreeHelper{ - c: c, - git: git, - contexts: contexts, - refHelper: refHelper, - model: model, - setCommitMessage: setCommitMessage, - commitsHelper: commitsHelper, - gpgHelper: gpgHelper, + c: c, + refHelper: refHelper, + commitsHelper: commitsHelper, + gpgHelper: gpgHelper, } } func (self *WorkingTreeHelper) AnyStagedFiles() bool { - for _, file := range self.model.Files { + for _, file := range self.c.Model().Files { if file.HasStagedChanges { return true } @@ -62,7 +49,7 @@ func (self *WorkingTreeHelper) AnyStagedFiles() bool { } func (self *WorkingTreeHelper) AnyTrackedFiles() bool { - for _, file := range self.model.Files { + for _, file := range self.c.Model().Files { if file.Tracked { return true } @@ -75,7 +62,7 @@ func (self *WorkingTreeHelper) IsWorkingTreeDirty() bool { } func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfig) *models.File { - for _, file := range self.model.Files { + for _, file := range self.c.Model().Files { if file.IsSubmodule([]*models.SubmoduleConfig{submodule}) { return file } @@ -91,7 +78,7 @@ func (self *WorkingTreeHelper) OpenMergeTool() error { HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.OpenMergeTool) return self.c.RunSubprocessAndRefresh( - self.git.WorkingTree.OpenMergeToolCmdObj(), + self.c.Git().WorkingTree.OpenMergeToolCmdObj(), ) }, }) @@ -102,7 +89,7 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin return self.c.Error(err) } - if len(self.model.Files) == 0 { + if len(self.c.Model().Files) == 0 { return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) } @@ -122,7 +109,7 @@ func (self *WorkingTreeHelper) HandleCommitPressWithMessage(initialMessage strin } func (self *WorkingTreeHelper) handleCommit(message string) error { - cmdObj := self.git.Commit.CommitCmdObj(message) + cmdObj := self.c.Git().Commit.CommitCmdObj(message) self.c.LogAction(self.c.Tr.Actions.Commit) _ = self.commitsHelper.PopCommitMessageContexts() return self.gpgHelper.WithGpgHandling(cmdObj, self.c.Tr.CommittingStatus, func() error { @@ -134,7 +121,7 @@ func (self *WorkingTreeHelper) handleCommit(message string) error { // HandleCommitEditorPress - handle when the user wants to commit changes via // their editor rather than via the popup panel func (self *WorkingTreeHelper) HandleCommitEditorPress() error { - if len(self.model.Files) == 0 { + if len(self.c.Model().Files) == 0 { return self.c.ErrorMsg(self.c.Tr.NoFilesStagedTitle) } @@ -144,7 +131,7 @@ func (self *WorkingTreeHelper) HandleCommitEditorPress() error { self.c.LogAction(self.c.Tr.Actions.Commit) return self.c.RunSubprocessAndRefresh( - self.git.Commit.CommitEditorCmdObj(), + self.c.Git().Commit.CommitEditorCmdObj(), ) } @@ -158,7 +145,7 @@ func (self *WorkingTreeHelper) HandleWIPCommitPress() error { } func (self *WorkingTreeHelper) HandleCommitPress() error { - message := self.contexts.CommitMessage.GetPreservedMessage() + message := self.c.Contexts().CommitMessage.GetPreservedMessage() if message != "" { commitPrefixConfig := self.commitPrefixConfigForRepo() @@ -183,7 +170,7 @@ func (self *WorkingTreeHelper) PromptToStageAllAndRetry(retry func() error) erro Prompt: self.c.Tr.NoFilesStagedPrompt, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.StageAllFiles) - if err := self.git.WorkingTree.StageAll(); err != nil { + if err := self.c.Git().WorkingTree.StageAll(); err != nil { return self.c.Error(err) } if err := self.syncRefresh(); err != nil { @@ -204,7 +191,7 @@ func (self *WorkingTreeHelper) prepareFilesForCommit() error { noStagedFiles := !self.AnyStagedFiles() if noStagedFiles && self.c.UserConfig.Gui.SkipNoStagedFilesWarning { self.c.LogAction(self.c.Tr.Actions.StageAllFiles) - err := self.git.WorkingTree.StageAll() + err := self.c.Git().WorkingTree.StageAll() if err != nil { return err } diff --git a/pkg/gui/controllers/jump_to_side_window_controller.go b/pkg/gui/controllers/jump_to_side_window_controller.go new file mode 100644 index 000000000..7ac407ab4 --- /dev/null +++ b/pkg/gui/controllers/jump_to_side_window_controller.go @@ -0,0 +1,53 @@ +package controllers + +import ( + "log" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +type JumpToSideWindowController struct { + baseController + c *ControllerCommon +} + +func NewJumpToSideWindowController( + common *ControllerCommon, +) *JumpToSideWindowController { + return &JumpToSideWindowController{ + baseController: baseController{}, + c: common, + } +} + +func (self *JumpToSideWindowController) Context() types.Context { + return nil +} + +func (self *JumpToSideWindowController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + windows := self.c.Helpers().Window.SideWindows() + + if len(opts.Config.Universal.JumpToBlock) != len(windows) { + log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.") + } + + return lo.Map(windows, func(window string, index int) *types.Binding { + return &types.Binding{ + ViewName: "", + // by default the keys are 1, 2, 3, etc + Key: opts.GetKey(opts.Config.Universal.JumpToBlock[index]), + Modifier: gocui.ModNone, + Handler: self.goToSideWindow(window), + } + }) +} + +func (self *JumpToSideWindowController) goToSideWindow(window string) func() error { + return func() error { + context := self.c.Helpers().Window.GetContextForWindow(window) + + return self.c.PushContext(context) + } +} diff --git a/pkg/gui/controllers/list_controller.go b/pkg/gui/controllers/list_controller.go index c74d87244..0a6de821a 100644 --- a/pkg/gui/controllers/list_controller.go +++ b/pkg/gui/controllers/list_controller.go @@ -6,10 +6,10 @@ import ( ) type ListControllerFactory struct { - c *types.HelperCommon + c *ControllerCommon } -func NewListControllerFactory(c *types.HelperCommon) *ListControllerFactory { +func NewListControllerFactory(c *ControllerCommon) *ListControllerFactory { return &ListControllerFactory{ c: c, } @@ -25,7 +25,7 @@ func (self *ListControllerFactory) Create(context types.IListContext) *ListContr type ListController struct { baseController - c *types.HelperCommon + c *ControllerCommon context types.IListContext } diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 3d0e6bc5c..7ac6e8671 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -13,26 +13,30 @@ import ( "github.com/samber/lo" ) +// after selecting the 200th commit, we'll load in all the rest +const COMMIT_THRESHOLD = 200 + type ( PullFilesFn func() error ) type LocalCommitsController struct { baseController - *controllerCommon + c *ControllerCommon + pullFiles PullFilesFn } var _ types.IController = &LocalCommitsController{} func NewLocalCommitsController( - common *controllerCommon, + common *ControllerCommon, pullFiles PullFilesFn, ) *LocalCommitsController { return &LocalCommitsController{ - baseController: baseController{}, - controllerCommon: common, - pullFiles: pullFiles, + baseController: baseController{}, + c: common, + pullFiles: pullFiles, } } @@ -150,8 +154,52 @@ func (self *LocalCommitsController) GetKeybindings(opts types.KeybindingsOpts) [ return bindings } +func (self *LocalCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + commit := self.context().GetSelected() + if commit == nil { + task = types.NewRenderStringTask(self.c.Tr.NoCommitsThisBranch) + } else if commit.Action == todo.UpdateRef { + task = types.NewRenderStringTask( + utils.ResolvePlaceholderString( + self.c.Tr.UpdateRefHere, + map[string]string{ + "ref": commit.Name, + })) + } else { + cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Patch", + Task: task, + }, + Secondary: secondaryPatchPanelUpdateOpts(self.c), + }) + }) + } +} + +func secondaryPatchPanelUpdateOpts(c *ControllerCommon) *types.ViewUpdateOpts { + if c.Git().Patch.PatchBuilder.Active() { + patch := c.Git().Patch.PatchBuilder.RenderAggregatedPatch(false) + + return &types.ViewUpdateOpts{ + Task: types.NewRenderStringWithoutScrollTask(patch), + Title: c.Tr.CustomPatch, + } + } + + return nil +} + func (self *LocalCommitsController) squashDown(commit *models.Commit) error { - if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { + if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) } @@ -176,7 +224,7 @@ func (self *LocalCommitsController) squashDown(commit *models.Commit) error { } func (self *LocalCommitsController) fixup(commit *models.Commit) error { - if self.context().GetSelectedLineIdx() >= len(self.model.Commits)-1 { + if self.context().GetSelectedLineIdx() >= len(self.c.Model().Commits)-1 { return self.c.ErrorMsg(self.c.Tr.CannotSquashOrFixupFirstCommit) } @@ -209,12 +257,12 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error { return nil } - commitMessage, err := self.git.Commit.GetCommitMessage(commit.Sha) + commitMessage, err := self.c.Git().Commit.GetCommitMessage(commit.Sha) if err != nil { return self.c.Error(err) } - return self.helpers.Commits.OpenCommitMessagePanel( + return self.c.Helpers().Commits.OpenCommitMessagePanel( &helpers.OpenCommitMessagePanelOpts{ CommitIndex: self.context().GetSelectedLineIdx(), InitialMessage: commitMessage, @@ -226,12 +274,12 @@ func (self *LocalCommitsController) reword(commit *models.Commit) error { } func (self *LocalCommitsController) handleReword(message string) error { - err := self.git.Rebase.RewordCommit(self.model.Commits, self.contexts.LocalCommits.GetSelectedLineIdx(), message) + err := self.c.Git().Rebase.RewordCommit(self.c.Model().Commits, self.c.Contexts().LocalCommits.GetSelectedLineIdx(), message) if err != nil { return self.c.Error(err) } - self.helpers.Commits.OnCommitSuccess() - _ = self.helpers.Commits.PopCommitMessageContexts() + self.c.Helpers().Commits.OnCommitSuccess() + _ = self.c.Helpers().Commits.PopCommitMessageContexts() return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } @@ -239,11 +287,11 @@ func (self *LocalCommitsController) doRewordEditor() error { self.c.LogAction(self.c.Tr.Actions.RewordCommit) if self.isHeadCommit() { - return self.c.RunSubprocessAndRefresh(self.os.Cmd.New("git commit --allow-empty --amend --only")) + return self.c.RunSubprocessAndRefresh(self.c.OS().Cmd.New("git commit --allow-empty --amend --only")) } - subProcess, err := self.git.Rebase.RewordCommitInEditor( - self.model.Commits, self.context().GetSelectedLineIdx(), + subProcess, err := self.c.Git().Rebase.RewordCommitInEditor( + self.c.Model().Commits, self.context().GetSelectedLineIdx(), ) if err != nil { return self.c.Error(err) @@ -307,8 +355,8 @@ func (self *LocalCommitsController) edit(commit *models.Commit) error { return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.EditCommit) - err := self.git.Rebase.InteractiveRebaseBreakAfter(self.model.Commits, self.context().GetSelectedLineIdx()) - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + err := self.c.Git().Rebase.InteractiveRebaseBreakAfter(self.c.Model().Commits, self.context().GetSelectedLineIdx()) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) }) } @@ -327,8 +375,8 @@ func (self *LocalCommitsController) pick(commit *models.Commit) error { } func (self *LocalCommitsController) interactiveRebase(action string) error { - err := self.git.Rebase.InteractiveRebase(self.model.Commits, self.context().GetSelectedLineIdx(), action) - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + err := self.c.Git().Rebase.InteractiveRebase(self.c.Model().Commits, self.context().GetSelectedLineIdx(), action) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) } // handleMidRebaseCommand sees if the selected commit is in fact a rebasing @@ -336,7 +384,7 @@ func (self *LocalCommitsController) interactiveRebase(action string) error { // begin a rebase. It then updates the todo file with that action func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoCommand, commit *models.Commit) (bool, error) { if !commit.IsTODO() { - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { // If we are in a rebase, the only action that is allowed for // non-todo commits is rewording the current head commit if !(action == todo.Reword && self.isHeadCommit()) { @@ -365,7 +413,7 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma false, ) - if err := self.git.Rebase.EditRebaseTodo(commit, action); err != nil { + if err := self.c.Git().Rebase.EditRebaseTodo(commit, action); err != nil { return false, self.c.Error(err) } @@ -376,7 +424,7 @@ func (self *LocalCommitsController) handleMidRebaseCommand(action todo.TodoComma func (self *LocalCommitsController) moveDown(commit *models.Commit) error { index := self.context().GetSelectedLineIdx() - commits := self.model.Commits + commits := self.c.Model().Commits // can't move past the initial commit if index >= len(commits)-1 { @@ -393,7 +441,7 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error { self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) self.c.LogCommand(fmt.Sprintf("Moving commit %s down", commit.ShortSha()), false) - if err := self.git.Rebase.MoveTodoDown(commit); err != nil { + if err := self.c.Git().Rebase.MoveTodoDown(commit); err != nil { return self.c.Error(err) } self.context().MoveSelectedLine(1) @@ -402,17 +450,17 @@ func (self *LocalCommitsController) moveDown(commit *models.Commit) error { }) } - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.MoveCommitDown) - err := self.git.Rebase.MoveCommitDown(self.model.Commits, index) + err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index) if err == nil { self.context().MoveSelectedLine(1) } - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) }) } @@ -431,7 +479,7 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error { false, ) - if err := self.git.Rebase.MoveTodoUp(self.model.Commits[index]); err != nil { + if err := self.c.Git().Rebase.MoveTodoUp(self.c.Model().Commits[index]); err != nil { return self.c.Error(err) } self.context().MoveSelectedLine(-1) @@ -440,29 +488,29 @@ func (self *LocalCommitsController) moveUp(commit *models.Commit) error { }) } - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } return self.c.WithWaitingStatus(self.c.Tr.MovingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.MoveCommitUp) - err := self.git.Rebase.MoveCommitDown(self.model.Commits, index-1) + err := self.c.Git().Rebase.MoveCommitDown(self.c.Model().Commits, index-1) if err == nil { self.context().MoveSelectedLine(-1) } - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) }) } func (self *LocalCommitsController) amendTo(commit *models.Commit) error { if self.isHeadCommit() { - if err := self.helpers.AmendHelper.AmendHead(); err != nil { + if err := self.c.Helpers().AmendHelper.AmendHead(); err != nil { return err } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) } - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } @@ -472,15 +520,15 @@ func (self *LocalCommitsController) amendTo(commit *models.Commit) error { HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.AmendCommit) - err := self.git.Rebase.AmendTo(commit) - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + err := self.c.Git().Rebase.AmendTo(commit) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) }) }, }) } func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error { - if self.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() { + if self.c.Git().Status.WorkingTreeState() != enums.REBASE_MODE_NONE && !self.isHeadCommit() { return self.c.ErrorMsg(self.c.Tr.AlreadyRebasing) } @@ -506,7 +554,7 @@ func (self *LocalCommitsController) amendAttribute(commit *models.Commit) error func (self *LocalCommitsController) resetAuthor() error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.ResetCommitAuthor) - if err := self.git.Rebase.ResetCommitAuthor(self.model.Commits, self.context().GetSelectedLineIdx()); err != nil { + if err := self.c.Git().Rebase.ResetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx()); err != nil { return self.c.Error(err) } @@ -517,11 +565,11 @@ func (self *LocalCommitsController) resetAuthor() error { func (self *LocalCommitsController) setAuthor() error { return self.c.Prompt(types.PromptOpts{ Title: self.c.Tr.SetAuthorPromptTitle, - FindSuggestionsFunc: self.helpers.Suggestions.GetAuthorsSuggestionsFunc(), + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetAuthorsSuggestionsFunc(), HandleConfirm: func(value string) error { return self.c.WithWaitingStatus(self.c.Tr.AmendingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.SetCommitAuthor) - if err := self.git.Rebase.SetCommitAuthor(self.model.Commits, self.context().GetSelectedLineIdx(), value); err != nil { + if err := self.c.Git().Rebase.SetCommitAuthor(self.c.Model().Commits, self.context().GetSelectedLineIdx(), value); err != nil { return self.c.Error(err) } @@ -544,7 +592,7 @@ func (self *LocalCommitsController) revert(commit *models.Commit) error { }), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.RevertCommit) - if err := self.git.Commit.Revert(commit.Sha); err != nil { + if err := self.c.Git().Commit.Revert(commit.Sha); err != nil { return self.c.Error(err) } return self.afterRevertCommit() @@ -557,7 +605,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C menuItems := make([]*types.MenuItem, len(commit.Parents)) for i, parentSha := range commit.Parents { i := i - message, err := self.git.Commit.GetCommitMessageFirstLine(parentSha) + message, err := self.c.Git().Commit.GetCommitMessageFirstLine(parentSha) if err != nil { return self.c.Error(err) } @@ -567,7 +615,7 @@ func (self *LocalCommitsController) createRevertMergeCommitMenu(commit *models.C OnPress: func() error { parentNumber := i + 1 self.c.LogAction(self.c.Tr.Actions.RevertCommit) - if err := self.git.Commit.RevertMerge(commit.Sha, parentNumber); err != nil { + if err := self.c.Git().Commit.RevertMerge(commit.Sha, parentNumber); err != nil { return self.c.Error(err) } return self.afterRevertCommit() @@ -598,7 +646,7 @@ func (self *LocalCommitsController) createFixupCommit(commit *models.Commit) err Prompt: prompt, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.CreateFixupCommit) - if err := self.git.Commit.CreateFixupCommit(commit.Sha); err != nil { + if err := self.c.Git().Commit.CreateFixupCommit(commit.Sha); err != nil { return self.c.Error(err) } @@ -619,15 +667,15 @@ func (self *LocalCommitsController) squashAllAboveFixupCommits(commit *models.Co HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.SquashingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.SquashAllAboveFixupCommits) - err := self.git.Rebase.SquashAllAboveFixupCommits(commit) - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + err := self.c.Git().Rebase.SquashAllAboveFixupCommits(commit) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) }) }, }) } func (self *LocalCommitsController) createTag(commit *models.Commit) error { - return self.helpers.Tags.CreateTagMenu(commit.Sha, func() {}) + return self.c.Helpers().Tags.CreateTagMenu(commit.Sha, func() {}) } func (self *LocalCommitsController) openSearch() error { @@ -759,20 +807,36 @@ func (self *LocalCommitsController) checkSelected(callback func(*models.Commit) } } +func (self *LocalCommitsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + context := self.context() + if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { + context.SetLimitCommits(false) + go utils.Safe(func() { + if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}); err != nil { + _ = self.c.Error(err) + } + }) + } + + return nil + } +} + func (self *LocalCommitsController) Context() types.Context { return self.context() } func (self *LocalCommitsController) context() *context.LocalCommitsContext { - return self.contexts.LocalCommits + return self.c.Contexts().LocalCommits } func (self *LocalCommitsController) paste() error { - return self.helpers.CherryPick.Paste() + return self.c.Helpers().CherryPick.Paste() } func (self *LocalCommitsController) isHeadCommit() bool { - return models.IsHeadCommit(self.model.Commits, self.context().GetSelectedLineIdx()) + return models.IsHeadCommit(self.c.Model().Commits, self.context().GetSelectedLineIdx()) } func isChangeOfRebaseTodoAllowed(action todo.TodoCommand) bool { diff --git a/pkg/gui/controllers/menu_controller.go b/pkg/gui/controllers/menu_controller.go index 6700fa99d..b687778ac 100644 --- a/pkg/gui/controllers/menu_controller.go +++ b/pkg/gui/controllers/menu_controller.go @@ -7,17 +7,17 @@ import ( type MenuController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &MenuController{} func NewMenuController( - common *controllerCommon, + common *ControllerCommon, ) *MenuController { return &MenuController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -28,12 +28,16 @@ func (self *MenuController) GetKeybindings(opts types.KeybindingsOpts) []*types. Handler: self.press, }, { - Key: opts.GetKey(opts.Config.Universal.Confirm), - Handler: self.press, + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: self.press, + Description: self.c.Tr.LcExecute, + Display: true, }, { - Key: opts.GetKey(opts.Config.Universal.Return), - Handler: self.close, + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: self.close, + Description: self.c.Tr.LcClose, + Display: true, }, } @@ -44,6 +48,14 @@ func (self *MenuController) GetOnClick() func() error { return self.press } +func (self *MenuController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + selectedMenuItem := self.context().GetSelected() + self.c.Views().Tooltip.SetContent(selectedMenuItem.Tooltip) + return nil + } +} + func (self *MenuController) press() error { return self.context().OnMenuPress(self.context().GetSelected()) } @@ -57,5 +69,5 @@ func (self *MenuController) Context() types.Context { } func (self *MenuController) context() *context.MenuContext { - return self.contexts.Menu + return self.c.Contexts().Menu } diff --git a/pkg/gui/controllers/merge_conflicts_controller.go b/pkg/gui/controllers/merge_conflicts_controller.go index de282d4c9..2d85040de 100644 --- a/pkg/gui/controllers/merge_conflicts_controller.go +++ b/pkg/gui/controllers/merge_conflicts_controller.go @@ -11,17 +11,17 @@ import ( type MergeConflictsController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &MergeConflictsController{} func NewMergeConflictsController( - common *controllerCommon, + common *ControllerCommon, ) *MergeConflictsController { return &MergeConflictsController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -41,21 +41,25 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.PrevBlock), Handler: self.withRenderAndFocus(self.PrevConflict), Description: self.c.Tr.PrevConflict, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.NextBlock), Handler: self.withRenderAndFocus(self.NextConflict), Description: self.c.Tr.NextConflict, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.PrevItem), Handler: self.withRenderAndFocus(self.PrevConflictHunk), Description: self.c.Tr.SelectPrevHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.NextItem), Handler: self.withRenderAndFocus(self.NextConflictHunk), Description: self.c.Tr.SelectNextHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt), @@ -89,21 +93,24 @@ func (self *MergeConflictsController) GetKeybindings(opts types.KeybindingsOpts) Key: opts.GetKey(opts.Config.Universal.Undo), Handler: self.withRenderAndFocus(self.HandleUndo), Description: self.c.Tr.LcUndo, + Display: true, }, { Key: opts.GetKey(opts.Config.Files.OpenMergeTool), - Handler: self.helpers.WorkingTree.OpenMergeTool, + Handler: self.c.Helpers().WorkingTree.OpenMergeTool, Description: self.c.Tr.LcOpenMergeTool, }, { Key: opts.GetKey(opts.Config.Universal.Select), Handler: self.withRenderAndFocus(self.HandlePickHunk), Description: self.c.Tr.PickHunk, + Display: true, }, { Key: opts.GetKey(opts.Config.Main.PickBothHunks), Handler: self.withRenderAndFocus(self.HandlePickAllHunks), Description: self.c.Tr.PickAllHunks, + Display: true, }, { Key: opts.GetKey(opts.Config.Universal.Return), @@ -134,6 +141,24 @@ func (self *MergeConflictsController) GetMouseKeybindings(opts types.Keybindings } } +func (self *MergeConflictsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + self.c.Views().MergeConflicts.Wrap = false + + return self.c.Helpers().MergeConflicts.Render(true) + } +} + +func (self *MergeConflictsController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.context().SetUserScrolling(false) + self.context().GetState().ResetConflictSelection() + self.c.Views().MergeConflicts.Wrap = true + + return nil + } +} + func (self *MergeConflictsController) HandleScrollUp() error { self.context().SetUserScrolling(true) self.context().GetViewTrait().ScrollUp(self.c.UserConfig.Gui.ScrollHeight) @@ -153,7 +178,7 @@ func (self *MergeConflictsController) Context() types.Context { } func (self *MergeConflictsController) context() *context.MergeConflictsContext { - return self.contexts.MergeConflicts + return self.c.Contexts().MergeConflicts } func (self *MergeConflictsController) Escape() error { @@ -162,11 +187,11 @@ func (self *MergeConflictsController) Escape() error { func (self *MergeConflictsController) HandleEditFile() error { lineNumber := self.context().GetState().GetSelectedLine() - return self.helpers.Files.EditFileAtLine(self.context().GetState().GetPath(), lineNumber) + return self.c.Helpers().Files.EditFileAtLine(self.context().GetState().GetPath(), lineNumber) } func (self *MergeConflictsController) HandleOpenFile() error { - return self.helpers.Files.OpenFile(self.context().GetState().GetPath()) + return self.c.Helpers().Files.OpenFile(self.context().GetState().GetPath()) } func (self *MergeConflictsController) HandleScrollLeft() error { diff --git a/pkg/gui/options_menu_panel.go b/pkg/gui/controllers/options_menu_action.go similarity index 80% rename from pkg/gui/options_menu_panel.go rename to pkg/gui/controllers/options_menu_action.go index b7b13698f..0e747a7ad 100644 --- a/pkg/gui/options_menu_panel.go +++ b/pkg/gui/controllers/options_menu_action.go @@ -1,23 +1,52 @@ -package gui +package controllers import ( - "log" - "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" ) -func (gui *Gui) getBindings(context types.Context) []*types.Binding { +type OptionsMenuAction struct { + c *ControllerCommon +} + +func (self *OptionsMenuAction) Call() error { + ctx := self.c.CurrentContext() + // Don't show menu while displaying popup. + if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP { + return nil + } + + bindings := self.getBindings(ctx) + + menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem { + return &types.MenuItem{ + OpensMenu: binding.OpensMenu, + Label: binding.Description, + OnPress: func() error { + if binding.Handler == nil { + return nil + } + + return binding.Handler() + }, + Key: binding.Key, + Tooltip: binding.Tooltip, + } + }) + + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.MenuTitle, + Items: menuItems, + HideCancel: true, + }) +} + +func (self *OptionsMenuAction) getBindings(context types.Context) []*types.Binding { var bindingsGlobal, bindingsPanel, bindingsNavigation []*types.Binding - bindings, _ := gui.GetInitialKeybindings() - customBindings, err := gui.CustomCommandsClient.GetCustomCommandKeybindings() - if err != nil { - log.Fatal(err) - } - bindings = append(customBindings, bindings...) + bindings, _ := self.c.GetInitialKeybindingsWithCustomCommands() for _, binding := range bindings { if keybindings.LabelFromKey(binding.Key) != "" && binding.Description != "" { @@ -48,35 +77,3 @@ func uniqueBindings(bindings []*types.Binding) []*types.Binding { return binding.Description }) } - -func (gui *Gui) handleCreateOptionsMenu() error { - ctx := gui.currentContext() - // Don't show menu while displaying popup. - if ctx.GetKind() == types.PERSISTENT_POPUP || ctx.GetKind() == types.TEMPORARY_POPUP { - return nil - } - - bindings := gui.getBindings(ctx) - - menuItems := slices.Map(bindings, func(binding *types.Binding) *types.MenuItem { - return &types.MenuItem{ - OpensMenu: binding.OpensMenu, - Label: binding.Description, - OnPress: func() error { - if binding.Handler == nil { - return nil - } - - return binding.Handler() - }, - Key: binding.Key, - Tooltip: binding.Tooltip, - } - }) - - return gui.c.Menu(types.CreateMenuOptions{ - Title: gui.c.Tr.MenuTitle, - Items: menuItems, - HideCancel: true, - }) -} diff --git a/pkg/gui/controllers/patch_building_controller.go b/pkg/gui/controllers/patch_building_controller.go index 329ae0ea1..4f2bd44cb 100644 --- a/pkg/gui/controllers/patch_building_controller.go +++ b/pkg/gui/controllers/patch_building_controller.go @@ -8,17 +8,17 @@ import ( type PatchBuildingController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &PatchBuildingController{} func NewPatchBuildingController( - common *controllerCommon, + common *ControllerCommon, ) *PatchBuildingController { return &PatchBuildingController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -48,42 +48,63 @@ func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts) } func (self *PatchBuildingController) Context() types.Context { - return self.contexts.CustomPatchBuilder + return self.c.Contexts().CustomPatchBuilder } func (self *PatchBuildingController) context() types.IPatchExplorerContext { - return self.contexts.CustomPatchBuilder + return self.c.Contexts().CustomPatchBuilder } func (self *PatchBuildingController) GetMouseKeybindings(opts types.KeybindingsOpts) []*gocui.ViewMouseBinding { return []*gocui.ViewMouseBinding{} } +func (self *PatchBuildingController) GetOnFocus() func(types.OnFocusOpts) error { + return func(opts types.OnFocusOpts) error { + // no need to change wrap on the secondary view because it can't be interacted with + self.c.Views().PatchBuilding.Wrap = false + + return self.c.Helpers().PatchBuilding.RefreshPatchBuildingPanel(opts) + } +} + +func (self *PatchBuildingController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(opts types.OnFocusLostOpts) error { + self.c.Views().PatchBuilding.Wrap = true + + if self.c.Git().Patch.PatchBuilder.IsEmpty() { + self.c.Git().Patch.PatchBuilder.Reset() + } + + return nil + } +} + func (self *PatchBuildingController) OpenFile() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() - path := self.contexts.CommitFiles.GetSelectedPath() + path := self.c.Contexts().CommitFiles.GetSelectedPath() if path == "" { return nil } - return self.helpers.Files.OpenFile(path) + return self.c.Helpers().Files.OpenFile(path) } func (self *PatchBuildingController) EditFile() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() - path := self.contexts.CommitFiles.GetSelectedPath() + path := self.c.Contexts().CommitFiles.GetSelectedPath() if path == "" { return nil } lineNumber := self.context().GetState().CurrentLineNumber() - return self.helpers.Files.EditFileAtLine(path, lineNumber) + return self.c.Helpers().Files.EditFileAtLine(path, lineNumber) } func (self *PatchBuildingController) ToggleSelectionAndRefresh() error { @@ -100,21 +121,21 @@ func (self *PatchBuildingController) toggleSelection() error { self.context().GetMutex().Lock() defer self.context().GetMutex().Unlock() - toggleFunc := self.git.Patch.PatchBuilder.AddFileLineRange - filename := self.contexts.CommitFiles.GetSelectedPath() + toggleFunc := self.c.Git().Patch.PatchBuilder.AddFileLineRange + filename := self.c.Contexts().CommitFiles.GetSelectedPath() if filename == "" { return nil } state := self.context().GetState() - includedLineIndices, err := self.git.Patch.PatchBuilder.GetFileIncLineIndices(filename) + includedLineIndices, err := self.c.Git().Patch.PatchBuilder.GetFileIncLineIndices(filename) if err != nil { return err } currentLineIsStaged := lo.Contains(includedLineIndices, state.GetSelectedLineIdx()) if currentLineIsStaged { - toggleFunc = self.git.Patch.PatchBuilder.RemoveFileLineRange + toggleFunc = self.c.Git().Patch.PatchBuilder.RemoveFileLineRange } // add range of lines to those set for the file @@ -133,5 +154,5 @@ func (self *PatchBuildingController) toggleSelection() error { } func (self *PatchBuildingController) Escape() error { - return self.helpers.PatchBuilding.Escape() + return self.c.Helpers().PatchBuilding.Escape() } diff --git a/pkg/gui/controllers/patch_explorer_controller.go b/pkg/gui/controllers/patch_explorer_controller.go index dac63a7b1..e81cfe1c9 100644 --- a/pkg/gui/controllers/patch_explorer_controller.go +++ b/pkg/gui/controllers/patch_explorer_controller.go @@ -6,26 +6,26 @@ import ( ) type PatchExplorerControllerFactory struct { - *controllerCommon + c *ControllerCommon } -func NewPatchExplorerControllerFactory(c *controllerCommon) *PatchExplorerControllerFactory { +func NewPatchExplorerControllerFactory(c *ControllerCommon) *PatchExplorerControllerFactory { return &PatchExplorerControllerFactory{ - controllerCommon: c, + c: c, } } func (self *PatchExplorerControllerFactory) Create(context types.IPatchExplorerContext) *PatchExplorerController { return &PatchExplorerController{ - baseController: baseController{}, - controllerCommon: self.controllerCommon, - context: context, + baseController: baseController{}, + c: self.c, + context: context, } } type PatchExplorerController struct { baseController - *controllerCommon + c *ControllerCommon context types.IPatchExplorerContext } @@ -254,7 +254,7 @@ func (self *PatchExplorerController) CopySelectedToClipboard() error { selected := self.context.GetState().PlainRenderSelected() self.c.LogAction(self.c.Tr.Actions.CopySelectedTextToClipboard) - if err := self.os.CopyToClipboard(selected); err != nil { + if err := self.c.OS().CopyToClipboard(selected); err != nil { return self.c.Error(err) } diff --git a/pkg/gui/controllers/quit_actions.go b/pkg/gui/controllers/quit_actions.go new file mode 100644 index 000000000..2487a62fe --- /dev/null +++ b/pkg/gui/controllers/quit_actions.go @@ -0,0 +1,75 @@ +package controllers + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type QuitActions struct { + c *ControllerCommon +} + +func (self *QuitActions) Quit() error { + self.c.State().SetRetainOriginalDir(false) + return self.quitAux() +} + +func (self *QuitActions) QuitWithoutChangingDirectory() error { + self.c.State().SetRetainOriginalDir(true) + return self.quitAux() +} + +func (self *QuitActions) quitAux() error { + if self.c.State().GetUpdating() { + return self.confirmQuitDuringUpdate() + } + + if self.c.UserConfig.ConfirmOnQuit { + return self.c.Confirm(types.ConfirmOpts{ + Title: "", + Prompt: self.c.Tr.ConfirmQuit, + HandleConfirm: func() error { + return gocui.ErrQuit + }, + }) + } + + return gocui.ErrQuit +} + +func (self *QuitActions) confirmQuitDuringUpdate() error { + return self.c.Confirm(types.ConfirmOpts{ + Title: self.c.Tr.ConfirmQuitDuringUpdateTitle, + Prompt: self.c.Tr.ConfirmQuitDuringUpdate, + HandleConfirm: func() error { + return gocui.ErrQuit + }, + }) +} + +func (self *QuitActions) Escape() error { + currentContext := self.c.CurrentContext() + + parentContext, hasParent := currentContext.GetParentContext() + if hasParent && currentContext != nil && parentContext != nil { + // TODO: think about whether this should be marked as a return rather than adding to the stack + return self.c.PushContext(parentContext) + } + + for _, mode := range self.c.Helpers().Mode.Statuses() { + if mode.IsActive() { + return mode.Reset() + } + } + + repoPathStack := self.c.State().GetRepoPathStack() + if !repoPathStack.IsEmpty() { + return self.c.Helpers().Repos.DispatchSwitchToRepo(repoPathStack.Pop(), true) + } + + if self.c.UserConfig.QuitOnTopLevelReturn { + return self.Quit() + } + + return nil +} diff --git a/pkg/gui/controllers/reflog_commits_controller.go b/pkg/gui/controllers/reflog_commits_controller.go new file mode 100644 index 000000000..735e0434f --- /dev/null +++ b/pkg/gui/controllers/reflog_commits_controller.go @@ -0,0 +1,54 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ReflogCommitsController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &ReflogCommitsController{} + +func NewReflogCommitsController( + common *ControllerCommon, +) *ReflogCommitsController { + return &ReflogCommitsController{ + baseController: baseController{}, + c: common, + } +} + +func (self *ReflogCommitsController) Context() types.Context { + return self.context() +} + +func (self *ReflogCommitsController) context() *context.ReflogCommitsContext { + return self.c.Contexts().ReflogCommits +} + +func (self *ReflogCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + commit := self.context().GetSelected() + var task types.UpdateTask + if commit == nil { + task = types.NewRenderStringTask("No reflog history") + } else { + cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Reflog Entry", + Task: task, + }, + }) + }) + } +} diff --git a/pkg/gui/controllers/remote_branches_controller.go b/pkg/gui/controllers/remote_branches_controller.go index dcedde8c0..c6ee1dd57 100644 --- a/pkg/gui/controllers/remote_branches_controller.go +++ b/pkg/gui/controllers/remote_branches_controller.go @@ -12,17 +12,17 @@ import ( type RemoteBranchesController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &RemoteBranchesController{} func NewRemoteBranchesController( - common *controllerCommon, + common *ControllerCommon, ) *RemoteBranchesController { return &RemoteBranchesController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -73,12 +73,35 @@ func (self *RemoteBranchesController) GetKeybindings(opts types.KeybindingsOpts) } } +func (self *RemoteBranchesController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + remoteBranch := self.context().GetSelected() + if remoteBranch == nil { + task = types.NewRenderStringTask("No branches for this remote") + } else { + cmdObj := self.c.Git().Branch.GetGraphCmdObj(remoteBranch.FullRefName()) + task = types.NewRunCommandTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Remote Branch", + Task: task, + }, + }) + }) + } +} + func (self *RemoteBranchesController) Context() types.Context { return self.context() } func (self *RemoteBranchesController) context() *context.RemoteBranchesContext { - return self.contexts.RemoteBranches + return self.c.Contexts().RemoteBranches } func (self *RemoteBranchesController) checkSelected(callback func(*models.RemoteBranch) error) func() error { @@ -93,7 +116,7 @@ func (self *RemoteBranchesController) checkSelected(callback func(*models.Remote } func (self *RemoteBranchesController) escape() error { - return self.c.PushContext(self.contexts.Remotes) + return self.c.PushContext(self.c.Contexts().Remotes) } func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch) error { @@ -105,7 +128,7 @@ func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch HandleConfirm: func() error { return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func() error { self.c.LogAction(self.c.Tr.Actions.DeleteRemoteBranch) - err := self.git.Remote.DeleteRemoteBranch(selectedBranch.RemoteName, selectedBranch.Name) + err := self.c.Git().Remote.DeleteRemoteBranch(selectedBranch.RemoteName, selectedBranch.Name) if err != nil { _ = self.c.Error(err) } @@ -117,19 +140,19 @@ func (self *RemoteBranchesController) delete(selectedBranch *models.RemoteBranch } func (self *RemoteBranchesController) merge(selectedBranch *models.RemoteBranch) error { - return self.helpers.MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranch.FullName()) + return self.c.Helpers().MergeAndRebase.MergeRefIntoCheckedOutBranch(selectedBranch.FullName()) } func (self *RemoteBranchesController) rebase(selectedBranch *models.RemoteBranch) error { - return self.helpers.MergeAndRebase.RebaseOntoRef(selectedBranch.FullName()) + return self.c.Helpers().MergeAndRebase.RebaseOntoRef(selectedBranch.FullName()) } func (self *RemoteBranchesController) createResetMenu(selectedBranch *models.RemoteBranch) error { - return self.helpers.Refs.CreateGitResetMenu(selectedBranch.FullName()) + return self.c.Helpers().Refs.CreateGitResetMenu(selectedBranch.FullName()) } func (self *RemoteBranchesController) setAsUpstream(selectedBranch *models.RemoteBranch) error { - checkedOutBranch := self.helpers.Refs.GetCheckedOutRef() + checkedOutBranch := self.c.Helpers().Refs.GetCheckedOutRef() message := utils.ResolvePlaceholderString( self.c.Tr.SetUpstreamMessage, @@ -144,7 +167,7 @@ func (self *RemoteBranchesController) setAsUpstream(selectedBranch *models.Remot Prompt: message, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.SetBranchUpstream) - if err := self.git.Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { + if err := self.c.Git().Branch.SetUpstream(selectedBranch.RemoteName, selectedBranch.Name, checkedOutBranch.Name); err != nil { return self.c.Error(err) } @@ -157,5 +180,5 @@ func (self *RemoteBranchesController) newLocalBranch(selectedBranch *models.Remo // will set to the remote's branch name without the remote name nameSuggestion := strings.SplitAfterN(selectedBranch.RefName(), "/", 2)[1] - return self.helpers.Refs.NewBranch(selectedBranch.RefName(), selectedBranch.RefName(), nameSuggestion) + return self.c.Helpers().Refs.NewBranch(selectedBranch.RefName(), selectedBranch.RefName(), nameSuggestion) } diff --git a/pkg/gui/controllers/remotes_controller.go b/pkg/gui/controllers/remotes_controller.go index 03427b9b7..3d6b4b09d 100644 --- a/pkg/gui/controllers/remotes_controller.go +++ b/pkg/gui/controllers/remotes_controller.go @@ -1,16 +1,19 @@ package controllers import ( + "fmt" + "strings" + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) type RemotesController struct { baseController - *controllerCommon - context *context.RemotesContext + c *ControllerCommon setRemoteBranches func([]*models.RemoteBranch) } @@ -18,13 +21,12 @@ type RemotesController struct { var _ types.IController = &RemotesController{} func NewRemotesController( - common *controllerCommon, + common *ControllerCommon, setRemoteBranches func([]*models.RemoteBranch), ) *RemotesController { return &RemotesController{ baseController: baseController{}, - controllerCommon: common, - context: common.contexts.Remotes, + c: common, setRemoteBranches: setRemoteBranches, } } @@ -60,6 +62,36 @@ func (self *RemotesController) GetKeybindings(opts types.KeybindingsOpts) []*typ return bindings } +func (self *RemotesController) Context() types.Context { + return self.context() +} + +func (self *RemotesController) context() *context.RemotesContext { + return self.c.Contexts().Remotes +} + +func (self *RemotesController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + remote := self.context().GetSelected() + if remote == nil { + task = types.NewRenderStringTask("No remotes") + } else { + task = types.NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", style.FgGreen.Sprint(remote.Name), strings.Join(remote.Urls, "\n"))) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Remote", + Task: task, + }, + }) + }) + } +} + func (self *RemotesController) GetOnClick() func() error { return self.checkSelected(self.enter) } @@ -72,14 +104,14 @@ func (self *RemotesController) enter(remote *models.Remote) error { if len(remote.Branches) == 0 { newSelectedLine = -1 } - self.contexts.RemoteBranches.SetSelectedLineIdx(newSelectedLine) - self.contexts.RemoteBranches.SetTitleRef(remote.Name) + self.c.Contexts().RemoteBranches.SetSelectedLineIdx(newSelectedLine) + self.c.Contexts().RemoteBranches.SetTitleRef(remote.Name) - if err := self.c.PostRefreshUpdate(self.contexts.RemoteBranches); err != nil { + if err := self.c.PostRefreshUpdate(self.c.Contexts().RemoteBranches); err != nil { return err } - return self.c.PushContext(self.contexts.RemoteBranches) + return self.c.PushContext(self.c.Contexts().RemoteBranches) } func (self *RemotesController) add() error { @@ -90,7 +122,7 @@ func (self *RemotesController) add() error { Title: self.c.Tr.LcNewRemoteUrl, HandleConfirm: func(remoteUrl string) error { self.c.LogAction(self.c.Tr.Actions.AddRemote) - if err := self.git.Remote.AddRemote(remoteName, remoteUrl); err != nil { + if err := self.c.Git().Remote.AddRemote(remoteName, remoteUrl); err != nil { return err } return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.REMOTES}}) @@ -106,7 +138,7 @@ func (self *RemotesController) remove(remote *models.Remote) error { Prompt: self.c.Tr.LcRemoveRemotePrompt + " '" + remote.Name + "'?", HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveRemote) - if err := self.git.Remote.RemoveRemote(remote.Name); err != nil { + if err := self.c.Git().Remote.RemoveRemote(remote.Name); err != nil { return self.c.Error(err) } @@ -129,7 +161,7 @@ func (self *RemotesController) edit(remote *models.Remote) error { HandleConfirm: func(updatedRemoteName string) error { if updatedRemoteName != remote.Name { self.c.LogAction(self.c.Tr.Actions.UpdateRemote) - if err := self.git.Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil { + if err := self.c.Git().Remote.RenameRemote(remote.Name, updatedRemoteName); err != nil { return self.c.Error(err) } } @@ -152,7 +184,7 @@ func (self *RemotesController) edit(remote *models.Remote) error { InitialContent: url, HandleConfirm: func(updatedRemoteUrl string) error { self.c.LogAction(self.c.Tr.Actions.UpdateRemote) - if err := self.git.Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil { + if err := self.c.Git().Remote.UpdateRemoteUrl(updatedRemoteName, updatedRemoteUrl); err != nil { return self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.REMOTES}}) @@ -164,7 +196,7 @@ func (self *RemotesController) edit(remote *models.Remote) error { func (self *RemotesController) fetch(remote *models.Remote) error { return self.c.WithWaitingStatus(self.c.Tr.FetchingRemoteStatus, func() error { - err := self.git.Sync.FetchRemote(remote.Name) + err := self.c.Git().Sync.FetchRemote(remote.Name) if err != nil { _ = self.c.Error(err) } @@ -175,7 +207,7 @@ func (self *RemotesController) fetch(remote *models.Remote) error { func (self *RemotesController) checkSelected(callback func(*models.Remote) error) func() error { return func() error { - file := self.context.GetSelected() + file := self.context().GetSelected() if file == nil { return nil } @@ -183,7 +215,3 @@ func (self *RemotesController) checkSelected(callback func(*models.Remote) error return callback(file) } } - -func (self *RemotesController) Context() types.Context { - return self.context -} diff --git a/pkg/gui/controllers/screen_mode_actions.go b/pkg/gui/controllers/screen_mode_actions.go new file mode 100644 index 000000000..d31154636 --- /dev/null +++ b/pkg/gui/controllers/screen_mode_actions.go @@ -0,0 +1,79 @@ +package controllers + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ScreenModeActions struct { + c *ControllerCommon +} + +func (self *ScreenModeActions) Next() error { + self.c.State().GetRepoState().SetScreenMode( + nextIntInCycle( + []types.WindowMaximisation{types.SCREEN_NORMAL, types.SCREEN_HALF, types.SCREEN_FULL}, + self.c.State().GetRepoState().GetScreenMode(), + ), + ) + + return self.rerenderViewsWithScreenModeDependentContent() +} + +func (self *ScreenModeActions) Prev() error { + self.c.State().GetRepoState().SetScreenMode( + prevIntInCycle( + []types.WindowMaximisation{types.SCREEN_NORMAL, types.SCREEN_HALF, types.SCREEN_FULL}, + self.c.State().GetRepoState().GetScreenMode(), + ), + ) + + return self.rerenderViewsWithScreenModeDependentContent() +} + +// these views need to be re-rendered when the screen mode changes. The commits view, +// for example, will show authorship information in half and full screen mode. +func (self *ScreenModeActions) rerenderViewsWithScreenModeDependentContent() error { + // for now we re-render all list views. + for _, context := range self.c.Context().AllList() { + if err := self.rerenderView(context.GetView()); err != nil { + return err + } + } + + return nil +} + +func (self *ScreenModeActions) rerenderView(view *gocui.View) error { + context, ok := self.c.Helpers().View.ContextForView(view.Name()) + if !ok { + self.c.Log.Errorf("no context found for view %s", view.Name()) + return nil + } + + return context.HandleRender() +} + +func nextIntInCycle(sl []types.WindowMaximisation, current types.WindowMaximisation) types.WindowMaximisation { + for i, val := range sl { + if val == current { + if i == len(sl)-1 { + return sl[0] + } + return sl[i+1] + } + } + return sl[0] +} + +func prevIntInCycle(sl []types.WindowMaximisation, current types.WindowMaximisation) types.WindowMaximisation { + for i, val := range sl { + if val == current { + if i > 0 { + return sl[i-1] + } + return sl[len(sl)-1] + } + } + return sl[len(sl)-1] +} diff --git a/pkg/gui/controllers/side_window_controller.go b/pkg/gui/controllers/side_window_controller.go new file mode 100644 index 000000000..a2325c54d --- /dev/null +++ b/pkg/gui/controllers/side_window_controller.go @@ -0,0 +1,96 @@ +package controllers + +import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type SideWindowControllerFactory struct { + c *ControllerCommon +} + +func NewSideWindowControllerFactory(common *ControllerCommon) *SideWindowControllerFactory { + return &SideWindowControllerFactory{c: common} +} + +func (self *SideWindowControllerFactory) Create(context types.Context) types.IController { + return NewSideWindowController(self.c, context) +} + +type SideWindowController struct { + baseController + c *ControllerCommon + context types.Context +} + +func NewSideWindowController( + common *ControllerCommon, + context types.Context, +) *SideWindowController { + return &SideWindowController{ + baseController: baseController{}, + c: common, + context: context, + } +} + +func (self *SideWindowController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + return []*types.Binding{ + {Key: opts.GetKey(opts.Config.Universal.PrevBlock), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, + {Key: opts.GetKey(opts.Config.Universal.NextBlock), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, + {Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, + {Key: opts.GetKey(opts.Config.Universal.NextBlockAlt), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, + {Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt2), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, + {Key: opts.GetKey(opts.Config.Universal.NextBlockAlt2), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, + } +} + +func (self *SideWindowController) Context() types.Context { + return nil +} + +func (self *SideWindowController) previousSideWindow() error { + windows := self.c.Helpers().Window.SideWindows() + currentWindow := self.c.Helpers().Window.CurrentWindow() + var newWindow string + if currentWindow == "" || currentWindow == windows[0] { + newWindow = windows[len(windows)-1] + } else { + for i := range windows { + if currentWindow == windows[i] { + newWindow = windows[i-1] + break + } + if i == len(windows)-1 { + return nil + } + } + } + + context := self.c.Helpers().Window.GetContextForWindow(newWindow) + + return self.c.PushContext(context) +} + +func (self *SideWindowController) nextSideWindow() error { + windows := self.c.Helpers().Window.SideWindows() + currentWindow := self.c.Helpers().Window.CurrentWindow() + var newWindow string + if currentWindow == "" || currentWindow == windows[len(windows)-1] { + newWindow = windows[0] + } else { + for i := range windows { + if currentWindow == windows[i] { + newWindow = windows[i+1] + break + } + if i == len(windows)-1 { + return nil + } + } + } + + context := self.c.Helpers().Window.GetContextForWindow(newWindow) + + return self.c.PushContext(context) +} diff --git a/pkg/gui/controllers/snake_controller.go b/pkg/gui/controllers/snake_controller.go index 4217878e3..074a4a6fb 100644 --- a/pkg/gui/controllers/snake_controller.go +++ b/pkg/gui/controllers/snake_controller.go @@ -7,21 +7,17 @@ import ( type SnakeController struct { baseController - *controllerCommon - - getGame func() *snake.Game + c *ControllerCommon } var _ types.IController = &SnakeController{} func NewSnakeController( - common *controllerCommon, - getGame func() *snake.Game, + common *ControllerCommon, ) *SnakeController { return &SnakeController{ - baseController: baseController{}, - controllerCommon: common, - getGame: getGame, + baseController: baseController{}, + c: common, } } @@ -53,16 +49,31 @@ func (self *SnakeController) GetKeybindings(opts types.KeybindingsOpts) []*types } func (self *SnakeController) Context() types.Context { - return self.contexts.Snake + return self.c.Contexts().Snake +} + +func (self *SnakeController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + self.c.Helpers().Snake.StartGame() + return nil + } +} + +func (self *SnakeController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.c.Helpers().Snake.ExitGame() + self.c.Helpers().Window.MoveToTopOfWindow(self.c.Contexts().Submodules) + return nil + } } func (self *SnakeController) SetDirection(direction snake.Direction) func() error { return func() error { - self.getGame().SetDirection(direction) + self.c.Helpers().Snake.SetDirection(direction) return nil } } func (self *SnakeController) Escape() error { - return self.c.PushContext(self.contexts.Submodules) + return self.c.PushContext(self.c.Contexts().Submodules) } diff --git a/pkg/gui/controllers/staging_controller.go b/pkg/gui/controllers/staging_controller.go index bcd6d5114..da31ce23b 100644 --- a/pkg/gui/controllers/staging_controller.go +++ b/pkg/gui/controllers/staging_controller.go @@ -10,7 +10,7 @@ import ( type StagingController struct { baseController - *controllerCommon + c *ControllerCommon context types.IPatchExplorerContext otherContext types.IPatchExplorerContext @@ -22,17 +22,17 @@ type StagingController struct { var _ types.IController = &StagingController{} func NewStagingController( - common *controllerCommon, + common *ControllerCommon, context types.IPatchExplorerContext, otherContext types.IPatchExplorerContext, staged bool, ) *StagingController { return &StagingController{ - baseController: baseController{}, - controllerCommon: common, - context: context, - otherContext: otherContext, - staged: staged, + baseController: baseController{}, + c: common, + context: context, + otherContext: otherContext, + staged: staged, } } @@ -75,17 +75,17 @@ func (self *StagingController) GetKeybindings(opts types.KeybindingsOpts) []*typ }, { Key: opts.GetKey(opts.Config.Files.CommitChanges), - Handler: self.helpers.WorkingTree.HandleCommitPress, + Handler: self.c.Helpers().WorkingTree.HandleCommitPress, Description: self.c.Tr.CommitChanges, }, { Key: opts.GetKey(opts.Config.Files.CommitChangesWithoutHook), - Handler: self.helpers.WorkingTree.HandleWIPCommitPress, + Handler: self.c.Helpers().WorkingTree.HandleWIPCommitPress, Description: self.c.Tr.LcCommitChangesWithoutHook, }, { Key: opts.GetKey(opts.Config.Files.CommitChangesWithEditor), - Handler: self.helpers.WorkingTree.HandleCommitEditorPress, + Handler: self.c.Helpers().WorkingTree.HandleCommitEditorPress, Description: self.c.Tr.CommitChangesWithEditor, }, } @@ -99,6 +99,29 @@ func (self *StagingController) GetMouseKeybindings(opts types.KeybindingsOpts) [ return []*gocui.ViewMouseBinding{} } +func (self *StagingController) GetOnFocus() func(types.OnFocusOpts) error { + return func(opts types.OnFocusOpts) error { + self.c.Views().Staging.Wrap = false + self.c.Views().StagingSecondary.Wrap = false + + return self.c.Helpers().Staging.RefreshStagingPanel(opts) + } +} + +func (self *StagingController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(opts types.OnFocusLostOpts) error { + self.context.SetState(nil) + + if opts.NewContextKey != self.otherContext.GetKey() { + self.c.Views().Staging.Wrap = true + self.c.Views().StagingSecondary.Wrap = true + _ = self.c.Contexts().Staging.Render(false) + _ = self.c.Contexts().StagingSecondary.Render(false) + } + return nil + } +} + func (self *StagingController) OpenFile() error { self.context.GetMutex().Lock() defer self.context.GetMutex().Unlock() @@ -109,7 +132,7 @@ func (self *StagingController) OpenFile() error { return nil } - return self.helpers.Files.OpenFile(path) + return self.c.Helpers().Files.OpenFile(path) } func (self *StagingController) EditFile() error { @@ -123,7 +146,7 @@ func (self *StagingController) EditFile() error { } lineNumber := self.context.GetState().CurrentLineNumber() - return self.helpers.Files.EditFileAtLine(path, lineNumber) + return self.c.Helpers().Files.EditFileAtLine(path, lineNumber) } func (self *StagingController) Escape() error { @@ -198,7 +221,7 @@ func (self *StagingController) applySelection(reverse bool) error { applyFlags = append(applyFlags, "cached") } self.c.LogAction(self.c.Tr.Actions.ApplyPatch) - err := self.git.WorkingTree.ApplyPatch(patchToApply, applyFlags...) + err := self.c.Git().WorkingTree.ApplyPatch(patchToApply, applyFlags...) if err != nil { return self.c.Error(err) } @@ -239,18 +262,18 @@ func (self *StagingController) editHunk() error { }). FormatPlain() - patchFilepath, err := self.git.WorkingTree.SaveTemporaryPatch(patchText) + patchFilepath, err := self.c.Git().WorkingTree.SaveTemporaryPatch(patchText) if err != nil { return err } lineOffset := 3 lineIdxInHunk := state.GetSelectedLineIdx() - hunkStartIdx - if err := self.helpers.Files.EditFileAtLineAndWait(patchFilepath, lineIdxInHunk+lineOffset); err != nil { + if err := self.c.Helpers().Files.EditFileAtLineAndWait(patchFilepath, lineIdxInHunk+lineOffset); err != nil { return err } - editedPatchText, err := self.git.File.Cat(patchFilepath) + editedPatchText, err := self.c.Git().File.Cat(patchFilepath) if err != nil { return err } @@ -270,7 +293,7 @@ func (self *StagingController) editHunk() error { if self.staged { applyFlags = append(applyFlags, "reverse") } - if err := self.git.WorkingTree.ApplyPatch(newPatchText, applyFlags...); err != nil { + if err := self.c.Git().WorkingTree.ApplyPatch(newPatchText, applyFlags...); err != nil { return self.c.Error(err) } @@ -278,5 +301,5 @@ func (self *StagingController) editHunk() error { } func (self *StagingController) FilePath() string { - return self.contexts.Files.GetSelectedPath() + return self.c.Contexts().Files.GetSelectedPath() } diff --git a/pkg/gui/controllers/stash_controller.go b/pkg/gui/controllers/stash_controller.go index 68a121931..3e1b65ce8 100644 --- a/pkg/gui/controllers/stash_controller.go +++ b/pkg/gui/controllers/stash_controller.go @@ -9,17 +9,17 @@ import ( type StashController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &StashController{} func NewStashController( - common *controllerCommon, + common *ControllerCommon, ) *StashController { return &StashController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -55,6 +55,28 @@ func (self *StashController) GetKeybindings(opts types.KeybindingsOpts) []*types return bindings } +func (self *StashController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + stashEntry := self.context().GetSelected() + if stashEntry == nil { + task = types.NewRenderStringTask(self.c.Tr.NoStashEntries) + } else { + task = types.NewRunPtyTask(self.c.Git().Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Stash", + Task: task, + }, + }) + }) + } +} + func (self *StashController) checkSelected(callback func(*models.StashEntry) error) func() error { return func() error { item := self.context().GetSelected() @@ -71,13 +93,13 @@ func (self *StashController) Context() types.Context { } func (self *StashController) context() *context.StashContext { - return self.contexts.Stash + return self.c.Contexts().Stash } func (self *StashController) handleStashApply(stashEntry *models.StashEntry) error { apply := func() error { self.c.LogAction(self.c.Tr.Actions.Stash) - err := self.git.Stash.Apply(stashEntry.Index) + err := self.c.Git().Stash.Apply(stashEntry.Index) _ = self.postStashRefresh() if err != nil { return self.c.Error(err) @@ -101,7 +123,7 @@ func (self *StashController) handleStashApply(stashEntry *models.StashEntry) err func (self *StashController) handleStashPop(stashEntry *models.StashEntry) error { pop := func() error { self.c.LogAction(self.c.Tr.Actions.Stash) - err := self.git.Stash.Pop(stashEntry.Index) + err := self.c.Git().Stash.Pop(stashEntry.Index) _ = self.postStashRefresh() if err != nil { return self.c.Error(err) @@ -128,7 +150,7 @@ func (self *StashController) handleStashDrop(stashEntry *models.StashEntry) erro Prompt: self.c.Tr.SureDropStashEntry, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.Stash) - err := self.git.Stash.Drop(stashEntry.Index) + err := self.c.Git().Stash.Drop(stashEntry.Index) _ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}}) if err != nil { return self.c.Error(err) @@ -143,7 +165,7 @@ func (self *StashController) postStashRefresh() error { } func (self *StashController) handleNewBranchOffStashEntry(stashEntry *models.StashEntry) error { - return self.helpers.Refs.NewBranch(stashEntry.RefName(), stashEntry.Description(), "") + return self.c.Helpers().Refs.NewBranch(stashEntry.RefName(), stashEntry.Description(), "") } func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntry) error { @@ -159,7 +181,7 @@ func (self *StashController) handleRenameStashEntry(stashEntry *models.StashEntr InitialContent: stashEntry.Name, HandleConfirm: func(response string) error { self.c.LogAction(self.c.Tr.Actions.RenameStash) - err := self.git.Stash.Rename(stashEntry.Index, response) + err := self.c.Git().Stash.Rename(stashEntry.Index, response) _ = self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.STASH}}) if err != nil { return err diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go new file mode 100644 index 000000000..f78a83321 --- /dev/null +++ b/pkg/gui/controllers/status_controller.go @@ -0,0 +1,198 @@ +package controllers + +import ( + "errors" + "fmt" + "strings" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + "github.com/jesseduffield/lazygit/pkg/constants" + "github.com/jesseduffield/lazygit/pkg/gui/presentation" + "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type StatusController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &StatusController{} + +func NewStatusController( + common *ControllerCommon, +) *StatusController { + return &StatusController{ + baseController: baseController{}, + c: common, + } +} + +func (self *StatusController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.OpenFile), + Handler: self.openConfig, + Description: self.c.Tr.OpenConfig, + }, + { + Key: opts.GetKey(opts.Config.Universal.Edit), + Handler: self.editConfig, + Description: self.c.Tr.EditConfig, + }, + { + Key: opts.GetKey(opts.Config.Status.CheckForUpdate), + Handler: self.handleCheckForUpdate, + Description: self.c.Tr.LcCheckForUpdate, + }, + { + Key: opts.GetKey(opts.Config.Status.RecentRepos), + Handler: self.c.Helpers().Repos.CreateRecentReposMenu, + Description: self.c.Tr.SwitchRepo, + }, + { + Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph), + Handler: self.showAllBranchLogs, + Description: self.c.Tr.LcAllBranchesLogGraph, + }, + } + + return bindings +} + +func (self *StatusController) GetOnRenderToMain() func() error { + return func() error { + dashboardString := strings.Join( + []string{ + lazygitTitle(), + "Copyright 2022 Jesse Duffield", + fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings), + fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config), + fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial), + fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues), + fmt.Sprintf("Release Notes: %s", constants.Links.Releases), + style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free + }, "\n\n") + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.StatusTitle, + Task: types.NewRenderStringTask(dashboardString), + }, + }) + } +} + +func (self *StatusController) GetOnClick() func() error { + return self.onClick +} + +func (self *StatusController) Context() types.Context { + return self.c.Contexts().Status +} + +func (self *StatusController) onClick() error { + // TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives) + currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() + if currentBranch == nil { + // need to wait for branches to refresh + return nil + } + + if err := self.c.PushContext(self.Context()); err != nil { + return err + } + + cx, _ := self.c.Views().Status.Cursor() + upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) + repoName := utils.GetCurrentRepoName() + workingTreeState := self.c.Git().Status.WorkingTreeState() + switch workingTreeState { + case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: + workingTreeStatus := fmt.Sprintf("(%s)", presentation.FormatWorkingTreeState(workingTreeState)) + if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { + return self.c.Helpers().MergeAndRebase.CreateRebaseOptionsMenu() + } + if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { + return self.c.Helpers().Repos.CreateRecentReposMenu() + } + default: + if cursorInSubstring(cx, upstreamStatus+" ", repoName) { + return self.c.Helpers().Repos.CreateRecentReposMenu() + } + } + + return nil +} + +func runeCount(str string) int { + return len([]rune(str)) +} + +func cursorInSubstring(cx int, prefix string, substring string) bool { + return cx >= runeCount(prefix) && cx < runeCount(prefix+substring) +} + +func lazygitTitle() string { + return ` + _ _ _ + | | (_) | + | | __ _ _____ _ __ _ _| |_ + | |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __| + | | (_| |/ /| |_| | (_| | | |_ + |_|\__,_/___|\__, |\__, |_|\__| + __/ | __/ | + |___/ |___/ ` +} + +func (self *StatusController) askForConfigFile(action func(file string) error) error { + confPaths := self.c.GetConfig().GetUserConfigPaths() + switch len(confPaths) { + case 0: + return errors.New(self.c.Tr.NoConfigFileFoundErr) + case 1: + return action(confPaths[0]) + default: + menuItems := slices.Map(confPaths, func(path string) *types.MenuItem { + return &types.MenuItem{ + Label: path, + OnPress: func() error { + return action(path) + }, + } + }) + + return self.c.Menu(types.CreateMenuOptions{ + Title: self.c.Tr.SelectConfigFile, + Items: menuItems, + }) + } +} + +func (self *StatusController) openConfig() error { + return self.askForConfigFile(self.c.Helpers().Files.OpenFile) +} + +func (self *StatusController) editConfig() error { + return self.askForConfigFile(self.c.Helpers().Files.EditFile) +} + +func (self *StatusController) showAllBranchLogs() error { + cmdObj := self.c.Git().Branch.AllBranchesLogCmdObj() + task := types.NewRunPtyTask(cmdObj.GetCmd()) + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: self.c.Tr.LogTitle, + Task: task, + }, + }) +} + +func (self *StatusController) handleCheckForUpdate() error { + return self.c.Helpers().Update.CheckForUpdateInForeground() +} diff --git a/pkg/gui/controllers/sub_commits_controller.go b/pkg/gui/controllers/sub_commits_controller.go new file mode 100644 index 000000000..e887b29d6 --- /dev/null +++ b/pkg/gui/controllers/sub_commits_controller.go @@ -0,0 +1,71 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type SubCommitsController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &SubCommitsController{} + +func NewSubCommitsController( + common *ControllerCommon, +) *SubCommitsController { + return &SubCommitsController{ + baseController: baseController{}, + c: common, + } +} + +func (self *SubCommitsController) Context() types.Context { + return self.context() +} + +func (self *SubCommitsController) context() *context.SubCommitsContext { + return self.c.Contexts().SubCommits +} + +func (self *SubCommitsController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + commit := self.context().GetSelected() + var task types.UpdateTask + if commit == nil { + task = types.NewRenderStringTask("No commits") + } else { + cmdObj := self.c.Git().Commit.ShowCmdObj(commit.Sha, self.c.Modes().Filtering.GetPath(), self.c.State().GetIgnoreWhitespaceInDiffView()) + + task = types.NewRunPtyTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Commit", + Task: task, + }, + }) + }) + } +} + +func (self *SubCommitsController) GetOnFocus() func(types.OnFocusOpts) error { + return func(types.OnFocusOpts) error { + context := self.context() + if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { + context.SetLimitCommits(false) + go utils.Safe(func() { + if err := self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.SUB_COMMITS}}); err != nil { + _ = self.c.Error(err) + } + }) + } + + return nil + } +} diff --git a/pkg/gui/controllers/submodules_controller.go b/pkg/gui/controllers/submodules_controller.go index e6a6f8d17..9a9f5af6b 100644 --- a/pkg/gui/controllers/submodules_controller.go +++ b/pkg/gui/controllers/submodules_controller.go @@ -13,21 +13,17 @@ import ( type SubmodulesController struct { baseController - *controllerCommon - - enterSubmodule func(submodule *models.SubmoduleConfig) error + c *ControllerCommon } var _ types.IController = &SubmodulesController{} func NewSubmodulesController( - controllerCommon *controllerCommon, - enterSubmodule func(submodule *models.SubmoduleConfig) error, + controllerCommon *ControllerCommon, ) *SubmodulesController { return &SubmodulesController{ - baseController: baseController{}, - controllerCommon: controllerCommon, - enterSubmodule: enterSubmodule, + baseController: baseController{}, + c: controllerCommon, } } @@ -81,8 +77,43 @@ func (self *SubmodulesController) GetOnClick() func() error { return self.checkSelected(self.enter) } +func (self *SubmodulesController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + submodule := self.context().GetSelected() + if submodule == nil { + task = types.NewRenderStringTask("No submodules") + } else { + prefix := fmt.Sprintf( + "Name: %s\nPath: %s\nUrl: %s\n\n", + style.FgGreen.Sprint(submodule.Name), + style.FgYellow.Sprint(submodule.Path), + style.FgCyan.Sprint(submodule.Url), + ) + + file := self.c.Helpers().WorkingTree.FileForSubmodule(submodule) + if file == nil { + task = types.NewRenderStringTask(prefix) + } else { + cmdObj := self.c.Git().WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, self.c.State().GetIgnoreWhitespaceInDiffView()) + task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) + } + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Submodule", + Task: task, + }, + }) + }) + } +} + func (self *SubmodulesController) enter(submodule *models.SubmoduleConfig) error { - return self.enterSubmodule(submodule) + return self.c.Helpers().Repos.EnterSubmodule(submodule) } func (self *SubmodulesController) add() error { @@ -101,7 +132,7 @@ func (self *SubmodulesController) add() error { HandleConfirm: func(submodulePath string) error { return self.c.WithWaitingStatus(self.c.Tr.LcAddingSubmoduleStatus, func() error { self.c.LogAction(self.c.Tr.Actions.AddSubmodule) - err := self.git.Submodule.Add(submoduleName, submodulePath, submoduleUrl) + err := self.c.Git().Submodule.Add(submoduleName, submodulePath, submoduleUrl) if err != nil { _ = self.c.Error(err) } @@ -123,7 +154,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err HandleConfirm: func(newUrl string) error { return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingSubmoduleUrlStatus, func() error { self.c.LogAction(self.c.Tr.Actions.UpdateSubmoduleUrl) - err := self.git.Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) + err := self.c.Git().Submodule.UpdateUrl(submodule.Name, submodule.Path, newUrl) if err != nil { _ = self.c.Error(err) } @@ -137,7 +168,7 @@ func (self *SubmodulesController) editURL(submodule *models.SubmoduleConfig) err func (self *SubmodulesController) init(submodule *models.SubmoduleConfig) error { return self.c.WithWaitingStatus(self.c.Tr.LcInitializingSubmoduleStatus, func() error { self.c.LogAction(self.c.Tr.Actions.InitialiseSubmodule) - err := self.git.Submodule.Init(submodule.Path) + err := self.c.Git().Submodule.Init(submodule.Path) if err != nil { _ = self.c.Error(err) } @@ -151,11 +182,11 @@ func (self *SubmodulesController) openBulkActionsMenu() error { Title: self.c.Tr.LcBulkSubmoduleOptions, Items: []*types.MenuItem{ { - LabelColumns: []string{self.c.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(self.git.Submodule.BulkInitCmdObj().ToString())}, + LabelColumns: []string{self.c.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(self.c.Git().Submodule.BulkInitCmdObj().ToString())}, OnPress: func() error { return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { self.c.LogAction(self.c.Tr.Actions.BulkInitialiseSubmodules) - err := self.git.Submodule.BulkInitCmdObj().Run() + err := self.c.Git().Submodule.BulkInitCmdObj().Run() if err != nil { return self.c.Error(err) } @@ -166,11 +197,11 @@ func (self *SubmodulesController) openBulkActionsMenu() error { Key: 'i', }, { - LabelColumns: []string{self.c.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(self.git.Submodule.BulkUpdateCmdObj().ToString())}, + LabelColumns: []string{self.c.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(self.c.Git().Submodule.BulkUpdateCmdObj().ToString())}, OnPress: func() error { return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { self.c.LogAction(self.c.Tr.Actions.BulkUpdateSubmodules) - if err := self.git.Submodule.BulkUpdateCmdObj().Run(); err != nil { + if err := self.c.Git().Submodule.BulkUpdateCmdObj().Run(); err != nil { return self.c.Error(err) } @@ -180,11 +211,11 @@ func (self *SubmodulesController) openBulkActionsMenu() error { Key: 'u', }, { - LabelColumns: []string{self.c.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(self.git.Submodule.BulkDeinitCmdObj().ToString())}, + LabelColumns: []string{self.c.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(self.c.Git().Submodule.BulkDeinitCmdObj().ToString())}, OnPress: func() error { return self.c.WithWaitingStatus(self.c.Tr.LcRunningCommand, func() error { self.c.LogAction(self.c.Tr.Actions.BulkDeinitialiseSubmodules) - if err := self.git.Submodule.BulkDeinitCmdObj().Run(); err != nil { + if err := self.c.Git().Submodule.BulkDeinitCmdObj().Run(); err != nil { return self.c.Error(err) } @@ -200,7 +231,7 @@ func (self *SubmodulesController) openBulkActionsMenu() error { func (self *SubmodulesController) update(submodule *models.SubmoduleConfig) error { return self.c.WithWaitingStatus(self.c.Tr.LcUpdatingSubmoduleStatus, func() error { self.c.LogAction(self.c.Tr.Actions.UpdateSubmodule) - err := self.git.Submodule.Update(submodule.Path) + err := self.c.Git().Submodule.Update(submodule.Path) if err != nil { _ = self.c.Error(err) } @@ -215,7 +246,7 @@ func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) erro Prompt: fmt.Sprintf(self.c.Tr.RemoveSubmodulePrompt, submodule.Name), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveSubmodule) - if err := self.git.Submodule.Delete(submodule); err != nil { + if err := self.c.Git().Submodule.Delete(submodule); err != nil { return self.c.Error(err) } @@ -225,7 +256,7 @@ func (self *SubmodulesController) remove(submodule *models.SubmoduleConfig) erro } func (self *SubmodulesController) easterEgg() error { - return self.c.PushContext(self.contexts.Snake) + return self.c.PushContext(self.c.Contexts().Snake) } func (self *SubmodulesController) checkSelected(callback func(*models.SubmoduleConfig) error) func() error { @@ -244,5 +275,5 @@ func (self *SubmodulesController) Context() types.Context { } func (self *SubmodulesController) context() *context.SubmodulesContext { - return self.contexts.Submodules + return self.c.Contexts().Submodules } diff --git a/pkg/gui/controllers/suggestions_controller.go b/pkg/gui/controllers/suggestions_controller.go new file mode 100644 index 000000000..17b8915a1 --- /dev/null +++ b/pkg/gui/controllers/suggestions_controller.go @@ -0,0 +1,56 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type SuggestionsController struct { + baseController + c *ControllerCommon +} + +var _ types.IController = &SuggestionsController{} + +func NewSuggestionsController( + common *ControllerCommon, +) *SuggestionsController { + return &SuggestionsController{ + baseController: baseController{}, + c: common, + } +} + +func (self *SuggestionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { + bindings := []*types.Binding{ + { + Key: opts.GetKey(opts.Config.Universal.Confirm), + Handler: func() error { return self.context().State.OnConfirm() }, + }, + { + Key: opts.GetKey(opts.Config.Universal.Return), + Handler: func() error { return self.context().State.OnClose() }, + }, + { + Key: opts.GetKey(opts.Config.Universal.TogglePanel), + Handler: func() error { return self.c.ReplaceContext(self.c.Contexts().Confirmation) }, + }, + } + + return bindings +} + +func (self *SuggestionsController) GetOnFocusLost() func(types.OnFocusLostOpts) error { + return func(types.OnFocusLostOpts) error { + self.c.Helpers().Confirmation.DeactivateConfirmationPrompt() + return nil + } +} + +func (self *SuggestionsController) Context() types.Context { + return self.context() +} + +func (self *SuggestionsController) context() *context.SuggestionsContext { + return self.c.Contexts().Suggestions +} diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index 275a5ebb2..8aa929587 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -1,6 +1,7 @@ package controllers import ( + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -16,21 +17,21 @@ type CanSwitchToDiffFiles interface { type SwitchToDiffFilesController struct { baseController - *controllerCommon - context CanSwitchToDiffFiles - viewFiles func(SwitchToCommitFilesContextOpts) error + c *ControllerCommon + context CanSwitchToDiffFiles + diffFilesContext *context.CommitFilesContext } func NewSwitchToDiffFilesController( - controllerCommon *controllerCommon, - viewFiles func(SwitchToCommitFilesContextOpts) error, + c *ControllerCommon, context CanSwitchToDiffFiles, + diffFilesContext *context.CommitFilesContext, ) *SwitchToDiffFilesController { return &SwitchToDiffFilesController{ baseController: baseController{}, - controllerCommon: controllerCommon, + c: c, context: context, - viewFiles: viewFiles, + diffFilesContext: diffFilesContext, } } @@ -72,3 +73,22 @@ func (self *SwitchToDiffFilesController) enter(ref types.Ref) error { func (self *SwitchToDiffFilesController) Context() types.Context { return self.context } + +func (self *SwitchToDiffFilesController) viewFiles(opts SwitchToCommitFilesContextOpts) error { + diffFilesContext := self.diffFilesContext + + diffFilesContext.SetSelectedLineIdx(0) + diffFilesContext.SetRef(opts.Ref) + diffFilesContext.SetTitleRef(opts.Ref.Description()) + diffFilesContext.SetCanRebase(opts.CanRebase) + diffFilesContext.SetParentContext(opts.Context) + diffFilesContext.SetWindowName(opts.Context.GetWindowName()) + + if err := self.c.Refresh(types.RefreshOptions{ + Scope: []types.RefreshableView{types.COMMIT_FILES}, + }); err != nil { + return err + } + + return self.c.PushContext(diffFilesContext) +} diff --git a/pkg/gui/controllers/switch_to_sub_commits_controller.go b/pkg/gui/controllers/switch_to_sub_commits_controller.go index 2e9d25e09..c66151190 100644 --- a/pkg/gui/controllers/switch_to_sub_commits_controller.go +++ b/pkg/gui/controllers/switch_to_sub_commits_controller.go @@ -15,22 +15,22 @@ type CanSwitchToSubCommits interface { type SwitchToSubCommitsController struct { baseController - *controllerCommon + c *ControllerCommon context CanSwitchToSubCommits setSubCommits func([]*models.Commit) } func NewSwitchToSubCommitsController( - controllerCommon *controllerCommon, + controllerCommon *ControllerCommon, setSubCommits func([]*models.Commit), context CanSwitchToSubCommits, ) *SwitchToSubCommitsController { return &SwitchToSubCommitsController{ - baseController: baseController{}, - controllerCommon: controllerCommon, - context: context, - setSubCommits: setSubCommits, + baseController: baseController{}, + c: controllerCommon, + context: context, + setSubCommits: setSubCommits, } } @@ -57,10 +57,10 @@ func (self *SwitchToSubCommitsController) viewCommits() error { } // need to populate my sub commits - commits, err := self.git.Loaders.CommitLoader.GetCommits( + commits, err := self.c.Git().Loaders.CommitLoader.GetCommits( git_commands.GetCommitsOptions{ Limit: true, - FilterPath: self.modes.Filtering.GetPath(), + FilterPath: self.c.Modes().Filtering.GetPath(), IncludeRebaseCommits: false, RefName: ref.FullRefName(), }, @@ -71,19 +71,19 @@ func (self *SwitchToSubCommitsController) viewCommits() error { self.setSubCommits(commits) - self.contexts.SubCommits.SetSelectedLineIdx(0) - self.contexts.SubCommits.SetParentContext(self.context) - self.contexts.SubCommits.SetWindowName(self.context.GetWindowName()) - self.contexts.SubCommits.SetTitleRef(ref.Description()) - self.contexts.SubCommits.SetRef(ref) - self.contexts.SubCommits.SetLimitCommits(true) + self.c.Contexts().SubCommits.SetSelectedLineIdx(0) + self.c.Contexts().SubCommits.SetParentContext(self.context) + self.c.Contexts().SubCommits.SetWindowName(self.context.GetWindowName()) + self.c.Contexts().SubCommits.SetTitleRef(ref.Description()) + self.c.Contexts().SubCommits.SetRef(ref) + self.c.Contexts().SubCommits.SetLimitCommits(true) - err = self.c.PostRefreshUpdate(self.contexts.SubCommits) + err = self.c.PostRefreshUpdate(self.c.Contexts().SubCommits) if err != nil { return err } - return self.c.PushContext(self.contexts.SubCommits) + return self.c.PushContext(self.c.Contexts().SubCommits) } func (self *SwitchToSubCommitsController) Context() types.Context { diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index 5add485c4..120d9fdac 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -11,17 +11,17 @@ import ( type SyncController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &SyncController{} func NewSyncController( - common *controllerCommon, + common *ControllerCommon, ) *SyncController { return &SyncController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -56,7 +56,7 @@ func (self *SyncController) HandlePull() error { func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error { return func() error { - currentBranch := self.helpers.Refs.GetCheckedOutRef() + currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() if currentBranch == nil { // need to wait for branches to refresh return nil @@ -76,11 +76,11 @@ func (self *SyncController) push(currentBranch *models.Branch) error { return self.pushAux(opts) } } else { - if self.git.Config.GetPushToCurrent() { + if self.c.Git().Config.GetPushToCurrent() { return self.pushAux(pushOpts{setUpstream: true}) } else { - return self.helpers.Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { - upstreamRemote, upstreamBranch, err := self.helpers.Upstream.ParseUpstream(upstream) + return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { + upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return self.c.Error(err) } @@ -100,7 +100,7 @@ func (self *SyncController) pull(currentBranch *models.Branch) error { // if we have no upstream branch we need to set that first if !currentBranch.IsTrackingRemote() { - return self.helpers.Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { + return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { if err := self.setCurrentBranchUpstream(upstream); err != nil { return self.c.Error(err) } @@ -113,12 +113,12 @@ func (self *SyncController) pull(currentBranch *models.Branch) error { } func (self *SyncController) setCurrentBranchUpstream(upstream string) error { - upstreamRemote, upstreamBranch, err := self.helpers.Upstream.ParseUpstream(upstream) + upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) if err != nil { return err } - if err := self.git.Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil { + if err := self.c.Git().Branch.SetCurrentBranchUpstream(upstreamRemote, upstreamBranch); err != nil { if strings.Contains(err.Error(), "does not exist") { return fmt.Errorf( "upstream branch %s/%s not found.\nIf you expect it to exist, you should fetch (with 'f').\nOtherwise, you should push (with 'shift+P')", @@ -146,7 +146,7 @@ func (self *SyncController) PullAux(opts PullFilesOptions) error { func (self *SyncController) pullWithLock(opts PullFilesOptions) error { self.c.LogAction(opts.Action) - err := self.git.Sync.Pull( + err := self.c.Git().Sync.Pull( git_commands.PullOptions{ RemoteName: opts.UpstreamRemote, BranchName: opts.UpstreamBranch, @@ -154,7 +154,7 @@ func (self *SyncController) pullWithLock(opts PullFilesOptions) error { }, ) - return self.helpers.MergeAndRebase.CheckMergeOrRebase(err) + return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) } type pushOpts struct { @@ -167,7 +167,7 @@ type pushOpts struct { func (self *SyncController) pushAux(opts pushOpts) error { return self.c.WithLoaderPanel(self.c.Tr.PushWait, func() error { self.c.LogAction(self.c.Tr.Actions.Push) - err := self.git.Sync.Push(git_commands.PushOpts{ + err := self.c.Git().Sync.Push(git_commands.PushOpts{ Force: opts.force, UpstreamRemote: opts.upstreamRemote, UpstreamBranch: opts.upstreamBranch, diff --git a/pkg/gui/controllers/tags_controller.go b/pkg/gui/controllers/tags_controller.go index f4b23374c..39fedc58e 100644 --- a/pkg/gui/controllers/tags_controller.go +++ b/pkg/gui/controllers/tags_controller.go @@ -9,17 +9,17 @@ import ( type TagsController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &TagsController{} func NewTagsController( - common *controllerCommon, + common *ControllerCommon, ) *TagsController { return &TagsController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -56,12 +56,35 @@ func (self *TagsController) GetKeybindings(opts types.KeybindingsOpts) []*types. return bindings } +func (self *TagsController) GetOnRenderToMain() func() error { + return func() error { + return self.c.Helpers().Diff.WithDiffModeCheck(func() error { + var task types.UpdateTask + tag := self.context().GetSelected() + if tag == nil { + task = types.NewRenderStringTask("No tags") + } else { + cmdObj := self.c.Git().Branch.GetGraphCmdObj(tag.FullRefName()) + task = types.NewRunCommandTask(cmdObj.GetCmd()) + } + + return self.c.RenderToMainViews(types.RefreshMainOpts{ + Pair: self.c.MainViewPairs().Normal, + Main: &types.ViewUpdateOpts{ + Title: "Tag", + Task: task, + }, + }) + }) + } +} + func (self *TagsController) checkout(tag *models.Tag) error { self.c.LogAction(self.c.Tr.Actions.CheckoutTag) - if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil { + if err := self.c.Helpers().Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil { return err } - return self.c.PushContext(self.contexts.Branches) + return self.c.PushContext(self.c.Contexts().Branches) } func (self *TagsController) delete(tag *models.Tag) error { @@ -77,7 +100,7 @@ func (self *TagsController) delete(tag *models.Tag) error { Prompt: prompt, HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.DeleteTag) - if err := self.git.Tag.Delete(tag.Name); err != nil { + if err := self.c.Git().Tag.Delete(tag.Name); err != nil { return self.c.Error(err) } return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) @@ -96,11 +119,11 @@ func (self *TagsController) push(tag *models.Tag) error { return self.c.Prompt(types.PromptOpts{ Title: title, InitialContent: "origin", - FindSuggestionsFunc: self.helpers.Suggestions.GetRemoteSuggestionsFunc(), + FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(), HandleConfirm: func(response string) error { return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func() error { self.c.LogAction(self.c.Tr.Actions.PushTag) - err := self.git.Tag.Push(response, tag.Name) + err := self.c.Git().Tag.Push(response, tag.Name) if err != nil { _ = self.c.Error(err) } @@ -112,12 +135,12 @@ func (self *TagsController) push(tag *models.Tag) error { } func (self *TagsController) createResetMenu(tag *models.Tag) error { - return self.helpers.Refs.CreateGitResetMenu(tag.Name) + return self.c.Helpers().Refs.CreateGitResetMenu(tag.Name) } func (self *TagsController) create() error { // leaving commit SHA blank so that we're just creating the tag for the current commit - return self.helpers.Tags.CreateTagMenu("", func() { self.context().SetSelectedLineIdx(0) }) + return self.c.Helpers().Tags.CreateTagMenu("", func() { self.context().SetSelectedLineIdx(0) }) } func (self *TagsController) withSelectedTag(f func(tag *models.Tag) error) func() error { @@ -136,5 +159,5 @@ func (self *TagsController) Context() types.Context { } func (self *TagsController) context() *context.TagsContext { - return self.contexts.Tags + return self.c.Contexts().Tags } diff --git a/pkg/gui/controllers/toggle_whitespace_action.go b/pkg/gui/controllers/toggle_whitespace_action.go new file mode 100644 index 000000000..56eb023f3 --- /dev/null +++ b/pkg/gui/controllers/toggle_whitespace_action.go @@ -0,0 +1,21 @@ +package controllers + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" +) + +type ToggleWhitespaceAction struct { + c *ControllerCommon +} + +func (self *ToggleWhitespaceAction) Call() error { + self.c.State().SetIgnoreWhitespaceInDiffView(!self.c.State().GetIgnoreWhitespaceInDiffView()) + + toastMessage := self.c.Tr.ShowingWhitespaceInDiffView + if self.c.State().GetIgnoreWhitespaceInDiffView() { + toastMessage = self.c.Tr.IgnoringWhitespaceInDiffView + } + self.c.Toast(toastMessage) + + return self.c.CurrentSideContext().HandleFocus(types.OnFocusOpts{}) +} diff --git a/pkg/gui/controllers/undo_controller.go b/pkg/gui/controllers/undo_controller.go index 19fc19e27..bb87bf89d 100644 --- a/pkg/gui/controllers/undo_controller.go +++ b/pkg/gui/controllers/undo_controller.go @@ -20,17 +20,17 @@ import ( type UndoController struct { baseController - *controllerCommon + c *ControllerCommon } var _ types.IController = &UndoController{} func NewUndoController( - common *controllerCommon, + common *ControllerCommon, ) *UndoController { return &UndoController{ - baseController: baseController{}, - controllerCommon: common, + baseController: baseController{}, + c: common, } } @@ -76,7 +76,7 @@ func (self *UndoController) reflogUndo() error { undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"} undoingStatus := self.c.Tr.UndoingStatus - if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { return self.c.ErrorMsg(self.c.Tr.LcCantUndoWhileRebasing) } @@ -104,7 +104,7 @@ func (self *UndoController) reflogUndo() error { Prompt: fmt.Sprintf(self.c.Tr.CheckoutPrompt, action.from), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.Undo) - return self.helpers.Refs.CheckoutRef(action.from, types.CheckoutRefOptions{ + return self.c.Helpers().Refs.CheckoutRef(action.from, types.CheckoutRefOptions{ EnvVars: undoEnvVars, WaitingStatus: undoingStatus, }) @@ -124,7 +124,7 @@ func (self *UndoController) reflogRedo() error { redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"} redoingStatus := self.c.Tr.RedoingStatus - if self.git.Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { + if self.c.Git().Status.WorkingTreeState() == enums.REBASE_MODE_REBASING { return self.c.ErrorMsg(self.c.Tr.LcCantRedoWhileRebasing) } @@ -156,7 +156,7 @@ func (self *UndoController) reflogRedo() error { Prompt: fmt.Sprintf(self.c.Tr.CheckoutPrompt, action.to), HandleConfirm: func() error { self.c.LogAction(self.c.Tr.Actions.Redo) - return self.helpers.Refs.CheckoutRef(action.to, types.CheckoutRefOptions{ + return self.c.Helpers().Refs.CheckoutRef(action.to, types.CheckoutRefOptions{ EnvVars: redoEnvVars, WaitingStatus: redoingStatus, }) @@ -179,7 +179,7 @@ func (self *UndoController) reflogRedo() error { // Though we might support this later, hence the use of the CURRENT_REBASE action kind. func (self *UndoController) parseReflogForActions(onUserAction func(counter int, action reflogAction) (bool, error)) error { counter := 0 - reflogCommits := self.model.FilteredReflogCommits + reflogCommits := self.c.Model().FilteredReflogCommits rebaseFinishCommitSha := "" var action *reflogAction for reflogCommitIdx, reflogCommit := range reflogCommits { @@ -233,14 +233,14 @@ type hardResetOptions struct { // only to be used in the undo flow for now (does an autostash) func (self *UndoController) hardResetWithAutoStash(commitSha string, options hardResetOptions) error { reset := func() error { - if err := self.helpers.Refs.ResetToRef(commitSha, "hard", options.EnvVars); err != nil { + if err := self.c.Helpers().Refs.ResetToRef(commitSha, "hard", options.EnvVars); err != nil { return self.c.Error(err) } return nil } // if we have any modified tracked files we need to ask the user if they want us to stash for them - dirtyWorkingTree := self.helpers.WorkingTree.IsWorkingTreeDirty() + dirtyWorkingTree := self.c.Helpers().WorkingTree.IsWorkingTreeDirty() if dirtyWorkingTree { // offer to autostash changes return self.c.Confirm(types.ConfirmOpts{ @@ -248,14 +248,14 @@ func (self *UndoController) hardResetWithAutoStash(commitSha string, options har Prompt: self.c.Tr.AutoStashPrompt, HandleConfirm: func() error { return self.c.WithWaitingStatus(options.WaitingStatus, func() error { - if err := self.git.Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil { + if err := self.c.Git().Stash.Save(self.c.Tr.StashPrefix + commitSha); err != nil { return self.c.Error(err) } if err := reset(); err != nil { return err } - err := self.git.Stash.Pop(0) + err := self.c.Git().Stash.Pop(0) if err := self.c.Refresh(types.RefreshOptions{}); err != nil { return err } diff --git a/pkg/gui/controllers/vertical_scroll_controller.go b/pkg/gui/controllers/vertical_scroll_controller.go index 388574df6..90958fadd 100644 --- a/pkg/gui/controllers/vertical_scroll_controller.go +++ b/pkg/gui/controllers/vertical_scroll_controller.go @@ -9,13 +9,13 @@ import ( // given we have no fields here, arguably we shouldn't even need this factory // struct, but we're maintaining consistency with the other files. type VerticalScrollControllerFactory struct { - controllerCommon *controllerCommon + c *ControllerCommon viewBufferManagerMap *map[string]*tasks.ViewBufferManager } -func NewVerticalScrollControllerFactory(c *controllerCommon, viewBufferManagerMap *map[string]*tasks.ViewBufferManager) *VerticalScrollControllerFactory { +func NewVerticalScrollControllerFactory(c *ControllerCommon, viewBufferManagerMap *map[string]*tasks.ViewBufferManager) *VerticalScrollControllerFactory { return &VerticalScrollControllerFactory{ - controllerCommon: c, + c: c, viewBufferManagerMap: viewBufferManagerMap, } } @@ -23,7 +23,7 @@ func NewVerticalScrollControllerFactory(c *controllerCommon, viewBufferManagerMa func (self *VerticalScrollControllerFactory) Create(context types.Context) types.IController { return &VerticalScrollController{ baseController: baseController{}, - controllerCommon: self.controllerCommon, + c: self.c, context: context, viewBufferManagerMap: self.viewBufferManagerMap, } @@ -31,7 +31,7 @@ func (self *VerticalScrollControllerFactory) Create(context types.Context) types type VerticalScrollController struct { baseController - *controllerCommon + c *ControllerCommon context types.Context viewBufferManagerMap *map[string]*tasks.ViewBufferManager diff --git a/pkg/gui/controllers/workspace_reset_controller.go b/pkg/gui/controllers/workspace_reset_controller.go index 825d54214..5104b5ac8 100644 --- a/pkg/gui/controllers/workspace_reset_controller.go +++ b/pkg/gui/controllers/workspace_reset_controller.go @@ -13,7 +13,7 @@ func (self *FilesController) createResetMenu() error { red := style.FgRed nukeStr := "git reset --hard HEAD && git clean -fd" - if len(self.model.Submodules) > 0 { + if len(self.c.Model().Submodules) > 0 { nukeStr = fmt.Sprintf("%s (%s)", nukeStr, self.c.Tr.LcAndResetSubmodules) } @@ -25,7 +25,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.NukeWorkingTree) - if err := self.git.WorkingTree.ResetAndClean(); err != nil { + if err := self.c.Git().WorkingTree.ResetAndClean(); err != nil { return self.c.Error(err) } @@ -41,7 +41,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.DiscardUnstagedFileChanges) - if err := self.git.WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { + if err := self.c.Git().WorkingTree.DiscardAnyUnstagedFileChanges(); err != nil { return self.c.Error(err) } @@ -56,7 +56,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveUntrackedFiles) - if err := self.git.WorkingTree.RemoveUntrackedFiles(); err != nil { + if err := self.c.Git().WorkingTree.RemoveUntrackedFiles(); err != nil { return self.c.Error(err) } @@ -72,13 +72,13 @@ func (self *FilesController) createResetMenu() error { Tooltip: self.c.Tr.DiscardStagedChangesDescription, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.RemoveStagedFiles) - if !self.helpers.WorkingTree.IsWorkingTreeDirty() { + if !self.c.Helpers().WorkingTree.IsWorkingTreeDirty() { return self.c.ErrorMsg(self.c.Tr.NoTrackedStagedFilesStash) } - if err := self.git.Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil { + if err := self.c.Git().Stash.SaveStagedChanges("[lazygit] tmp stash"); err != nil { return self.c.Error(err) } - if err := self.git.Stash.DropNewest(); err != nil { + if err := self.c.Git().Stash.DropNewest(); err != nil { return self.c.Error(err) } @@ -93,7 +93,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.SoftReset) - if err := self.git.WorkingTree.ResetSoft("HEAD"); err != nil { + if err := self.c.Git().WorkingTree.ResetSoft("HEAD"); err != nil { return self.c.Error(err) } @@ -108,7 +108,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.MixedReset) - if err := self.git.WorkingTree.ResetMixed("HEAD"); err != nil { + if err := self.c.Git().WorkingTree.ResetMixed("HEAD"); err != nil { return self.c.Error(err) } @@ -123,7 +123,7 @@ func (self *FilesController) createResetMenu() error { }, OnPress: func() error { self.c.LogAction(self.c.Tr.Actions.HardReset) - if err := self.git.WorkingTree.ResetHard("HEAD"); err != nil { + if err := self.c.Git().WorkingTree.ResetHard("HEAD"); err != nil { return self.c.Error(err) } diff --git a/pkg/gui/custom_patch_options_panel.go b/pkg/gui/custom_patch_options_panel.go deleted file mode 100644 index a25fc9580..000000000 --- a/pkg/gui/custom_patch_options_panel.go +++ /dev/null @@ -1,215 +0,0 @@ -package gui - -import ( - "fmt" - - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) handleCreatePatchOptionsMenu() error { - if !gui.git.Patch.PatchBuilder.Active() { - return gui.c.ErrorMsg(gui.c.Tr.NoPatchError) - } - - menuItems := []*types.MenuItem{ - { - Label: "reset patch", - OnPress: gui.helpers.PatchBuilding.Reset, - Key: 'c', - }, - { - Label: "apply patch", - OnPress: func() error { return gui.handleApplyPatch(false) }, - Key: 'a', - }, - { - Label: "apply patch in reverse", - OnPress: func() error { return gui.handleApplyPatch(true) }, - Key: 'r', - }, - } - - if gui.git.Patch.PatchBuilder.CanRebase && gui.git.Status.WorkingTreeState() == enums.REBASE_MODE_NONE { - menuItems = append(menuItems, []*types.MenuItem{ - { - Label: fmt.Sprintf("remove patch from original commit (%s)", gui.git.Patch.PatchBuilder.To), - OnPress: gui.handleDeletePatchFromCommit, - Key: 'd', - }, - { - Label: "move patch out into index", - OnPress: gui.handleMovePatchIntoWorkingTree, - Key: 'i', - }, - { - Label: "move patch into new commit", - OnPress: gui.handlePullPatchIntoNewCommit, - Key: 'n', - }, - }...) - - if gui.currentContext().GetKey() == gui.State.Contexts.LocalCommits.GetKey() { - selectedCommit := gui.getSelectedLocalCommit() - if selectedCommit != nil && gui.git.Patch.PatchBuilder.To != selectedCommit.Sha { - // adding this option to index 1 - menuItems = append( - menuItems[:1], - append( - []*types.MenuItem{ - { - Label: fmt.Sprintf("move patch to selected commit (%s)", selectedCommit.Sha), - OnPress: gui.handleMovePatchToSelectedCommit, - Key: 'm', - }, - }, menuItems[1:]..., - )..., - ) - } - } - } - - menuItems = append(menuItems, []*types.MenuItem{ - { - Label: "copy patch to clipboard", - OnPress: func() error { return gui.copyPatchToClipboard() }, - Key: 'y', - }, - }...) - - return gui.c.Menu(types.CreateMenuOptions{Title: gui.c.Tr.PatchOptionsTitle, Items: menuItems}) -} - -func (gui *Gui) getPatchCommitIndex() int { - for index, commit := range gui.State.Model.Commits { - if commit.Sha == gui.git.Patch.PatchBuilder.To { - return index - } - } - return -1 -} - -func (gui *Gui) validateNormalWorkingTreeState() (bool, error) { - if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE { - return false, gui.c.ErrorMsg(gui.c.Tr.CantPatchWhileRebasingError) - } - return true, nil -} - -func (gui *Gui) returnFocusFromPatchExplorerIfNecessary() error { - if gui.currentContext().GetKey() == gui.State.Contexts.CustomPatchBuilder.GetKey() { - return gui.helpers.PatchBuilding.Escape() - } - return nil -} - -func (gui *Gui) handleDeletePatchFromCommit() error { - if ok, err := gui.validateNormalWorkingTreeState(); !ok { - return err - } - - if err := gui.returnFocusFromPatchExplorerIfNecessary(); err != nil { - return err - } - - return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error { - commitIndex := gui.getPatchCommitIndex() - gui.c.LogAction(gui.c.Tr.Actions.RemovePatchFromCommit) - err := gui.git.Patch.DeletePatchesFromCommit(gui.State.Model.Commits, commitIndex) - return gui.helpers.MergeAndRebase.CheckMergeOrRebase(err) - }) -} - -func (gui *Gui) handleMovePatchToSelectedCommit() error { - if ok, err := gui.validateNormalWorkingTreeState(); !ok { - return err - } - - if err := gui.returnFocusFromPatchExplorerIfNecessary(); err != nil { - return err - } - - return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error { - commitIndex := gui.getPatchCommitIndex() - gui.c.LogAction(gui.c.Tr.Actions.MovePatchToSelectedCommit) - err := gui.git.Patch.MovePatchToSelectedCommit(gui.State.Model.Commits, commitIndex, gui.State.Contexts.LocalCommits.GetSelectedLineIdx()) - return gui.helpers.MergeAndRebase.CheckMergeOrRebase(err) - }) -} - -func (gui *Gui) handleMovePatchIntoWorkingTree() error { - if ok, err := gui.validateNormalWorkingTreeState(); !ok { - return err - } - - if err := gui.returnFocusFromPatchExplorerIfNecessary(); err != nil { - return err - } - - pull := func(stash bool) error { - return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error { - commitIndex := gui.getPatchCommitIndex() - gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoIndex) - err := gui.git.Patch.MovePatchIntoIndex(gui.State.Model.Commits, commitIndex, stash) - return gui.helpers.MergeAndRebase.CheckMergeOrRebase(err) - }) - } - - if gui.helpers.WorkingTree.IsWorkingTreeDirty() { - return gui.c.Confirm(types.ConfirmOpts{ - Title: gui.c.Tr.MustStashTitle, - Prompt: gui.c.Tr.MustStashWarning, - HandleConfirm: func() error { - return pull(true) - }, - }) - } else { - return pull(false) - } -} - -func (gui *Gui) handlePullPatchIntoNewCommit() error { - if ok, err := gui.validateNormalWorkingTreeState(); !ok { - return err - } - - if err := gui.returnFocusFromPatchExplorerIfNecessary(); err != nil { - return err - } - - return gui.c.WithWaitingStatus(gui.c.Tr.RebasingStatus, func() error { - commitIndex := gui.getPatchCommitIndex() - gui.c.LogAction(gui.c.Tr.Actions.MovePatchIntoNewCommit) - err := gui.git.Patch.PullPatchIntoNewCommit(gui.State.Model.Commits, commitIndex) - return gui.helpers.MergeAndRebase.CheckMergeOrRebase(err) - }) -} - -func (gui *Gui) handleApplyPatch(reverse bool) error { - if err := gui.returnFocusFromPatchExplorerIfNecessary(); err != nil { - return err - } - - action := gui.c.Tr.Actions.ApplyPatch - if reverse { - action = "Apply patch in reverse" - } - gui.c.LogAction(action) - if err := gui.git.Patch.PatchBuilder.ApplyPatches(reverse); err != nil { - return gui.c.Error(err) - } - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) -} - -func (gui *Gui) copyPatchToClipboard() error { - patch := gui.git.Patch.PatchBuilder.RenderAggregatedPatch(true) - - gui.c.LogAction(gui.c.Tr.Actions.CopyPatchToClipboard) - if err := gui.os.CopyToClipboard(patch); err != nil { - return gui.c.Error(err) - } - - gui.c.Toast(gui.c.Tr.PatchCopiedToClipboard) - - return nil -} diff --git a/pkg/gui/diff_context_size_test.go b/pkg/gui/diff_context_size_test.go deleted file mode 100644 index 62a784380..000000000 --- a/pkg/gui/diff_context_size_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package gui - -// const diffForTest = `diff --git a/pkg/gui/diff_context_size.go b/pkg/gui/diff_context_size.go -// index 0da0a982..742b7dcf 100644 -// --- a/pkg/gui/diff_context_size.go -// +++ b/pkg/gui/diff_context_size.go -// @@ -9,12 +9,12 @@ func getRefreshFunction(gui *Gui) func()error { -// } -// } else if key == context.MAIN_STAGING_CONTEXT_KEY { -// return func() error { -// - selectedLine := gui.Views.Secondary.SelectedLineIdx() -// + selectedLine := gui.State.Panels.LineByLine.GetSelectedLineIdx() -// return gui.handleRefreshStagingPanel(false, selectedLine) -// } -// } else if key == context.MAIN_PATCH_BUILDING_CONTEXT_KEY { -// ` - -// func setupGuiForTest(gui *Gui) { -// gui.g = &gocui.Gui{} -// gui.Views.Main, _ = gui.prepareView("main") -// gui.Views.Secondary, _ = gui.prepareView("secondary") -// gui.Views.Options, _ = gui.prepareView("options") -// gui.git.Patch.PatchManager = &patch.PatchManager{} -// _, _ = gui.refreshLineByLinePanel(diffForTest, "", false, 11) -// } - -// func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Files }, -// func(gui *Gui) types.Context { return gui.State.Contexts.BranchCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommitFiles }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Stash }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Staging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.PatchBuilding }, -// func(gui *Gui) types.Context { return gui.State.Contexts.SubCommits }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 -// _ = gui.c.PushContext(context) - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Status }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Submodules }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Remotes }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Normal }, -// func(gui *Gui) types.Context { return gui.State.Contexts.ReflogCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.RemoteBranches }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Tags }, -// // not testing this because it will kick straight back to the files context -// // upon pushing the context -// // func(gui *Gui) types.Context { return gui.State.Contexts.Merging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommandLog }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 -// _ = gui.c.PushContext(context) - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Files }, -// func(gui *Gui) types.Context { return gui.State.Contexts.BranchCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommitFiles }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Stash }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Staging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.PatchBuilding }, -// func(gui *Gui) types.Context { return gui.State.Contexts.SubCommits }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(context) - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) { -// contexts := []func(gui *Gui) types.Context{ -// func(gui *Gui) types.Context { return gui.State.Contexts.Status }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Submodules }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Remotes }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Normal }, -// func(gui *Gui) types.Context { return gui.State.Contexts.ReflogCommits }, -// func(gui *Gui) types.Context { return gui.State.Contexts.RemoteBranches }, -// func(gui *Gui) types.Context { return gui.State.Contexts.Tags }, -// // not testing this because it will kick straight back to the files context -// // upon pushing the context -// // func(gui *Gui) types.Context { return gui.State.Contexts.Merging }, -// func(gui *Gui) types.Context { return gui.State.Contexts.CommandLog }, -// } - -// for _, c := range contexts { -// gui := NewDummyGui() -// context := c(gui) -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(context) - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize, string(context.GetKey())) -// } -// } - -// func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(gui.State.Contexts.CommitFiles) -// gui.git.Patch.PatchManager.Start("from", "to", false, false) - -// errorCount := 0 -// gui.PopupHandler = &popup.TestPopupHandler{ -// OnErrorMsg: func(message string) error { -// assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message) -// errorCount += 1 -// return nil -// }, -// } - -// _ = gui.IncreaseContextInDiffView() - -// assert.Equal(t, 1, errorCount) -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize) -// } - -// func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 2 -// _ = gui.c.PushContext(gui.State.Contexts.CommitFiles) -// gui.git.Patch.PatchManager.Start("from", "to", false, false) - -// errorCount := 0 -// gui.PopupHandler = &popup.TestPopupHandler{ -// OnErrorMsg: func(message string) error { -// assert.Equal(t, gui.c.Tr.CantChangeContextSizeError, message) -// errorCount += 1 -// return nil -// }, -// } - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 2, gui.c.UserConfig.Git.DiffContextSize) -// } - -// func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) { -// gui := NewDummyGui() -// setupGuiForTest(gui) -// gui.c.UserConfig.Git.DiffContextSize = 1 - -// _ = gui.DecreaseContextInDiffView() - -// assert.Equal(t, 1, gui.c.UserConfig.Git.DiffContextSize) -// } diff --git a/pkg/gui/diffing.go b/pkg/gui/diffing.go deleted file mode 100644 index def73d2f1..000000000 --- a/pkg/gui/diffing.go +++ /dev/null @@ -1,166 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) exitDiffMode() error { - gui.State.Modes.Diffing = diffing.New() - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) -} - -func (gui *Gui) renderDiff() error { - cmdObj := gui.os.Cmd.New( - fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()), - ) - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Diff", - Task: task, - }, - }) -} - -// currentDiffTerminals returns the current diff terminals of the currently selected item. -// in the case of a branch it returns both the branch and it's upstream name, -// which becomes an option when you bring up the diff menu, but when you're just -// flicking through branches it will be using the local branch name. -func (gui *Gui) currentDiffTerminals() []string { - c := gui.currentSideContext() - - if c.GetKey() == "" { - return nil - } - - switch v := c.(type) { - case *context.WorkingTreeContext, *context.SubmodulesContext: - // TODO: should we just return nil here? - return []string{""} - case *context.CommitFilesContext: - return []string{v.GetRef().RefName()} - case *context.BranchesContext: - // for our local branches we want to include both the branch and its upstream - branch := gui.State.Contexts.Branches.GetSelected() - if branch != nil { - names := []string{branch.ID()} - if branch.IsTrackingRemote() { - names = append(names, branch.ID()+"@{u}") - } - return names - } - return nil - case types.IListContext: - itemId := v.GetSelectedItemId() - - return []string{itemId} - } - - return nil -} - -func (gui *Gui) currentDiffTerminal() string { - names := gui.currentDiffTerminals() - if len(names) == 0 { - return "" - } - return names[0] -} - -func (gui *Gui) currentlySelectedFilename() string { - switch gui.currentContext().GetKey() { - case context.FILES_CONTEXT_KEY, context.COMMIT_FILES_CONTEXT_KEY: - return gui.getSideContextSelectedItemId() - default: - return "" - } -} - -func (gui *Gui) diffStr() string { - output := gui.State.Modes.Diffing.Ref - - right := gui.currentDiffTerminal() - if right != "" { - output += " " + right - } - - if gui.State.Modes.Diffing.Reverse { - output += " -R" - } - - if gui.IgnoreWhitespaceInDiffView { - output += " --ignore-all-space" - } - - file := gui.currentlySelectedFilename() - if file != "" { - output += " -- " + file - } else if gui.State.Modes.Filtering.Active() { - output += " -- " + gui.State.Modes.Filtering.GetPath() - } - - return output -} - -func (gui *Gui) handleCreateDiffingMenuPanel() error { - names := gui.currentDiffTerminals() - - menuItems := []*types.MenuItem{} - for _, name := range names { - name := name - menuItems = append(menuItems, []*types.MenuItem{ - { - Label: fmt.Sprintf("%s %s", gui.c.Tr.LcDiff, name), - OnPress: func() error { - gui.State.Modes.Diffing.Ref = name - // can scope this down based on current view but too lazy right now - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - }, - }, - }...) - } - - menuItems = append(menuItems, []*types.MenuItem{ - { - Label: gui.c.Tr.LcEnterRefToDiff, - OnPress: func() error { - return gui.c.Prompt(types.PromptOpts{ - Title: gui.c.Tr.LcEnteRefName, - FindSuggestionsFunc: gui.helpers.Suggestions.GetRefsSuggestionsFunc(), - HandleConfirm: func(response string) error { - gui.State.Modes.Diffing.Ref = strings.TrimSpace(response) - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - }, - }) - }, - }, - }...) - - if gui.State.Modes.Diffing.Active() { - menuItems = append(menuItems, []*types.MenuItem{ - { - Label: gui.c.Tr.LcSwapDiff, - OnPress: func() error { - gui.State.Modes.Diffing.Reverse = !gui.State.Modes.Diffing.Reverse - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - }, - }, - { - Label: gui.c.Tr.LcExitDiffMode, - OnPress: func() error { - gui.State.Modes.Diffing = diffing.New() - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) - }, - }, - }...) - } - - return gui.c.Menu(types.CreateMenuOptions{Title: gui.c.Tr.DiffingMenuTitle, Items: menuItems}) -} diff --git a/pkg/gui/editors.go b/pkg/gui/editors.go index 94f01db74..1fbba2aad 100644 --- a/pkg/gui/editors.go +++ b/pkg/gui/editors.go @@ -62,14 +62,14 @@ func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch func (gui *Gui) commitMessageEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, false) v.RenderTextArea() - gui.RenderCommitLength() + gui.c.Contexts().CommitMessage.RenderCommitLength() return matched } func (gui *Gui) commitDescriptionEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool { matched := gui.handleEditorKeypress(v.TextArea, key, ch, mod, true) v.RenderTextArea() - gui.RenderCommitLength() + gui.c.Contexts().CommitMessage.RenderCommitLength() return matched } @@ -78,11 +78,12 @@ func (gui *Gui) promptEditor(v *gocui.View, key gocui.Key, ch rune, mod gocui.Mo v.RenderTextArea() - if gui.findSuggestions != nil { + suggestionsContext := gui.State.Contexts.Suggestions + if suggestionsContext.State.FindSuggestions != nil { input := v.TextArea.GetContent() - gui.suggestionsAsyncHandler.Do(func() func() { - suggestions := gui.findSuggestions(input) - return func() { gui.setSuggestions(suggestions) } + suggestionsContext.State.AsyncHandler.Do(func() func() { + suggestions := suggestionsContext.State.FindSuggestions(input) + return func() { suggestionsContext.SetSuggestions(suggestions) } }) } diff --git a/pkg/gui/extras_panel.go b/pkg/gui/extras_panel.go index c36f12a66..927250627 100644 --- a/pkg/gui/extras_panel.go +++ b/pkg/gui/extras_panel.go @@ -15,14 +15,14 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { { Label: gui.c.Tr.ToggleShowCommandLog, OnPress: func() error { - currentContext := gui.currentStaticContext() - if gui.ShowExtrasWindow && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY { + currentContext := gui.c.CurrentStaticContext() + if gui.c.State().GetShowExtrasWindow() && currentContext.GetKey() == context.COMMAND_LOG_CONTEXT_KEY { if err := gui.c.PopContext(); err != nil { return err } } - show := !gui.ShowExtrasWindow - gui.ShowExtrasWindow = show + show := !gui.c.State().GetShowExtrasWindow() + gui.c.State().SetShowExtrasWindow(show) gui.c.GetAppState().HideCommandLog = !show _ = gui.c.SaveAppState() return nil @@ -37,9 +37,9 @@ func (gui *Gui) handleCreateExtrasMenuPanel() error { } func (gui *Gui) handleFocusCommandLog() error { - gui.ShowExtrasWindow = true + gui.c.State().SetShowExtrasWindow(true) // TODO: is this necessary? Can't I just call 'return from context'? - gui.State.Contexts.CommandLog.SetParentContext(gui.currentSideContext()) + gui.State.Contexts.CommandLog.SetParentContext(gui.c.CurrentSideContext()) return gui.c.PushContext(gui.State.Contexts.CommandLog) } diff --git a/pkg/gui/file_watching.go b/pkg/gui/file_watching.go index 01a2d0b88..ff04353e2 100644 --- a/pkg/gui/file_watching.go +++ b/pkg/gui/file_watching.go @@ -17,6 +17,8 @@ import ( // file watching is only really an added bonus for faster refreshing. const MAX_WATCHED_FILES = 50 +var _ types.IFileWatcher = new(fileWatcher) + type fileWatcher struct { Watcher *fsnotify.Watcher WatchedFilenames []string @@ -60,7 +62,7 @@ func (w *fileWatcher) watchFilename(filename string) { w.WatchedFilenames = append(w.WatchedFilenames, filename) } -func (w *fileWatcher) addFilesToFileWatcher(files []*models.File) error { +func (w *fileWatcher) AddFilesToFileWatcher(files []*models.File) error { if w.Disabled { return nil } @@ -102,7 +104,7 @@ func min(a int, b int) int { // NOTE: given that we often edit files ourselves, this may make us end up refreshing files too often // TODO: consider watching the whole directory recursively (could be more expensive) -func (gui *Gui) watchFilesForChanges() { +func (gui *Gui) WatchFilesForChanges() { gui.fileWatcher = NewFileWatcher(gui.Log) if gui.fileWatcher.Disabled { return @@ -117,7 +119,7 @@ func (gui *Gui) watchFilesForChanges() { continue } // only refresh if we're not already - if !gui.State.IsRefreshingFiles { + if !gui.IsRefreshingFiles { _ = gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.FILES}}) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go deleted file mode 100644 index 61ec83125..000000000 --- a/pkg/gui/files_panel.go +++ /dev/null @@ -1,95 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/filetree" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) getSelectedFileNode() *filetree.FileNode { - return gui.State.Contexts.Files.GetSelected() -} - -func (gui *Gui) getSelectedFile() *models.File { - node := gui.getSelectedFileNode() - if node == nil { - return nil - } - return node.File -} - -func (gui *Gui) filesRenderToMain() error { - node := gui.getSelectedFileNode() - - if node == nil { - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.DiffTitle, - Task: types.NewRenderStringTask(gui.c.Tr.NoChangedFiles), - }, - }) - } - - if node.File != nil && node.File.HasInlineMergeConflicts { - hasConflicts, err := gui.helpers.MergeConflicts.SetMergeState(node.GetPath()) - if err != nil { - return err - } - - if hasConflicts { - return gui.refreshMergePanel(false) - } - } - - gui.helpers.MergeConflicts.ResetMergeState() - - pair := gui.c.MainViewPairs().Normal - if node.File != nil { - pair = gui.c.MainViewPairs().Staging - } - - split := gui.c.UserConfig.Gui.SplitDiff == "always" || (node.GetHasUnstagedChanges() && node.GetHasStagedChanges()) - mainShowsStaged := !split && node.GetHasStagedChanges() - - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, mainShowsStaged, gui.IgnoreWhitespaceInDiffView) - title := gui.c.Tr.UnstagedChanges - if mainShowsStaged { - title = gui.c.Tr.StagedChanges - } - refreshOpts := types.RefreshMainOpts{ - Pair: pair, - Main: &types.ViewUpdateOpts{ - Task: types.NewRunPtyTask(cmdObj.GetCmd()), - Title: title, - }, - } - - if split { - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(node, false, true, gui.IgnoreWhitespaceInDiffView) - - title := gui.c.Tr.StagedChanges - if mainShowsStaged { - title = gui.c.Tr.UnstagedChanges - } - - refreshOpts.Secondary = &types.ViewUpdateOpts{ - Title: title, - Task: types.NewRunPtyTask(cmdObj.GetCmd()), - } - } - - return gui.c.RenderToMainViews(refreshOpts) -} - -func (gui *Gui) getCommitMessageSetTextareaTextFn(getView func() *gocui.View) func(string) { - return func(text string) { - // using a getView function so that we don't need to worry about when the view is created - view := getView() - view.ClearTextArea() - view.TextArea.TypeString(text) - gui.resizeCommitMessagePanels() - view.RenderTextArea() - } -} diff --git a/pkg/gui/filtering.go b/pkg/gui/filtering.go deleted file mode 100644 index 2746d0e2b..000000000 --- a/pkg/gui/filtering.go +++ /dev/null @@ -1,56 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) validateNotInFilterMode() bool { - if gui.State.Modes.Filtering.Active() { - _ = gui.c.Confirm(types.ConfirmOpts{ - Title: gui.c.Tr.MustExitFilterModeTitle, - Prompt: gui.c.Tr.MustExitFilterModePrompt, - HandleConfirm: gui.exitFilterMode, - }) - - return false - } - return true -} - -func (gui *Gui) outsideFilterMode(f func() error) func() error { - return func() error { - if !gui.validateNotInFilterMode() { - return nil - } - - return f() - } -} - -func (gui *Gui) exitFilterMode() error { - return gui.clearFiltering() -} - -func (gui *Gui) clearFiltering() error { - gui.State.Modes.Filtering.Reset() - if gui.State.ScreenMode == SCREEN_HALF { - gui.State.ScreenMode = SCREEN_NORMAL - } - - return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}}) -} - -func (gui *Gui) setFiltering(path string) error { - gui.State.Modes.Filtering.SetPath(path) - if gui.State.ScreenMode == SCREEN_NORMAL { - gui.State.ScreenMode = SCREEN_HALF - } - - if err := gui.c.PushContext(gui.State.Contexts.LocalCommits); err != nil { - return err - } - - return gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.COMMITS}, Then: func() { - gui.State.Contexts.LocalCommits.SetSelectedLineIdx(0) - }}) -} diff --git a/pkg/gui/filtering_menu_panel.go b/pkg/gui/filtering_menu_panel.go deleted file mode 100644 index 97327324e..000000000 --- a/pkg/gui/filtering_menu_panel.go +++ /dev/null @@ -1,57 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) handleCreateFilteringMenuPanel() error { - fileName := "" - switch gui.currentSideListContext() { - case gui.State.Contexts.Files: - node := gui.getSelectedFileNode() - if node != nil { - fileName = node.GetPath() - } - case gui.State.Contexts.CommitFiles: - node := gui.State.Contexts.CommitFiles.GetSelected() - if node != nil { - fileName = node.GetPath() - } - } - - menuItems := []*types.MenuItem{} - - if fileName != "" { - menuItems = append(menuItems, &types.MenuItem{ - Label: fmt.Sprintf("%s '%s'", gui.c.Tr.LcFilterBy, fileName), - OnPress: func() error { - return gui.setFiltering(fileName) - }, - }) - } - - menuItems = append(menuItems, &types.MenuItem{ - Label: gui.c.Tr.LcFilterPathOption, - OnPress: func() error { - return gui.c.Prompt(types.PromptOpts{ - FindSuggestionsFunc: gui.helpers.Suggestions.GetFilePathSuggestionsFunc(), - Title: gui.c.Tr.EnterFileName, - HandleConfirm: func(response string) error { - return gui.setFiltering(strings.TrimSpace(response)) - }, - }) - }, - }) - - if gui.State.Modes.Filtering.Active() { - menuItems = append(menuItems, &types.MenuItem{ - Label: gui.c.Tr.LcExitFilterMode, - OnPress: gui.clearFiltering, - }) - } - - return gui.c.Menu(types.CreateMenuOptions{Title: gui.c.Tr.FilteringMenuTitle, Items: menuItems}) -} diff --git a/pkg/gui/global_handlers.go b/pkg/gui/global_handlers.go index 326b856bd..fc07b5846 100644 --- a/pkg/gui/global_handlers.go +++ b/pkg/gui/global_handlers.go @@ -5,62 +5,12 @@ import ( "strings" "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) const HORIZONTAL_SCROLL_FACTOR = 3 -// these views need to be re-rendered when the screen mode changes. The commits view, -// for example, will show authorship information in half and full screen mode. -func (gui *Gui) rerenderViewsWithScreenModeDependentContent() error { - // for now we re-render all list views. - for _, context := range gui.getListContexts() { - if err := gui.rerenderView(context.GetView()); err != nil { - return err - } - } - - return nil -} - -func nextIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { - for i, val := range sl { - if val == current { - if i == len(sl)-1 { - return sl[0] - } - return sl[i+1] - } - } - return sl[0] -} - -func prevIntInCycle(sl []WindowMaximisation, current WindowMaximisation) WindowMaximisation { - for i, val := range sl { - if val == current { - if i > 0 { - return sl[i-1] - } - return sl[len(sl)-1] - } - } - return sl[len(sl)-1] -} - -func (gui *Gui) nextScreenMode() error { - gui.State.ScreenMode = nextIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) - - return gui.rerenderViewsWithScreenModeDependentContent() -} - -func (gui *Gui) prevScreenMode() error { - gui.State.ScreenMode = prevIntInCycle([]WindowMaximisation{SCREEN_NORMAL, SCREEN_HALF, SCREEN_FULL}, gui.State.ScreenMode) - - return gui.rerenderViewsWithScreenModeDependentContent() -} - func (gui *Gui) scrollUpView(view *gocui.View) { view.ScrollUp(gui.c.UserConfig.Gui.ScrollHeight) } @@ -113,13 +63,13 @@ func (gui *Gui) scrollDownMain() error { } func (gui *Gui) mainView() *gocui.View { - viewName := gui.getViewNameForWindow("main") + viewName := gui.helpers.Window.GetViewNameForWindow("main") view, _ := gui.g.View(viewName) return view } func (gui *Gui) secondaryView() *gocui.View { - viewName := gui.getViewNameForWindow("secondary") + viewName := gui.helpers.Window.GetViewNameForWindow("secondary") view, _ := gui.g.View(viewName) return view } @@ -158,21 +108,19 @@ func (gui *Gui) scrollDownConfirmationPanel() error { return nil } -func (gui *Gui) handleRefresh() error { - return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) -} - -func (gui *Gui) backgroundFetch() (err error) { - err = gui.git.Sync.Fetch(git_commands.FetchOptions{Background: true}) - - _ = gui.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.BRANCHES, types.COMMITS, types.REMOTES, types.TAGS}, Mode: types.ASYNC}) - - return err -} - func (gui *Gui) handleCopySelectedSideContextItemToClipboard() error { // important to note that this assumes we've selected an item in a side context - itemId := gui.getSideContextSelectedItemId() + currentSideContext := gui.c.CurrentSideContext() + if currentSideContext == nil { + return nil + } + + listContext, ok := currentSideContext.(types.IListContext) + if !ok { + return nil + } + + itemId := listContext.GetSelectedItemId() if itemId == "" { return nil diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 2d1e3e334..6b42da4fc 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -1,6 +1,7 @@ package gui import ( + goContext "context" "fmt" "io" "os" @@ -8,6 +9,7 @@ import ( "sync" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazycore/pkg/boxlayout" appTypes "github.com/jesseduffield/lazygit/pkg/app/types" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -28,11 +30,11 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/presentation/graph" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands" + "github.com/jesseduffield/lazygit/pkg/gui/status" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/integration/components" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" - "github.com/jesseduffield/lazygit/pkg/snake" "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/updates" @@ -41,35 +43,11 @@ import ( "gopkg.in/ozeidan/fuzzy-patricia.v3/patricia" ) -// screen sizing determines how much space your selected window takes up (window -// as in panel, not your terminal's window). Sometimes you want a bit more space -// to see the contents of a panel, and this keeps track of how much maximisation -// you've set -type WindowMaximisation int - -const ( - SCREEN_NORMAL WindowMaximisation = iota - SCREEN_HALF - SCREEN_FULL -) - const StartupPopupVersion = 5 // OverlappingEdges determines if panel edges overlap var OverlappingEdges = false -type ContextManager struct { - ContextStack []types.Context - sync.RWMutex -} - -func NewContextManager() ContextManager { - return ContextManager{ - ContextStack: []types.Context{}, - RWMutex: sync.RWMutex{}, - } -} - type Repo string // Gui wraps the gocui Gui object which handles rendering and events @@ -90,7 +68,7 @@ type Gui struct { RepoStateMap map[Repo]*GuiRepoState Config config.AppConfigurer Updater *updates.Updater - statusManager *statusManager + statusManager *status.StatusManager waitForIntro sync.WaitGroup fileWatcher *fileWatcher viewBufferManagerMap map[string]*tasks.ViewBufferManager @@ -106,10 +84,6 @@ type Gui struct { Mutexes types.Mutexes - // findSuggestions will take a string that the user has typed into a prompt - // and return a slice of suggestions which match that string. - findSuggestions func(string) []*types.Suggestion - // when you enter into a submodule we'll append the superproject's path to this array // so that you can return to the superproject RepoPathStack *utils.StringStack @@ -117,12 +91,7 @@ type Gui struct { // this tells us whether our views have been initially set up ViewsSetup bool - Views Views - - // if we've suspended the gui (e.g. because we've switched to a subprocess) - // we typically want to pause some things that are running like background - // file refreshes - PauseBackgroundThreads bool + Views types.Views // Log of the commands that get run, to be displayed to the user. CmdLog []string @@ -130,8 +99,6 @@ type Gui struct { // the extras window contains things like the command log ShowExtrasWindow bool - suggestionsAsyncHandler *tasks.AsyncHandler - PopupHandler types.IPopupHandler IsNewRepo bool @@ -139,6 +106,8 @@ type Gui struct { // flag as to whether or not the diff view should ignore whitespace IgnoreWhitespaceInDiffView bool + IsRefreshingFiles bool + // we use this to decide whether we'll return to the original directory that // lazygit was opened in, or if we'll retain the one we're currently in. RetainOriginalDir bool @@ -153,10 +122,68 @@ type Gui struct { // process InitialDir string - c *types.HelperCommon - helpers *helpers.Helpers + BackgroundRoutineMgr *BackgroundRoutineMgr + // for accessing the gui's state from outside this package + stateAccessor *StateAccessor - snakeGame *snake.Game + Updating bool + + c *helpers.HelperCommon + helpers *helpers.Helpers +} + +type StateAccessor struct { + gui *Gui +} + +var _ types.IStateAccessor = new(StateAccessor) + +func (self *StateAccessor) GetIgnoreWhitespaceInDiffView() bool { + return self.gui.IgnoreWhitespaceInDiffView +} + +func (self *StateAccessor) SetIgnoreWhitespaceInDiffView(value bool) { + self.gui.IgnoreWhitespaceInDiffView = value +} + +func (self *StateAccessor) GetRepoPathStack() *utils.StringStack { + return self.gui.RepoPathStack +} + +func (self *StateAccessor) GetUpdating() bool { + return self.gui.Updating +} + +func (self *StateAccessor) SetUpdating(value bool) { + self.gui.Updating = value +} + +func (self *StateAccessor) GetRepoState() types.IRepoStateAccessor { + return self.gui.State +} + +func (self *StateAccessor) GetIsRefreshingFiles() bool { + return self.gui.IsRefreshingFiles +} + +func (self *StateAccessor) SetIsRefreshingFiles(value bool) { + self.gui.IsRefreshingFiles = value +} + +func (self *StateAccessor) GetShowExtrasWindow() bool { + return self.gui.ShowExtrasWindow +} + +func (self *StateAccessor) SetShowExtrasWindow(value bool) { + self.gui.ShowExtrasWindow = value +} + +func (self *StateAccessor) GetRetainOriginalDir() bool { + return self.gui.RetainOriginalDir +} + +func (self *StateAccessor) SetRetainOriginalDir(value bool) { + self.gui.RetainOriginalDir = value } // we keep track of some stuff from one render to the next to see if certain @@ -171,19 +198,14 @@ type GuiRepoState struct { Model *types.Model Modes *types.Modes - // Suggestions will sometimes appear when typing into a prompt - Suggestions []*types.Suggestion - - Updating bool SplitMainPanel bool LimitCommits bool - IsRefreshingFiles bool - Searching searchingState - StartupStage StartupStage // Allows us to not load everything at once + Searching searchingState + StartupStage types.StartupStage // Allows us to not load everything at once - ContextManager ContextManager - Contexts *context.ContextTree + ContextMgr *ContextMgr + Contexts *context.ContextTree // WindowViewNameMap is a mapping of windows to the current view of that window. // Some views move between windows for example the commitFiles view and when cycling through @@ -195,25 +217,63 @@ type GuiRepoState struct { // back in sync with the repo state ViewsSetup bool - ScreenMode WindowMaximisation + ScreenMode types.WindowMaximisation CurrentPopupOpts *types.CreatePopupPanelOpts } +var _ types.IRepoStateAccessor = new(GuiRepoState) + +func (self *GuiRepoState) GetViewsSetup() bool { + return self.ViewsSetup +} + +func (self *GuiRepoState) GetWindowViewNameMap() *utils.ThreadSafeMap[string, string] { + return self.WindowViewNameMap +} + +func (self *GuiRepoState) GetStartupStage() types.StartupStage { + return self.StartupStage +} + +func (self *GuiRepoState) SetStartupStage(value types.StartupStage) { + self.StartupStage = value +} + +func (self *GuiRepoState) GetCurrentPopupOpts() *types.CreatePopupPanelOpts { + return self.CurrentPopupOpts +} + +func (self *GuiRepoState) SetCurrentPopupOpts(value *types.CreatePopupPanelOpts) { + self.CurrentPopupOpts = value +} + +func (self *GuiRepoState) GetScreenMode() types.WindowMaximisation { + return self.ScreenMode +} + +func (self *GuiRepoState) SetScreenMode(value types.WindowMaximisation) { + self.ScreenMode = value +} + +func (self *GuiRepoState) IsSearching() bool { + return self.Searching.isSearching +} + +func (self *GuiRepoState) SetSplitMainPanel(value bool) { + self.SplitMainPanel = value +} + +func (self *GuiRepoState) GetSplitMainPanel() bool { + return self.SplitMainPanel +} + type searchingState struct { view *gocui.View isSearching bool searchString string } -// startup stages so we don't need to load everything at once -type StartupStage int - -const ( - INITIAL StartupStage = iota - COMPLETE -) - func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { var err error gui.git, err = commands.NewGitCommand( @@ -227,14 +287,18 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { return err } - gui.resetState(startArgs, reuseState) + contextToPush := gui.resetState(startArgs, reuseState) - gui.resetControllers() + gui.resetHelpersAndControllers() if err := gui.resetKeybindings(); err != nil { return err } + if err := gui.c.PushContext(contextToPush); err != nil { + return err + } + return nil } @@ -247,7 +311,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error { // it gets a bit confusing to land back in the status panel when visiting a repo // you've already switched from. There's no doubt some easy way to make the UX // optimal for all cases but I'm too lazy to think about what that is right now -func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { +func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) types.Context { currentDir, err := os.Getwd() if reuseState { @@ -262,7 +326,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { gui.State.CurrentPopupOpts = nil gui.Mutexes.PopupMutex.Unlock() - return + return gui.c.CurrentContext() } } else { gui.c.Log.Error(err) @@ -271,11 +335,8 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { contextTree := gui.contextTree() - initialContext := initialContext(contextTree, startArgs) initialScreenMode := initialScreenMode(startArgs, gui.Config) - initialWindowViewNameMap := gui.initialWindowViewNameMap(contextTree) - gui.State = &GuiRepoState{ Model: &types.Model{ CommitFiles: nil, @@ -293,32 +354,40 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) { Diffing: diffing.New(), }, ScreenMode: initialScreenMode, - // TODO: put contexts in the context manager - ContextManager: NewContextManager(), + // TODO: only use contexts from context manager + ContextMgr: NewContextMgr(gui, contextTree), Contexts: contextTree, - WindowViewNameMap: initialWindowViewNameMap, - } - - if err := gui.c.PushContext(initialContext); err != nil { - gui.c.Log.Error(err) + WindowViewNameMap: initialWindowViewNameMap(contextTree), } gui.RepoStateMap[Repo(currentDir)] = gui.State + + return initialContext(contextTree, startArgs) } -func initialScreenMode(startArgs appTypes.StartArgs, config config.AppConfigurer) WindowMaximisation { +func initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] { + result := utils.NewThreadSafeMap[string, string]() + + for _, context := range contextTree.Flatten() { + result.Set(context.GetWindowName(), context.GetViewName()) + } + + return result +} + +func initialScreenMode(startArgs appTypes.StartArgs, config config.AppConfigurer) types.WindowMaximisation { if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone { - return SCREEN_HALF + return types.SCREEN_HALF } else { defaultWindowSize := config.GetUserConfig().Gui.WindowSize switch defaultWindowSize { case "half": - return SCREEN_HALF + return types.SCREEN_HALF case "full": - return SCREEN_FULL + return types.SCREEN_FULL default: - return SCREEN_NORMAL + return types.SCREEN_NORMAL } } } @@ -346,6 +415,10 @@ func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartAr return initialContext } +func (gui *Gui) Contexts() *context.ContextTree { + return gui.State.Contexts +} + // for now the split view will always be on // NewGui builds a new gui handler func NewGui( @@ -357,18 +430,17 @@ func NewGui( initialDir string, ) (*Gui, error) { gui := &Gui{ - Common: cmn, - gitVersion: gitVersion, - Config: config, - Updater: updater, - statusManager: &statusManager{}, - viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, - viewPtmxMap: map[string]*os.File{}, - showRecentRepos: showRecentRepos, - RepoPathStack: &utils.StringStack{}, - RepoStateMap: map[Repo]*GuiRepoState{}, - CmdLog: []string{}, - suggestionsAsyncHandler: tasks.NewAsyncHandler(), + Common: cmn, + gitVersion: gitVersion, + Config: config, + Updater: updater, + statusManager: status.NewStatusManager(), + viewBufferManagerMap: map[string]*tasks.ViewBufferManager{}, + viewPtmxMap: map[string]*os.File{}, + showRecentRepos: showRecentRepos, + RepoPathStack: &utils.StringStack{}, + RepoStateMap: map[Repo]*GuiRepoState{}, + CmdLog: []string{}, // originally we could only hide the command log permanently via the config // but now we do it via state. So we need to still support the config for the @@ -387,22 +459,24 @@ func NewGui( InitialDir: initialDir, } - gui.watchFilesForChanges() + gui.WatchFilesForChanges() gui.PopupHandler = popup.NewPopupHandler( cmn, - gui.createPopupPanel, + func(ctx goContext.Context, opts types.CreatePopupPanelOpts) error { + return gui.helpers.Confirmation.CreatePopupPanel(ctx, opts) + }, func() error { return gui.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) }, - gui.popContext, - gui.currentContext, + func() error { return gui.State.ContextMgr.Pop() }, + func() types.Context { return gui.State.ContextMgr.Current() }, gui.createMenu, - gui.withWaitingStatus, - gui.toast, + func(message string, f func() error) { gui.helpers.AppStatus.WithWaitingStatus(message, f) }, + func(message string) { gui.helpers.AppStatus.Toast(message) }, func() string { return gui.Views.Confirmation.TextArea.GetContent() }, ) guiCommon := &guiCommon{gui: gui, IPopupHandler: gui.PopupHandler} - helperCommon := &types.HelperCommon{IGuiCommon: guiCommon, Common: cmn} + helperCommon := &helpers.HelperCommon{IGuiCommon: guiCommon, Common: cmn, IGetContexts: gui} credentialsHelper := helpers.NewCredentialsHelper(helperCommon) @@ -425,6 +499,9 @@ func NewGui( icons.SetIconEnabled(gui.UserConfig.Gui.ShowIcons) presentation.SetCustomBranches(gui.UserConfig.Gui.BranchColors) + gui.BackgroundRoutineMgr = &BackgroundRoutineMgr{gui: gui} + gui.stateAccessor = &StateAccessor{gui: gui} + return gui, nil } @@ -535,7 +612,7 @@ func (gui *Gui) Run(startArgs appTypes.StartArgs) error { gui.waitForIntro.Add(1) - gui.startBackgroundRoutines() + gui.BackgroundRoutineMgr.startBackgroundRoutines() gui.c.Log.Info("starting main loop") @@ -560,12 +637,12 @@ func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error { switch err { case gocui.ErrQuit: - if gui.RetainOriginalDir { - if err := gui.recordDirectory(gui.InitialDir); err != nil { + if gui.c.State().GetRetainOriginalDir() { + if err := gui.helpers.RecordDirectory.RecordDirectory(gui.InitialDir); err != nil { return err } } else { - if err := gui.recordCurrentDirectory(); err != nil { + if err := gui.helpers.RecordDirectory.RecordCurrentDirectory(); err != nil { return err } } @@ -635,7 +712,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, return false, gui.c.Error(err) } - gui.PauseBackgroundThreads = true + gui.BackgroundRoutineMgr.PauseBackgroundThreads(true) + defer gui.BackgroundRoutineMgr.PauseBackgroundThreads(false) cmdErr := gui.runSubprocess(subprocess) @@ -643,8 +721,6 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, return false, err } - gui.PauseBackgroundThreads = false - if cmdErr != nil { return false, gui.c.Error(cmdErr) } @@ -747,3 +823,7 @@ func (gui *Gui) onUIThread(f func() error) { return f() }) } + +func (gui *Gui) getWindowDimensions(informationStr string, appStatus string) map[string]boxlayout.Dimensions { + return gui.helpers.WindowArrangement.GetWindowDimensions(informationStr, appStatus) +} diff --git a/pkg/gui/gui_common.go b/pkg/gui/gui_common.go index 7c306a0d5..f5f977272 100644 --- a/pkg/gui/gui_common.go +++ b/pkg/gui/gui_common.go @@ -1,8 +1,8 @@ package gui import ( - "errors" - + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/types" @@ -25,7 +25,7 @@ func (self *guiCommon) LogCommand(cmdStr string, isCommandLine bool) { } func (self *guiCommon) Refresh(opts types.RefreshOptions) error { - return self.gui.Refresh(opts) + return self.gui.helpers.Refresh.Refresh(opts) } func (self *guiCommon) PostRefreshUpdate(context types.Context) error { @@ -41,37 +41,39 @@ func (self *guiCommon) RunSubprocess(cmdObj oscommands.ICmdObj) (bool, error) { } func (self *guiCommon) PushContext(context types.Context, opts ...types.OnFocusOpts) error { - singleOpts := types.OnFocusOpts{} - if len(opts) > 0 { - // using triple dot but you should only ever pass one of these opt structs - if len(opts) > 1 { - return errors.New("cannot pass multiple opts to pushContext") - } - - singleOpts = opts[0] - } - - return self.gui.pushContext(context, singleOpts) + return self.gui.State.ContextMgr.Push(context, opts...) } func (self *guiCommon) PopContext() error { - return self.gui.popContext() + return self.gui.State.ContextMgr.Pop() +} + +func (self *guiCommon) ReplaceContext(context types.Context) error { + return self.gui.State.ContextMgr.Replace(context) } func (self *guiCommon) RemoveContexts(contexts []types.Context) error { - return self.gui.removeContexts(contexts) + return self.gui.State.ContextMgr.RemoveContexts(contexts) } func (self *guiCommon) CurrentContext() types.Context { - return self.gui.currentContext() + return self.gui.State.ContextMgr.Current() } func (self *guiCommon) CurrentStaticContext() types.Context { - return self.gui.currentStaticContext() + return self.gui.State.ContextMgr.CurrentStatic() +} + +func (self *guiCommon) CurrentSideContext() types.Context { + return self.gui.State.ContextMgr.CurrentSide() } func (self *guiCommon) IsCurrentContext(c types.Context) bool { - return self.CurrentContext().GetKey() == c.GetKey() + return self.gui.State.ContextMgr.IsCurrent(c) +} + +func (self *guiCommon) Context() types.IContextMgr { + return self.gui.State.ContextMgr } func (self *guiCommon) GetAppState() *config.AppState { @@ -82,14 +84,54 @@ func (self *guiCommon) SaveAppState() error { return self.gui.Config.SaveAppState() } +func (self *guiCommon) GetConfig() config.AppConfigurer { + return self.gui.Config +} + +func (self *guiCommon) ResetViewOrigin(view *gocui.View) { + self.gui.resetViewOrigin(view) +} + +func (self *guiCommon) SetViewContent(view *gocui.View, content string) { + self.gui.setViewContent(view, content) +} + func (self *guiCommon) Render() { self.gui.render() } +func (self *guiCommon) Views() types.Views { + return self.gui.Views +} + +func (self *guiCommon) Git() *commands.GitCommand { + return self.gui.git +} + +func (self *guiCommon) OS() *oscommands.OSCommand { + return self.gui.os +} + +func (self *guiCommon) Modes() *types.Modes { + return self.gui.State.Modes +} + +func (self *guiCommon) Model() *types.Model { + return self.gui.State.Model +} + +func (self *guiCommon) Mutexes() types.Mutexes { + return self.gui.Mutexes +} + func (self *guiCommon) OpenSearch() { _ = self.gui.handleOpenSearch(self.gui.currentViewName()) } +func (self *guiCommon) GocuiGui() *gocui.Gui { + return self.gui.g +} + func (self *guiCommon) OnUIThread(f func() error) { self.gui.onUIThread(f) } @@ -106,3 +148,19 @@ func (self *guiCommon) MainViewPairs() types.MainViewPairs { MergeConflicts: self.gui.mergingMainContextPair(), } } + +func (self *guiCommon) State() types.IStateAccessor { + return self.gui.stateAccessor +} + +func (self *guiCommon) KeybindingsOpts() types.KeybindingsOpts { + return self.gui.keybindingOpts() +} + +func (self *guiCommon) IsAnyModeActive() bool { + return self.gui.helpers.Mode.IsAnyModeActive() +} + +func (self *guiCommon) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []*gocui.ViewMouseBinding) { + return self.gui.GetInitialKeybindingsWithCustomCommands() +} diff --git a/pkg/gui/gui_driver.go b/pkg/gui/gui_driver.go index 35b6a870d..c409ebdb9 100644 --- a/pkg/gui/gui_driver.go +++ b/pkg/gui/gui_driver.go @@ -50,7 +50,7 @@ func (self *GuiDriver) CurrentContext() types.Context { } func (self *GuiDriver) ContextForView(viewName string) types.Context { - context, ok := self.gui.contextForView(viewName) + context, ok := self.gui.helpers.View.ContextForView(viewName) if !ok { return nil } diff --git a/pkg/gui/information_panel.go b/pkg/gui/information_panel.go index 1577e3a2e..5de212b4d 100644 --- a/pkg/gui/information_panel.go +++ b/pkg/gui/information_panel.go @@ -3,15 +3,14 @@ package gui import ( "fmt" - "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/constants" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/mattn/go-runewidth" ) func (gui *Gui) informationStr() string { - if activeMode, ok := gui.getActiveMode(); ok { - return activeMode.description() + if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok { + return activeMode.Description() } if gui.g.Mouse { @@ -23,18 +22,6 @@ func (gui *Gui) informationStr() string { } } -func (gui *Gui) getActiveMode() (modeStatus, bool) { - return slices.Find(gui.modeStatuses(), func(mode modeStatus) bool { - return mode.isActive() - }) -} - -func (gui *Gui) isAnyModeActive() bool { - return slices.Some(gui.modeStatuses(), func(mode modeStatus) bool { - return mode.isActive() - }) -} - func (gui *Gui) handleInfoClick() error { if !gui.g.Mouse { return nil @@ -45,11 +32,11 @@ func (gui *Gui) handleInfoClick() error { cx, _ := view.Cursor() width, _ := view.Size() - if activeMode, ok := gui.getActiveMode(); ok { + if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok { if width-cx > runewidth.StringWidth(gui.c.Tr.ResetInParentheses) { return nil } - return activeMode.reset() + return activeMode.Reset() } // if we're not in an active mode we show the donate button diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index b63c91905..c4715de52 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -11,7 +11,7 @@ import ( func (gui *Gui) noPopupPanel(f func() error) func() error { return func() error { - if gui.popupPanelFocused() { + if gui.helpers.Confirmation.IsPopupPanelFocused() { return nil } @@ -19,6 +19,29 @@ func (gui *Gui) noPopupPanel(f func() error) func() error { } } +func (gui *Gui) outsideFilterMode(f func() error) func() error { + return func() error { + if !gui.validateNotInFilterMode() { + return nil + } + + return f() + } +} + +func (gui *Gui) validateNotInFilterMode() bool { + if gui.State.Modes.Filtering.Active() { + _ = gui.c.Confirm(types.ConfirmOpts{ + Title: gui.c.Tr.MustExitFilterModeTitle, + Prompt: gui.c.Tr.MustExitFilterModePrompt, + HandleConfirm: gui.helpers.Mode.ExitFilterMode, + }) + + return false + } + return true +} + // only to be called from the cheatsheet generate script. This mutates the Gui struct. func (self *Gui) GetCheatsheetKeybindings() []*types.Binding { self.g = &gocui.Gui{} @@ -29,13 +52,13 @@ func (self *Gui) GetCheatsheetKeybindings() []*types.Binding { self.helpers = helpers.NewStubHelpers() self.State = &GuiRepoState{} self.State.Contexts = self.contextTree() - self.resetControllers() + self.State.ContextMgr = NewContextMgr(self, self.State.Contexts) + self.resetHelpersAndControllers() bindings, _ := self.GetInitialKeybindings() return bindings } -// renaming receiver to 'self' to aid refactoring. Will probably end up moving all Gui handlers to this pattern eventually. -func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBinding) { +func (self *Gui) keybindingOpts() types.KeybindingsOpts { config := self.c.UserConfig.Keybinding guards := types.KeybindingGuards{ @@ -43,41 +66,22 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi NoPopupPanel: self.noPopupPanel, } - opts := types.KeybindingsOpts{ + return types.KeybindingsOpts{ GetKey: keybindings.GetKey, Config: config, Guards: guards, } +} + +// renaming receiver to 'self' to aid refactoring. Will probably end up moving all Gui handlers to this pattern eventually. +func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBinding) { + opts := self.c.KeybindingsOpts() bindings := []*types.Binding{ - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.Quit), - Modifier: gocui.ModNone, - Handler: self.handleQuit, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.QuitWithoutChangingDirectory), - Modifier: gocui.ModNone, - Handler: self.handleQuitWithoutChangingDirectory, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.QuitAlt1), - Modifier: gocui.ModNone, - Handler: self.handleQuit, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.Return), - Modifier: gocui.ModNone, - Handler: self.handleTopLevelReturn, - }, { ViewName: "", Key: opts.GetKey(opts.Config.Universal.OpenRecentRepos), - Handler: self.handleCreateRecentReposMenu, + Handler: self.helpers.Repos.CreateRecentReposMenu, Description: self.c.Tr.SwitchRepo, }, { @@ -118,83 +122,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Modifier: gocui.ModNone, Handler: self.scrollDownMain, }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.CreateRebaseOptionsMenu), - Handler: self.helpers.MergeAndRebase.CreateRebaseOptionsMenu, - Description: self.c.Tr.ViewMergeRebaseOptions, - OpensMenu: true, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.CreatePatchOptionsMenu), - Handler: self.handleCreatePatchOptionsMenu, - Description: self.c.Tr.ViewPatchOptions, - OpensMenu: true, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.Refresh), - Handler: self.handleRefresh, - Description: self.c.Tr.LcRefresh, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.OptionMenu), - Handler: self.handleCreateOptionsMenu, - OpensMenu: true, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.OptionMenuAlt1), - Modifier: gocui.ModNone, - // we have the description on the alt key and not the main key for legacy reasons - // (the original main key was 'x' but we've reassigned that to other purposes) - Description: self.c.Tr.LcOpenMenu, - Handler: self.handleCreateOptionsMenu, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Universal.Edit), - Handler: self.handleEditConfig, - Description: self.c.Tr.EditConfig, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.NextScreenMode), - Handler: self.nextScreenMode, - Description: self.c.Tr.LcNextScreenMode, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.PrevScreenMode), - Handler: self.prevScreenMode, - Description: self.c.Tr.LcPrevScreenMode, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Universal.OpenFile), - Handler: self.handleOpenConfig, - Description: self.c.Tr.OpenConfig, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.CheckForUpdate), - Handler: self.handleCheckForUpdate, - Description: self.c.Tr.LcCheckForUpdate, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.RecentRepos), - Handler: self.handleCreateRecentReposMenu, - Description: self.c.Tr.SwitchRepo, - }, - { - ViewName: "status", - Key: opts.GetKey(opts.Config.Status.AllBranchesLogGraph), - Handler: self.handleShowAllBranchLogs, - Description: self.c.Tr.LcAllBranchesLogGraph, - }, { ViewName: "files", Key: opts.GetKey(opts.Config.Universal.CopyToClipboard), @@ -249,27 +176,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.handleCopySelectedSideContextItemToClipboard, Description: self.c.Tr.LcCopyCommitFileNameToClipboard, }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.FilteringMenu), - Handler: self.handleCreateFilteringMenuPanel, - Description: self.c.Tr.LcOpenFilteringMenu, - OpensMenu: true, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.DiffingMenu), - Handler: self.handleCreateDiffingMenuPanel, - Description: self.c.Tr.LcOpenDiffingMenu, - OpensMenu: true, - }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.DiffingMenuAlt), - Handler: self.handleCreateDiffingMenuPanel, - Description: self.c.Tr.LcOpenDiffingMenu, - OpensMenu: true, - }, { ViewName: "", Key: opts.GetKey(opts.Config.Universal.ExtrasMenu), @@ -309,12 +215,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Modifier: gocui.ModNone, Handler: self.scrollUpSecondary, }, - { - ViewName: "status", - Key: gocui.MouseLeft, - Modifier: gocui.ModNone, - Handler: self.handleStatusClick, - }, { ViewName: "search", Key: opts.GetKey(opts.Config.Universal.Confirm), @@ -357,12 +257,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi Handler: self.handleCopySelectedSideContextItemToClipboard, Description: self.c.Tr.LcCopySubmoduleNameToClipboard, }, - { - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.ToggleWhitespaceInDiffView), - Handler: self.toggleWhitespaceInDiffView, - Description: self.c.Tr.ToggleWhitespaceInDiffView, - }, { ViewName: "extras", Key: gocui.MouseWheelUp, @@ -422,33 +316,6 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi mouseKeybindings = append(mouseKeybindings, c.GetMouseKeybindings(opts)...) } - for _, viewName := range []string{"status", "remotes", "tags", "localBranches", "remoteBranches", "files", "submodules", "reflogCommits", "commits", "commitFiles", "subCommits", "stash"} { - bindings = append(bindings, []*types.Binding{ - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.PrevBlock), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.NextBlock), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.NextBlockAlt), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.PrevBlockAlt2), Modifier: gocui.ModNone, Handler: self.previousSideWindow}, - {ViewName: viewName, Key: opts.GetKey(opts.Config.Universal.NextBlockAlt2), Modifier: gocui.ModNone, Handler: self.nextSideWindow}, - }...) - } - - // Appends keybindings to jump to a particular sideView using numbers - windows := []string{"status", "files", "branches", "commits", "stash"} - - if len(config.Universal.JumpToBlock) != len(windows) { - log.Fatal("Jump to block keybindings cannot be set. Exactly 5 keybindings must be supplied.") - } else { - for i, window := range windows { - bindings = append(bindings, &types.Binding{ - ViewName: "", - Key: opts.GetKey(opts.Config.Universal.JumpToBlock[i]), - Modifier: gocui.ModNone, - Handler: self.goToSideWindow(window), - }) - } - } - bindings = append(bindings, []*types.Binding{ { ViewName: "", @@ -469,17 +336,21 @@ func (self *Gui) GetInitialKeybindings() ([]*types.Binding, []*gocui.ViewMouseBi return bindings, mouseKeybindings } -func (gui *Gui) resetKeybindings() error { - gui.g.DeleteAllKeybindings() - - bindings, mouseBindings := gui.GetInitialKeybindings() - - // prepending because we want to give our custom keybindings precedence over default keybindings - customBindings, err := gui.CustomCommandsClient.GetCustomCommandKeybindings() +func (self *Gui) GetInitialKeybindingsWithCustomCommands() ([]*types.Binding, []*gocui.ViewMouseBinding) { + bindings, mouseBindings := self.GetInitialKeybindings() + customBindings, err := self.CustomCommandsClient.GetCustomCommandKeybindings() if err != nil { log.Fatal(err) } + // prepending because we want to give our custom keybindings precedence over default keybindings bindings = append(customBindings, bindings...) + return bindings, mouseBindings +} + +func (gui *Gui) resetKeybindings() error { + gui.g.DeleteAllKeybindings() + + bindings, mouseBindings := gui.GetInitialKeybindingsWithCustomCommands() for _, binding := range bindings { if err := gui.SetKeybinding(binding); err != nil { @@ -496,7 +367,9 @@ func (gui *Gui) resetKeybindings() error { for _, values := range gui.viewTabMap() { for _, value := range values { viewName := value.ViewName - tabClickCallback := func(tabIndex int) error { return gui.onViewTabClick(gui.windowForView(viewName), tabIndex) } + tabClickCallback := func(tabIndex int) error { + return gui.onViewTabClick(gui.helpers.Window.WindowForView(viewName), tabIndex) + } if err := gui.g.SetTabClickBinding(viewName, tabClickCallback); err != nil { return err @@ -519,7 +392,7 @@ func (gui *Gui) SetKeybinding(binding *types.Binding) error { if gocui.IsMouseKey(binding.Key) { handler = func() error { // we ignore click events on views that aren't popup panels, when a popup panel is focused - if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { + if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != binding.ViewName { return nil } @@ -535,7 +408,7 @@ func (gui *Gui) SetMouseKeybinding(binding *gocui.ViewMouseBinding) error { baseHandler := binding.Handler newHandler := func(opts gocui.ViewMouseBindingOpts) error { // we ignore click events on views that aren't popup panels, when a popup panel is focused - if gui.popupPanelFocused() && gui.currentViewName() != binding.ViewName { + if gui.helpers.Confirmation.IsPopupPanelFocused() && gui.currentViewName() != binding.ViewName { return nil } diff --git a/pkg/gui/layout.go b/pkg/gui/layout.go index 0deb37d2e..30177ac2f 100644 --- a/pkg/gui/layout.go +++ b/pkg/gui/layout.go @@ -4,11 +4,10 @@ import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" ) -const SEARCH_PREFIX = "search: " - // layout is called for every screen re-render e.g. when the screen is resized func (gui *Gui) layout(g *gocui.Gui) error { if !gui.ViewsSetup { @@ -23,7 +22,8 @@ func (gui *Gui) layout(g *gocui.Gui) error { width, height := g.Size() informationStr := gui.informationStr() - appStatus := gui.statusManager.getStatusString() + + appStatus := gui.helpers.AppStatus.GetStatusString() viewDimensions := gui.getWindowDimensions(informationStr, appStatus) @@ -96,16 +96,16 @@ func (gui *Gui) layout(g *gocui.Gui) error { gui.Views.Tooltip.Visible = gui.Views.Menu.Visible && gui.Views.Tooltip.Buffer() != "" - for _, context := range gui.TransientContexts() { + for _, context := range gui.transientContexts() { view, err := gui.g.View(context.GetViewName()) if err != nil && !gocui.IsUnknownView(err) { return err } - view.Visible = gui.getViewNameForWindow(context.GetWindowName()) == context.GetViewName() + view.Visible = gui.helpers.Window.GetViewNameForWindow(context.GetWindowName()) == context.GetViewName() } if gui.PrevLayout.Information != informationStr { - gui.setViewContent(gui.Views.Information, informationStr) + gui.c.SetViewContent(gui.Views.Information, informationStr) gui.PrevLayout.Information = informationStr } @@ -125,7 +125,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { gui.State.ViewsSetup = true } - for _, listContext := range gui.getListContexts() { + for _, listContext := range gui.c.Context().AllList() { view, err := gui.g.View(listContext.GetViewName()) if err != nil { continue @@ -139,7 +139,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { view.SetOnSelectItem(gui.onSelectItemWrapper(listContext.OnSearchSelect)) } - for _, context := range gui.getPatchExplorerContexts() { + for _, context := range gui.c.Context().AllPatchExplorer() { context := context context.GetView().SetOnSelectItem(gui.onSelectItemWrapper( func(selectedLineIdx int) error { @@ -163,7 +163,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { // if you run `lazygit --logs` // this will let you see these branches as prettified json // gui.c.Log.Info(utils.AsJson(gui.State.Model.Branches[0:4])) - return gui.resizeCurrentPopupPanel() + return gui.helpers.Confirmation.ResizeCurrentPopupPanel() } func (gui *Gui) prepareView(viewName string) (*gocui.View, error) { @@ -181,7 +181,7 @@ func (gui *Gui) onInitialViewsCreationForRepo() error { } } - initialContext := gui.currentSideContext() + initialContext := gui.c.CurrentSideContext() if err := gui.c.PushContext(initialContext); err != nil { return err } @@ -189,6 +189,16 @@ func (gui *Gui) onInitialViewsCreationForRepo() error { return gui.loadNewRepo() } +func (gui *Gui) popupViewNames() []string { + popups := slices.Filter(gui.State.Contexts.Flatten(), func(c types.Context) bool { + return c.GetKind() == types.PERSISTENT_POPUP || c.GetKind() == types.TEMPORARY_POPUP + }) + + return slices.Map(popups, func(c types.Context) string { + return c.GetViewName() + }) +} + func (gui *Gui) onInitialViewsCreation() error { // now we order the views (in order of bottom first) for _, view := range gui.orderedViews() { @@ -226,15 +236,50 @@ func (gui *Gui) onInitialViewsCreation() error { } if gui.showRecentRepos { - if err := gui.handleCreateRecentReposMenu(); err != nil { + if err := gui.helpers.Repos.CreateRecentReposMenu(); err != nil { return err } gui.showRecentRepos = false } - gui.Updater.CheckForNewUpdate(gui.onBackgroundUpdateCheckFinish, false) + gui.helpers.Update.CheckForUpdateInBackground() gui.waitForIntro.Done() return nil } + +// getFocusLayout returns a manager function for when view gain and lose focus +func (gui *Gui) getFocusLayout() func(g *gocui.Gui) error { + var previousView *gocui.View + return func(g *gocui.Gui) error { + newView := gui.g.CurrentView() + // for now we don't consider losing focus to a popup panel as actually losing focus + if newView != previousView && !gui.helpers.Confirmation.IsPopupPanel(newView.Name()) { + if err := gui.onViewFocusLost(previousView); err != nil { + return err + } + + previousView = newView + } + return nil + } +} + +func (gui *Gui) onViewFocusLost(oldView *gocui.View) error { + if oldView == nil { + return nil + } + + oldView.Highlight = false + + _ = oldView.SetOriginX(0) + + return nil +} + +func (gui *Gui) transientContexts() []types.Context { + return slices.Filter(gui.State.Contexts.Flatten(), func(context types.Context) bool { + return context.IsTransient() + }) +} diff --git a/pkg/gui/list_context_config.go b/pkg/gui/list_context_config.go deleted file mode 100644 index 7969c4d20..000000000 --- a/pkg/gui/list_context_config.go +++ /dev/null @@ -1,305 +0,0 @@ -package gui - -import ( - "log" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) menuListContext() *context.MenuContext { - return context.NewMenuContext( - gui.Views.Menu, - gui.c, - gui.getMenuOptions, - func(content string) { - gui.Views.Tooltip.SetContent(content) - }, - ) -} - -func (gui *Gui) filesListContext() *context.WorkingTreeContext { - return context.NewWorkingTreeContext( - func() []*models.File { return gui.State.Model.Files }, - gui.Views.Files, - func(startIdx int, length int) [][]string { - lines := presentation.RenderFileTree(gui.State.Contexts.Files.FileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.State.Model.Submodules) - return slices.Map(lines, func(line string) []string { - return []string{line} - }) - }, - nil, - gui.withDiffModeCheck(gui.filesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) branchesListContext() *context.BranchesContext { - return context.NewBranchesContext( - func() []*models.Branch { return gui.State.Model.Branches }, - gui.Views.Branches, - func(startIdx int, length int) [][]string { - return presentation.GetBranchListDisplayStrings(gui.State.Model.Branches, gui.State.ScreenMode != SCREEN_NORMAL, gui.State.Modes.Diffing.Ref, gui.Tr) - }, - nil, - gui.withDiffModeCheck(gui.branchesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) remotesListContext() *context.RemotesContext { - return context.NewRemotesContext( - func() []*models.Remote { return gui.State.Model.Remotes }, - gui.Views.Remotes, - func(startIdx int, length int) [][]string { - return presentation.GetRemoteListDisplayStrings(gui.State.Model.Remotes, gui.State.Modes.Diffing.Ref) - }, - nil, - gui.withDiffModeCheck(gui.remotesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) remoteBranchesListContext() *context.RemoteBranchesContext { - return context.NewRemoteBranchesContext( - func() []*models.RemoteBranch { return gui.State.Model.RemoteBranches }, - gui.Views.RemoteBranches, - func(startIdx int, length int) [][]string { - return presentation.GetRemoteBranchListDisplayStrings(gui.State.Model.RemoteBranches, gui.State.Modes.Diffing.Ref) - }, - nil, - gui.withDiffModeCheck(gui.remoteBranchesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) withDiffModeCheck(f func() error) func() error { - return func() error { - if gui.State.Modes.Diffing.Active() { - return gui.renderDiff() - } - - return f() - } -} - -func (gui *Gui) tagsListContext() *context.TagsContext { - return context.NewTagsContext( - func() []*models.Tag { return gui.State.Model.Tags }, - gui.Views.Tags, - func(startIdx int, length int) [][]string { - return presentation.GetTagListDisplayStrings(gui.State.Model.Tags, gui.State.Modes.Diffing.Ref) - }, - nil, - gui.withDiffModeCheck(gui.tagsRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) branchCommitsListContext() *context.LocalCommitsContext { - return context.NewLocalCommitsContext( - func() []*models.Commit { return gui.State.Model.Commits }, - gui.Views.Commits, - func(startIdx int, length int) [][]string { - selectedCommitSha := "" - if gui.currentContext().GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { - selectedCommit := gui.State.Contexts.LocalCommits.GetSelected() - if selectedCommit != nil { - selectedCommitSha = selectedCommit.Sha - } - } - - showYouAreHereLabel := gui.State.Model.WorkingTreeStateAtLastCommitRefresh == enums.REBASE_MODE_REBASING - - return presentation.GetCommitListDisplayStrings( - gui.Common, - gui.State.Model.Commits, - gui.State.ScreenMode != SCREEN_NORMAL, - gui.helpers.CherryPick.CherryPickedCommitShaSet(), - gui.State.Modes.Diffing.Ref, - gui.c.UserConfig.Gui.TimeFormat, - gui.c.UserConfig.Git.ParseEmoji, - selectedCommitSha, - startIdx, - length, - gui.shouldShowGraph(), - gui.State.Model.BisectInfo, - showYouAreHereLabel, - ) - }, - OnFocusWrapper(gui.onCommitFocus), - gui.withDiffModeCheck(gui.branchCommitsRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) subCommitsListContext() *context.SubCommitsContext { - return context.NewSubCommitsContext( - func() []*models.Commit { return gui.State.Model.SubCommits }, - gui.Views.SubCommits, - func(startIdx int, length int) [][]string { - selectedCommitSha := "" - if gui.currentContext().GetKey() == context.SUB_COMMITS_CONTEXT_KEY { - selectedCommit := gui.State.Contexts.SubCommits.GetSelected() - if selectedCommit != nil { - selectedCommitSha = selectedCommit.Sha - } - } - return presentation.GetCommitListDisplayStrings( - gui.Common, - gui.State.Model.SubCommits, - gui.State.ScreenMode != SCREEN_NORMAL, - gui.helpers.CherryPick.CherryPickedCommitShaSet(), - gui.State.Modes.Diffing.Ref, - gui.c.UserConfig.Gui.TimeFormat, - gui.c.UserConfig.Git.ParseEmoji, - selectedCommitSha, - startIdx, - length, - gui.shouldShowGraph(), - git_commands.NewNullBisectInfo(), - false, - ) - }, - OnFocusWrapper(gui.onSubCommitFocus), - gui.withDiffModeCheck(gui.subCommitsRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) shouldShowGraph() bool { - if gui.State.Modes.Filtering.Active() { - return false - } - - value := gui.c.UserConfig.Git.Log.ShowGraph - switch value { - case "always": - return true - case "never": - return false - case "when-maximised": - return gui.State.ScreenMode != SCREEN_NORMAL - } - - log.Fatalf("Unknown value for git.log.showGraph: %s. Expected one of: 'always', 'never', 'when-maximised'", value) - return false -} - -func (gui *Gui) reflogCommitsListContext() *context.ReflogCommitsContext { - return context.NewReflogCommitsContext( - func() []*models.Commit { return gui.State.Model.FilteredReflogCommits }, - gui.Views.ReflogCommits, - func(startIdx int, length int) [][]string { - return presentation.GetReflogCommitListDisplayStrings( - gui.State.Model.FilteredReflogCommits, - gui.State.ScreenMode != SCREEN_NORMAL, - gui.helpers.CherryPick.CherryPickedCommitShaSet(), - gui.State.Modes.Diffing.Ref, - gui.c.UserConfig.Gui.TimeFormat, - gui.c.UserConfig.Git.ParseEmoji, - ) - }, - nil, - gui.withDiffModeCheck(gui.reflogCommitsRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) stashListContext() *context.StashContext { - return context.NewStashContext( - func() []*models.StashEntry { return gui.State.Model.StashEntries }, - gui.Views.Stash, - func(startIdx int, length int) [][]string { - return presentation.GetStashEntryListDisplayStrings(gui.State.Model.StashEntries, gui.State.Modes.Diffing.Ref) - }, - nil, - gui.withDiffModeCheck(gui.stashRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) commitFilesListContext() *context.CommitFilesContext { - return context.NewCommitFilesContext( - func() []*models.CommitFile { return gui.State.Model.CommitFiles }, - gui.Views.CommitFiles, - func(startIdx int, length int) [][]string { - if gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.Len() == 0 { - return [][]string{{style.FgRed.Sprint("(none)")}} - } - - lines := presentation.RenderCommitFileTree(gui.State.Contexts.CommitFiles.CommitFileTreeViewModel, gui.State.Modes.Diffing.Ref, gui.git.Patch.PatchBuilder) - return slices.Map(lines, func(line string) []string { - return []string{line} - }) - }, - nil, - gui.withDiffModeCheck(gui.commitFilesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) submodulesListContext() *context.SubmodulesContext { - return context.NewSubmodulesContext( - func() []*models.SubmoduleConfig { return gui.State.Model.Submodules }, - gui.Views.Submodules, - func(startIdx int, length int) [][]string { - return presentation.GetSubmoduleListDisplayStrings(gui.State.Model.Submodules) - }, - nil, - gui.withDiffModeCheck(gui.submodulesRenderToMain), - nil, - gui.c, - ) -} - -func (gui *Gui) suggestionsListContext() *context.SuggestionsContext { - return context.NewSuggestionsContext( - func() []*types.Suggestion { return gui.State.Suggestions }, - gui.Views.Suggestions, - func(startIdx int, length int) [][]string { - return presentation.GetSuggestionListDisplayStrings(gui.State.Suggestions) - }, - nil, - nil, - func(types.OnFocusLostOpts) error { - gui.deactivateConfirmationPrompt() - return nil - }, - gui.c, - ) -} - -func (gui *Gui) getListContexts() []types.IListContext { - return []types.IListContext{ - gui.State.Contexts.Menu, - gui.State.Contexts.Files, - gui.State.Contexts.Branches, - gui.State.Contexts.Remotes, - gui.State.Contexts.RemoteBranches, - gui.State.Contexts.Tags, - gui.State.Contexts.LocalCommits, - gui.State.Contexts.ReflogCommits, - gui.State.Contexts.SubCommits, - gui.State.Contexts.Stash, - gui.State.Contexts.CommitFiles, - gui.State.Contexts.Submodules, - gui.State.Contexts.Suggestions, - } -} diff --git a/pkg/gui/main_panels.go b/pkg/gui/main_panels.go index 9e36c18e9..e7c87c5e5 100644 --- a/pkg/gui/main_panels.go +++ b/pkg/gui/main_panels.go @@ -34,11 +34,11 @@ func (gui *Gui) moveMainContextPairToTop(pair types.MainContextPair) { } func (gui *Gui) moveMainContextToTop(context types.Context) { - gui.setWindowContext(context) + gui.helpers.Window.SetWindowContext(context) view := context.GetView() - topView := gui.topViewInWindow(context.GetWindowName()) + topView := gui.helpers.Window.TopViewInWindow(context.GetWindowName()) if topView == nil { gui.Log.Error("unexpected: topView is nil") return @@ -146,7 +146,3 @@ func (gui *Gui) refreshMainViews(opts types.RefreshMainOpts) error { func (gui *Gui) splitMainPanel(splitMainPanel bool) { gui.State.SplitMainPanel = splitMainPanel } - -func (gui *Gui) isMainPanelSplit() bool { - return gui.State.SplitMainPanel -} diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index bc3d087c3..24d9ff681 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -1,25 +1,12 @@ package gui import ( - "fmt" - - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/presentation" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getMenuOptions() map[string]string { - keybindingConfig := gui.c.UserConfig.Keybinding - - return map[string]string{ - keybindings.Label(keybindingConfig.Universal.Return): gui.c.Tr.LcClose, - fmt.Sprintf("%s %s", keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate, - keybindings.Label(keybindingConfig.Universal.Select): gui.c.Tr.LcExecute, - } -} - // note: items option is mutated by this function func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { if !opts.HideCancel { diff --git a/pkg/gui/modes.go b/pkg/gui/modes.go deleted file mode 100644 index 750d1ad92..000000000 --- a/pkg/gui/modes.go +++ /dev/null @@ -1,103 +0,0 @@ -package gui - -import ( - "fmt" - - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/style" -) - -type modeStatus struct { - isActive func() bool - description func() string - reset func() error -} - -func (gui *Gui) modeStatuses() []modeStatus { - return []modeStatus{ - { - isActive: gui.State.Modes.Diffing.Active, - description: func() string { - return gui.withResetButton( - fmt.Sprintf( - "%s %s", - gui.c.Tr.LcShowingGitDiff, - "git diff "+gui.diffStr(), - ), - style.FgMagenta, - ) - }, - reset: gui.exitDiffMode, - }, - { - isActive: gui.git.Patch.PatchBuilder.Active, - description: func() string { - return gui.withResetButton(gui.c.Tr.LcBuildingPatch, style.FgYellow.SetBold()) - }, - reset: gui.helpers.PatchBuilding.Reset, - }, - { - isActive: gui.State.Modes.Filtering.Active, - description: func() string { - return gui.withResetButton( - fmt.Sprintf( - "%s '%s'", - gui.c.Tr.LcFilteringBy, - gui.State.Modes.Filtering.GetPath(), - ), - style.FgRed, - ) - }, - reset: gui.exitFilterMode, - }, - { - isActive: gui.State.Modes.CherryPicking.Active, - description: func() string { - copiedCount := len(gui.State.Modes.CherryPicking.CherryPickedCommits) - text := gui.c.Tr.LcCommitsCopied - if copiedCount == 1 { - text = gui.c.Tr.LcCommitCopied - } - - return gui.withResetButton( - fmt.Sprintf( - "%d %s", - copiedCount, - text, - ), - style.FgCyan, - ) - }, - reset: gui.helpers.CherryPick.Reset, - }, - { - isActive: func() bool { - return gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE - }, - description: func() string { - workingTreeState := gui.git.Status.WorkingTreeState() - return gui.withResetButton( - formatWorkingTreeState(workingTreeState), style.FgYellow, - ) - }, - reset: gui.helpers.MergeAndRebase.AbortMergeOrRebaseWithConfirm, - }, - { - isActive: func() bool { - return gui.State.Model.BisectInfo.Started() - }, - description: func() string { - return gui.withResetButton("bisecting", style.FgGreen) - }, - reset: gui.helpers.Bisect.Reset, - }, - } -} - -func (gui *Gui) withResetButton(content string, textStyle style.TextStyle) string { - return textStyle.Sprintf( - "%s %s", - content, - style.AttrUnderline.Sprint(gui.c.Tr.ResetInParentheses), - ) -} diff --git a/pkg/gui/modes/cherrypicking/cherry_picking.go b/pkg/gui/modes/cherrypicking/cherry_picking.go index bd5c6437a..1fd34d473 100644 --- a/pkg/gui/modes/cherrypicking/cherry_picking.go +++ b/pkg/gui/modes/cherrypicking/cherry_picking.go @@ -1,6 +1,8 @@ package cherrypicking import ( + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/lazygit/pkg/commands/models" ) @@ -18,6 +20,37 @@ func New() *CherryPicking { } } -func (m *CherryPicking) Active() bool { - return len(m.CherryPickedCommits) > 0 +func (self *CherryPicking) Active() bool { + return len(self.CherryPickedCommits) > 0 +} + +func (self *CherryPicking) SelectedShaSet() *set.Set[string] { + shas := slices.Map(self.CherryPickedCommits, func(commit *models.Commit) string { + return commit.Sha + }) + return set.NewFromSlice(shas) +} + +func (self *CherryPicking) Add(selectedCommit *models.Commit, commitsList []*models.Commit) { + commitSet := self.SelectedShaSet() + commitSet.Add(selectedCommit.Sha) + + self.update(commitSet, commitsList) +} + +func (self *CherryPicking) Remove(selectedCommit *models.Commit, commitsList []*models.Commit) { + commitSet := self.SelectedShaSet() + commitSet.Remove(selectedCommit.Sha) + + self.update(commitSet, commitsList) +} + +func (self *CherryPicking) update(selectedShaSet *set.Set[string], commitsList []*models.Commit) { + cherryPickedCommits := slices.Filter(commitsList, func(commit *models.Commit) bool { + return selectedShaSet.Includes(commit.Sha) + }) + + self.CherryPickedCommits = slices.Map(cherryPickedCommits, func(commit *models.Commit) *models.Commit { + return &models.Commit{Name: commit.Name, Sha: commit.Sha} + }) } diff --git a/pkg/gui/options_map.go b/pkg/gui/options_map.go new file mode 100644 index 000000000..5f5c008dd --- /dev/null +++ b/pkg/gui/options_map.go @@ -0,0 +1,93 @@ +package gui + +import ( + "fmt" + "strings" + + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" + "github.com/jesseduffield/lazygit/pkg/gui/keybindings" + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" +) + +type OptionsMapMgr struct { + c *helpers.HelperCommon +} + +func (gui *Gui) renderContextOptionsMap(c types.Context) { + mgr := OptionsMapMgr{c: gui.c} + mgr.renderContextOptionsMap(c) +} + +// render the options available for the current context at the bottom of the screen +func (self *OptionsMapMgr) renderContextOptionsMap(c types.Context) { + bindingsToDisplay := lo.Filter(c.GetKeybindings(self.c.KeybindingsOpts()), func(binding *types.Binding, _ int) bool { + return binding.Display + }) + + var optionsMap []bindingInfo + if len(bindingsToDisplay) == 0 { + optionsMap = self.globalOptions() + } else { + optionsMap = lo.Map(bindingsToDisplay, func(binding *types.Binding, _ int) bindingInfo { + return bindingInfo{ + key: keybindings.LabelFromKey(binding.Key), + description: binding.Description, + } + }) + } + + self.renderOptions(self.formatBindingInfos(optionsMap)) +} + +func (self *OptionsMapMgr) formatBindingInfos(bindingInfos []bindingInfo) string { + return strings.Join( + lo.Map(bindingInfos, func(bindingInfo bindingInfo, _ int) string { + return fmt.Sprintf("%s: %s", bindingInfo.key, bindingInfo.description) + }), + ", ") +} + +func (self *OptionsMapMgr) renderOptions(options string) { + self.c.SetViewContent(self.c.Views().Options, options) +} + +func (self *OptionsMapMgr) globalOptions() []bindingInfo { + keybindingConfig := self.c.UserConfig.Keybinding + + return []bindingInfo{ + { + key: fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)), + description: self.c.Tr.LcScroll, + }, + { + key: fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)), + description: self.c.Tr.LcNavigate, + }, + { + key: keybindings.Label(keybindingConfig.Universal.Return), + description: self.c.Tr.LcCancel, + }, + { + key: keybindings.Label(keybindingConfig.Universal.Quit), + description: self.c.Tr.LcQuit, + }, + { + key: keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1), + description: self.c.Tr.LcMenu, + }, + { + key: fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])), + description: self.c.Tr.LcJump, + }, + { + key: fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)), + description: self.c.Tr.LcScrollLeftRight, + }, + } +} + +type bindingInfo struct { + key string + description string +} diff --git a/pkg/gui/popup/popup_handler.go b/pkg/gui/popup/popup_handler.go index 4e50b6326..633e91a55 100644 --- a/pkg/gui/popup/popup_handler.go +++ b/pkg/gui/popup/popup_handler.go @@ -22,7 +22,7 @@ type PopupHandler struct { popContextFn func() error currentContextFn func() types.Context createMenuFn func(types.CreateMenuOptions) error - withWaitingStatusFn func(message string, f func() error) error + withWaitingStatusFn func(message string, f func() error) toastFn func(message string) getPromptInputFn func() string } @@ -36,7 +36,7 @@ func NewPopupHandler( popContextFn func() error, currentContextFn func() types.Context, createMenuFn func(types.CreateMenuOptions) error, - withWaitingStatusFn func(message string, f func() error) error, + withWaitingStatusFn func(message string, f func() error), toastFn func(message string), getPromptInputFn func() string, ) *PopupHandler { @@ -63,7 +63,8 @@ func (self *PopupHandler) Toast(message string) { } func (self *PopupHandler) WithWaitingStatus(message string, f func() error) error { - return self.withWaitingStatusFn(message, f) + self.withWaitingStatusFn(message, f) + return nil } func (self *PopupHandler) Error(err error) error { diff --git a/pkg/gui/presentation/working_tree.go b/pkg/gui/presentation/working_tree.go new file mode 100644 index 000000000..5ce46b734 --- /dev/null +++ b/pkg/gui/presentation/working_tree.go @@ -0,0 +1,14 @@ +package presentation + +import "github.com/jesseduffield/lazygit/pkg/commands/types/enums" + +func FormatWorkingTreeState(rebaseMode enums.RebaseMode) string { + switch rebaseMode { + case enums.REBASE_MODE_REBASING: + return "rebasing" + case enums.REBASE_MODE_MERGING: + return "merging" + default: + return "none" + } +} diff --git a/pkg/gui/quitting.go b/pkg/gui/quitting.go deleted file mode 100644 index cdb1ff09a..000000000 --- a/pkg/gui/quitting.go +++ /dev/null @@ -1,86 +0,0 @@ -package gui - -import ( - "os" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -// when a user runs lazygit with the LAZYGIT_NEW_DIR_FILE env variable defined -// we will write the current directory to that file on exit so that their -// shell can then change to that directory. That means you don't get kicked -// back to the directory that you started with. -func (gui *Gui) recordCurrentDirectory() error { - // determine current directory, set it in LAZYGIT_NEW_DIR_FILE - dirName, err := os.Getwd() - if err != nil { - return err - } - return gui.recordDirectory(dirName) -} - -func (gui *Gui) recordDirectory(dirName string) error { - newDirFilePath := os.Getenv("LAZYGIT_NEW_DIR_FILE") - if newDirFilePath == "" { - return nil - } - return gui.os.CreateFileWithContent(newDirFilePath, dirName) -} - -func (gui *Gui) handleQuitWithoutChangingDirectory() error { - gui.RetainOriginalDir = true - return gui.quit() -} - -func (gui *Gui) handleQuit() error { - gui.RetainOriginalDir = false - return gui.quit() -} - -func (gui *Gui) handleTopLevelReturn() error { - currentContext := gui.currentContext() - - parentContext, hasParent := currentContext.GetParentContext() - if hasParent && currentContext != nil && parentContext != nil { - // TODO: think about whether this should be marked as a return rather than adding to the stack - return gui.c.PushContext(parentContext) - } - - for _, mode := range gui.modeStatuses() { - if mode.isActive() { - return mode.reset() - } - } - - repoPathStack := gui.RepoPathStack - if !repoPathStack.IsEmpty() { - path := repoPathStack.Pop() - - return gui.dispatchSwitchToRepo(path, true) - } - - if gui.c.UserConfig.QuitOnTopLevelReturn { - return gui.handleQuit() - } - - return nil -} - -func (gui *Gui) quit() error { - if gui.State.Updating { - return gui.createUpdateQuitConfirmation() - } - - if gui.c.UserConfig.ConfirmOnQuit { - return gui.c.Confirm(types.ConfirmOpts{ - Title: "", - Prompt: gui.c.Tr.ConfirmQuit, - HandleConfirm: func() error { - return gocui.ErrQuit - }, - }) - } - - return gocui.ErrQuit -} diff --git a/pkg/gui/recent_repos_panel.go b/pkg/gui/recent_repos_panel.go index b10a83078..a6953732a 100644 --- a/pkg/gui/recent_repos_panel.go +++ b/pkg/gui/recent_repos_panel.go @@ -1,160 +1,10 @@ package gui import ( - "fmt" "os" "path/filepath" - "strings" - "sync" - - "github.com/jesseduffield/generics/slices" - appTypes "github.com/jesseduffield/lazygit/pkg/app/types" - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/env" - "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" ) -func (gui *Gui) getCurrentBranch(path string) string { - readHeadFile := func(path string) (string, error) { - headFile, err := os.ReadFile(filepath.Join(path, "HEAD")) - if err == nil { - content := strings.TrimSpace(string(headFile)) - refsPrefix := "ref: refs/heads/" - var branchDisplay string - if strings.HasPrefix(content, refsPrefix) { - // is a branch - branchDisplay = strings.TrimPrefix(content, refsPrefix) - } else { - // detached HEAD state, displaying short SHA - branchDisplay = utils.ShortSha(content) - } - return branchDisplay, nil - } - return "", err - } - - gitDirPath := filepath.Join(path, ".git") - - if gitDir, err := os.Stat(gitDirPath); err == nil { - if gitDir.IsDir() { - // ordinary repo - if branch, err := readHeadFile(gitDirPath); err == nil { - return branch - } - } else { - // worktree - if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil { - content := strings.TrimSpace(string(worktreeGitDir)) - worktreePath := strings.TrimPrefix(content, "gitdir: ") - if branch, err := readHeadFile(worktreePath); err == nil { - return branch - } - } - } - } - - return gui.c.Tr.LcBranchUnknown -} - -func (gui *Gui) handleCreateRecentReposMenu() error { - // we'll show an empty panel if there are no recent repos - recentRepoPaths := []string{} - if len(gui.c.GetAppState().RecentRepos) > 0 { - // we skip the first one because we're currently in it - recentRepoPaths = gui.c.GetAppState().RecentRepos[1:] - } - - currentBranches := sync.Map{} - - wg := sync.WaitGroup{} - wg.Add(len(recentRepoPaths)) - - for _, path := range recentRepoPaths { - go func(path string) { - defer wg.Done() - currentBranches.Store(path, gui.getCurrentBranch(path)) - }(path) - } - - wg.Wait() - - menuItems := slices.Map(recentRepoPaths, func(path string) *types.MenuItem { - branchName, _ := currentBranches.Load(path) - if icons.IsIconEnabled() { - branchName = icons.BRANCH_ICON + " " + fmt.Sprintf("%v", branchName) - } - - return &types.MenuItem{ - LabelColumns: []string{ - filepath.Base(path), - style.FgCyan.Sprint(branchName), - style.FgMagenta.Sprint(path), - }, - OnPress: func() error { - // if we were in a submodule, we want to forget about that stack of repos - // so that hitting escape in the new repo does nothing - gui.RepoPathStack.Clear() - return gui.dispatchSwitchToRepo(path, false) - }, - } - }) - - return gui.c.Menu(types.CreateMenuOptions{Title: gui.c.Tr.RecentRepos, Items: menuItems}) -} - -func (gui *Gui) handleShowAllBranchLogs() error { - cmdObj := gui.git.Branch.AllBranchesLogCmdObj() - task := types.NewRunPtyTask(cmdObj.GetCmd()) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.LogTitle, - Task: task, - }, - }) -} - -func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error { - env.UnsetGitDirEnvs() - originalPath, err := os.Getwd() - if err != nil { - return nil - } - - if err := os.Chdir(path); err != nil { - if os.IsNotExist(err) { - return gui.c.ErrorMsg(gui.c.Tr.ErrRepositoryMovedOrDeleted) - } - return err - } - - if err := commands.VerifyInGitRepo(gui.os); err != nil { - if err := os.Chdir(originalPath); err != nil { - return err - } - - return err - } - - if err := gui.recordCurrentDirectory(); err != nil { - return err - } - - // these two mutexes are used by our background goroutines (triggered via `gui.goEvery`. We don't want to - // switch to a repo while one of these goroutines is in the process of updating something - gui.Mutexes.SyncMutex.Lock() - defer gui.Mutexes.SyncMutex.Unlock() - - gui.Mutexes.RefreshingFilesMutex.Lock() - defer gui.Mutexes.RefreshingFilesMutex.Unlock() - - return gui.onNewRepo(appTypes.StartArgs{}, reuse) -} - // updateRecentRepoList registers the fact that we opened lazygit in this repo, // so that we can open the same repo via the 'recent repos' menu func (gui *Gui) updateRecentRepoList() error { diff --git a/pkg/gui/reflog_panel.go b/pkg/gui/reflog_panel.go deleted file mode 100644 index 6c5d0a68c..000000000 --- a/pkg/gui/reflog_panel.go +++ /dev/null @@ -1,24 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) reflogCommitsRenderToMain() error { - commit := gui.State.Contexts.ReflogCommits.GetSelected() - var task types.UpdateTask - if commit == nil { - task = types.NewRenderStringTask("No reflog history") - } else { - cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(), - gui.IgnoreWhitespaceInDiffView) - - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Reflog Entry", - Task: task, - }, - }) -} diff --git a/pkg/gui/refresh.go b/pkg/gui/refresh.go deleted file mode 100644 index 5fd68dedc..000000000 --- a/pkg/gui/refresh.go +++ /dev/null @@ -1,758 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - "sync" - - "github.com/jesseduffield/generics/set" - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/git_commands" - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/filetree" - "github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts" - "github.com/jesseduffield/lazygit/pkg/gui/patch_exploring" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func getScopeNames(scopes []types.RefreshableView) []string { - scopeNameMap := map[types.RefreshableView]string{ - types.COMMITS: "commits", - types.BRANCHES: "branches", - types.FILES: "files", - types.SUBMODULES: "submodules", - types.STASH: "stash", - types.REFLOG: "reflog", - types.TAGS: "tags", - types.REMOTES: "remotes", - types.STATUS: "status", - types.BISECT_INFO: "bisect", - types.STAGING: "staging", - types.MERGE_CONFLICTS: "mergeConflicts", - } - - return slices.Map(scopes, func(scope types.RefreshableView) string { - return scopeNameMap[scope] - }) -} - -func getModeName(mode types.RefreshMode) string { - switch mode { - case types.SYNC: - return "sync" - case types.ASYNC: - return "async" - case types.BLOCK_UI: - return "block-ui" - default: - return "unknown mode" - } -} - -func (gui *Gui) Refresh(options types.RefreshOptions) error { - if options.Scope == nil { - gui.c.Log.Infof( - "refreshing all scopes in %s mode", - getModeName(options.Mode), - ) - } else { - gui.c.Log.Infof( - "refreshing the following scopes in %s mode: %s", - getModeName(options.Mode), - strings.Join(getScopeNames(options.Scope), ","), - ) - } - - wg := sync.WaitGroup{} - - f := func() { - var scopeSet *set.Set[types.RefreshableView] - if len(options.Scope) == 0 { - // not refreshing staging/patch-building unless explicitly requested because we only need - // to refresh those while focused. - scopeSet = set.NewFromSlice([]types.RefreshableView{ - types.COMMITS, - types.BRANCHES, - types.FILES, - types.STASH, - types.REFLOG, - types.TAGS, - types.REMOTES, - types.STATUS, - types.BISECT_INFO, - types.STAGING, - }) - } else { - scopeSet = set.NewFromSlice(options.Scope) - } - - refresh := func(f func()) { - wg.Add(1) - func() { - if options.Mode == types.ASYNC { - go utils.Safe(f) - } else { - f() - } - wg.Done() - }() - } - - if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { - refresh(gui.refreshCommits) - } else if scopeSet.Includes(types.REBASE_COMMITS) { - // the above block handles rebase commits so we only need to call this one - // if we've asked specifically for rebase commits and not those other things - refresh(func() { _ = gui.refreshRebaseCommits() }) - } - - // reason we're not doing this if the COMMITS type is included is that if the COMMITS type _is_ included we will refresh the commit files context anyway - if scopeSet.Includes(types.COMMIT_FILES) && !scopeSet.Includes(types.COMMITS) { - refresh(func() { _ = gui.refreshCommitFilesContext() }) - } - - if scopeSet.Includes(types.FILES) || scopeSet.Includes(types.SUBMODULES) { - refresh(func() { _ = gui.refreshFilesAndSubmodules() }) - } - - if scopeSet.Includes(types.STASH) { - refresh(func() { _ = gui.refreshStashEntries() }) - } - - if scopeSet.Includes(types.TAGS) { - refresh(func() { _ = gui.refreshTags() }) - } - - if scopeSet.Includes(types.REMOTES) { - refresh(func() { _ = gui.refreshRemotes() }) - } - - if scopeSet.Includes(types.STAGING) { - refresh(func() { _ = gui.refreshStagingPanel(types.OnFocusOpts{}) }) - } - - if scopeSet.Includes(types.PATCH_BUILDING) { - refresh(func() { _ = gui.refreshPatchBuildingPanel(types.OnFocusOpts{}) }) - } - - if scopeSet.Includes(types.MERGE_CONFLICTS) || scopeSet.Includes(types.FILES) { - refresh(func() { _ = gui.refreshMergeState() }) - } - - wg.Wait() - - gui.refreshStatus() - - if options.Then != nil { - options.Then() - } - } - - if options.Mode == types.BLOCK_UI { - gui.c.OnUIThread(func() error { - f() - return nil - }) - } else { - f() - } - - return nil -} - -// during startup, the bottleneck is fetching the reflog entries. We need these -// on startup to sort the branches by recency. So we have two phases: INITIAL, and COMPLETE. -// In the initial phase we don't get any reflog commits, but we asynchronously get them -// and refresh the branches after that -func (gui *Gui) refreshReflogCommitsConsideringStartup() { - switch gui.State.StartupStage { - case INITIAL: - go utils.Safe(func() { - _ = gui.refreshReflogCommits() - gui.refreshBranches() - gui.State.StartupStage = COMPLETE - }) - - case COMPLETE: - _ = gui.refreshReflogCommits() - } -} - -// whenever we change commits, we should update branches because the upstream/downstream -// counts can change. Whenever we change branches we should probably also change commits -// e.g. in the case of switching branches. -func (gui *Gui) refreshCommits() { - wg := sync.WaitGroup{} - wg.Add(2) - - go utils.Safe(func() { - gui.refreshReflogCommitsConsideringStartup() - - gui.refreshBranches() - wg.Done() - }) - - go utils.Safe(func() { - _ = gui.refreshCommitsWithLimit() - ctx, ok := gui.State.Contexts.CommitFiles.GetParentContext() - if ok && ctx.GetKey() == context.LOCAL_COMMITS_CONTEXT_KEY { - // This makes sense when we've e.g. just amended a commit, meaning we get a new commit SHA at the same position. - // However if we've just added a brand new commit, it pushes the list down by one and so we would end up - // showing the contents of a different commit than the one we initially entered. - // Ideally we would know when to refresh the commit files context and when not to, - // or perhaps we could just pop that context off the stack whenever cycling windows. - // For now the awkwardness remains. - commit := gui.getSelectedLocalCommit() - if commit != nil { - gui.State.Contexts.CommitFiles.SetRef(commit) - gui.State.Contexts.CommitFiles.SetTitleRef(commit.RefName()) - _ = gui.refreshCommitFilesContext() - } - } - wg.Done() - }) - - wg.Wait() -} - -func (gui *Gui) refreshCommitsWithLimit() error { - gui.Mutexes.LocalCommitsMutex.Lock() - defer gui.Mutexes.LocalCommitsMutex.Unlock() - - commits, err := gui.git.Loaders.CommitLoader.GetCommits( - git_commands.GetCommitsOptions{ - Limit: gui.State.Contexts.LocalCommits.GetLimitCommits(), - FilterPath: gui.State.Modes.Filtering.GetPath(), - IncludeRebaseCommits: true, - RefName: gui.refForLog(), - All: gui.State.Contexts.LocalCommits.GetShowWholeGitGraph(), - }, - ) - if err != nil { - return err - } - gui.State.Model.Commits = commits - gui.State.Model.WorkingTreeStateAtLastCommitRefresh = gui.git.Status.WorkingTreeState() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.LocalCommits) -} - -func (gui *Gui) refreshCommitFilesContext() error { - ref := gui.State.Contexts.CommitFiles.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - - files, err := gui.git.Loaders.CommitFileLoader.GetFilesInDiff(from, to, reverse) - if err != nil { - return gui.c.Error(err) - } - gui.State.Model.CommitFiles = files - gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.SetTree() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.CommitFiles) -} - -func (gui *Gui) refreshRebaseCommits() error { - gui.Mutexes.LocalCommitsMutex.Lock() - defer gui.Mutexes.LocalCommitsMutex.Unlock() - - updatedCommits, err := gui.git.Loaders.CommitLoader.MergeRebasingCommits(gui.State.Model.Commits) - if err != nil { - return err - } - gui.State.Model.Commits = updatedCommits - gui.State.Model.WorkingTreeStateAtLastCommitRefresh = gui.git.Status.WorkingTreeState() - - return gui.c.PostRefreshUpdate(gui.State.Contexts.LocalCommits) -} - -func (self *Gui) refreshTags() error { - tags, err := self.git.Loaders.TagLoader.GetTags() - if err != nil { - return self.c.Error(err) - } - - self.State.Model.Tags = tags - - return self.postRefreshUpdate(self.State.Contexts.Tags) -} - -func (gui *Gui) refreshStateSubmoduleConfigs() error { - configs, err := gui.git.Submodule.GetConfigs() - if err != nil { - return err - } - - gui.State.Model.Submodules = configs - - return nil -} - -// gui.refreshStatus is called at the end of this because that's when we can -// be sure there is a State.Model.Branches array to pick the current branch from -func (gui *Gui) refreshBranches() { - reflogCommits := gui.State.Model.FilteredReflogCommits - if gui.State.Modes.Filtering.Active() { - // in filter mode we filter our reflog commits to just those containing the path - // however we need all the reflog entries to populate the recencies of our branches - // which allows us to order them correctly. So if we're filtering we'll just - // manually load all the reflog commits here - var err error - reflogCommits, _, err = gui.git.Loaders.ReflogCommitLoader.GetReflogCommits(nil, "") - if err != nil { - gui.c.Log.Error(err) - } - } - - branches, err := gui.git.Loaders.BranchLoader.Load(reflogCommits) - if err != nil { - _ = gui.c.Error(err) - } - - gui.State.Model.Branches = branches - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Branches); err != nil { - gui.c.Log.Error(err) - } - - gui.refreshStatus() -} - -func (gui *Gui) refreshFilesAndSubmodules() error { - gui.Mutexes.RefreshingFilesMutex.Lock() - gui.State.IsRefreshingFiles = true - defer func() { - gui.State.IsRefreshingFiles = false - gui.Mutexes.RefreshingFilesMutex.Unlock() - }() - - if err := gui.refreshStateSubmoduleConfigs(); err != nil { - return err - } - - if err := gui.refreshStateFiles(); err != nil { - return err - } - - gui.c.OnUIThread(func() error { - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Submodules); err != nil { - gui.c.Log.Error(err) - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Files); err != nil { - gui.c.Log.Error(err) - } - - return nil - }) - - return nil -} - -func (gui *Gui) refreshMergeState() error { - gui.State.Contexts.MergeConflicts.GetMutex().Lock() - defer gui.State.Contexts.MergeConflicts.GetMutex().Unlock() - - if gui.currentContext().GetKey() != context.MERGE_CONFLICTS_CONTEXT_KEY { - return nil - } - - hasConflicts, err := gui.helpers.MergeConflicts.SetConflictsAndRender(gui.State.Contexts.MergeConflicts.GetState().GetPath(), true) - if err != nil { - return gui.c.Error(err) - } - - if !hasConflicts { - return gui.helpers.MergeConflicts.EscapeMerge() - } - - return nil -} - -func (gui *Gui) refreshStateFiles() error { - state := gui.State - - fileTreeViewModel := state.Contexts.Files.FileTreeViewModel - - // If git thinks any of our files have inline merge conflicts, but they actually don't, - // we stage them. - // Note that if files with merge conflicts have both arisen and have been resolved - // between refreshes, we won't stage them here. This is super unlikely though, - // and this approach spares us from having to call `git status` twice in a row. - // Although this also means that at startup we won't be staging anything until - // we call git status again. - pathsToStage := []string{} - prevConflictFileCount := 0 - for _, file := range gui.State.Model.Files { - if file.HasMergeConflicts { - prevConflictFileCount++ - } - if file.HasInlineMergeConflicts { - hasConflicts, err := mergeconflicts.FileHasConflictMarkers(file.Name) - if err != nil { - gui.Log.Error(err) - } else if !hasConflicts { - pathsToStage = append(pathsToStage, file.Name) - } - } - } - - if len(pathsToStage) > 0 { - gui.c.LogAction(gui.Tr.Actions.StageResolvedFiles) - if err := gui.git.WorkingTree.StageFiles(pathsToStage); err != nil { - return gui.c.Error(err) - } - } - - files := gui.git.Loaders.FileLoader. - GetStatusFiles(git_commands.GetStatusFileOptions{}) - - conflictFileCount := 0 - for _, file := range files { - if file.HasMergeConflicts { - conflictFileCount++ - } - } - - if gui.git.Status.WorkingTreeState() != enums.REBASE_MODE_NONE && conflictFileCount == 0 && prevConflictFileCount > 0 { - gui.c.OnUIThread(func() error { return gui.helpers.MergeAndRebase.PromptToContinueRebase() }) - } - - fileTreeViewModel.RWMutex.Lock() - - // only taking over the filter if it hasn't already been set by the user. - // Though this does make it impossible for the user to actually say they want to display all if - // conflicts are currently being shown. Hmm. Worth it I reckon. If we need to add some - // extra state here to see if the user's set the filter themselves we can do that, but - // I'd prefer to maintain as little state as possible. - if conflictFileCount > 0 { - if fileTreeViewModel.GetFilter() == filetree.DisplayAll { - fileTreeViewModel.SetFilter(filetree.DisplayConflicted) - } - } else if fileTreeViewModel.GetFilter() == filetree.DisplayConflicted { - fileTreeViewModel.SetFilter(filetree.DisplayAll) - } - - state.Model.Files = files - fileTreeViewModel.SetTree() - fileTreeViewModel.RWMutex.Unlock() - - if err := gui.fileWatcher.addFilesToFileWatcher(files); err != nil { - return err - } - - return nil -} - -// the reflogs panel is the only panel where we cache data, in that we only -// load entries that have been created since we last ran the call. This means -// we need to be more careful with how we use this, and to ensure we're emptying -// the reflogs array when changing contexts. -// This method also manages two things: ReflogCommits and FilteredReflogCommits. -// FilteredReflogCommits are rendered in the reflogs panel, and ReflogCommits -// are used by the branches panel to obtain recency values for sorting. -func (gui *Gui) refreshReflogCommits() error { - // pulling state into its own variable incase it gets swapped out for another state - // and we get an out of bounds exception - state := gui.State - var lastReflogCommit *models.Commit - if len(state.Model.ReflogCommits) > 0 { - lastReflogCommit = state.Model.ReflogCommits[0] - } - - refresh := func(stateCommits *[]*models.Commit, filterPath string) error { - commits, onlyObtainedNewReflogCommits, err := gui.git.Loaders.ReflogCommitLoader. - GetReflogCommits(lastReflogCommit, filterPath) - if err != nil { - return gui.c.Error(err) - } - - if onlyObtainedNewReflogCommits { - *stateCommits = append(commits, *stateCommits...) - } else { - *stateCommits = commits - } - return nil - } - - if err := refresh(&state.Model.ReflogCommits, ""); err != nil { - return err - } - - if gui.State.Modes.Filtering.Active() { - if err := refresh(&state.Model.FilteredReflogCommits, state.Modes.Filtering.GetPath()); err != nil { - return err - } - } else { - state.Model.FilteredReflogCommits = state.Model.ReflogCommits - } - - return gui.c.PostRefreshUpdate(gui.State.Contexts.ReflogCommits) -} - -func (gui *Gui) refreshRemotes() error { - prevSelectedRemote := gui.State.Contexts.Remotes.GetSelected() - - remotes, err := gui.git.Loaders.RemoteLoader.GetRemotes() - if err != nil { - return gui.c.Error(err) - } - - gui.State.Model.Remotes = remotes - - // we need to ensure our selected remote branches aren't now outdated - if prevSelectedRemote != nil && gui.State.Model.RemoteBranches != nil { - // find remote now - for _, remote := range remotes { - if remote.Name == prevSelectedRemote.Name { - gui.State.Model.RemoteBranches = remote.Branches - break - } - } - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.Remotes); err != nil { - return err - } - - if err := gui.c.PostRefreshUpdate(gui.State.Contexts.RemoteBranches); err != nil { - return err - } - - return nil -} - -func (gui *Gui) refreshStashEntries() error { - gui.State.Model.StashEntries = gui.git.Loaders.StashLoader. - GetStashEntries(gui.State.Modes.Filtering.GetPath()) - - return gui.postRefreshUpdate(gui.State.Contexts.Stash) -} - -// never call this on its own, it should only be called from within refreshCommits() -func (gui *Gui) refreshStatus() { - gui.Mutexes.RefreshingStatusMutex.Lock() - defer gui.Mutexes.RefreshingStatusMutex.Unlock() - - currentBranch := gui.helpers.Refs.GetCheckedOutRef() - if currentBranch == nil { - // need to wait for branches to refresh - return - } - status := "" - - if currentBranch.IsRealBranch() { - status += presentation.ColoredBranchStatus(currentBranch, gui.Tr) + " " - } - - workingTreeState := gui.git.Status.WorkingTreeState() - if workingTreeState != enums.REBASE_MODE_NONE { - status += style.FgYellow.Sprintf("(%s) ", formatWorkingTreeState(workingTreeState)) - } - - name := presentation.GetBranchTextStyle(currentBranch.Name).Sprint(currentBranch.Name) - repoName := utils.GetCurrentRepoName() - status += fmt.Sprintf("%s → %s ", repoName, name) - - gui.setViewContent(gui.Views.Status, status) -} - -func (gui *Gui) refreshStagingPanel(focusOpts types.OnFocusOpts) error { - secondaryFocused := gui.secondaryStagingFocused() - mainFocused := gui.mainStagingFocused() - - // this method could be called when the staging panel is not being used, - // in which case we don't want to do anything. - if !mainFocused && !secondaryFocused { - return nil - } - - mainSelectedLineIdx := -1 - secondarySelectedLineIdx := -1 - if focusOpts.ClickedViewLineIdx > 0 { - if secondaryFocused { - secondarySelectedLineIdx = focusOpts.ClickedViewLineIdx - } else { - mainSelectedLineIdx = focusOpts.ClickedViewLineIdx - } - } - - mainContext := gui.State.Contexts.Staging - secondaryContext := gui.State.Contexts.StagingSecondary - - file := gui.getSelectedFile() - if file == nil || (!file.HasUnstagedChanges && !file.HasStagedChanges) { - return gui.handleStagingEscape() - } - - mainDiff := gui.git.WorkingTree.WorktreeFileDiff(file, true, false, false) - secondaryDiff := gui.git.WorkingTree.WorktreeFileDiff(file, true, true, false) - - // grabbing locks here and releasing before we finish the function - // because pushing say the secondary context could mean entering this function - // again, and we don't want to have a deadlock - mainContext.GetMutex().Lock() - secondaryContext.GetMutex().Lock() - - mainContext.SetState( - patch_exploring.NewState(mainDiff, mainSelectedLineIdx, mainContext.GetState(), gui.Log), - ) - - secondaryContext.SetState( - patch_exploring.NewState(secondaryDiff, secondarySelectedLineIdx, secondaryContext.GetState(), gui.Log), - ) - - mainState := mainContext.GetState() - secondaryState := secondaryContext.GetState() - - mainContent := mainContext.GetContentToRender(!secondaryFocused) - secondaryContent := secondaryContext.GetContentToRender(secondaryFocused) - - mainContext.GetMutex().Unlock() - secondaryContext.GetMutex().Unlock() - - if mainState == nil && secondaryState == nil { - return gui.handleStagingEscape() - } - - if mainState == nil && !secondaryFocused { - return gui.c.PushContext(secondaryContext, focusOpts) - } - - if secondaryState == nil && secondaryFocused { - return gui.c.PushContext(mainContext, focusOpts) - } - - if secondaryFocused { - gui.State.Contexts.StagingSecondary.FocusSelection() - } else { - gui.State.Contexts.Staging.FocusSelection() - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Staging, - Main: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(mainContent), - Title: gui.Tr.UnstagedChanges, - }, - Secondary: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(secondaryContent), - Title: gui.Tr.StagedChanges, - }, - }) -} - -func (gui *Gui) handleStagingEscape() error { - return gui.c.PushContext(gui.State.Contexts.Files) -} - -func (gui *Gui) secondaryStagingFocused() bool { - return gui.currentStaticContext().GetKey() == gui.State.Contexts.StagingSecondary.GetKey() -} - -func (gui *Gui) mainStagingFocused() bool { - return gui.currentStaticContext().GetKey() == gui.State.Contexts.Staging.GetKey() -} - -func (gui *Gui) refreshPatchBuildingPanel(opts types.OnFocusOpts) error { - selectedLineIdx := -1 - if opts.ClickedWindowName == "main" { - selectedLineIdx = opts.ClickedViewLineIdx - } - - if !gui.git.Patch.PatchBuilder.Active() { - return gui.helpers.PatchBuilding.Escape() - } - - // get diff from commit file that's currently selected - path := gui.State.Contexts.CommitFiles.GetSelectedPath() - if path == "" { - return nil - } - - ref := gui.State.Contexts.CommitFiles.CommitFileTreeViewModel.GetRef() - to := ref.RefName() - from, reverse := gui.State.Modes.Diffing.GetFromAndReverseArgsForDiff(ref.ParentRefName()) - diff, err := gui.git.WorkingTree.ShowFileDiff(from, to, reverse, path, true, - gui.IgnoreWhitespaceInDiffView) - if err != nil { - return err - } - - secondaryDiff := gui.git.Patch.PatchBuilder.RenderPatchForFile(path, false, false) - if err != nil { - return err - } - - context := gui.State.Contexts.CustomPatchBuilder - - oldState := context.GetState() - - state := patch_exploring.NewState(diff, selectedLineIdx, oldState, gui.Log) - context.SetState(state) - if state == nil { - return gui.helpers.PatchBuilding.Escape() - } - - gui.State.Contexts.CustomPatchBuilder.FocusSelection() - - mainContent := context.GetContentToRender(true) - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().PatchBuilding, - Main: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(mainContent), - Title: gui.Tr.Patch, - }, - Secondary: &types.ViewUpdateOpts{ - Task: types.NewRenderStringWithoutScrollTask(secondaryDiff), - Title: gui.Tr.CustomPatch, - }, - }) -} - -func (gui *Gui) refreshMergePanel(isFocused bool) error { - content := gui.State.Contexts.MergeConflicts.GetContentToRender(isFocused) - - var task types.UpdateTask - if gui.State.Contexts.MergeConflicts.IsUserScrolling() { - task = types.NewRenderStringWithoutScrollTask(content) - } else { - originY := gui.State.Contexts.MergeConflicts.GetOriginY() - task = types.NewRenderStringWithScrollTask(content, 0, originY) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().MergeConflicts, - Main: &types.ViewUpdateOpts{ - Task: task, - }, - }) -} - -func (gui *Gui) refreshSubCommitsWithLimit() error { - gui.Mutexes.SubCommitsMutex.Lock() - defer gui.Mutexes.SubCommitsMutex.Unlock() - - context := gui.State.Contexts.SubCommits - - commits, err := gui.git.Loaders.CommitLoader.GetCommits( - git_commands.GetCommitsOptions{ - Limit: context.GetLimitCommits(), - FilterPath: gui.State.Modes.Filtering.GetPath(), - IncludeRebaseCommits: false, - RefName: context.GetRef().FullRefName(), - }, - ) - if err != nil { - return err - } - gui.State.Model.SubCommits = commits - - return gui.c.PostRefreshUpdate(gui.State.Contexts.SubCommits) -} diff --git a/pkg/gui/remote_branches_panel.go b/pkg/gui/remote_branches_panel.go deleted file mode 100644 index 6e9a8e779..000000000 --- a/pkg/gui/remote_branches_panel.go +++ /dev/null @@ -1,22 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) remoteBranchesRenderToMain() error { - var task types.UpdateTask - remoteBranch := gui.State.Contexts.RemoteBranches.GetSelected() - if remoteBranch == nil { - task = types.NewRenderStringTask("No branches for this remote") - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(remoteBranch.FullRefName()) - task = types.NewRunCommandTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Remote Branch", - Task: task, - }, - }) -} diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go deleted file mode 100644 index edaade8a8..000000000 --- a/pkg/gui/remotes_panel.go +++ /dev/null @@ -1,29 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -// list panel functions - -func (gui *Gui) remotesRenderToMain() error { - var task types.UpdateTask - remote := gui.State.Contexts.Remotes.GetSelected() - if remote == nil { - task = types.NewRenderStringTask("No remotes") - } else { - task = types.NewRenderStringTask(fmt.Sprintf("%s\nUrls:\n%s", style.FgGreen.Sprint(remote.Name), strings.Join(remote.Urls, "\n"))) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Remote", - Task: task, - }, - }) -} diff --git a/pkg/gui/searching.go b/pkg/gui/searching.go index a8580655c..270fe1efd 100644 --- a/pkg/gui/searching.go +++ b/pkg/gui/searching.go @@ -48,7 +48,7 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in return func(y int, index int, total int) error { if total == 0 { - return gui.renderString( + gui.c.SetViewContent( gui.Views.Search, fmt.Sprintf( "no matches for '%s' %s", @@ -56,8 +56,9 @@ func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, in theme.OptionsFgColor.Sprintf("%s: exit search mode", keybindings.Label(keybindingConfig.Universal.Return)), ), ) + return nil } - _ = gui.renderString( + gui.c.SetViewContent( gui.Views.Search, fmt.Sprintf( "matches for '%s' (%d of %d) %s", diff --git a/pkg/gui/services/custom_commands/client.go b/pkg/gui/services/custom_commands/client.go index aeaae084e..4cacba385 100644 --- a/pkg/gui/services/custom_commands/client.go +++ b/pkg/gui/services/custom_commands/client.go @@ -1,10 +1,7 @@ package custom_commands import ( - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/types" ) @@ -18,15 +15,12 @@ type Client struct { } func NewClient( - c *types.HelperCommon, - os *oscommands.OSCommand, - git *commands.GitCommand, - contexts *context.ContextTree, + c *helpers.HelperCommon, helpers *helpers.Helpers, ) *Client { - sessionStateLoader := NewSessionStateLoader(contexts, helpers) - handlerCreator := NewHandlerCreator(c, os, git, sessionStateLoader) - keybindingCreator := NewKeybindingCreator(contexts) + sessionStateLoader := NewSessionStateLoader(c, helpers.Refs) + handlerCreator := NewHandlerCreator(c, sessionStateLoader) + keybindingCreator := NewKeybindingCreator(c) customCommands := c.UserConfig.CustomCommands return &Client{ diff --git a/pkg/gui/services/custom_commands/handler_creator.go b/pkg/gui/services/custom_commands/handler_creator.go index 4a81b08b5..6ec005a37 100644 --- a/pkg/gui/services/custom_commands/handler_creator.go +++ b/pkg/gui/services/custom_commands/handler_creator.go @@ -5,9 +5,8 @@ import ( "text/template" "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands" - "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" @@ -15,18 +14,14 @@ import ( // takes a custom command and returns a function that will be called when the corresponding user-defined keybinding is pressed type HandlerCreator struct { - c *types.HelperCommon - os *oscommands.OSCommand - git *commands.GitCommand + c *helpers.HelperCommon sessionStateLoader *SessionStateLoader resolver *Resolver menuGenerator *MenuGenerator } func NewHandlerCreator( - c *types.HelperCommon, - os *oscommands.OSCommand, - git *commands.GitCommand, + c *helpers.HelperCommon, sessionStateLoader *SessionStateLoader, ) *HandlerCreator { resolver := NewResolver(c.Common) @@ -34,8 +29,6 @@ func NewHandlerCreator( return &HandlerCreator{ c: c, - os: os, - git: git, sessionStateLoader: sessionStateLoader, resolver: resolver, menuGenerator: menuGenerator, @@ -143,7 +136,7 @@ func (self *HandlerCreator) confirmPrompt(prompt *config.CustomCommandPrompt, ha func (self *HandlerCreator) menuPromptFromCommand(prompt *config.CustomCommandPrompt, wrappedF func(string) error) error { // Run and save output - message, err := self.git.Custom.RunWithOutput(prompt.Command) + message, err := self.c.Git().Custom.RunWithOutput(prompt.Command) if err != nil { return self.c.Error(err) } @@ -180,7 +173,7 @@ func (self *HandlerCreator) getResolveTemplateFn(form map[string]string, promptR } funcs := template.FuncMap{ - "quote": self.os.Quote, + "quote": self.c.OS().Quote, } return func(templateStr string) (string, error) { return utils.ResolveTemplate(templateStr, objects, funcs) } @@ -193,7 +186,7 @@ func (self *HandlerCreator) finalHandler(customCommand config.CustomCommand, ses return self.c.Error(err) } - cmdObj := self.os.Cmd.NewShell(cmdStr) + cmdObj := self.c.OS().Cmd.NewShell(cmdStr) if customCommand.Subprocess { return self.c.RunSubprocessAndRefresh(cmdObj) diff --git a/pkg/gui/services/custom_commands/keybinding_creator.go b/pkg/gui/services/custom_commands/keybinding_creator.go index 7251225fe..2482f03f2 100644 --- a/pkg/gui/services/custom_commands/keybinding_creator.go +++ b/pkg/gui/services/custom_commands/keybinding_creator.go @@ -8,18 +8,19 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/context" + "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" ) // KeybindingCreator takes a custom command along with its handler and returns a corresponding keybinding type KeybindingCreator struct { - contexts *context.ContextTree + c *helpers.HelperCommon } -func NewKeybindingCreator(contexts *context.ContextTree) *KeybindingCreator { +func NewKeybindingCreator(c *helpers.HelperCommon) *KeybindingCreator { return &KeybindingCreator{ - contexts: contexts, + c: c, } } @@ -62,7 +63,7 @@ func (self *KeybindingCreator) getViewNameAndContexts(customCommand config.Custo } func (self *KeybindingCreator) contextForContextKey(contextKey types.ContextKey) (types.Context, bool) { - for _, context := range self.contexts.Flatten() { + for _, context := range self.c.Contexts().Flatten() { if context.GetKey() == contextKey { return context, true } diff --git a/pkg/gui/services/custom_commands/session_state_loader.go b/pkg/gui/services/custom_commands/session_state_loader.go index 42f3403ec..3566841b7 100644 --- a/pkg/gui/services/custom_commands/session_state_loader.go +++ b/pkg/gui/services/custom_commands/session_state_loader.go @@ -2,21 +2,20 @@ package custom_commands import ( "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers" ) // loads the session state at the time that a custom command is invoked, for use // in the custom command's template strings type SessionStateLoader struct { - contexts *context.ContextTree - helpers *helpers.Helpers + c *helpers.HelperCommon + refsHelper *helpers.RefsHelper } -func NewSessionStateLoader(contexts *context.ContextTree, helpers *helpers.Helpers) *SessionStateLoader { +func NewSessionStateLoader(c *helpers.HelperCommon, refsHelper *helpers.RefsHelper) *SessionStateLoader { return &SessionStateLoader{ - contexts: contexts, - helpers: helpers, + c: c, + refsHelper: refsHelper, } } @@ -39,18 +38,18 @@ type SessionState struct { func (self *SessionStateLoader) call() *SessionState { return &SessionState{ - SelectedFile: self.contexts.Files.GetSelectedFile(), - SelectedPath: self.contexts.Files.GetSelectedPath(), - SelectedLocalCommit: self.contexts.LocalCommits.GetSelected(), - SelectedReflogCommit: self.contexts.ReflogCommits.GetSelected(), - SelectedLocalBranch: self.contexts.Branches.GetSelected(), - SelectedRemoteBranch: self.contexts.RemoteBranches.GetSelected(), - SelectedRemote: self.contexts.Remotes.GetSelected(), - SelectedTag: self.contexts.Tags.GetSelected(), - SelectedStashEntry: self.contexts.Stash.GetSelected(), - SelectedCommitFile: self.contexts.CommitFiles.GetSelectedFile(), - SelectedCommitFilePath: self.contexts.CommitFiles.GetSelectedPath(), - SelectedSubCommit: self.contexts.SubCommits.GetSelected(), - CheckedOutBranch: self.helpers.Refs.GetCheckedOutRef(), + SelectedFile: self.c.Contexts().Files.GetSelectedFile(), + SelectedPath: self.c.Contexts().Files.GetSelectedPath(), + SelectedLocalCommit: self.c.Contexts().LocalCommits.GetSelected(), + SelectedReflogCommit: self.c.Contexts().ReflogCommits.GetSelected(), + SelectedLocalBranch: self.c.Contexts().Branches.GetSelected(), + SelectedRemoteBranch: self.c.Contexts().RemoteBranches.GetSelected(), + SelectedRemote: self.c.Contexts().Remotes.GetSelected(), + SelectedTag: self.c.Contexts().Tags.GetSelected(), + SelectedStashEntry: self.c.Contexts().Stash.GetSelected(), + SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(), + SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(), + SelectedSubCommit: self.c.Contexts().SubCommits.GetSelected(), + CheckedOutBranch: self.refsHelper.GetCheckedOutRef(), } } diff --git a/pkg/gui/side_window.go b/pkg/gui/side_window.go deleted file mode 100644 index b57998d00..000000000 --- a/pkg/gui/side_window.go +++ /dev/null @@ -1,61 +0,0 @@ -package gui - -func (gui *Gui) nextSideWindow() error { - windows := gui.getCyclableWindows() - currentWindow := gui.currentWindow() - var newWindow string - if currentWindow == "" || currentWindow == windows[len(windows)-1] { - newWindow = windows[0] - } else { - for i := range windows { - if currentWindow == windows[i] { - newWindow = windows[i+1] - break - } - if i == len(windows)-1 { - return nil - } - } - } - if err := gui.resetOrigin(gui.Views.Main); err != nil { - return err - } - - context := gui.getContextForWindow(newWindow) - - return gui.c.PushContext(context) -} - -func (gui *Gui) previousSideWindow() error { - windows := gui.getCyclableWindows() - currentWindow := gui.currentWindow() - var newWindow string - if currentWindow == "" || currentWindow == windows[0] { - newWindow = windows[len(windows)-1] - } else { - for i := range windows { - if currentWindow == windows[i] { - newWindow = windows[i-1] - break - } - if i == len(windows)-1 { - return nil - } - } - } - if err := gui.resetOrigin(gui.Views.Main); err != nil { - return err - } - - context := gui.getContextForWindow(newWindow) - - return gui.c.PushContext(context) -} - -func (gui *Gui) goToSideWindow(window string) func() error { - return func() error { - context := gui.getContextForWindow(window) - - return gui.c.PushContext(context) - } -} diff --git a/pkg/gui/snake.go b/pkg/gui/snake.go deleted file mode 100644 index 9d82275f8..000000000 --- a/pkg/gui/snake.go +++ /dev/null @@ -1,56 +0,0 @@ -package gui - -import ( - "fmt" - "strings" - - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/snake" -) - -func (gui *Gui) startSnake() { - view := gui.Views.Snake - - game := snake.NewGame(view.Width(), view.Height(), gui.renderSnakeGame, gui.c.LogAction) - gui.snakeGame = game - game.Start() -} - -func (gui *Gui) renderSnakeGame(cells [][]snake.CellType, alive bool) { - view := gui.Views.Snake - - if !alive { - _ = gui.c.ErrorMsg(gui.Tr.YouDied) - return - } - - output := drawSnakeGame(cells) - - view.Clear() - fmt.Fprint(view, output) - gui.c.Render() -} - -func drawSnakeGame(cells [][]snake.CellType) string { - writer := &strings.Builder{} - - for i, row := range cells { - for _, cell := range row { - switch cell { - case snake.None: - writer.WriteString(" ") - case snake.Snake: - writer.WriteString("█") - case snake.Food: - writer.WriteString(style.FgMagenta.Sprint("█")) - } - } - - if i < len(cells) { - writer.WriteString("\n") - } - } - - output := writer.String() - return output -} diff --git a/pkg/gui/stash_panel.go b/pkg/gui/stash_panel.go deleted file mode 100644 index 439d8205e..000000000 --- a/pkg/gui/stash_panel.go +++ /dev/null @@ -1,21 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) stashRenderToMain() error { - var task types.UpdateTask - stashEntry := gui.State.Contexts.Stash.GetSelected() - if stashEntry == nil { - task = types.NewRenderStringTask(gui.c.Tr.NoStashEntries) - } else { - task = types.NewRunPtyTask(gui.git.Stash.ShowStashEntryCmdObj(stashEntry.Index).GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Stash", - Task: task, - }, - }) -} diff --git a/pkg/gui/status/status_manager.go b/pkg/gui/status/status_manager.go new file mode 100644 index 000000000..78e4d37e3 --- /dev/null +++ b/pkg/gui/status/status_manager.go @@ -0,0 +1,94 @@ +package status + +import ( + "time" + + "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/sasha-s/go-deadlock" +) + +// StatusManager's job is to handle queuing of loading states and toast notifications +// that you see at the bottom left of the screen. +type StatusManager struct { + statuses []appStatus + nextId int + mutex deadlock.Mutex +} + +type appStatus struct { + message string + statusType string + id int +} + +func NewStatusManager() *StatusManager { + return &StatusManager{} +} + +func (self *StatusManager) WithWaitingStatus(message string, f func()) { + self.mutex.Lock() + + self.nextId += 1 + id := self.nextId + + newStatus := appStatus{ + message: message, + statusType: "waiting", + id: id, + } + self.statuses = append([]appStatus{newStatus}, self.statuses...) + + self.mutex.Unlock() + + f() + + self.removeStatus(id) +} + +func (self *StatusManager) AddToastStatus(message string) int { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.nextId++ + id := self.nextId + + newStatus := appStatus{ + message: message, + statusType: "toast", + id: id, + } + self.statuses = append([]appStatus{newStatus}, self.statuses...) + + go func() { + time.Sleep(time.Second * 2) + + self.removeStatus(id) + }() + + return id +} + +func (self *StatusManager) GetStatusString() string { + if len(self.statuses) == 0 { + return "" + } + topStatus := self.statuses[0] + if topStatus.statusType == "waiting" { + return topStatus.message + " " + utils.Loader() + } + return topStatus.message +} + +func (self *StatusManager) HasStatus() bool { + return len(self.statuses) > 0 +} + +func (self *StatusManager) removeStatus(id int) { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.statuses = slices.Filter(self.statuses, func(status appStatus) bool { + return status.id != id + }) +} diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go deleted file mode 100644 index 75f69b736..000000000 --- a/pkg/gui/status_panel.go +++ /dev/null @@ -1,141 +0,0 @@ -package gui - -import ( - "errors" - "fmt" - "strings" - - "github.com/jesseduffield/generics/slices" - "github.com/jesseduffield/lazygit/pkg/commands/types/enums" - "github.com/jesseduffield/lazygit/pkg/constants" - "github.com/jesseduffield/lazygit/pkg/gui/presentation" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func runeCount(str string) int { - return len([]rune(str)) -} - -func cursorInSubstring(cx int, prefix string, substring string) bool { - return cx >= runeCount(prefix) && cx < runeCount(prefix+substring) -} - -func (gui *Gui) handleCheckForUpdate() error { - return gui.c.WithWaitingStatus(gui.c.Tr.CheckingForUpdates, func() error { - gui.Updater.CheckForNewUpdate(gui.onUserUpdateCheckFinish, true) - return nil - }) -} - -func (gui *Gui) handleStatusClick() error { - // TODO: move into some abstraction (status is currently not a listViewContext where a lot of this code lives) - currentBranch := gui.helpers.Refs.GetCheckedOutRef() - if currentBranch == nil { - // need to wait for branches to refresh - return nil - } - - if err := gui.c.PushContext(gui.State.Contexts.Status); err != nil { - return err - } - - cx, _ := gui.Views.Status.Cursor() - upstreamStatus := presentation.BranchStatus(currentBranch, gui.Tr) - repoName := utils.GetCurrentRepoName() - workingTreeState := gui.git.Status.WorkingTreeState() - switch workingTreeState { - case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING: - workingTreeStatus := fmt.Sprintf("(%s)", formatWorkingTreeState(workingTreeState)) - if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) { - return gui.helpers.MergeAndRebase.CreateRebaseOptionsMenu() - } - if cursorInSubstring(cx, upstreamStatus+" "+workingTreeStatus+" ", repoName) { - return gui.handleCreateRecentReposMenu() - } - default: - if cursorInSubstring(cx, upstreamStatus+" ", repoName) { - return gui.handleCreateRecentReposMenu() - } - } - - return nil -} - -func formatWorkingTreeState(rebaseMode enums.RebaseMode) string { - switch rebaseMode { - case enums.REBASE_MODE_REBASING: - return "rebasing" - case enums.REBASE_MODE_MERGING: - return "merging" - default: - return "none" - } -} - -func (gui *Gui) statusRenderToMain() error { - dashboardString := strings.Join( - []string{ - lazygitTitle(), - "Copyright 2022 Jesse Duffield", - fmt.Sprintf("Keybindings: %s", constants.Links.Docs.Keybindings), - fmt.Sprintf("Config Options: %s", constants.Links.Docs.Config), - fmt.Sprintf("Tutorial: %s", constants.Links.Docs.Tutorial), - fmt.Sprintf("Raise an Issue: %s", constants.Links.Issues), - fmt.Sprintf("Release Notes: %s", constants.Links.Releases), - style.FgMagenta.Sprintf("Become a sponsor: %s", constants.Links.Donate), // caffeine ain't free - }, "\n\n") - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: gui.c.Tr.StatusTitle, - Task: types.NewRenderStringTask(dashboardString), - }, - }) -} - -func (gui *Gui) askForConfigFile(action func(file string) error) error { - confPaths := gui.Config.GetUserConfigPaths() - switch len(confPaths) { - case 0: - return errors.New(gui.c.Tr.NoConfigFileFoundErr) - case 1: - return action(confPaths[0]) - default: - menuItems := slices.Map(confPaths, func(path string) *types.MenuItem { - return &types.MenuItem{ - Label: path, - OnPress: func() error { - return action(path) - }, - } - }) - - return gui.c.Menu(types.CreateMenuOptions{ - Title: gui.c.Tr.SelectConfigFile, - Items: menuItems, - }) - } -} - -func (gui *Gui) handleOpenConfig() error { - return gui.askForConfigFile(gui.helpers.Files.OpenFile) -} - -func (gui *Gui) handleEditConfig() error { - return gui.askForConfigFile(gui.helpers.Files.EditFile) -} - -func lazygitTitle() string { - return ` - _ _ _ - | | (_) | - | | __ _ _____ _ __ _ _| |_ - | |/ _` + "`" + ` |_ / | | |/ _` + "`" + ` | | __| - | | (_| |/ /| |_| | (_| | | |_ - |_|\__,_/___|\__, |\__, |_|\__| - __/ | __/ | - |___/ |___/ ` -} diff --git a/pkg/gui/sub_commits_panel.go b/pkg/gui/sub_commits_panel.go deleted file mode 100644 index bcc63bbb7..000000000 --- a/pkg/gui/sub_commits_panel.go +++ /dev/null @@ -1,43 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -// list panel functions - -func (gui *Gui) onSubCommitFocus() error { - context := gui.State.Contexts.SubCommits - if context.GetSelectedLineIdx() > COMMIT_THRESHOLD && context.GetLimitCommits() { - context.SetLimitCommits(false) - go utils.Safe(func() { - if err := gui.refreshSubCommitsWithLimit(); err != nil { - _ = gui.c.Error(err) - } - }) - } - - return nil -} - -func (gui *Gui) subCommitsRenderToMain() error { - commit := gui.State.Contexts.SubCommits.GetSelected() - var task types.UpdateTask - if commit == nil { - task = types.NewRenderStringTask("No commits") - } else { - cmdObj := gui.git.Commit.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath(), - gui.IgnoreWhitespaceInDiffView) - - task = types.NewRunPtyTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Commit", - Task: task, - }, - }) -} diff --git a/pkg/gui/submodules_panel.go b/pkg/gui/submodules_panel.go deleted file mode 100644 index 163234153..000000000 --- a/pkg/gui/submodules_panel.go +++ /dev/null @@ -1,51 +0,0 @@ -package gui - -import ( - "fmt" - "os" - - "github.com/jesseduffield/lazygit/pkg/commands/models" - "github.com/jesseduffield/lazygit/pkg/gui/style" - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) submodulesRenderToMain() error { - var task types.UpdateTask - submodule := gui.State.Contexts.Submodules.GetSelected() - if submodule == nil { - task = types.NewRenderStringTask("No submodules") - } else { - prefix := fmt.Sprintf( - "Name: %s\nPath: %s\nUrl: %s\n\n", - style.FgGreen.Sprint(submodule.Name), - style.FgYellow.Sprint(submodule.Path), - style.FgCyan.Sprint(submodule.Url), - ) - - file := gui.helpers.WorkingTree.FileForSubmodule(submodule) - if file == nil { - task = types.NewRenderStringTask(prefix) - } else { - cmdObj := gui.git.WorkingTree.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.IgnoreWhitespaceInDiffView) - task = types.NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix) - } - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Submodule", - Task: task, - }, - }) -} - -func (gui *Gui) enterSubmodule(submodule *models.SubmoduleConfig) error { - wd, err := os.Getwd() - if err != nil { - return err - } - gui.RepoPathStack.Push(wd) - - return gui.dispatchSwitchToRepo(submodule.Path, true) -} diff --git a/pkg/gui/suggestions_panel.go b/pkg/gui/suggestions_panel.go deleted file mode 100644 index d7b8b0d2b..000000000 --- a/pkg/gui/suggestions_panel.go +++ /dev/null @@ -1,26 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) getSelectedSuggestionValue() string { - selectedSuggestion := gui.getSelectedSuggestion() - - if selectedSuggestion != nil { - return selectedSuggestion.Value - } - - return "" -} - -func (gui *Gui) getSelectedSuggestion() *types.Suggestion { - return gui.State.Contexts.Suggestions.GetSelected() -} - -func (gui *Gui) setSuggestions(suggestions []*types.Suggestion) { - gui.State.Suggestions = suggestions - gui.State.Contexts.Suggestions.SetSelectedLineIdx(0) - _ = gui.resetOrigin(gui.Views.Suggestions) - _ = gui.State.Contexts.Suggestions.HandleRender() -} diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go deleted file mode 100644 index af09e4242..000000000 --- a/pkg/gui/tags_panel.go +++ /dev/null @@ -1,22 +0,0 @@ -package gui - -import "github.com/jesseduffield/lazygit/pkg/gui/types" - -func (gui *Gui) tagsRenderToMain() error { - var task types.UpdateTask - tag := gui.State.Contexts.Tags.GetSelected() - if tag == nil { - task = types.NewRenderStringTask("No tags") - } else { - cmdObj := gui.git.Branch.GetGraphCmdObj(tag.FullRefName()) - task = types.NewRunCommandTask(cmdObj.GetCmd()) - } - - return gui.c.RenderToMainViews(types.RefreshMainOpts{ - Pair: gui.c.MainViewPairs().Normal, - Main: &types.ViewUpdateOpts{ - Title: "Tag", - Task: task, - }, - }) -} diff --git a/pkg/gui/tasks_adapter.go b/pkg/gui/tasks_adapter.go index 69b64d7b1..2bd7be4f5 100644 --- a/pkg/gui/tasks_adapter.go +++ b/pkg/gui/tasks_adapter.go @@ -49,7 +49,7 @@ func (gui *Gui) newStringTaskWithoutScroll(view *gocui.View, str string) error { manager := gui.getManager(view) f := func(stop chan struct{}) error { - gui.setViewContent(view, str) + gui.c.SetViewContent(view, str) return nil } @@ -66,7 +66,7 @@ func (gui *Gui) newStringTaskWithScroll(view *gocui.View, str string, originX in manager := gui.getManager(view) f := func(stop chan struct{}) error { - gui.setViewContent(view, str) + gui.c.SetViewContent(view, str) _ = view.SetOrigin(originX, originY) return nil } @@ -82,7 +82,9 @@ func (gui *Gui) newStringTaskWithKey(view *gocui.View, str string, key string) e manager := gui.getManager(view) f := func(stop chan struct{}) error { - return gui.renderString(view, str) + gui.c.ResetViewOrigin(view) + gui.c.SetViewContent(view, str) + return nil } if err := manager.NewTask(f, key); err != nil { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index fc6b26989..90be93a32 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -1,17 +1,24 @@ package types import ( + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sasha-s/go-deadlock" "gopkg.in/ozeidan/fuzzy-patricia.v3/patricia" ) type HelperCommon struct { + *ContextCommon +} + +type ContextCommon struct { *common.Common IGuiCommon } @@ -27,6 +34,12 @@ type IGuiCommon interface { // e.g. expanding or collapsing a folder in a file view. Calling 'Refresh' in this // case would be overkill, although refresh will internally call 'PostRefreshUpdate' PostRefreshUpdate(Context) error + + // renders string to a view without resetting its origin + SetViewContent(view *gocui.View, content string) + // resets cursor and origin of view. Often used before calling SetViewContent + ResetViewOrigin(view *gocui.View) + // this just re-renders the screen Render() // allows rendering to main views (i.e. the ones to the right of the side panel) @@ -42,16 +55,22 @@ type IGuiCommon interface { PushContext(context Context, opts ...OnFocusOpts) error PopContext() error + ReplaceContext(context Context) error // Removes all given contexts from the stack. If a given context is not in the stack, it is ignored. // This is for when you have a group of contexts that are bundled together e.g. with the commit message panel. // If you want to remove a single context, you should probably use PopContext instead. RemoveContexts([]Context) error CurrentContext() Context CurrentStaticContext() Context + CurrentSideContext() Context IsCurrentContext(Context) bool + // TODO: replace the above context-based methods with just using Context() e.g. replace PushContext() with Context().Push() + Context() IContextMgr + // enters search mode for the current view OpenSearch() + GetConfig() config.AppConfigurer GetAppState() *config.AppState SaveAppState() error @@ -59,6 +78,31 @@ type IGuiCommon interface { // Only necessary to call if you're not already on the UI thread i.e. you're inside a goroutine. // All controller handlers are executed on the UI thread. OnUIThread(f func() error) + + // returns the gocui Gui struct. There is a good chance you don't actually want to use + // this struct and instead want to use another method above + GocuiGui() *gocui.Gui + + Views() Views + + Git() *commands.GitCommand + OS() *oscommands.OSCommand + Model() *Model + + Modes() *Modes + + Mutexes() Mutexes + + State() IStateAccessor + + KeybindingsOpts() KeybindingsOpts + + // hopefully we can remove this once we've moved all our keybinding stuff out of the gui god struct. + GetInitialKeybindingsWithCustomCommands() ([]*Binding, []*gocui.ViewMouseBinding) +} + +type IModeMgr interface { + IsAnyModeActive() bool } type IPopupHandler interface { @@ -180,3 +224,57 @@ type Mutexes struct { PopupMutex *deadlock.Mutex PtyMutex *deadlock.Mutex } + +type IStateAccessor interface { + GetIgnoreWhitespaceInDiffView() bool + SetIgnoreWhitespaceInDiffView(value bool) + GetRepoPathStack() *utils.StringStack + GetRepoState() IRepoStateAccessor + // tells us whether we're currently updating lazygit + GetUpdating() bool + SetUpdating(bool) + SetIsRefreshingFiles(bool) + GetIsRefreshingFiles() bool + GetShowExtrasWindow() bool + SetShowExtrasWindow(bool) + GetRetainOriginalDir() bool + SetRetainOriginalDir(bool) +} + +type IRepoStateAccessor interface { + GetViewsSetup() bool + GetWindowViewNameMap() *utils.ThreadSafeMap[string, string] + GetStartupStage() StartupStage + SetStartupStage(stage StartupStage) + GetCurrentPopupOpts() *CreatePopupPanelOpts + SetCurrentPopupOpts(*CreatePopupPanelOpts) + GetScreenMode() WindowMaximisation + SetScreenMode(WindowMaximisation) + IsSearching() bool + SetSplitMainPanel(bool) + GetSplitMainPanel() bool +} + +// startup stages so we don't need to load everything at once +type StartupStage int + +const ( + INITIAL StartupStage = iota + COMPLETE +) + +type IFileWatcher interface { + AddFilesToFileWatcher(files []*models.File) error +} + +// screen sizing determines how much space your selected window takes up (window +// as in panel, not your terminal's window). Sometimes you want a bit more space +// to see the contents of a panel, and this keeps track of how much maximisation +// you've set +type WindowMaximisation int + +const ( + SCREEN_NORMAL WindowMaximisation = iota + SCREEN_HALF + SCREEN_FULL +) diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index e88d0d0f9..bb8630bff 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -72,6 +72,10 @@ type IBaseContext interface { // our list controller can come along and wrap it in a list-specific click handler. // We'll need to think of a better way to do this. AddOnClickFn(func() error) + + AddOnRenderToMainFn(func() error) + AddOnFocusFn(func(OnFocusOpts) error) + AddOnFocusLostFn(func(OnFocusLostOpts) error) } type Context interface { @@ -83,6 +87,16 @@ type Context interface { HandleRenderToMain() error } +type DiffableContext interface { + Context + + // Returns the current diff terminals of the currently selected item. + // in the case of a branch it returns both the branch and it's upstream name, + // which becomes an option when you bring up the diff menu, but when you're just + // flicking through branches it will be using the local branch name. + GetDiffTerminals() []string +} + type IListContext interface { Context @@ -92,6 +106,7 @@ type IListContext interface { OnSearchSelect(selectedLineIdx int) error FocusLine() + IsListContext() // used for type switch } type IPatchExplorerContext interface { @@ -106,6 +121,7 @@ type IPatchExplorerContext interface { GetContentToRender(isFocused bool) string NavigateTo(isFocused bool, selectedLineIdx int) error GetMutex() *deadlock.Mutex + IsPatchExplorerContext() // used for type switch } type IViewTrait interface { @@ -150,6 +166,9 @@ type HasKeybindings interface { GetKeybindings(opts KeybindingsOpts) []*Binding GetMouseKeybindings(opts KeybindingsOpts) []*gocui.ViewMouseBinding GetOnClick() func() error + GetOnRenderToMain() func() error + GetOnFocus() func(OnFocusOpts) error + GetOnFocusLost() func(OnFocusLostOpts) error } type IController interface { @@ -181,3 +200,16 @@ type ListItem interface { // Description is something we would show in a message e.g. '123as14: push blah' for a commit Description() string } + +type IContextMgr interface { + Push(context Context, opts ...OnFocusOpts) error + Pop() error + Replace(context Context) error + Current() Context + CurrentStatic() Context + CurrentSide() Context + IsCurrent(c Context) bool + ForEach(func(Context)) + AllList() []IListContext + AllPatchExplorer() []IPatchExplorerContext +} diff --git a/pkg/gui/types/keybindings.go b/pkg/gui/types/keybindings.go index c945ce3ab..95978e762 100644 --- a/pkg/gui/types/keybindings.go +++ b/pkg/gui/types/keybindings.go @@ -17,6 +17,12 @@ type Binding struct { Tag string // e.g. 'navigation'. Used for grouping things in the cheatsheet OpensMenu bool + // If true, the keybinding will appear at the bottom of the screen. If + // the given view has no bindings with Display: true, the default keybindings + // will be displayed instead. + // TODO: implement this + Display bool + // to be displayed if the keybinding is highlighted from within a menu Tooltip string } diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go index 475b90942..6d6c6f8a4 100644 --- a/pkg/gui/types/refresh.go +++ b/pkg/gui/types/refresh.go @@ -6,6 +6,7 @@ type RefreshableView int const ( COMMITS RefreshableView = iota REBASE_COMMITS + SUB_COMMITS BRANCHES FILES STASH @@ -32,6 +33,6 @@ const ( type RefreshOptions struct { Then func() - Scope []RefreshableView // e.g. []int{COMMITS, BRANCHES}. Leave empty to refresh everything + Scope []RefreshableView // e.g. []RefreshableView{COMMITS, BRANCHES}. Leave empty to refresh everything Mode RefreshMode // one of SYNC (default), ASYNC, and BLOCK_UI } diff --git a/pkg/gui/types/views.go b/pkg/gui/types/views.go new file mode 100644 index 000000000..8b8a62e61 --- /dev/null +++ b/pkg/gui/types/views.go @@ -0,0 +1,43 @@ +package types + +import "github.com/jesseduffield/gocui" + +type Views struct { + Status *gocui.View + Submodules *gocui.View + Files *gocui.View + Branches *gocui.View + Remotes *gocui.View + Tags *gocui.View + RemoteBranches *gocui.View + ReflogCommits *gocui.View + Commits *gocui.View + Stash *gocui.View + + Main *gocui.View + Secondary *gocui.View + Staging *gocui.View + StagingSecondary *gocui.View + PatchBuilding *gocui.View + PatchBuildingSecondary *gocui.View + MergeConflicts *gocui.View + + Options *gocui.View + Confirmation *gocui.View + Menu *gocui.View + CommitMessage *gocui.View + CommitDescription *gocui.View + CommitFiles *gocui.View + SubCommits *gocui.View + Information *gocui.View + AppStatus *gocui.View + Search *gocui.View + SearchPrefix *gocui.View + Limit *gocui.View + Suggestions *gocui.View + Tooltip *gocui.View + Extras *gocui.View + + // for playing the easter egg snake game + Snake *gocui.View +} diff --git a/pkg/gui/updates.go b/pkg/gui/updates.go deleted file mode 100644 index 93231e4f0..000000000 --- a/pkg/gui/updates.go +++ /dev/null @@ -1,85 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" -) - -func (gui *Gui) showUpdatePrompt(newVersion string) error { - message := utils.ResolvePlaceholderString( - gui.Tr.UpdateAvailable, map[string]string{ - "newVersion": newVersion, - }, - ) - - return gui.c.Confirm(types.ConfirmOpts{ - Title: gui.Tr.UpdateAvailableTitle, - Prompt: message, - HandleConfirm: func() error { - gui.startUpdating(newVersion) - return nil - }, - }) -} - -func (gui *Gui) onUserUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - return gui.c.Error(err) - } - if newVersion == "" { - return gui.c.ErrorMsg(gui.Tr.FailedToRetrieveLatestVersionErr) - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) error { - if err != nil { - // ignoring the error for now so that I'm not annoying users - gui.c.Log.Error(err.Error()) - return nil - } - if newVersion == "" { - return nil - } - if gui.c.UserConfig.Update.Method == "background" { - gui.startUpdating(newVersion) - return nil - } - return gui.showUpdatePrompt(newVersion) -} - -func (gui *Gui) startUpdating(newVersion string) { - gui.State.Updating = true - statusId := gui.statusManager.addWaitingStatus(gui.Tr.UpdateInProgressWaitingStatus) - gui.Updater.Update(newVersion, func(err error) error { return gui.onUpdateFinish(statusId, err) }) -} - -func (gui *Gui) onUpdateFinish(statusId int, err error) error { - gui.State.Updating = false - gui.statusManager.removeStatus(statusId) - gui.c.OnUIThread(func() error { - _ = gui.renderString(gui.Views.AppStatus, "") - if err != nil { - errMessage := utils.ResolvePlaceholderString( - gui.Tr.UpdateFailedErr, map[string]string{ - "errMessage": err.Error(), - }, - ) - return gui.c.ErrorMsg(errMessage) - } - return gui.c.Alert(gui.Tr.UpdateCompletedTitle, gui.Tr.UpdateCompleted) - }) - - return nil -} - -func (gui *Gui) createUpdateQuitConfirmation() error { - return gui.c.Confirm(types.ConfirmOpts{ - Title: gui.Tr.ConfirmQuitDuringUpdateTitle, - Prompt: gui.Tr.ConfirmQuitDuringUpdate, - HandleConfirm: func() error { - return gocui.ErrQuit - }, - }) -} diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 9c6d5539f..85ec21128 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -1,19 +1,21 @@ package gui import ( - "fmt" - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/keybindings" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/tasks" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/spkg/bom" ) -func (gui *Gui) resetOrigin(v *gocui.View) error { - _ = v.SetCursor(0, 0) - return v.SetOrigin(0, 0) +func (gui *Gui) resetViewOrigin(v *gocui.View) { + if err := v.SetCursor(0, 0); err != nil { + gui.Log.Error(err) + } + + if err := v.SetOrigin(0, 0); err != nil { + gui.Log.Error(err) + } } // Returns the number of lines that we should read initially from a cmd task so @@ -52,18 +54,6 @@ func (gui *Gui) setViewContent(v *gocui.View, s string) { v.SetContent(gui.cleanString(s)) } -// renderString resets the origin of a view and sets its content -func (gui *Gui) renderString(view *gocui.View, s string) error { - if err := view.SetOrigin(0, 0); err != nil { - return err - } - if err := view.SetCursor(0, 0); err != nil { - return err - } - gui.setViewContent(view, s) - return nil -} - func (gui *Gui) currentViewName() string { currentView := gui.g.CurrentView() if currentView == nil { @@ -72,96 +62,6 @@ func (gui *Gui) currentViewName() string { return currentView.Name() } -func (gui *Gui) resizeCurrentPopupPanel() error { - v := gui.g.CurrentView() - if v == nil { - return nil - } - - c := gui.c.CurrentContext() - - if c == gui.State.Contexts.Menu { - gui.resizeMenu() - } else if c == gui.State.Contexts.Confirmation || c == gui.State.Contexts.Suggestions { - gui.resizeConfirmationPanel() - } else if c == gui.State.Contexts.CommitMessage || c == gui.State.Contexts.CommitDescription { - gui.resizeCommitMessagePanels() - } - - return nil -} - -func (gui *Gui) resizeMenu() { - itemCount := gui.State.Contexts.Menu.GetList().Len() - offset := 3 - panelWidth := gui.getConfirmationPanelWidth() - x0, y0, x1, y1 := gui.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) - menuBottom := y1 - offset - _, _ = gui.g.SetView(gui.Views.Menu.Name(), x0, y0, x1, menuBottom, 0) - - tooltipTop := menuBottom + 1 - tooltipHeight := gui.getMessageHeight(true, gui.State.Contexts.Menu.GetSelected().Tooltip, panelWidth) + 2 // plus 2 for the frame - _, _ = gui.g.SetView(gui.Views.Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) -} - -func (gui *Gui) resizeConfirmationPanel() { - suggestionsViewHeight := 0 - if gui.Views.Suggestions.Visible { - suggestionsViewHeight = 11 - } - panelWidth := gui.getConfirmationPanelWidth() - prompt := gui.Views.Confirmation.Buffer() - wrap := true - if gui.Views.Confirmation.Editable { - prompt = gui.Views.Confirmation.TextArea.GetContent() - wrap = false - } - panelHeight := gui.getMessageHeight(wrap, prompt, panelWidth) + suggestionsViewHeight - x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight) - confirmationViewBottom := y1 - suggestionsViewHeight - _, _ = gui.g.SetView(gui.Views.Confirmation.Name(), x0, y0, x1, confirmationViewBottom, 0) - - suggestionsViewTop := confirmationViewBottom + 1 - _, _ = gui.g.SetView(gui.Views.Suggestions.Name(), x0, suggestionsViewTop, x1, suggestionsViewTop+suggestionsViewHeight, 0) -} - -func (gui *Gui) resizeCommitMessagePanels() { - panelWidth := gui.getConfirmationPanelWidth() - content := gui.Views.CommitDescription.TextArea.GetContent() - summaryViewHeight := 3 - panelHeight := gui.getMessageHeight(false, content, panelWidth) - minHeight := 7 - if panelHeight < minHeight { - panelHeight = minHeight - } - x0, y0, x1, y1 := gui.getPopupPanelDimensionsAux(panelWidth, panelHeight) - - _, _ = gui.g.SetView(gui.Views.CommitMessage.Name(), x0, y0, x1, y0+summaryViewHeight-1, 0) - _, _ = gui.g.SetView(gui.Views.CommitDescription.Name(), x0, y0+summaryViewHeight, x1, y1+summaryViewHeight, 0) -} - -func (gui *Gui) globalOptionsMap() map[string]string { - keybindingConfig := gui.c.UserConfig.Keybinding - - return map[string]string{ - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollUpMain), keybindings.Label(keybindingConfig.Universal.ScrollDownMain)): gui.c.Tr.LcScroll, - fmt.Sprintf("%s %s %s %s", keybindings.Label(keybindingConfig.Universal.PrevBlock), keybindings.Label(keybindingConfig.Universal.NextBlock), keybindings.Label(keybindingConfig.Universal.PrevItem), keybindings.Label(keybindingConfig.Universal.NextItem)): gui.c.Tr.LcNavigate, - keybindings.Label(keybindingConfig.Universal.Return): gui.c.Tr.LcCancel, - keybindings.Label(keybindingConfig.Universal.Quit): gui.c.Tr.LcQuit, - keybindings.Label(keybindingConfig.Universal.OptionMenuAlt1): gui.c.Tr.LcMenu, - fmt.Sprintf("%s-%s", keybindings.Label(keybindingConfig.Universal.JumpToBlock[0]), keybindings.Label(keybindingConfig.Universal.JumpToBlock[len(keybindingConfig.Universal.JumpToBlock)-1])): gui.c.Tr.LcJump, - fmt.Sprintf("%s/%s", keybindings.Label(keybindingConfig.Universal.ScrollLeft), keybindings.Label(keybindingConfig.Universal.ScrollRight)): gui.c.Tr.LcScrollLeftRight, - } -} - -func (gui *Gui) isPopupPanel(viewName string) bool { - return viewName == "commitMessage" || viewName == "confirmation" || viewName == "menu" -} - -func (gui *Gui) popupPanelFocused() bool { - return gui.isPopupPanel(gui.currentViewName()) -} - func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { tabs := gui.viewTabMap()[windowName] if len(tabs) == 0 { @@ -170,7 +70,7 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { viewName := tabs[tabIndex].ViewName - context, ok := gui.contextForView(viewName) + context, ok := gui.helpers.View.ContextForView(viewName) if !ok { return nil } @@ -178,21 +78,6 @@ func (gui *Gui) onViewTabClick(windowName string, tabIndex int) error { return gui.c.PushContext(context) } -func (gui *Gui) contextForView(viewName string) (types.Context, bool) { - view, err := gui.g.View(viewName) - if err != nil { - return nil, false - } - - for _, context := range gui.State.Contexts.Flatten() { - if context.GetViewName() == view.Name() { - return context, true - } - } - - return nil, false -} - func (gui *Gui) handleNextTab() error { view := getTabbedView(gui) if view == nil { @@ -231,7 +116,7 @@ func (gui *Gui) handlePrevTab() error { func getTabbedView(gui *Gui) *gocui.View { // It safe assumption that only static contexts have tabs - context := gui.currentStaticContext() + context := gui.c.CurrentStaticContext() view, _ := gui.g.View(context.GetViewName()) return view } @@ -239,3 +124,20 @@ func getTabbedView(gui *Gui) *gocui.View { func (gui *Gui) render() { gui.c.OnUIThread(func() error { return nil }) } + +// postRefreshUpdate is to be called on a context after the state that it depends on has been refreshed +// if the context's view is set to another context we do nothing. +// if the context's view is the current view we trigger a focus; re-selecting the current item. +func (gui *Gui) postRefreshUpdate(c types.Context) error { + if err := c.HandleRender(); err != nil { + return err + } + + if gui.currentViewName() == c.GetViewName() { + if err := c.HandleFocus(types.OnFocusOpts{}); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/gui/views.go b/pkg/gui/views.go index 579e4da48..e761d4cad 100644 --- a/pkg/gui/views.go +++ b/pkg/gui/views.go @@ -3,49 +3,10 @@ package gui import ( "github.com/jesseduffield/generics/slices" "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/gui/constants" "github.com/jesseduffield/lazygit/pkg/theme" ) -type Views struct { - Status *gocui.View - Submodules *gocui.View - Files *gocui.View - Branches *gocui.View - Remotes *gocui.View - Tags *gocui.View - RemoteBranches *gocui.View - ReflogCommits *gocui.View - Commits *gocui.View - Stash *gocui.View - - Main *gocui.View - Secondary *gocui.View - Staging *gocui.View - StagingSecondary *gocui.View - PatchBuilding *gocui.View - PatchBuildingSecondary *gocui.View - MergeConflicts *gocui.View - - Options *gocui.View - Confirmation *gocui.View - Menu *gocui.View - CommitMessage *gocui.View - CommitDescription *gocui.View - CommitFiles *gocui.View - SubCommits *gocui.View - Information *gocui.View - AppStatus *gocui.View - Search *gocui.View - SearchPrefix *gocui.View - Limit *gocui.View - Suggestions *gocui.View - Tooltip *gocui.View - Extras *gocui.View - - // for playing the easter egg snake game - Snake *gocui.View -} - type viewNameMapping struct { viewPtr **gocui.View name string @@ -106,15 +67,6 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping { } } -func (gui *Gui) windowForView(viewName string) string { - context, ok := gui.contextForView(viewName) - if !ok { - panic("todo: deal with this") - } - - return context.GetWindowName() -} - func (gui *Gui) createAllViews() error { frameRunes := []rune{'─', '│', '┌', '┐', '└', '┘'} switch gui.c.UserConfig.Gui.Border { @@ -142,7 +94,7 @@ func (gui *Gui) createAllViews() error { gui.Views.SearchPrefix.BgColor = gocui.ColorDefault gui.Views.SearchPrefix.FgColor = gocui.ColorGreen gui.Views.SearchPrefix.Frame = false - gui.setViewContent(gui.Views.SearchPrefix, SEARCH_PREFIX) + gui.c.SetViewContent(gui.Views.SearchPrefix, constants.SEARCH_PREFIX) gui.Views.Stash.Title = gui.c.Tr.StashTitle @@ -213,6 +165,7 @@ func (gui *Gui) createAllViews() error { gui.Views.CommitDescription.Editor = gocui.EditorFunc(gui.commitDescriptionEditor) gui.Views.Confirmation.Visible = false + gui.Views.Confirmation.Editor = gocui.EditorFunc(gui.promptEditor) gui.Views.Suggestions.Visible = false diff --git a/pkg/gui/whitespace-toggle.go b/pkg/gui/whitespace-toggle.go deleted file mode 100644 index b4ee798cc..000000000 --- a/pkg/gui/whitespace-toggle.go +++ /dev/null @@ -1,17 +0,0 @@ -package gui - -import ( - "github.com/jesseduffield/lazygit/pkg/gui/types" -) - -func (gui *Gui) toggleWhitespaceInDiffView() error { - gui.IgnoreWhitespaceInDiffView = !gui.IgnoreWhitespaceInDiffView - - toastMessage := gui.c.Tr.ShowingWhitespaceInDiffView - if gui.IgnoreWhitespaceInDiffView { - toastMessage = gui.c.Tr.IgnoringWhitespaceInDiffView - } - gui.c.Toast(toastMessage) - - return gui.currentSideListContext().HandleFocus(types.OnFocusOpts{}) -} diff --git a/pkg/gui/window.go b/pkg/gui/window.go deleted file mode 100644 index c5355bcea..000000000 --- a/pkg/gui/window.go +++ /dev/null @@ -1,121 +0,0 @@ -package gui - -import ( - "fmt" - - "github.com/jesseduffield/gocui" - "github.com/jesseduffield/lazygit/pkg/gui/context" - "github.com/jesseduffield/lazygit/pkg/gui/types" - "github.com/jesseduffield/lazygit/pkg/utils" - "github.com/samber/lo" -) - -// A window refers to a place on the screen which can hold one or more views. -// A view is a box that renders content, and within a window only one view will -// appear at a time. When a view appears within a window, it occupies the whole -// space. Right now most windows are 1:1 with views, except for commitFiles which -// is a view that moves between windows - -func (gui *Gui) initialWindowViewNameMap(contextTree *context.ContextTree) *utils.ThreadSafeMap[string, string] { - result := utils.NewThreadSafeMap[string, string]() - - for _, context := range contextTree.Flatten() { - result.Set(context.GetWindowName(), context.GetViewName()) - } - - return result -} - -func (gui *Gui) getViewNameForWindow(window string) string { - viewName, ok := gui.State.WindowViewNameMap.Get(window) - if !ok { - panic(fmt.Sprintf("Viewname not found for window: %s", window)) - } - - return viewName -} - -func (gui *Gui) getContextForWindow(window string) types.Context { - viewName := gui.getViewNameForWindow(window) - - context, ok := gui.contextForView(viewName) - if !ok { - panic("TODO: fix this") - } - - return context -} - -// for now all we actually care about is the context's view so we're storing that -func (gui *Gui) setWindowContext(c types.Context) { - if c.IsTransient() { - gui.resetWindowContext(c) - } - - gui.State.WindowViewNameMap.Set(c.GetWindowName(), c.GetViewName()) -} - -func (gui *Gui) currentWindow() string { - return gui.currentContext().GetWindowName() -} - -// assumes the context's windowName has been set to the new window if necessary -func (gui *Gui) resetWindowContext(c types.Context) { - for _, windowName := range gui.State.WindowViewNameMap.Keys() { - viewName, ok := gui.State.WindowViewNameMap.Get(windowName) - if !ok { - continue - } - if viewName == c.GetViewName() && windowName != c.GetWindowName() { - for _, context := range gui.State.Contexts.Flatten() { - if context.GetKey() != c.GetKey() && context.GetWindowName() == windowName { - gui.State.WindowViewNameMap.Set(windowName, context.GetViewName()) - } - } - } - } -} - -// moves given context's view to the top of the window -func (gui *Gui) moveToTopOfWindow(context types.Context) { - view := context.GetView() - if view == nil { - return - } - - window := context.GetWindowName() - - topView := gui.topViewInWindow(window) - - if view.Name() != topView.Name() { - if err := gui.g.SetViewOnTopOf(view.Name(), topView.Name()); err != nil { - gui.Log.Error(err) - } - } -} - -func (gui *Gui) topViewInWindow(windowName string) *gocui.View { - // now I need to find all views in that same window, via contexts. And I guess then I need to find the index of the highest view in that list. - viewNamesInWindow := gui.viewNamesInWindow(windowName) - - // The views list is ordered highest-last, so we're grabbing the last view of the window - var topView *gocui.View - for _, currentView := range gui.g.Views() { - if lo.Contains(viewNamesInWindow, currentView.Name()) { - topView = currentView - } - } - - return topView -} - -func (gui *Gui) viewNamesInWindow(windowName string) []string { - result := []string{} - for _, context := range gui.State.Contexts.Flatten() { - if context.GetWindowName() == windowName { - result = append(result, context.GetViewName()) - } - } - - return result -} diff --git a/pkg/i18n/chinese.go b/pkg/i18n/chinese.go index 95e02d099..decbb4ea8 100644 --- a/pkg/i18n/chinese.go +++ b/pkg/i18n/chinese.go @@ -94,9 +94,9 @@ func chineseTranslationSet() TranslationSet { LcNewBranch: "新分支", LcDeleteBranch: "删除分支", NoBranchesThisRepo: "此仓库中没有分支", - CommitMessageConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认", CommitWithoutMessageErr: "您必须编写提交消息才能进行提交", - CloseConfirm: "{{.keyBindClose}}:关闭,{{.keyBindConfirm}}:确认", + LcCloseCancel: "关闭", + LcConfirm: "确认", LcClose: "关闭", LcQuit: "退出", LcSquashDown: "向下压缩", @@ -117,8 +117,6 @@ func chineseTranslationSet() TranslationSet { LcAmendToCommit: "用已暂存的更改来修补提交", LcRenameCommitEditor: "使用编辑器重命名提交", Error: "错误", - LcSelectHunk: "切换区块", - LcNavigateConflicts: "浏览冲突", LcPickHunk: "选中区块", LcPickAllHunks: "选中所有区块", LcUndo: "撤销", diff --git a/pkg/i18n/dutch.go b/pkg/i18n/dutch.go index a3bdc1778..8e8c9d21a 100644 --- a/pkg/i18n/dutch.go +++ b/pkg/i18n/dutch.go @@ -60,9 +60,9 @@ func dutchTranslationSet() TranslationSet { LcNewBranch: "nieuwe branch", LcDeleteBranch: "verwijder branch", NoBranchesThisRepo: "Geen branches voor deze repo", - CommitMessageConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig", CommitWithoutMessageErr: "Je kan geen commit maken zonder commit bericht", - CloseConfirm: "{{.keyBindClose}}: Sluiten, {{.keyBindConfirm}}: Bevestig", + LcCloseCancel: "sluiten", + LcConfirm: "bevestig", LcClose: "sluiten", LcQuit: "quit", LcSquashDown: "squash beneden", @@ -83,8 +83,6 @@ func dutchTranslationSet() TranslationSet { LcRenameCommitEditor: "hernoem commit met editor", NoCommitsThisBranch: "Geen commits in deze branch", Error: "Foutmelding", - LcSelectHunk: "selecteer stuk", - LcNavigateConflicts: "navigeer conflicts", LcPickHunk: "kies stuk", LcPickAllHunks: "kies beide stukken", LcUndo: "ongedaan maken", diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index d8d5e4727..5e34022d3 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -80,10 +80,10 @@ type TranslationSet struct { LcNewBranch string LcDeleteBranch string NoBranchesThisRepo string - CommitMessageConfirm string CommitWithoutMessageErr string - CloseConfirm string LcClose string + LcCloseCancel string + LcConfirm string LcQuit string LcSquashDown string LcFixupCommit string @@ -107,8 +107,6 @@ type TranslationSet struct { NoCommitsThisBranch string UpdateRefHere string Error string - LcSelectHunk string - LcNavigateConflicts string LcPickHunk string LcPickAllHunks string LcUndo string @@ -752,10 +750,10 @@ func EnglishTranslationSet() TranslationSet { LcNewBranch: "new branch", LcDeleteBranch: "delete branch", NoBranchesThisRepo: "No branches for this repo", - CommitMessageConfirm: "{{.keyBindClose}}: close, {{.keyBindConfirm}}: confirm", CommitWithoutMessageErr: "You cannot commit without a commit message", - CloseConfirm: "{{.keyBindClose}}: close/cancel, {{.keyBindConfirm}}: confirm", LcClose: "close", + LcCloseCancel: "close/cancel", + LcConfirm: "confirm", LcQuit: "quit", LcSquashDown: "squash down", LcFixupCommit: "fixup commit", @@ -779,8 +777,6 @@ func EnglishTranslationSet() TranslationSet { SureResetCommitAuthor: "The author field of this commit will be updated to match the configured user. This also renews the author timestamp. Continue?", LcRenameCommitEditor: "reword commit with editor", Error: "Error", - LcSelectHunk: "select hunk", - LcNavigateConflicts: "navigate conflicts", LcPickHunk: "pick hunk", LcPickAllHunks: "pick all hunks", LcUndo: "undo", diff --git a/pkg/i18n/japanese.go b/pkg/i18n/japanese.go index d68056812..1defdf1c9 100644 --- a/pkg/i18n/japanese.go +++ b/pkg/i18n/japanese.go @@ -85,9 +85,9 @@ func japaneseTranslationSet() TranslationSet { LcNewBranch: "新しいブランチを作成", LcDeleteBranch: "ブランチを削除", NoBranchesThisRepo: "リポジトリにブランチが存在しません", - CommitMessageConfirm: "{{.keyBindClose}}: 閉じる, {{.keyBindConfirm}}: 確定", CommitWithoutMessageErr: "コミットメッセージを入力してください", - CloseConfirm: "{{.keyBindClose}}: 閉じる/キャンセル, {{.keyBindConfirm}}: 確認", + LcCloseCancel: "閉じる/キャンセル", + LcConfirm: "確認", LcClose: "閉じる", LcQuit: "終了", // LcSquashDown: "squash down", @@ -108,8 +108,6 @@ func japaneseTranslationSet() TranslationSet { LcAmendToCommit: "ステージされた変更でamendコミット", LcRenameCommitEditor: "エディタでコミットメッセージを編集", Error: "エラー", - LcSelectHunk: "hunkを選択", - LcNavigateConflicts: "コンフリクトを移動", // LcPickHunk: "pick hunk", // LcPickAllHunks: "pick all hunks", LcUndo: "アンドゥ", diff --git a/pkg/i18n/korean.go b/pkg/i18n/korean.go index 7a150dcb6..ebdcb2ea5 100644 --- a/pkg/i18n/korean.go +++ b/pkg/i18n/korean.go @@ -84,9 +84,9 @@ func koreanTranslationSet() TranslationSet { LcNewBranch: "새 브랜치 생성", LcDeleteBranch: "브랜치 삭제", NoBranchesThisRepo: "저장소에 브랜치가 존재하지 않습니다.", - CommitMessageConfirm: "{{.keyBindClose}}: 닫기, {{.keyBindConfirm}}: 확인", CommitWithoutMessageErr: "커밋 메시지를 입력하세요.", - CloseConfirm: "{{.keyBindClose}}: 닫기/취소, {{.keyBindConfirm}}: 확인", + LcCloseCancel: "닫기/취소", + LcConfirm: "확인", LcClose: "닫기", LcQuit: "종료", LcSquashDown: "squash down", @@ -109,8 +109,6 @@ func koreanTranslationSet() TranslationSet { SureResetCommitAuthor: "The author field of this commit will be updated to match the configured user. This also renews the author timestamp. Continue?", LcRenameCommitEditor: "에디터에서 커밋메시지 수정", Error: "오류", - LcSelectHunk: "hunk를 선택", - LcNavigateConflicts: "navigate conflicts", LcPickHunk: "pick hunk", LcPickAllHunks: "pick all hunks", LcUndo: "되돌리기", diff --git a/pkg/i18n/polish.go b/pkg/i18n/polish.go index a658cff65..5fdbbad74 100644 --- a/pkg/i18n/polish.go +++ b/pkg/i18n/polish.go @@ -55,9 +55,9 @@ func polishTranslationSet() TranslationSet { LcNewBranch: "nowa gałąź", LcDeleteBranch: "usuń gałąź", NoBranchesThisRepo: "Brak gałęzi dla tego repozytorium", - CommitMessageConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", CommitWithoutMessageErr: "Nie możesz commitować bez komunikatu", - CloseConfirm: "{{.keyBindClose}}: zamknij, {{.keyBindConfirm}}: potwierdź", + LcCloseCancel: "zamknij", + LcConfirm: "potwierdź", LcClose: "zamknij", LcSquashDown: "ściśnij", LcFixupCommit: "napraw commit", @@ -68,8 +68,6 @@ func polishTranslationSet() TranslationSet { LcRewordCommit: "zmień nazwę commita", LcRenameCommitEditor: "zmień nazwę commita w edytorze", Error: "Błąd", - LcSelectHunk: "wybierz kawałek", - LcNavigateConflicts: "nawiguj konflikty", LcPickHunk: "wybierz kawałek", LcPickAllHunks: "wybierz oba kawałki", LcUndo: "cofnij", diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index 03ca81295..5c2bfca7e 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -95,8 +95,6 @@ func runTest( return nil } - logf("path: %s", paths.Root()) - if err := prepareTestDir(test, paths, projectRootDir); err != nil { return err } diff --git a/pkg/integration/tests/diff/ignore_whitespace.go b/pkg/integration/tests/diff/ignore_whitespace.go index e4c01963f..157d3e7ff 100644 --- a/pkg/integration/tests/diff/ignore_whitespace.go +++ b/pkg/integration/tests/diff/ignore_whitespace.go @@ -5,29 +5,60 @@ import ( . "github.com/jesseduffield/lazygit/pkg/integration/components" ) +var ( + initialFileContent = "first-line\nold-second-line\nthird-line\n" + // We're indenting each line and modifying the second line + updatedFileContent = " first-line\n new-second-line\n third-line\n" +) + var IgnoreWhitespace = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "View diff with and without ignoring whitespace", + Description: "Toggle whitespace in the diff", ExtraCmdArgs: "", Skip: false, SetupConfig: func(config *config.AppConfig) {}, SetupRepo: func(shell *Shell) { - shell.CreateFileAndAdd("file1", "first line\nsecond line\n") - shell.Commit("first commit") - // First line has a real change, second line changes only indentation: - shell.UpdateFileAndAdd("file1", "first line changed\n second line\n") - shell.Commit("second commit") + shell.CreateFileAndAdd("myfile", initialFileContent) + shell.Commit("initial commit") + shell.UpdateFile("myfile", updatedFileContent) }, Run: func(t *TestDriver, keys config.KeybindingConfig) { - t.Views().Commits(). - Focus(). - Tap(func() { - // By default, both changes are shown in the diff: - t.Views().Main().Content(Contains("-first line\n-second line\n+first line changed\n+ second line\n")) - }). - Press(keys.Universal.ToggleWhitespaceInDiffView). - Tap(func() { - // After enabling ignore whitespace, only the real change remains: - t.Views().Main().Content(Contains("-first line\n+first line changed\n")) - }) + t.Views().Main().ContainsLines( + Contains(`-first-line`), + Contains(`-old-second-line`), + Contains(`-third-line`), + Contains(`+ first-line`), + Contains(`+ new-second-line`), + Contains(`+ third-line`), + ) + + t.Views().Files(). + IsFocused(). + Press(keys.Universal.ToggleWhitespaceInDiffView) + + t.ExpectToast(Equals("Whitespace will be ignored in the diff view")) + + // lines with only whitespace changes are ignored (first and third lines) + t.Views().Main().ContainsLines( + Contains(` first-line`), + Contains(`-old-second-line`), + Contains(`+ new-second-line`), + Contains(` third-line`), + ) + + // when toggling again it goes back to showing whitespace + t.Views().Files(). + IsFocused(). + Press(keys.Universal.ToggleWhitespaceInDiffView) + + t.ExpectToast(Equals("Whitespace will be shown in the diff view")) + + t.Views().Main().ContainsLines( + Contains(`-first-line`), + Contains(`-old-second-line`), + Contains(`-third-line`), + Contains(`+ first-line`), + Contains(`+ new-second-line`), + Contains(`+ third-line`), + ) }, }) diff --git a/pkg/updates/updates.go b/pkg/updates/updates.go index 58c93fa7d..d375c0eda 100644 --- a/pkg/updates/updates.go +++ b/pkg/updates/updates.go @@ -235,13 +235,8 @@ func (u *Updater) getBinaryUrl(newVersion string) string { } // Update downloads the latest binary and replaces the current binary with it -func (u *Updater) Update(newVersion string, onFinish func(error) error) { - go utils.Safe(func() { - err := u.update(newVersion) - if err = onFinish(err); err != nil { - u.Log.Error(err) - } - }) +func (u *Updater) Update(newVersion string) error { + return u.update(newVersion) } func (u *Updater) update(newVersion string) error {