diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f22c1b7..3f838af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - name: Download JetBrains Runtime (JBR 21) run: | - wget https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.8-linux-x64-b1038.68.tar.gz -O jbr.tar.gz + wget https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.9-linux-x64-b1038.76.tar.gz -O jbr.tar.gz mkdir -p jbr tar -xzf jbr.tar.gz -C jbr --strip-components=1 @@ -37,7 +37,7 @@ jobs: run: ./gradlew buildPlugin - name: Upload Plugin Artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: plugin-artifact path: build/distributions/*.zip \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c2552..c3f33e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## [2025.3] + +### Fixes + +- Fixed [Indent guides broken on PyCharm](https://github.com/comod/git-scope-pro/issues/68) +- Fixed [File list does not update after switching branch](https://github.com/comod/git-scope-pro/issues/62) +- Fixed [Commit panel diff sometimes not working](https://github.com/comod/git-scope-pro/issues/56) + ## [2025.2.1] ### Fixes diff --git a/build.gradle.kts b/build.gradle.kts index 36a403f..2fdf278 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,9 +6,9 @@ val platformType = properties("platformType") plugins { id("java") - id("org.jetbrains.kotlin.jvm") version "2.2.20" - id("org.jetbrains.intellij.platform") version "2.10.0" - id("org.jetbrains.changelog") version "2.4.0" + id("org.jetbrains.kotlin.jvm") version "2.2.21" + id("org.jetbrains.intellij.platform") version "2.10.5" + id("org.jetbrains.changelog") version "2.5.0" } group = properties("pluginGroup") @@ -105,6 +105,9 @@ tasks { buildSearchableOptions { enabled = false } + prepareJarSearchableOptions { + enabled = false + } wrapper { gradleVersion = providers.gradleProperty("gradleVersion").get() distributionType = Wrapper.DistributionType.BIN diff --git a/gradle.properties b/gradle.properties index 87ec694..6abc130 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,15 +2,15 @@ pluginGroup=org.woelkit.plugins pluginName=Git Scope pluginRepositoryUrl=https://github.com/comod/git-scope-pro -pluginVersion=2025.2.1 +pluginVersion=2025.3 pluginSinceBuild=243 platformType=IU #platformVersion=LATEST-EAP-SNAPSHOT -platformVersion=2025.2.3 +platformVersion=2025.2.5 platformBundledPlugins=Git4Idea -gradleVersion=9.1.0 +gradleVersion=9.2.1 kotlin.stdlib.default.dependency=false org.gradle.configuration-cache=true org.gradle.caching = true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e11132..23449a2 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index ef07e01..adff685 100755 --- a/gradlew +++ b/gradlew @@ -114,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -172,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +210,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/gradlew.bat b/gradlew.bat index db3a6ac..c4bdd3a 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,10 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java b/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java new file mode 100644 index 0000000..ee600af --- /dev/null +++ b/src/main/java/implementation/lineStatusTracker/CommitDiffWorkaround.java @@ -0,0 +1,386 @@ +package implementation.lineStatusTracker; + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorKind; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vcs.VcsException; +import com.intellij.openapi.vcs.changes.Change; +import com.intellij.openapi.vcs.changes.ChangeListManager; +import com.intellij.openapi.vcs.changes.ContentRevision; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import system.Defs; + +import java.awt.*; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Workaround for IntelliJ IDEA commit panel diff issue. + * + * Problem: When viewing diffs in the commit panel, IDEA shows the diff between the current + * working copy and the custom base revision set by Git Scope. This causes incorrect diffs + * to be displayed - the commit panel should always show diffs against HEAD (the last commit), + * not against a custom branch base. + * + * Solution: This class detects when commit panel diff editors are active and temporarily + * switches the base revision to HEAD. When the user switches away from the commit panel, + * it restores the custom base revision. + */ +public class CommitDiffWorkaround implements Disposable { + private static final Logger LOG = Defs.getLogger(CommitDiffWorkaround.class); + private static final int ACTIVATION_DELAY_MS = 300; + + private final Project project; + private final AtomicBoolean disposing = new AtomicBoolean(false); + + // Map: Document -> Set of commit diff Editors for that document + private final Map> commitDiffEditors = new HashMap<>(); + + // Track which documents are currently showing HEAD base (active in commit diff) + private final Set activeCommitDiffs = new HashSet<>(); + + // Callback interface for base revision switching + private final BaseRevisionSwitcher baseRevisionSwitcher; + + /** + * Interface for switching base revisions. + */ + public interface BaseRevisionSwitcher { + /** + * Switch to HEAD base revision for the given document. + * @param document The document to switch + * @param headContent The HEAD revision content + */ + void switchToHeadBase(@NotNull Document document, @NotNull String headContent); + + /** + * Switch to custom base revision for the given document. + * @param document The document to switch + * @param customContent The custom base revision content + */ + void switchToCustomBase(@NotNull Document document, @NotNull String customContent); + + /** + * Get the cached HEAD content for a document, or null if not cached. + */ + @Nullable String getCachedHeadContent(@NotNull Document document); + + /** + * Get the cached custom base content for a document, or null if not cached. + */ + @Nullable String getCachedCustomBaseContent(@NotNull Document document); + + /** + * Cache HEAD content for a document. + */ + void cacheHeadContent(@NotNull Document document, @NotNull String headContent); + + /** + * Check if document is tracked and held. + */ + boolean isTracked(@NotNull Document document); + + /** + * Mark document as showing HEAD base. + */ + void markShowingHeadBase(@NotNull Document document, boolean showing); + + /** + * Check if document is showing HEAD base. + */ + boolean isShowingHeadBase(@NotNull Document document); + } + + public CommitDiffWorkaround(@NotNull Project project, @NotNull BaseRevisionSwitcher switcher) { + this.project = project; + this.baseRevisionSwitcher = switcher; + } + + @Override + public void dispose() { + disposing.set(true); + synchronized (this) { + commitDiffEditors.clear(); + activeCommitDiffs.clear(); + } + } + + /** + * Check if an editor is a commit panel diff editor. + * Call this when a DIFF editor is created to determine if it needs special handling. + */ + public boolean isCommitPanelDiff(@NotNull Editor editor) { + if (editor.getEditorKind() != EditorKind.DIFF) { + return false; + } + + return isInCommitToolWindowHierarchy(editor); + } + + /** + * Handle commit panel diff editor created. + * Call this when a commit panel diff editor is created. + */ + public void handleCommitDiffEditorCreated(@NotNull Editor editor) { + Document doc = editor.getDocument(); + VirtualFile file = FileDocumentManager.getInstance().getFile(doc); + + if (file == null) { + return; + } + + synchronized (this) { + // Register the editor + commitDiffEditors.computeIfAbsent(doc, k -> new HashSet<>()).add(editor); + + // Pre-cache HEAD content if tracked + if (baseRevisionSwitcher.isTracked(doc)) { + if (baseRevisionSwitcher.getCachedHeadContent(doc) == null) { + String headContent = fetchHeadRevisionContent(file); + if (headContent != null) { + baseRevisionSwitcher.cacheHeadContent(doc, headContent); + } + } + } + } + + // Check if a commit diff editor is currently selected - if so, activate HEAD base + scheduleActivationIfCommitDiffSelected(); + } + + /** + * Handle commit panel diff editor released (closed). + * Call this when a commit panel diff editor is closed. + */ + public void handleCommitDiffEditorReleased(@NotNull Editor editor) { + Document doc = editor.getDocument(); + VirtualFile file = FileDocumentManager.getInstance().getFile(doc); + + if (file == null) { + return; + } + + synchronized (this) { + // Unregister the editor + Set editors = commitDiffEditors.get(doc); + if (editors != null) { + editors.remove(editor); + if (editors.isEmpty()) { + commitDiffEditors.remove(doc); + } + } + + // Remove from active tracking + activeCommitDiffs.remove(doc); + + // Restore custom base if no longer showing in any commit diff + if (!commitDiffEditors.containsKey(doc) && baseRevisionSwitcher.isShowingHeadBase(doc)) { + restoreCustomBaseForDocument(doc); + } + } + } + + /** + * Handle editor selection changed to a commit diff editor. + * Call this when the user switches to a commit panel diff tab. + */ + public void handleSwitchedToCommitDiff() { + // Delay activation to allow diff window to fully render before switching base + com.intellij.util.Alarm alarm = new com.intellij.util.Alarm(this); + alarm.addRequest(() -> { + if (disposing.get()) return; + activateHeadBaseForAllCommitDiffs(); + }, ACTIVATION_DELAY_MS); + } + + /** + * Handle editor selection changed away from a commit diff editor. + * Call this when the user switches away from a commit panel diff tab. + */ + public void handleSwitchedAwayFromCommitDiff() { + restoreCustomBaseForAllDocuments(); + } + + /** + * Check if document should skip custom base update (because it's showing HEAD base). + * Call this before applying a custom base update to check if it should be skipped. + */ + public boolean shouldSkipCustomBaseUpdate(@NotNull Document document) { + return baseRevisionSwitcher.isShowingHeadBase(document); + } + + /** + * Check if there are commit diff editors open for the given document. + * Used to determine if a document's tracker should be kept alive. + */ + public boolean hasCommitDiffEditorsFor(@NotNull Document document) { + synchronized (this) { + Set editors = commitDiffEditors.get(document); + return editors != null && !editors.isEmpty(); + } + } + + // Private implementation methods + + private boolean isInCommitToolWindowHierarchy(@NotNull Editor editor) { + try { + Component parent = editor.getComponent(); + + // Walk up component hierarchy looking for DiffRequestProcessor + int depth = 0; + while (parent != null && depth < 50) { + if (parent.getClass().getName().contains("DiffRequestProcessor")) { + return true; + } + parent = parent.getParent(); + depth++; + } + } catch (Exception e) { + LOG.warn("Error checking commit tool window hierarchy", e); + } + + return false; + } + + private void scheduleActivationIfCommitDiffSelected() { + ApplicationManager.getApplication().invokeLater(() -> { + if (disposing.get()) return; + + FileEditorManager fileEditorManager = FileEditorManager.getInstance(project); + FileEditor selectedEditor = fileEditorManager.getSelectedEditor(); + + if (selectedEditor != null && + selectedEditor.getClass().getSimpleName().equals("BackendDiffRequestProcessorEditor")) { + // Delay activation to allow diff window to fully render + com.intellij.util.Alarm alarm = new com.intellij.util.Alarm(this); + alarm.addRequest(() -> { + if (disposing.get()) return; + activateHeadBaseForAllCommitDiffs(); + }, ACTIVATION_DELAY_MS); + } + }); + } + + private void activateHeadBaseForAllCommitDiffs() { + synchronized (this) { + for (Map.Entry> entry : commitDiffEditors.entrySet()) { + Document doc = entry.getKey(); + Set diffEditors = entry.getValue(); + + if (diffEditors.isEmpty()) { + continue; + } + + VirtualFile file = FileDocumentManager.getInstance().getFile(doc); + if (file == null) { + continue; + } + + if (!baseRevisionSwitcher.isTracked(doc)) { + continue; + } + + if (baseRevisionSwitcher.isShowingHeadBase(doc)) { + continue; + } + + // Get HEAD content (fetch if not cached) + String headContent = baseRevisionSwitcher.getCachedHeadContent(doc); + if (headContent == null) { + headContent = fetchHeadRevisionContent(file); + if (headContent != null) { + baseRevisionSwitcher.cacheHeadContent(doc, headContent); + } + } + + if (headContent != null) { + baseRevisionSwitcher.markShowingHeadBase(doc, true); + activeCommitDiffs.add(doc); + + String finalHeadContent = headContent; + ApplicationManager.getApplication().invokeLater(() -> { + if (disposing.get()) return; + baseRevisionSwitcher.switchToHeadBase(doc, finalHeadContent); + }); + } + } + } + } + + private void restoreCustomBaseForAllDocuments() { + synchronized (this) { + for (Document doc : new HashSet<>(activeCommitDiffs)) { + restoreCustomBaseForDocument(doc); + } + } + } + + private void restoreCustomBaseForDocument(@NotNull Document doc) { + VirtualFile file = FileDocumentManager.getInstance().getFile(doc); + if (file == null) { + return; + } + + if (!baseRevisionSwitcher.isShowingHeadBase(doc)) { + return; + } + + String customContent = baseRevisionSwitcher.getCachedCustomBaseContent(doc); + if (customContent != null) { + baseRevisionSwitcher.markShowingHeadBase(doc, false); + activeCommitDiffs.remove(doc); + + ApplicationManager.getApplication().invokeLater(() -> { + if (disposing.get()) return; + baseRevisionSwitcher.switchToCustomBase(doc, customContent); + }); + } + } + + private String fetchHeadRevisionContent(@NotNull VirtualFile file) { + try { + if (project == null || project.isDisposed()) { + return null; + } + + ChangeListManager changeListManager = ChangeListManager.getInstance(project); + Collection allChanges = changeListManager.getAllChanges(); + + // Find the change for this file + for (Change change : allChanges) { + VirtualFile changeFile = change.getVirtualFile(); + if (changeFile != null && changeFile.equals(file)) { + // The "before" revision is HEAD + ContentRevision beforeRevision = change.getBeforeRevision(); + if (beforeRevision != null) { + String content = beforeRevision.getContent(); + if (content != null) { + return StringUtil.convertLineSeparators(content); + } + } + } + } + + // File is not modified - current content IS HEAD + Document doc = FileDocumentManager.getInstance().getDocument(file); + if (doc != null) { + return doc.getText(); + } + + return null; + } catch (Exception e) { + LOG.warn("Error fetching HEAD revision content for " + file.getName(), e); + return null; + } + } +} diff --git a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java index 3c1ff66..26df044 100644 --- a/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java +++ b/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java @@ -2,14 +2,19 @@ import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; import com.intellij.openapi.editor.EditorKind; +import com.intellij.openapi.editor.event.EditorFactoryEvent; +import com.intellij.openapi.editor.event.EditorFactoryListener; +import com.intellij.openapi.editor.ex.DocumentEx; +import com.intellij.util.DocumentUtil; import com.intellij.openapi.editor.ex.EditorGutterComponentEx; import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.FileEditorManagerEvent; import com.intellij.openapi.fileEditor.FileEditorManagerListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; @@ -25,18 +30,19 @@ import com.intellij.util.messages.MessageBusConnection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import system.Defs; import java.lang.reflect.Method; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; +import java.util.*; import java.util.concurrent.atomic.AtomicBoolean; public class MyLineStatusTrackerImpl implements Disposable { - private static final Logger LOG = Logger.getInstance(MyLineStatusTrackerImpl.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(MyLineStatusTrackerImpl.class); + private final Project project; private final MessageBusConnection messageBusConnection; private final LineStatusTrackerManagerI trackerManager; + private final CommitDiffWorkaround commitDiffWorkaround; // Single, consistent requester for this component's lifetime private final Object requester = new Object(); @@ -47,11 +53,15 @@ public class MyLineStatusTrackerImpl implements Disposable { private static final class TrackerInfo { volatile boolean held; - volatile String baseContent; + volatile String customBaseContent; // Custom base (target branch) + volatile String headBaseContent; // HEAD revision (for commit panel diffs) + volatile boolean isShowingHeadBase; // Currently showing HEAD base (vs custom base) TrackerInfo(boolean held, String baseContent) { this.held = held; - this.baseContent = baseContent; + this.customBaseContent = baseContent; + this.headBaseContent = null; + this.isShowingHeadBase = false; } } @@ -61,12 +71,72 @@ public void dispose() { } public MyLineStatusTrackerImpl(Project project, Disposable parentDisposable) { + this.project = project; this.trackerManager = project.getService(LineStatusTrackerManagerI.class); + // Initialize the commit diff workaround with our base revision switcher implementation + this.commitDiffWorkaround = new CommitDiffWorkaround(project, new CommitDiffWorkaround.BaseRevisionSwitcher() { + @Override + public void switchToHeadBase(@NotNull Document document, @NotNull String headContent) { + LineStatusTracker tracker = trackerManager.getLineStatusTracker(document); + if (tracker != null) { + updateTrackerBaseRevision(tracker, headContent); + } + } + + @Override + public void switchToCustomBase(@NotNull Document document, @NotNull String customContent) { + LineStatusTracker tracker = trackerManager.getLineStatusTracker(document); + if (tracker != null) { + updateTrackerBaseRevision(tracker, customContent); + } + } + + @Override + public @Nullable String getCachedHeadContent(@NotNull Document document) { + TrackerInfo info = trackers.get(document); + return info != null ? info.headBaseContent : null; + } + + @Override + public @Nullable String getCachedCustomBaseContent(@NotNull Document document) { + TrackerInfo info = trackers.get(document); + return info != null ? info.customBaseContent : null; + } + + @Override + public void cacheHeadContent(@NotNull Document document, @NotNull String headContent) { + TrackerInfo info = trackers.get(document); + if (info != null) { + info.headBaseContent = headContent; + } + } + + @Override + public boolean isTracked(@NotNull Document document) { + TrackerInfo info = trackers.get(document); + return info != null && info.held; + } + + @Override + public void markShowingHeadBase(@NotNull Document document, boolean showing) { + TrackerInfo info = trackers.get(document); + if (info != null) { + info.isShowingHeadBase = showing; + } + } + + @Override + public boolean isShowingHeadBase(@NotNull Document document) { + TrackerInfo info = trackers.get(document); + return info != null && info.isShowingHeadBase; + } + }); + MessageBus messageBus = project.getMessageBus(); this.messageBusConnection = messageBus.connect(); - // Listen to file open/close events + // Listen to file open/close/selection events messageBusConnection.subscribe( FileEditorManagerListener.FILE_EDITOR_MANAGER, new FileEditorManagerListener() { @@ -82,13 +152,56 @@ public void fileOpened(@NotNull FileEditorManager fileEditorManager, @NotNull Vi public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { Document doc = FileDocumentManager.getInstance().getDocument(file); if (doc != null) { - safeRelease(doc); + // Workaround: Don't release if commit diff editors still need this document + if (!commitDiffWorkaround.hasCommitDiffEditorsFor(doc)) { + safeRelease(doc); + } + } + + // Workaround: Check if closing this file returns focus to commit diff + FileEditor selectedEditor = source.getSelectedEditor(); + if (selectedEditor != null && + selectedEditor.getClass().getSimpleName().equals("BackendDiffRequestProcessorEditor")) { + commitDiffWorkaround.handleSwitchedToCommitDiff(); } } + + @Override + public void selectionChanged(@NotNull FileEditorManagerEvent event) { + handleEditorSelectionChanged(event); + } } ); + // Listen to editor lifecycle to detect commit panel diff editors + EditorFactory.getInstance().addEditorFactoryListener( + new EditorFactoryListener() { + @Override + public void editorCreated(@NotNull EditorFactoryEvent event) { + Editor editor = event.getEditor(); + if (editor.getEditorKind() == EditorKind.DIFF) { + // Check if it's a commit panel diff - hierarchy might not be ready yet + ApplicationManager.getApplication().invokeLater(() -> { + if (commitDiffWorkaround.isCommitPanelDiff(editor)) { + commitDiffWorkaround.handleCommitDiffEditorCreated(editor); + } + }); + } + } + + @Override + public void editorReleased(@NotNull EditorFactoryEvent event) { + Editor editor = event.getEditor(); + if (commitDiffWorkaround.isCommitPanelDiff(editor)) { + commitDiffWorkaround.handleCommitDiffEditorReleased(editor); + } + } + }, + parentDisposable + ); + Disposer.register(parentDisposable, this); + Disposer.register(parentDisposable, commitDiffWorkaround); } private boolean isDiffView(Editor editor) { @@ -96,12 +209,11 @@ private boolean isDiffView(Editor editor) { } private void refreshEditor(Editor editor) { - editor.getMarkupModel().removeAllHighlighters(); + // 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(); - gutter.repaint(); } - editor.getComponent().repaint(); } public void update(Collection changes, @Nullable VirtualFile targetFile) { @@ -209,7 +321,12 @@ private void updateTrackerBaseContent(Document document, String content) { TrackerInfo info = trackers.get(document); if (info != null) { - info.baseContent = finalContent; + info.customBaseContent = finalContent; + + // Workaround: Skip custom base update if commit diff is active (showing HEAD base) + if (commitDiffWorkaround.shouldSkipCustomBaseUpdate(document)) { + return; + } } LineStatusTracker tracker = trackerManager.getLineStatusTracker(document); @@ -223,7 +340,7 @@ private void updateTrackerBaseContent(Document document, String content) { } /** - * Request a tracker for the editor’s document (no duplicate requester). + * Request a tracker for the editor's document (no duplicate requester). */ private void requestLineStatusTracker(@Nullable Editor editor) { if (editor == null || disposing.get()) return; @@ -231,17 +348,12 @@ private void requestLineStatusTracker(@Nullable Editor editor) { Document document = editor.getDocument(); ensureRequested(document); - ApplicationManager.getApplication().invokeLater(() -> { - if (disposing.get()) return; - - if (editor.getGutter() instanceof EditorGutterComponentEx gutter) { - gutter.revalidateMarkup(); - } - }); + // Platform handles gutter repainting automatically - no need to force it } /** * Use reflection to call the setBaseRevision method on the tracker. + * Uses bulk update mode to batch document changes and reduce daemon restarts. */ private void updateTrackerBaseRevision(LineStatusTracker tracker, String content) { try { @@ -254,9 +366,17 @@ private void updateTrackerBaseRevision(LineStatusTracker tracker, String cont setBaseRevisionMethod.setAccessible(true); ApplicationManager.getApplication().runWriteAction(() -> { try { - setBaseRevisionMethod.invoke(tracker, content); + // Use bulk update mode to batch changes and prevent flickering + Document document = tracker.getDocument(); + DocumentUtil.executeInBulk(document, () -> { + try { + setBaseRevisionMethod.invoke(tracker, content); + } catch (Exception e) { + LOG.error("Failed to invoke setBaseRevision method", e); + } + }); } catch (Exception e) { - LOG.error("Failed to invoke setBaseRevision method", e); + LOG.error("Failed to execute in bulk mode", e); } }); } else { @@ -339,4 +459,23 @@ public void releaseAll() { ApplicationManager.getApplication().invokeAndWait(release); } } -} \ No newline at end of file + + /** + * Handle editor selection changed - detect switching to/from commit diff tabs. + * Delegates to CommitDiffWorkaround for handling commit panel diff base revision switching. + */ + private void handleEditorSelectionChanged(@NotNull FileEditorManagerEvent event) { + FileEditor oldEditor = event.getOldEditor(); + FileEditor newEditor = event.getNewEditor(); + + // Workaround: Check if switching away from commit diff editor + if (oldEditor != null && oldEditor.getClass().getSimpleName().equals("BackendDiffRequestProcessorEditor")) { + commitDiffWorkaround.handleSwitchedAwayFromCommitDiff(); + } + + // Workaround: Check if switching to commit diff editor + if (newEditor != null && newEditor.getClass().getSimpleName().equals("BackendDiffRequestProcessorEditor")) { + commitDiffWorkaround.handleSwitchedToCommitDiff(); + } + } +} diff --git a/src/main/java/listener/MyChangeListListener.java b/src/main/java/listener/MyChangeListListener.java index 9935566..66fbf5d 100644 --- a/src/main/java/listener/MyChangeListListener.java +++ b/src/main/java/listener/MyChangeListListener.java @@ -3,8 +3,10 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.changes.ChangeListListener; import service.ViewService; +import system.Defs; public class MyChangeListListener implements ChangeListListener { + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(MyChangeListListener.class); private final ViewService viewService; public MyChangeListListener(Project project) { @@ -12,6 +14,7 @@ public MyChangeListListener(Project project) { } public void changeListUpdateDone() { + LOG.debug("changeListUpdateDone() called - triggering update"); // TODO: collectChanges: VcsTree is updated viewService.incrementUpdate(); viewService.collectChanges(true); diff --git a/src/main/java/listener/MyTabContentListener.java b/src/main/java/listener/MyTabContentListener.java index 74e8dea..c0d52a5 100644 --- a/src/main/java/listener/MyTabContentListener.java +++ b/src/main/java/listener/MyTabContentListener.java @@ -1,6 +1,5 @@ package listener; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.NlsContexts; import com.intellij.ui.content.ContentManagerEvent; @@ -8,6 +7,7 @@ import org.jetbrains.annotations.NotNull; import service.ViewService; import service.ToolWindowServiceInterface; +import system.Defs; import toolwindow.elements.VcsTree; import javax.swing.*; @@ -16,7 +16,7 @@ import static service.ViewService.PLUS_TAB_LABEL; public class MyTabContentListener implements ContentManagerListener { - private static final Logger LOG = Logger.getInstance(MyTabContentListener.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(MyTabContentListener.class); private final Project project; // Use lazy initialization for the service diff --git a/src/main/java/service/ViewService.java b/src/main/java/service/ViewService.java index d8f064e..8ca3a31 100644 --- a/src/main/java/service/ViewService.java +++ b/src/main/java/service/ViewService.java @@ -32,6 +32,7 @@ import java.util.function.Consumer; public class ViewService implements Disposable { + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(ViewService.class); public static final String PLUS_TAB_LABEL = "+"; public static final int DEBOUNCE_MS = 50; @@ -352,7 +353,10 @@ private void subscribeToObservable(MyModel model) { } } // TODO: collectChanges: tab switched - case active -> collectChanges(model, true); + case active -> { + incrementUpdate(); // Increment generation to cancel any stale updates for previous tab + collectChanges(model, true); + } case tabName -> { if (!isProcessingTabRename) { String customName = model.getCustomTabName(); @@ -409,7 +413,8 @@ private String getTargetBranchDisplay(MyModel model) { } public void incrementUpdate() { - applyGeneration.incrementAndGet(); + long newGen = applyGeneration.incrementAndGet(); + LOG.debug("incrementUpdate() -> generation = " + newGen); } public CompletableFuture collectChanges(boolean checkFs) { @@ -429,14 +434,19 @@ public CompletableFuture collectChanges(MyModel model, boolean checkFs) { } final long gen = applyGeneration.get(); + LOG.debug("collectChanges() scheduled with generation = " + gen); // serialize collection behind a single-threaded executor changesExecutor.execute(() -> { changesService.collectChangesWithCallback(targetBranchMap, changes -> { ApplicationManager.getApplication().invokeLater(() -> { try { - if (!project.isDisposed() && applyGeneration.get() == gen) { + long currentGen = applyGeneration.get(); + if (!project.isDisposed() && currentGen == gen) { + LOG.debug("Applying changes for generation " + gen); model.setChanges(changes); + } else { + LOG.debug("Discarding changes for generation " + gen + " (current generation is " + currentGen + ")"); } } finally { done.complete(null); @@ -510,7 +520,9 @@ public void setTabIndex(int index) { lastTabIndex = currentTabIndex; } currentTabIndex = index; - applyGeneration.incrementAndGet(); // bump generation on tab change to invalidate any ongoing update + // Don't increment generation here - it will be incremented when setActiveModel() triggers collectChanges() + // This prevents a race condition where events between setTabIndex() and the observable firing get lost + LOG.debug("setTabIndex(" + index + ")"); save(); } diff --git a/src/main/java/state/WindowPositionTracker.java b/src/main/java/state/WindowPositionTracker.java index a6e3b77..78e2ed2 100644 --- a/src/main/java/state/WindowPositionTracker.java +++ b/src/main/java/state/WindowPositionTracker.java @@ -1,6 +1,6 @@ package state; -import com.intellij.openapi.diagnostic.Logger; +import system.Defs; import javax.swing.*; import java.awt.*; @@ -15,7 +15,7 @@ import java.util.function.Supplier; public class WindowPositionTracker { - private static final Logger LOG = Logger.getInstance(WindowPositionTracker.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(WindowPositionTracker.class); private static final int SCROLL_SAVE_DELAY_MS = 500; private static final long USER_ACTIVITY_TIMEOUT = 2000; // 2 seconds private static final int MAX_SCROLL_EVENTS_PER_TAB = 100; // Limit scroll event history size diff --git a/src/main/java/system/Defs.java b/src/main/java/system/Defs.java index 7b20118..3a7dede 100644 --- a/src/main/java/system/Defs.java +++ b/src/main/java/system/Defs.java @@ -1,6 +1,7 @@ package system; import com.intellij.icons.AllIcons; +import com.intellij.openapi.diagnostic.Logger; import javax.swing.*; @@ -8,4 +9,19 @@ public class Defs { public static String APPLICATION_NAME = "Git Scope"; public static String TOOL_WINDOW_NAME = "Git Scope"; public static Icon ICON = AllIcons.Actions.Diff; + + /** + * Global logger category for Git Scope plugin. + * To enable debug logging for all Git Scope components, add this to Debug Log Settings: + * #gitscope + */ + public static final String LOG_CATEGORY = "gitscope"; + + /** + * Creates a logger instance for the given class that uses the global Git Scope category. + * This allows enabling all plugin debug logs with a single setting: #gitscope + */ + public static Logger getLogger(Class clazz) { + return Logger.getInstance(LOG_CATEGORY + "." + clazz.getSimpleName()); + } } diff --git a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java index 8bffabd..444266e 100644 --- a/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java +++ b/src/main/java/toolwindow/elements/MySimpleChangesBrowser.java @@ -3,7 +3,6 @@ import com.intellij.ide.ui.UISettings; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.LogicalPosition; import com.intellij.openapi.editor.ScrollType; @@ -15,6 +14,7 @@ import com.intellij.openapi.vcs.changes.ui.SimpleAsyncChangesBrowser; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; +import system.Defs; import toolwindow.VcsTreeActions; import javax.swing.*; @@ -27,7 +27,7 @@ import java.util.concurrent.*; public class MySimpleChangesBrowser extends SimpleAsyncChangesBrowser { - private static final Logger LOG = Logger.getInstance(MySimpleChangesBrowser.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(MySimpleChangesBrowser.class); private final Project myProject; UISettings uiSettings = UISettings.getInstance(); diff --git a/src/main/java/toolwindow/elements/VcsTree.java b/src/main/java/toolwindow/elements/VcsTree.java index 1957cd5..08f05e1 100644 --- a/src/main/java/toolwindow/elements/VcsTree.java +++ b/src/main/java/toolwindow/elements/VcsTree.java @@ -1,7 +1,6 @@ package toolwindow.elements; import com.intellij.icons.AllIcons; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.FilePath; import com.intellij.openapi.vcs.changes.Change; @@ -14,6 +13,7 @@ import service.ViewService; import state.WindowPositionTracker; import state.WindowPositionTracker.ScrollPosition; +import system.Defs; import javax.swing.*; import java.awt.*; @@ -28,7 +28,7 @@ import java.util.stream.Collectors; public class VcsTree extends JPanel { - private static final Logger LOG = Logger.getInstance(VcsTree.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(VcsTree.class); private static final int UPDATE_TIMEOUT_SECONDS = 30; private final Project project; diff --git a/src/main/java/utils/GitCommitReflection.java b/src/main/java/utils/GitCommitReflection.java index f8bc701..6099b8a 100644 --- a/src/main/java/utils/GitCommitReflection.java +++ b/src/main/java/utils/GitCommitReflection.java @@ -1,9 +1,9 @@ package utils; -import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.vcs.changes.Change; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import system.Defs; import java.lang.reflect.Method; import java.util.Collection; @@ -19,7 +19,7 @@ */ public final class GitCommitReflection { - private static final Logger LOG = Logger.getInstance(GitCommitReflection.class); + private static final com.intellij.openapi.diagnostic.Logger LOG = Defs.getLogger(GitCommitReflection.class); private GitCommitReflection() {}