From 831f58c1c56e5751befdb3a5bba3cde7336be621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Thu, 4 Dec 2025 10:11:21 +0100 Subject: [PATCH 01/10] Feature: Support moving tabs via pop-up menu --- CHANGELOG.md | 6 + gradle.properties | 2 +- .../java/listener/MyTabContentListener.java | 7 +- src/main/java/service/ToolWindowService.java | 23 +- .../service/ToolWindowServiceInterface.java | 5 + src/main/java/service/ViewService.java | 111 +++++++- src/main/java/toolwindow/TabMoveActions.java | 263 ++++++++++++++++++ .../{TabRename.java => TabOperations.java} | 47 +++- 8 files changed, 436 insertions(+), 28 deletions(-) create mode 100644 src/main/java/toolwindow/TabMoveActions.java rename src/main/java/toolwindow/{TabRename.java => TabOperations.java} (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3f33e8..ef048ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2025.3.1] + +### Added + +- Support reorder git scope tabs via right-click popup menu + ## [2025.3] ### Fixes diff --git a/gradle.properties b/gradle.properties index 6abc130..ad8ebad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,7 +2,7 @@ pluginGroup=org.woelkit.plugins pluginName=Git Scope pluginRepositoryUrl=https://github.com/comod/git-scope-pro -pluginVersion=2025.3 +pluginVersion=2025.3.1 pluginSinceBuild=243 platformType=IU diff --git a/src/main/java/listener/MyTabContentListener.java b/src/main/java/listener/MyTabContentListener.java index c0d52a5..4ca6728 100644 --- a/src/main/java/listener/MyTabContentListener.java +++ b/src/main/java/listener/MyTabContentListener.java @@ -80,6 +80,11 @@ public void contentRemoved(@NotNull ContentManagerEvent event) { if (Objects.equals(tabName, PLUS_TAB_LABEL)) { return; } - getViewService().removeTab(event.getIndex()); // Get service only when needed + + // Don't remove the model if we're just reordering tabs + ViewService viewService = getViewService(); + if (viewService != null && !viewService.isProcessingTabReorder()) { + viewService.removeTab(event.getIndex()); + } } } \ No newline at end of file diff --git a/src/main/java/service/ToolWindowService.java b/src/main/java/service/ToolWindowService.java index 996dd10..b86d5bc 100644 --- a/src/main/java/service/ToolWindowService.java +++ b/src/main/java/service/ToolWindowService.java @@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import system.Defs; -import toolwindow.TabRename; +import toolwindow.TabOperations; import toolwindow.ToolWindowView; import toolwindow.elements.VcsTree; @@ -24,11 +24,11 @@ public final class ToolWindowService implements ToolWindowServiceInterface { private final Project project; private final Map contentToViewMap = new HashMap<>(); - private final TabRename tabRename; + private final TabOperations tabOperations; public ToolWindowService(Project project) { this.project = project; - this.tabRename = new TabRename(project); + this.tabOperations = new TabOperations(project); } @Override @@ -86,18 +86,18 @@ public void addListener() { ContentManager contentManager = getContentManager(); contentManager.addContentManagerListener(new MyTabContentListener(project)); - // Register the rename action in the tab context menu - tabRename.registerRenameTabAction(); + // Register all tab actions (rename, reset, move) in the tab context menu + tabOperations.registerTabActions(); } @Override public void setupTabTooltip(MyModel model) { - tabRename.setupTabTooltip(model, contentToViewMap); + tabOperations.setupTabTooltip(model, contentToViewMap); } @Override public void changeTabName(String title) { - tabRename.changeTabName(title, getContentManager()); + tabOperations.changeTabName(title, getContentManager()); } public void removeTab(int index) { @@ -141,4 +141,13 @@ public void selectFile(VirtualFile file) { vcsTree.selectFile(file); } } + + @Override + public MyModel getModelForContent(Content content) { + ToolWindowView toolWindowView = contentToViewMap.get(content); + if (toolWindowView != null) { + return toolWindowView.getModel(); + } + return null; + } } \ No newline at end of file diff --git a/src/main/java/service/ToolWindowServiceInterface.java b/src/main/java/service/ToolWindowServiceInterface.java index 1be3c15..7670e32 100644 --- a/src/main/java/service/ToolWindowServiceInterface.java +++ b/src/main/java/service/ToolWindowServiceInterface.java @@ -1,6 +1,7 @@ package service; import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.ToolWindow; import model.MyModel; import toolwindow.elements.VcsTree; @@ -26,4 +27,8 @@ public interface ToolWindowServiceInterface { VcsTree getVcsTree(); void selectFile(VirtualFile file); + + ToolWindow getToolWindow(); + + MyModel getModelForContent(com.intellij.ui.content.Content content); } diff --git a/src/main/java/service/ViewService.java b/src/main/java/service/ViewService.java index 8ca3a31..37b975e 100644 --- a/src/main/java/service/ViewService.java +++ b/src/main/java/service/ViewService.java @@ -4,6 +4,8 @@ import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.changes.Change; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentManager; import com.intellij.util.concurrency.SequentialTaskExecutor; import implementation.compare.ChangesService; import implementation.lineStatusTracker.MyLineStatusTrackerImpl; @@ -41,6 +43,7 @@ public class ViewService implements Disposable { public Integer currentTabIndex = 0; private final Project project; private boolean isProcessingTabRename = false; + private boolean isProcessingTabReorder = false; private ToolWindowServiceInterface toolWindowService; private TargetBranchService targetBranchService; private ChangesService changesService; @@ -132,6 +135,7 @@ public void save() { // Save the target branch map TargetBranchMap targetBranchMap = myModel.getTargetBranchMap(); if (targetBranchMap == null) { + LOG.warn("Skipping model with null targetBranchMap during save"); return; } myModelBase.setTargetBranchMap(targetBranchMap); @@ -299,9 +303,27 @@ public void plusTabClicked() { } public MyModel addTabAndModel(String tabName) { - int currentIndexOfPlusTab = collection.size() + 1; MyModel myModel = addModel(); + // Get ContentManager to find current + tab position + ContentManager contentManager = toolWindowService.getToolWindow().getContentManager(); + int contentCountBefore = contentManager.getContentCount(); + + // Find the + tab (should be at the last position) + int plusTabIndex = -1; + for (int i = 0; i < contentCountBefore; i++) { + Content content = contentManager.getContent(i); + if (content != null && PLUS_TAB_LABEL.equals(content.getTabName())) { + plusTabIndex = i; + break; + } + } + + // Remove the old + tab first + if (plusTabIndex >= 0) { + toolWindowService.removeTab(plusTabIndex); + } + // Make sure to add the tab with closeable set to true toolWindowService.addTab(myModel, tabName, true); @@ -313,14 +335,11 @@ public MyModel addTabAndModel(String tabName) { // Add plus tab after the new tab addPlusTab(); - // Remove the old plus tab - toolWindowService.removeTab(currentIndexOfPlusTab); - // Set the current tab index and active model setTabIndex(collection.size()); setActiveModel(); - // Select the new tab + // Select the new tab (should be second to last, before the + tab) toolWindowService.selectNewTab(); return myModel; @@ -476,16 +495,32 @@ public MyModel addModel() { } public MyModel getCurrent() { - int currentModelIndex = getCurrentModelIndex(); - List collection = this.getCollection(); - int size = collection.size(); - if (currentModelIndex >= size) { + // Get the currently selected tab's model directly from ContentManager + ContentManager contentManager = toolWindowService.getToolWindow().getContentManager(); + Content selectedContent = contentManager.getSelectedContent(); + + if (selectedContent == null) { return myHeadModel; } - if (getTabIndex() == 0) { + + // Check if it's the HEAD tab + if (selectedContent.getTabName().equals(GitService.BRANCH_HEAD)) { return myHeadModel; } - return collection.get(currentModelIndex); + + // Check if it's the + tab + if (selectedContent.getTabName().equals(PLUS_TAB_LABEL)) { + return myHeadModel; // or handle differently + } + + // Get the model for this content + MyModel model = toolWindowService.getModelForContent(selectedContent); + if (model != null) { + return model; + } + + // Fallback to HEAD + return myHeadModel; } public List getCollection() { @@ -505,6 +540,60 @@ public void removeTab(int tabIndex) { } } + public void onTabReordered(int oldIndex, int newIndex) { + // Note: The isProcessingTabReorder flag should already be set by the caller + // before any UI changes are made, to prevent listener interference + + // After tabs are reordered in the UI, we need to rebuild the collection + // to match the new tab order + rebuildCollectionFromTabOrder(); + + // Save the new order + save(); + } + + /** + * Rebuilds the collection based on the current tab order in ContentManager. + * This ensures that collection[i] corresponds to tab[i+1] (accounting for HEAD at index 0). + */ + private void rebuildCollectionFromTabOrder() { + ContentManager contentManager = toolWindowService.getToolWindow().getContentManager(); + List newCollection = new ArrayList<>(); + + // Iterate through all tabs (skip HEAD at index 0, skip + at end) + int contentCount = contentManager.getContentCount(); + for (int i = 1; i < contentCount; i++) { + Content content = contentManager.getContent(i); + if (content != null && !PLUS_TAB_LABEL.equals(content.getTabName())) { + // Find the model for this content + MyModel model = findModelForContent(content); + if (model != null && !model.isHeadTab()) { + newCollection.add(model); + } else { + LOG.warn("Model not found for tab at index " + i + ": " + content.getTabName()); + } + } + } + + // Replace the collection with the reordered one + this.collection = newCollection; + } + + /** + * Finds the MyModel associated with a Content object by using the ToolWindowService. + */ + private MyModel findModelForContent(Content content) { + return toolWindowService.getModelForContent(content); + } + + public boolean isProcessingTabReorder() { + return isProcessingTabReorder; + } + + public void setProcessingTabReorder(boolean processing) { + this.isProcessingTabReorder = processing; + } + public void removeCurrentTab() { removeTab(currentTabIndex); // Also remove from the UI diff --git a/src/main/java/toolwindow/TabMoveActions.java b/src/main/java/toolwindow/TabMoveActions.java new file mode 100644 index 0000000..a61399f --- /dev/null +++ b/src/main/java/toolwindow/TabMoveActions.java @@ -0,0 +1,263 @@ +package toolwindow; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.PlatformDataKeys; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import service.ToolWindowServiceInterface; +import service.ViewService; +import system.Defs; + +import java.awt.*; +import java.lang.reflect.Field; + +import static service.ViewService.PLUS_TAB_LABEL; + +/** + * Actions to move tabs left and right in the Git Scope tool window. + */ +public class TabMoveActions { + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(TabMoveActions.class); + + /** + * Gets the Content that was right-clicked in a context menu event + */ + @Nullable + private static Content getContentFromContextMenuEvent(AnActionEvent e) { + Component contextComponent = e.getData(PlatformDataKeys.CONTEXT_COMPONENT); + if (contextComponent == null) { + return null; + } + try { + Field myContentField = contextComponent.getClass().getDeclaredField("myContent"); + myContentField.setAccessible(true); + Object myContentObject = myContentField.get(contextComponent); + if (myContentObject instanceof Content) { + return (Content) myContentObject; + } + } catch (NoSuchFieldException | IllegalAccessException ignored) { + } + return null; + } + + /** + * Action to move the current tab to the left + */ + public static class MoveTabLeft extends AnAction { + public MoveTabLeft() { + super("Move Tab Left"); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Project project = e.getProject(); + if (project == null) return; + + // Get the right-clicked tab content + Content targetContent = getContentFromContextMenuEvent(e); + if (targetContent == null) return; + + ToolWindowServiceInterface toolWindowService = project.getService(ToolWindowServiceInterface.class); + ContentManager contentManager = toolWindowService.getToolWindow().getContentManager(); + + int currentIndex = contentManager.getIndexOfContent(targetContent); + int newIndex = currentIndex - 1; + + // Cannot move HEAD tab (index 0) or move before HEAD tab + if (currentIndex <= 1 || newIndex < 1) { + return; + } + + // Cannot move + tab + if (PLUS_TAB_LABEL.equals(targetContent.getTabName())) { + return; + } + + moveTab(project, contentManager, targetContent, currentIndex, newIndex); + } + + @Override + public void update(@NotNull AnActionEvent e) { + // By default, hide the action + e.getPresentation().setEnabledAndVisible(false); + + Project project = e.getProject(); + if (project == null) { + return; + } + + // Check if this is our tool window + ToolWindow toolWindow = e.getData(PlatformDataKeys.TOOL_WINDOW); + if (toolWindow == null || !Defs.TOOL_WINDOW_NAME.equals(toolWindow.getId())) { + return; + } + + // Get the right-clicked tab content + Content targetContent = getContentFromContextMenuEvent(e); + if (targetContent == null) { + return; + } + + ContentManager contentManager = toolWindow.getContentManager(); + int currentIndex = contentManager.getIndexOfContent(targetContent); + String tabName = targetContent.getTabName(); + + // Enable only if not HEAD tab (index 0), not + tab, and can move left (index > 1) + boolean enabled = currentIndex > 1 && !PLUS_TAB_LABEL.equals(tabName); + e.getPresentation().setEnabledAndVisible(enabled); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + } + + /** + * Action to move the current tab to the right + */ + public static class MoveTabRight extends AnAction { + public MoveTabRight() { + super("Move Tab Right"); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + Project project = e.getProject(); + if (project == null) return; + + // Get the right-clicked tab content + Content targetContent = getContentFromContextMenuEvent(e); + if (targetContent == null) return; + + ToolWindowServiceInterface toolWindowService = project.getService(ToolWindowServiceInterface.class); + ContentManager contentManager = toolWindowService.getToolWindow().getContentManager(); + + int currentIndex = contentManager.getIndexOfContent(targetContent); + int newIndex = currentIndex + 1; + int lastIndex = contentManager.getContentCount() - 1; + + // Cannot move HEAD tab (index 0) or move past + tab + if (currentIndex == 0 || newIndex >= lastIndex) { + return; + } + + // Cannot move + tab + if (PLUS_TAB_LABEL.equals(targetContent.getTabName())) { + return; + } + + moveTab(project, contentManager, targetContent, currentIndex, newIndex); + } + + @Override + public void update(@NotNull AnActionEvent e) { + // By default, hide the action + e.getPresentation().setEnabledAndVisible(false); + + Project project = e.getProject(); + if (project == null) { + return; + } + + // Check if this is our tool window + ToolWindow toolWindow = e.getData(PlatformDataKeys.TOOL_WINDOW); + if (toolWindow == null || !Defs.TOOL_WINDOW_NAME.equals(toolWindow.getId())) { + return; + } + + // Get the right-clicked tab content + Content targetContent = getContentFromContextMenuEvent(e); + if (targetContent == null) { + return; + } + + ContentManager contentManager = toolWindow.getContentManager(); + int currentIndex = contentManager.getIndexOfContent(targetContent); + int lastIndex = contentManager.getContentCount() - 1; + String tabName = targetContent.getTabName(); + + // Enable only if not HEAD tab (index 0), not + tab, and can move right (not already at second-to-last position) + boolean enabled = currentIndex > 0 && currentIndex < lastIndex - 1 && !PLUS_TAB_LABEL.equals(tabName); + e.getPresentation().setEnabledAndVisible(enabled); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + } + + /** + * Helper method to move a tab from one position to another + */ + private static void moveTab(Project project, ContentManager contentManager, Content content, int oldIndex, int newIndex) { + ViewService viewService = null; + try { + LOG.info("Moving tab from index " + oldIndex + " to " + newIndex); + + // Additional validation to prevent moving + tab or moving to + tab position + int lastIndex = contentManager.getContentCount() - 1; + + // Cannot move HEAD tab (index 0) + if (oldIndex == 0) { + LOG.warn("Cannot move HEAD tab"); + return; + } + + // Cannot move + tab (last index) + if (oldIndex == lastIndex || PLUS_TAB_LABEL.equals(content.getTabName())) { + LOG.warn("Cannot move + tab"); + return; + } + + // Cannot move to position 0 (before HEAD) or to last position (where + tab is) + if (newIndex < 1 || newIndex >= lastIndex) { + LOG.warn("Invalid target index: " + newIndex); + return; + } + + // Verify the + tab is still at the last position + Content lastContent = contentManager.getContent(lastIndex); + if (lastContent == null || !PLUS_TAB_LABEL.equals(lastContent.getTabName())) { + LOG.error("+ tab is not at expected position!"); + return; + } + + // IMPORTANT: Set the flag BEFORE moving tabs to prevent listener interference + viewService = project.getService(ViewService.class); + if (viewService != null) { + viewService.setProcessingTabReorder(true); + } + + // Remove content from old position + contentManager.removeContent(content, false); + + // Add content at new position + contentManager.addContent(content, newIndex); + + // Select the moved tab + contentManager.setSelectedContent(content, true); + + // Now rebuild collection and save + if (viewService != null) { + viewService.onTabReordered(oldIndex, newIndex); + } + + LOG.info("Tab moved successfully"); + } catch (Exception e) { + LOG.error("Error moving tab: " + e.getMessage(), e); + } finally { + // Always clear the flag + if (viewService != null) { + viewService.setProcessingTabReorder(false); + } + } + } +} diff --git a/src/main/java/toolwindow/TabRename.java b/src/main/java/toolwindow/TabOperations.java similarity index 89% rename from src/main/java/toolwindow/TabRename.java rename to src/main/java/toolwindow/TabOperations.java index 3d31146..da84b2b 100644 --- a/src/main/java/toolwindow/TabRename.java +++ b/src/main/java/toolwindow/TabOperations.java @@ -19,14 +19,20 @@ import java.lang.reflect.Field; import java.util.Map; -public class TabRename { +public class TabOperations { private final Project project; - public TabRename(Project project) { + public TabOperations(Project project) { this.project = project; } - public void registerRenameTabAction() { + public void registerTabActions() { + // Create move left action + AnAction moveLeftAction = new TabMoveActions.MoveTabLeft(); + + // Create move right action + AnAction moveRightAction = new TabMoveActions.MoveTabRight(); + // Create a rename action that will be added to the tab context menu AnAction renameAction = new AnAction("Rename Tab") { @Override @@ -133,6 +139,24 @@ public void update(@NotNull AnActionEvent e) { // Get the action manager and register our actions ActionManager actionManager = ActionManager.getInstance(); + // Register the move left action + String moveLeftActionId = "GitScope.MoveTabLeft"; + if (actionManager.getAction(moveLeftActionId) == null) { + actionManager.registerAction(moveLeftActionId, moveLeftAction); + } else { + actionManager.unregisterAction(moveLeftActionId); + actionManager.registerAction(moveLeftActionId, moveLeftAction); + } + + // Register the move right action + String moveRightActionId = "GitScope.MoveTabRight"; + if (actionManager.getAction(moveRightActionId) == null) { + actionManager.registerAction(moveRightActionId, moveRightAction); + } else { + actionManager.unregisterAction(moveRightActionId); + actionManager.registerAction(moveRightActionId, moveRightAction); + } + // Register the rename action String renameActionId = "GitScope.RenameTab"; if (actionManager.getAction(renameActionId) == null) { @@ -158,16 +182,23 @@ public void update(@NotNull AnActionEvent e) { AnAction[] actions = contextMenuGroup.getChildActionsOrStubs(); for (AnAction action : actions) { if (action.getTemplateText() != null && - (action.getTemplateText().equals("Rename Tab") || action.getTemplateText().equals("Reset Tab Name"))) { + (action.getTemplateText().equals("Rename Tab") || + action.getTemplateText().equals("Reset Tab Name") || + action.getTemplateText().equals("Move Tab Left") || + action.getTemplateText().equals("Move Tab Right"))) { contextMenuGroup.remove(action); } } // Add our actions to the group in the desired order: - // 1. Rename Tab - // 2. Reset Tab Name - contextMenuGroup.add(resetTabNameAction, Constraints.FIRST); // Added second, appears first from bottom - contextMenuGroup.add(renameAction, Constraints.FIRST); // Added first, appears first from top + // 1. Move Tab Left + // 2. Move Tab Right + // 3. Rename Tab + // 4. Reset Tab Name + contextMenuGroup.add(resetTabNameAction, Constraints.FIRST); + contextMenuGroup.add(renameAction, Constraints.FIRST); + contextMenuGroup.add(moveRightAction, Constraints.FIRST); + contextMenuGroup.add(moveLeftAction, Constraints.FIRST); } } From 5965a7c1012fe1da0ec1b2be2676d7bd1bfceb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Sun, 7 Dec 2025 00:23:47 +0100 Subject: [PATCH 02/10] Add repository change listener to refresh also for repo events. --- gradle.properties | 4 +-- .../MyGitRepositoryChangeListener.java | 36 +++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 1 + 3 files changed, 39 insertions(+), 2 deletions(-) create mode 100644 src/main/java/listener/MyGitRepositoryChangeListener.java diff --git a/gradle.properties b/gradle.properties index ad8ebad..0501749 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,9 +5,9 @@ pluginRepositoryUrl=https://github.com/comod/git-scope-pro pluginVersion=2025.3.1 pluginSinceBuild=243 -platformType=IU +platformType=RM #platformVersion=LATEST-EAP-SNAPSHOT -platformVersion=2025.2.5 +platformVersion=2025.2.4 platformBundledPlugins=Git4Idea gradleVersion=9.2.1 diff --git a/src/main/java/listener/MyGitRepositoryChangeListener.java b/src/main/java/listener/MyGitRepositoryChangeListener.java new file mode 100644 index 0000000..84cfbbd --- /dev/null +++ b/src/main/java/listener/MyGitRepositoryChangeListener.java @@ -0,0 +1,36 @@ +package listener; + +import com.intellij.openapi.project.Project; +import git4idea.repo.GitRepository; +import git4idea.repo.GitRepositoryChangeListener; +import org.jetbrains.annotations.NotNull; +import service.ViewService; +import system.Defs; + +/** + * Listens to Git repository changes (branches, tags, HEAD changes, remote updates). + * This complements MyChangeListListener which only triggers on working tree changes. + * + * This listener is essential for detecting: + * - New tags being created + * - New branches being created/deleted + * - Branch checkouts + * - Remote reference updates (fetch/pull) + */ +public class MyGitRepositoryChangeListener implements GitRepositoryChangeListener { + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(MyGitRepositoryChangeListener.class); + + private final ViewService viewService; + + public MyGitRepositoryChangeListener(Project project) { + this.viewService = project.getService(ViewService.class); + } + + @Override + public void repositoryChanged(@NotNull GitRepository repository) { + LOG.debug("repositoryChanged() called for repository: " + repository.getRoot().getName()); + + // TODO: collectChanges: repository changed (branches, tags, HEAD, remotes updated) + viewService.collectChanges(true); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 338a3ec..f469215 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,6 +21,7 @@ + From c155102fff65e4f01cf8ebbe343c59f6b5aa3594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Tue, 9 Dec 2025 23:25:50 +0100 Subject: [PATCH 03/10] Remove unnecessary log entry --- gradle.properties | 4 ++-- src/main/java/service/ViewService.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle.properties b/gradle.properties index 0501749..ad8ebad 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,9 +5,9 @@ pluginRepositoryUrl=https://github.com/comod/git-scope-pro pluginVersion=2025.3.1 pluginSinceBuild=243 -platformType=RM +platformType=IU #platformVersion=LATEST-EAP-SNAPSHOT -platformVersion=2025.2.4 +platformVersion=2025.2.5 platformBundledPlugins=Git4Idea gradleVersion=9.2.1 diff --git a/src/main/java/service/ViewService.java b/src/main/java/service/ViewService.java index 37b975e..d16da8c 100644 --- a/src/main/java/service/ViewService.java +++ b/src/main/java/service/ViewService.java @@ -135,7 +135,7 @@ public void save() { // Save the target branch map TargetBranchMap targetBranchMap = myModel.getTargetBranchMap(); if (targetBranchMap == null) { - LOG.warn("Skipping model with null targetBranchMap during save"); + LOG.debug("Skipping model with null targetBranchMap during save"); return; } myModelBase.setTargetBranchMap(targetBranchMap); From 8fbdd1e16d5bee596735030e785992d0740a979a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Tue, 9 Dec 2025 23:41:39 +0100 Subject: [PATCH 04/10] Some fixes to "CommitDiffWorkaround" (comod/git-scope-pro#56) --- .../CommitDiffWorkaround.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java b/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java index ee600af..5691e7d 100644 --- a/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java +++ b/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java @@ -206,6 +206,9 @@ public void handleSwitchedToCommitDiff() { /** * Handle editor selection changed away from a commit diff editor. * Call this when the user switches away from a commit panel diff tab. + * + * Note: This safely restores ALL tracked documents. Documents that still have + * commit diff editors open will be skipped by restoreCustomBaseForDocument(). */ public void handleSwitchedAwayFromCommitDiff() { restoreCustomBaseForAllDocuments(); @@ -271,6 +274,18 @@ private void scheduleActivationIfCommitDiffSelected() { }); } + /** + * Activate HEAD base for all documents that have commit diff editors. + * + * Design note: This activates HEAD base for ALL documents with commit diffs, not just + * the currently visible one. This is intentional - when viewing commit diffs, we enter + * "commit review mode" where all files should show diffs against HEAD, not custom base. + * This provides consistent behavior and avoids confusion when switching between files. + * + * The restoration logic (via hasCommitDiffEditorsFor checks) ensures that when commit + * diffs are closed, only documents without any remaining commit diff editors get their + * custom base restored. + */ private void activateHeadBaseForAllCommitDiffs() { synchronized (this) { for (Map.Entry> entry : commitDiffEditors.entrySet()) { @@ -335,6 +350,11 @@ private void restoreCustomBaseForDocument(@NotNull Document doc) { return; } + // Fix Hole #2: Don't restore if commit diff editors are still open for this document + if (hasCommitDiffEditorsFor(doc)) { + return; + } + String customContent = baseRevisionSwitcher.getCachedCustomBaseContent(doc); if (customContent != null) { baseRevisionSwitcher.markShowingHeadBase(doc, false); From 7882603c5a20bd92773048b4551d0059a2ff4f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Wed, 10 Dec 2025 00:05:09 +0100 Subject: [PATCH 05/10] Fix: java.lang.Throwable: 'ChangesBrowser' toolbar creates new components for 20 updates in a row --- src/main/java/toolwindow/TabOperations.java | 17 +++++++++++++---- .../elements/MySimpleChangesBrowser.java | 13 +++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/main/java/toolwindow/TabOperations.java b/src/main/java/toolwindow/TabOperations.java index da84b2b..d307b26 100644 --- a/src/main/java/toolwindow/TabOperations.java +++ b/src/main/java/toolwindow/TabOperations.java @@ -195,10 +195,19 @@ public void update(@NotNull AnActionEvent e) { // 2. Move Tab Right // 3. Rename Tab // 4. Reset Tab Name - contextMenuGroup.add(resetTabNameAction, Constraints.FIRST); - contextMenuGroup.add(renameAction, Constraints.FIRST); - contextMenuGroup.add(moveRightAction, Constraints.FIRST); - contextMenuGroup.add(moveLeftAction, Constraints.FIRST); + // Note: Add null checks to prevent IllegalArgumentException in IntelliJ 2025.3+ + if (resetTabNameAction != null) { + contextMenuGroup.add(resetTabNameAction, Constraints.FIRST); + } + if (renameAction != null) { + contextMenuGroup.add(renameAction, Constraints.FIRST); + } + if (moveRightAction != null) { + contextMenuGroup.add(moveRightAction, Constraints.FIRST); + } + if (moveLeftAction != null) { + contextMenuGroup.add(moveLeftAction, Constraints.FIRST); + } } } diff --git a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java index 444266e..13d93d2 100644 --- a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java +++ b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java @@ -31,6 +31,12 @@ public class MySimpleChangesBrowser extends SimpleAsyncChangesBrowser { private final Project myProject; UISettings uiSettings = UISettings.getInstance(); + // Reuse action instances to avoid creating new instances on every toolbar update (IntelliJ 2025.3+ requirement) + // These must be static to be shared across all browser instances + private static final AnAction SELECT_OPENED_FILE_ACTION = new VcsTreeActions.SelectOpenedFileAction(); + private static final AnAction SHOW_IN_PROJECT_ACTION = new VcsTreeActions.ShowInProjectAction(); + private static final AnAction ROLLBACK_ACTION = new VcsTreeActions.RollbackAction(); + /** * Constructor for MySimpleChangesBrowser. * This MUST be called from the EDT, but only AFTER all slow operations @@ -48,16 +54,15 @@ private MySimpleChangesBrowser(@NotNull Project project, @NotNull Collection createPopupMenuActions() { List actions = new ArrayList<>(super.createPopupMenuActions()); - actions.add(new VcsTreeActions.ShowInProjectAction()); - actions.add(new VcsTreeActions.RollbackAction()); + actions.add(SHOW_IN_PROJECT_ACTION); + actions.add(ROLLBACK_ACTION); return actions; } @Override protected @NotNull List createToolbarActions() { List actions = new ArrayList<>(super.createToolbarActions()); - - actions.add(new VcsTreeActions.SelectOpenedFileAction()); + actions.add(SELECT_OPENED_FILE_ACTION); return actions; } From 27f08ed920c102cb114c8273f88643af31efc005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Wed, 10 Dec 2025 12:09:57 +0100 Subject: [PATCH 06/10] Fix java.lang.IndexOutOfBoundsException: Index -1 out of bounds for length 7 This is seen on IntelliJ 2025.3 platforms. --- gradle.properties | 2 +- .../lineStatusTracker/MyLineStatusTrackerImpl.java | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index ad8ebad..c9cbdb9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,7 +7,7 @@ pluginSinceBuild=243 platformType=IU #platformVersion=LATEST-EAP-SNAPSHOT -platformVersion=2025.2.5 +platformVersion=2025.3 platformBundledPlugins=Git4Idea gradleVersion=9.2.1 diff --git a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java index 26df044..a44af0f 100644 --- a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java +++ b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java @@ -364,10 +364,24 @@ private void updateTrackerBaseRevision(LineStatusTracker tracker, String cont if (setBaseRevisionMethod != null) { setBaseRevisionMethod.setAccessible(true); + // Guard against concurrent disposal + if (disposing.get()) { + return; + } + ApplicationManager.getApplication().runWriteAction(() -> { try { + // Double-check disposal state inside write action + if (disposing.get()) { + return; + } + // Use bulk update mode to batch changes and prevent flickering Document document = tracker.getDocument(); + if (document == null) { + return; + } + DocumentUtil.executeInBulk(document, () -> { try { setBaseRevisionMethod.invoke(tracker, content); From 08a9cbe6487311ff8e53a808fca387e0aa26e39c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Wed, 10 Dec 2025 12:33:51 +0100 Subject: [PATCH 07/10] Use correct commit hash as tab name when creating scope from Git revision --- src/main/java/listener/VcsContextMenuAction.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/listener/VcsContextMenuAction.java b/src/main/java/listener/VcsContextMenuAction.java index 4f3faab..af8d9b1 100644 --- a/src/main/java/listener/VcsContextMenuAction.java +++ b/src/main/java/listener/VcsContextMenuAction.java @@ -33,7 +33,9 @@ private static String getHashesAsString(@NotNull List revisions = getRevisionNumbersFromContext(e); revisions = ContainerUtil.reverse(revisions); - String rev = getHashesAsString(revisions); + + // Use only the first commit hash as the tab name + String rev = revisions.isEmpty() ? "" : revisions.getFirst().asString(); Project project = e.getProject(); ViewService viewService = Objects.requireNonNull(project).getService(ViewService.class); From 8800b64ba2d6de89dc53b4981e91981462fa1aef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Wed, 10 Dec 2025 22:35:35 +0100 Subject: [PATCH 08/10] Remove gutter revalidate and rely on internal IntelliJ refresh. --- .../lineStatusTracker/MyLineStatusTrackerImpl.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java index a44af0f..b620b88 100644 --- a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java +++ b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java @@ -208,14 +208,6 @@ private boolean isDiffView(Editor editor) { return editor.getEditorKind() == EditorKind.DIFF; } - private void refreshEditor(Editor editor) { - // Don't remove all highlighters - this removes indent guides and other system decorations - // The platform manages system highlighters automatically through the daemon code analyzer - if (editor.getGutter() instanceof EditorGutterComponentEx gutter) { - gutter.revalidateMarkup(); - } - } - public void update(Collection changes, @Nullable VirtualFile targetFile) { if (changes == null || disposing.get()) { return; @@ -232,10 +224,8 @@ public void update(Collection changes, @Nullable VirtualFile targetFile) Editor[] editors = EditorFactory.getInstance().getAllEditors(); for (Editor editor : editors) { if (isDiffView(editor)) continue; - if (updateLineStatusByChangesForEditorSafe(editor, fileToRevisionMap)) - { - refreshEditor(editor); - } + // Platform handles gutter repainting automatically - no need to force it + updateLineStatusByChangesForEditorSafe(editor, fileToRevisionMap); } }); }); From 71f09ae5605ac49bb4725e095ebf3aa8f45cd807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Thu, 11 Dec 2025 15:35:34 +0100 Subject: [PATCH 09/10] More fixes to avoid unnecessary creation of components Fixes comod/git-scope-pro#72 --- src/main/java/toolwindow/TabOperations.java | 22 +++-- src/main/java/toolwindow/VcsTreeActions.java | 12 ++- .../elements/MySimpleChangesBrowser.java | 19 ++-- .../java/toolwindow/elements/VcsTree.java | 91 ++++++++++++++++--- 4 files changed, 114 insertions(+), 30 deletions(-) diff --git a/src/main/java/toolwindow/TabOperations.java b/src/main/java/toolwindow/TabOperations.java index d307b26..f4fb717 100644 --- a/src/main/java/toolwindow/TabOperations.java +++ b/src/main/java/toolwindow/TabOperations.java @@ -22,19 +22,21 @@ public class TabOperations { private final Project project; + // Reuse action instances to avoid creating new instances on every registration (IntelliJ 2025.3+ requirement) + private final AnAction moveLeftAction; + private final AnAction moveRightAction; + private final AnAction renameAction; + private final AnAction resetTabNameAction; + public TabOperations(Project project) { this.project = project; - } - - public void registerTabActions() { - // Create move left action - AnAction moveLeftAction = new TabMoveActions.MoveTabLeft(); - // Create move right action - AnAction moveRightAction = new TabMoveActions.MoveTabRight(); + // Initialize actions once - they must be instance fields, not local variables + this.moveLeftAction = new TabMoveActions.MoveTabLeft(); + this.moveRightAction = new TabMoveActions.MoveTabRight(); // Create a rename action that will be added to the tab context menu - AnAction renameAction = new AnAction("Rename Tab") { + this.renameAction = new AnAction("Rename Tab") { @Override public @NotNull ActionUpdateThread getActionUpdateThread() { return ActionUpdateThread.EDT; @@ -81,7 +83,7 @@ public void update(@NotNull AnActionEvent e) { }; // Create a reset tab name action - AnAction resetTabNameAction = new AnAction("Reset Tab Name") { + this.resetTabNameAction = new AnAction("Reset Tab Name") { @Override public @NotNull ActionUpdateThread getActionUpdateThread() { return ActionUpdateThread.EDT; @@ -135,7 +137,9 @@ public void update(@NotNull AnActionEvent e) { } } }; + } + public void registerTabActions() { // Get the action manager and register our actions ActionManager actionManager = ActionManager.getInstance(); diff --git a/src/main/java/toolwindow/VcsTreeActions.java b/src/main/java/toolwindow/VcsTreeActions.java index 75431a7..6c0dc61 100644 --- a/src/main/java/toolwindow/VcsTreeActions.java +++ b/src/main/java/toolwindow/VcsTreeActions.java @@ -82,11 +82,21 @@ public void update(@NotNull AnActionEvent e) { } } - public static class SelectOpenedFileAction extends AnAction implements RightAlignedToolbarAction { + public static class SelectOpenedFileAction extends AnAction { public SelectOpenedFileAction() { super("Select Opened File", "Select the file currently open in the editor", AllIcons.General.Locate); } + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + // Keep presentation stable - no modifications needed + } + @Override public void actionPerformed(@NotNull AnActionEvent e) { Project project = e.getData(CommonDataKeys.PROJECT); diff --git a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java index 13d93d2..2362528 100644 --- a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java +++ b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java @@ -37,6 +37,14 @@ public class MySimpleChangesBrowser extends SimpleAsyncChangesBrowser { private static final AnAction SHOW_IN_PROJECT_ACTION = new VcsTreeActions.ShowInProjectAction(); private static final AnAction ROLLBACK_ACTION = new VcsTreeActions.RollbackAction(); + // Pre-create static immutable lists to ensure the SAME list instance is returned every time + private static final List STATIC_TOOLBAR_ACTIONS = java.util.Collections.unmodifiableList( + java.util.Collections.singletonList(SELECT_OPENED_FILE_ACTION) + ); + private static final List STATIC_POPUP_ACTIONS = java.util.Collections.unmodifiableList( + java.util.Arrays.asList(SHOW_IN_PROJECT_ACTION, ROLLBACK_ACTION) + ); + /** * Constructor for MySimpleChangesBrowser. * This MUST be called from the EDT, but only AFTER all slow operations @@ -53,17 +61,14 @@ private MySimpleChangesBrowser(@NotNull Project project, @NotNull Collection createPopupMenuActions() { - List actions = new ArrayList<>(super.createPopupMenuActions()); - actions.add(SHOW_IN_PROJECT_ACTION); - actions.add(ROLLBACK_ACTION); - return actions; + // Return the SAME static list instance every time to prevent toolbar recreation + return STATIC_POPUP_ACTIONS; } @Override protected @NotNull List createToolbarActions() { - List actions = new ArrayList<>(super.createToolbarActions()); - actions.add(SELECT_OPENED_FILE_ACTION); - return actions; + // Return the SAME static list instance every time to prevent toolbar recreation + return STATIC_TOOLBAR_ACTIONS; } /** diff --git a/src/main/java/toolwindow/elements/VcsTree.java b/src/main/java/toolwindow/elements/VcsTree.java index 08f05e1..c44f47d 100644 --- a/src/main/java/toolwindow/elements/VcsTree.java +++ b/src/main/java/toolwindow/elements/VcsTree.java @@ -44,6 +44,11 @@ public class VcsTree extends JPanel { private final Map> lastChangesPerTab = new ConcurrentHashMap<>(); private final Map lastChangesHashCodePerTab = new ConcurrentHashMap<>(); + // Use a single browser instance for this VcsTree to avoid toolbar recreation issues + // When switching tabs, we just update the browser's contents instead of swapping components + private MySimpleChangesBrowser singleBrowser = null; + private CompletableFuture pendingBrowserCreation = null; + public VcsTree(Project project) { this.project = project; this.positionTracker = new WindowPositionTracker( @@ -63,12 +68,17 @@ private void onVcsTreeLoaded(String tabId) { public void onTabSwitched() { SwingUtilities.invokeLater(() -> { try { + // Get the current tab ID to restore its scroll position + String currentTabId = getCurrentTabId(); + + // With single browser approach, the browser is already in place if it exists + // We just need to restore the scroll position for this tab + Component currentComponent = getComponentCount() > 0 ? getComponent(0) : null; if (currentComponent != null) { positionTracker.attachScrollListeners(currentComponent); SwingUtilities.invokeLater(() -> { - String currentTabId = getCurrentTabId(); ScrollPosition savedPosition = positionTracker.getSavedScrollPosition(currentTabId); if (savedPosition != null) { positionTracker.restoreScrollPosition(currentComponent, savedPosition); @@ -110,12 +120,13 @@ private String getCurrentTabId() { ViewService viewService = project.getService(ViewService.class); if (viewService != null) { int tabIndex = viewService.getTabIndex(); - return "tab_" + tabIndex; + // Include project name to avoid conflicts when cache is static across projects + return project.getName() + "_tab_" + tabIndex; } } catch (Exception e) { LOG.warn("Failed to get current tab ID", e); } - return "default_tab"; + return project.getName() + "_default_tab"; } public void createElement() { @@ -186,9 +197,9 @@ public void update(Collection changes) { return; } - String currentTabId = getCurrentTabId(); - lastChangesPerTab.put(currentTabId, changes != null ? new ArrayList<>(changes) : null); - lastChangesHashCodePerTab.put(currentTabId, calculateChangesHashCode(changes)); + final String tabId = getCurrentTabId(); + lastChangesPerTab.put(tabId, changes != null ? new ArrayList<>(changes) : null); + lastChangesHashCodePerTab.put(tabId, calculateChangesHashCode(changes)); lastChanges = changes; lastChangesHashCode = calculateChangesHashCode(changes); @@ -218,14 +229,58 @@ public void update(Collection changes) { if (!isCurrentSequence(sequenceNumber)) { throw new CompletionException(new InterruptedException("Update cancelled - sequence outdated")); } - return MySimpleChangesBrowser.createAsync(project, this, changesCopy); + + // Reuse single browser instance if it exists + if (singleBrowser != null) { + LOG.debug("VcsTree: Reusing single browser instance"); + return CompletableFuture.supplyAsync(() -> { + SwingUtilities.invokeLater(() -> { + if (!project.isDisposed()) { + singleBrowser.setChangesToDisplay(changesCopy); + } + }); + return singleBrowser; + }); + } + + // Check if browser is already being created + if (pendingBrowserCreation != null && !pendingBrowserCreation.isDone()) { + LOG.debug("VcsTree: Waiting for pending browser creation"); + // Wait for the pending creation to complete + return pendingBrowserCreation.thenApply(browser -> { + // Update with new changes + SwingUtilities.invokeLater(() -> { + if (!project.isDisposed()) { + browser.setChangesToDisplay(changesCopy); + } + }); + return browser; + }); + } + + // Create single browser instance + LOG.debug("VcsTree: Creating single browser instance"); + CompletableFuture creationFuture = + MySimpleChangesBrowser.createAsync(project, this, changesCopy) + .thenApply(newBrowser -> { + singleBrowser = newBrowser; + pendingBrowserCreation = null; + LOG.debug("VcsTree: Single browser created and cached"); + return newBrowser; + }); + + pendingBrowserCreation = creationFuture; + return creationFuture; }) .thenAccept(browser -> { SwingUtilities.invokeLater(() -> { if (isCurrentSequence(sequenceNumber) && !project.isDisposed()) { - setComponent(browser); - currentBrowser = browser; + // Set component if it's different from current + if (currentBrowser != browser) { + setComponent(browser); + currentBrowser = browser; + } } }); }) @@ -237,6 +292,8 @@ public void update(Collection changes) { JLabel errorLabel = createErrorLabel(throwable); setComponent(errorLabel); currentBrowser = null; + // Clear the single browser since we're showing an error + singleBrowser = null; } }); } @@ -311,10 +368,14 @@ private void setComponent(Component component) { positionTracker.setScrollPositionRestored(false); - this.removeAll(); - this.add(component, BorderLayout.CENTER); - this.revalidate(); - this.repaint(); + // Only remove/add if the component actually changed to avoid triggering toolbar updates + Component currentComponent = getComponentCount() > 0 ? getComponent(0) : null; + if (currentComponent != component) { + this.removeAll(); + this.add(component, BorderLayout.CENTER); + this.revalidate(); + this.repaint(); + } SwingUtilities.invokeLater(() -> { positionTracker.attachScrollListeners(component); @@ -354,5 +415,9 @@ public void removeNotify() { } lastChangesPerTab.clear(); lastChangesHashCodePerTab.clear(); + + // Clear the single browser instance for this VcsTree + singleBrowser = null; + pendingBrowserCreation = null; } } \ No newline at end of file From f0dd6a6c0239f842d057498577a73e08545e6c1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20W=C3=A5llberg?= Date: Thu, 11 Dec 2025 19:57:04 +0100 Subject: [PATCH 10/10] Update README.md for 2025.3.1 release --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef048ba..bebcc1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,12 @@ ## [2025.3.1] +### Fixes + +- Fixed [runtime issues on 2025.3 line of IDEs](https://github.com/comod/git-scope-pro/issues/72) + ### Added -- Support reorder git scope tabs via right-click popup menu +- Added [support to reorder git scope tabs](https://github.com/comod/git-scope-pro/issues/73) ## [2025.3]