From 85490939487d3e942761d81e32de87d1624e5cb9 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 10:41:59 +0200 Subject: [PATCH 1/9] Add open in external editor Add a QMenu Item, that will open the current repo inside the configured external edito. Add a new icon, for the QMenu Item. --- gitfourchette/assets/icons/code.svg | 1 + gitfourchette/mainwindow.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 gitfourchette/assets/icons/code.svg diff --git a/gitfourchette/assets/icons/code.svg b/gitfourchette/assets/icons/code.svg new file mode 100644 index 00000000..afcde270 --- /dev/null +++ b/gitfourchette/assets/icons/code.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/gitfourchette/mainwindow.py b/gitfourchette/mainwindow.py index d6ecbd78..7e8c5bbc 100644 --- a/gitfourchette/mainwindow.py +++ b/gitfourchette/mainwindow.py @@ -34,7 +34,7 @@ from gitfourchette.porcelain import * from gitfourchette.qt import * from gitfourchette.repowidget import RepoWidget -from gitfourchette.settings import PrefEffects, TabBarClick +from gitfourchette.settings import PrefEffects, TabBarClick, getExternalEditorName from gitfourchette.syntax import LexJobCache from gitfourchette.tasks import TaskBook, RepoTaskRunner from gitfourchette.tasks.newrepotasks import NewRepo @@ -1254,6 +1254,14 @@ def workdirProxy(): tip=_("Open a terminal in the repo’s working directory"), ), + ActionDef( + _(f"Op&en in {getExternalEditorName()}"), + lambda: ToolProcess.startTextEditor(self, workdirProxy()), + icon="code", + shortcuts=GlobalShortcuts.NO_SHORTCUT, + tip=_(f"Open this repo’s working directory in {getExternalEditorName()}"), + ), + ActionDef( _("Cop&y Repo Path"), lambda: self.repolessCopyPath(workdirProxy()), From 527fa84bdeaf605da45ed7177d06ce4375938230 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:42:17 +0200 Subject: [PATCH 2/9] If no editor has been selected user will be prompted to do so --- gitfourchette/mainwindow.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/gitfourchette/mainwindow.py b/gitfourchette/mainwindow.py index 7e8c5bbc..38d6de00 100644 --- a/gitfourchette/mainwindow.py +++ b/gitfourchette/mainwindow.py @@ -1256,8 +1256,8 @@ def workdirProxy(): ActionDef( _(f"Op&en in {getExternalEditorName()}"), - lambda: ToolProcess.startTextEditor(self, workdirProxy()), icon="code", + lambda: self.openRepoInEditor(workdirProxy()), shortcuts=GlobalShortcuts.NO_SHORTCUT, tip=_(f"Open this repo’s working directory in {getExternalEditorName()}"), ), @@ -1281,6 +1281,13 @@ def workdirProxy(): ), ] + def openRepoInEditor(self, workdir: str) -> None: + if settings.prefs.externalEditor == "": + setUpToolCommand(self, "externalEditor") + return + + ToolProcess.startTextEditor(self, workdir) + def repolessSetNickname(self, workdir: str): defaultName = Path(workdir).name oldNickname = settings.history.getRepoNickname(workdir, strict=True) From 147948f7b550e122abff33f7eaa608941151c1e1 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:42:55 +0200 Subject: [PATCH 3/9] missing import --- gitfourchette/mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitfourchette/mainwindow.py b/gitfourchette/mainwindow.py index 38d6de00..6bf4b1d1 100644 --- a/gitfourchette/mainwindow.py +++ b/gitfourchette/mainwindow.py @@ -19,7 +19,7 @@ from gitfourchette.application import GFApplication from gitfourchette.codeview.codeview import CodeView from gitfourchette.dropzone import DropAction, DropZone -from gitfourchette.exttools.toolprocess import ToolProcess +from gitfourchette.exttools.toolprocess import ToolProcess, setUpToolCommand from gitfourchette.exttools.usercommand import UserCommand from gitfourchette.forms.aboutdialog import AboutDialog from gitfourchette.forms.clonedialog import CloneDialog From e092d6cdf42c6929c4aef83dcc809030b0aa361b Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:43:25 +0200 Subject: [PATCH 4/9] 4. changed icon --- gitfourchette/assets/icons/code.svg | 1 - gitfourchette/mainwindow.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 gitfourchette/assets/icons/code.svg diff --git a/gitfourchette/assets/icons/code.svg b/gitfourchette/assets/icons/code.svg deleted file mode 100644 index afcde270..00000000 --- a/gitfourchette/assets/icons/code.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/gitfourchette/mainwindow.py b/gitfourchette/mainwindow.py index 6bf4b1d1..c10e695d 100644 --- a/gitfourchette/mainwindow.py +++ b/gitfourchette/mainwindow.py @@ -1256,8 +1256,8 @@ def workdirProxy(): ActionDef( _(f"Op&en in {getExternalEditorName()}"), - icon="code", lambda: self.openRepoInEditor(workdirProxy()), + icon="prefs-diff", shortcuts=GlobalShortcuts.NO_SHORTCUT, tip=_(f"Open this repo’s working directory in {getExternalEditorName()}"), ), From c79b62eec3be268d43906b8f48481dfe70ef312c Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:44:02 +0200 Subject: [PATCH 5/9] 1. rebuild menu bar if external Editor changes --- gitfourchette/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gitfourchette/settings.py b/gitfourchette/settings.py index 82620e41..8f1ec449 100644 --- a/gitfourchette/settings.py +++ b/gitfourchette/settings.py @@ -253,6 +253,7 @@ class PrefEffects: "language", "commands", "confirmCommands", + "externalEditor" } "Pref keys that trigger a rebuild of the main menu." From 6dcfc5f6972eef3e6030b103e1e8657f7a76d547 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:44:26 +0200 Subject: [PATCH 6/9] no more f-strings --- gitfourchette/mainwindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gitfourchette/mainwindow.py b/gitfourchette/mainwindow.py index c10e695d..adbd8269 100644 --- a/gitfourchette/mainwindow.py +++ b/gitfourchette/mainwindow.py @@ -1255,11 +1255,11 @@ def workdirProxy(): ), ActionDef( - _(f"Op&en in {getExternalEditorName()}"), + _("Op&en Repo in {0}", getExternalEditorName()), lambda: self.openRepoInEditor(workdirProxy()), icon="prefs-diff", shortcuts=GlobalShortcuts.NO_SHORTCUT, - tip=_(f"Open this repo’s working directory in {getExternalEditorName()}"), + tip=_("Open this repo’s working directory in {0}", getExternalEditorName()), ), ActionDef( From abdb1d918fd6dfe2aba6e35d4bc8d1c9b4663510 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:44:41 +0200 Subject: [PATCH 7/9] add test Added a test that verifies wether the external editor opens. --- test/test_mainwindow.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/test_mainwindow.py b/test/test_mainwindow.py index 9b388c78..05e24ddd 100644 --- a/test/test_mainwindow.py +++ b/test/test_mainwindow.py @@ -156,7 +156,10 @@ def testMainWindowMenuItems(tempDir, mainWindow): def testTabBarActions(tempDir, mainWindow): editorPath = getTestDataPath("editor-shim.py") scratchPath = f"{tempDir.name}/scratch file.txt" - GFApplication.applyPrefs(terminal=f'"{editorPath}" "{scratchPath}" "hello world" $COMMAND') + GFApplication.applyPrefs( + terminal=f'"{editorPath}" "{scratchPath}" "hello world" $COMMAND', + externalEditor=f'"{editorPath}" "{scratchPath}" "hello world"' + ) # Open two repos to test background and foreground tab actions wd0 = unpackRepo(tempDir, renameTo="repo0") @@ -190,6 +193,12 @@ def testTabBarActions(tempDir, mainWindow): assert terminalShimResult[0] == "hello world" assert terminalShimResult[1].endswith(".sh") # path to launcher script + triggerMenuAction(menu, "open repo in editor-shim.py") + waitForFile(scratchPath) + editorShimResult = readTextFile(scratchPath, unlink=True).splitlines() + assert terminalShimResult[0] == "hello world" # ensure editor opens + + menu.close() From 66511d6df09648ce18e42278a337478644c8a9c4 Mon Sep 17 00:00:00 2001 From: Genfood Date: Mon, 13 Jul 2026 14:55:24 +0200 Subject: [PATCH 8/9] copy paste error :clown: --- test/test_mainwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_mainwindow.py b/test/test_mainwindow.py index 05e24ddd..395a7c69 100644 --- a/test/test_mainwindow.py +++ b/test/test_mainwindow.py @@ -196,7 +196,7 @@ def testTabBarActions(tempDir, mainWindow): triggerMenuAction(menu, "open repo in editor-shim.py") waitForFile(scratchPath) editorShimResult = readTextFile(scratchPath, unlink=True).splitlines() - assert terminalShimResult[0] == "hello world" # ensure editor opens + assert editorShimResult[0] == "hello world" # ensure editor opens menu.close() From dab2bfaa84c85ac81731f5b9f8ee379419811133 Mon Sep 17 00:00:00 2001 From: Genfood Date: Wed, 15 Jul 2026 10:24:00 +0200 Subject: [PATCH 9/9] Tests - Add test for editor not set - fixes test to ensure also the wd --- test/test_mainwindow.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/test/test_mainwindow.py b/test/test_mainwindow.py index 395a7c69..a8e7dff9 100644 --- a/test/test_mainwindow.py +++ b/test/test_mainwindow.py @@ -157,9 +157,8 @@ def testTabBarActions(tempDir, mainWindow): editorPath = getTestDataPath("editor-shim.py") scratchPath = f"{tempDir.name}/scratch file.txt" GFApplication.applyPrefs( - terminal=f'"{editorPath}" "{scratchPath}" "hello world" $COMMAND', - externalEditor=f'"{editorPath}" "{scratchPath}" "hello world"' - ) + terminal=f'"{editorPath}" "{scratchPath}" "hello world" $COMMAND' + ) # Open two repos to test background and foreground tab actions wd0 = unpackRepo(tempDir, renameTo="repo0") @@ -175,11 +174,13 @@ def testTabBarActions(tempDir, mainWindow): assert isinstance(widget0, RepoWidget) assert isinstance(widget1, RepoStub) - for tabIndex, wd in enumerate([wd0, wd1]): + def getMenu(tIndex: int) -> QMenu: tabBar = mainWindow.tabs.tabs - tabRect = tabBar.tabRect(tabIndex) - menu = summonContextMenu(tabBar, tabRect.center()) + tabRect = tabBar.tabRect(tIndex) + return summonContextMenu(tabBar, tabRect.center()) + for tabIndex, wd in enumerate([wd0, wd1]): + menu = getMenu(tabIndex) triggerMenuAction(menu, "copy repo path") assert QApplication.clipboard().text() == wd @@ -193,11 +194,24 @@ def testTabBarActions(tempDir, mainWindow): assert terminalShimResult[0] == "hello world" assert terminalShimResult[1].endswith(".sh") # path to launcher script + # Test open repo in editor, with editor not set. + triggerMenuAction(menu, "open repo in external editor") + rejectQMessageBox(mainWindow, r"text editor.+isn.t configured") + + # Test open repo in editor, with editor is set + GFApplication.applyPrefs( + externalEditor=f'"{editorPath}" "{scratchPath}" "hello world"' + ) + menu.close() + menu = getMenu(tabIndex) # reload menu, and verify editor changed + is working. triggerMenuAction(menu, "open repo in editor-shim.py") waitForFile(scratchPath) editorShimResult = readTextFile(scratchPath, unlink=True).splitlines() - assert editorShimResult[0] == "hello world" # ensure editor opens - + assert editorShimResult[0] == "hello world" # ensure editor opens + assert Path(wd).samefile(editorShimResult[1]) + GFApplication.applyPrefs( + externalEditor="" + ) # revert to no external editor, for the next wd. menu.close()