vcs: refresh changes view on background

https://jetbrains.team/im/review/2L6Ks1naPkI

GitOrigin-RevId: 3edd6ae3ff7c2cec8038196b6504dce5e469a5cb
This commit is contained in:
Leonid Shalupov
2022-10-10 15:04:35 +02:00
committed by intellij-monorepo-bot
parent a1c53e78f9
commit 3fc55552b0
13 changed files with 455 additions and 104 deletions

View File

@@ -3,16 +3,19 @@ package com.intellij.vcs.commit
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.vcs.FilePath
import com.intellij.openapi.vcs.changes.Change
import com.intellij.openapi.vcs.changes.CommitExecutor
import com.intellij.openapi.vcs.changes.InclusionListener
import com.intellij.ui.TextAccessor
import org.jetbrains.concurrency.Promise
import java.util.*
interface CommitWorkflowUi : DataProvider, Disposable {
val commitMessageUi: CommitMessageUi
val modalityState: ModalityState // FIXME: make `refreshData` fullfil on EDT?
var defaultCommitActionName: @NlsContexts.Button String
@@ -22,7 +25,7 @@ interface CommitWorkflowUi : DataProvider, Disposable {
fun addExecutorListener(listener: CommitExecutorListener, parent: Disposable)
fun refreshData()
fun refreshData(): Promise<*>
fun getDisplayedChanges(): List<Change>
fun getIncludedChanges(): List<Change>

View File

@@ -88,7 +88,7 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
new Topic<>(LocalChangeListsLoadedListener.class, Topic.BroadcastDirection.NONE);
private final Project myProject;
private final ChangesViewI myChangesViewManager;
private final ChangesViewEx myChangesViewManager;
private final ChangelistConflictTracker myConflictTracker;
private final Scheduler myScheduler = new Scheduler(); // update thread
@@ -116,6 +116,8 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
@NotNull private final Set<String> myListsToBeDeletedSilently = new HashSet<>();
@NotNull private final Set<String> myListsToBeDeleted = new HashSet<>();
private boolean myEmptyListDeletionScheduled;
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private boolean myModalNotificationsBlocked;
private final List<CommitExecutor> myRegisteredCommitExecutors = new ArrayList<>();
@@ -126,7 +128,7 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
public ChangeListManagerImpl(@NotNull Project project) {
myProject = project;
myChangesViewManager = myProject.isDefault() ? new DummyChangesView(myProject) : ChangesViewManager.getInstance(myProject);
myChangesViewManager = myProject.isDefault() ? new DummyChangesView() : ChangesViewManager.getInstanceEx(myProject);
myConflictTracker = new ChangelistConflictTracker(project, this);
myComposite = FileHolderComposite.create(project);
@@ -439,12 +441,12 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
myUpdateException = null;
myAdditionalInfo = null;
myDelayedNotificator.changedFileStatusChanged(true);
myDelayedNotificator.unchangedFileStatusChanged(true);
myDelayedNotificator.changeListUpdateDone();
((ChangesViewEx)myChangesViewManager).refreshImmediately();
}
myDelayedNotificator.changedFileStatusChanged(true);
myDelayedNotificator.unchangedFileStatusChanged(true);
myDelayedNotificator.changeListUpdateDone();
myChangesViewManager.resetViewImmediatelyAndRefreshLater();
}
catch (Exception | AssertionError ex) {
LOG.error(ex);
@@ -1237,6 +1239,7 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
myListeners.removeListener(listener);
}
@SuppressWarnings("removal")
@Override
public void registerCommitExecutor(@NotNull CommitExecutor executor) {
myRegisteredCommitExecutors.add(executor);
@@ -1301,6 +1304,7 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
}
// used in TeamCity
@SuppressWarnings("removal")
@Override
public void reopenFiles(@NotNull List<? extends FilePath> paths) {
final ReadonlyStatusHandlerImpl readonlyStatusHandler = (ReadonlyStatusHandlerImpl)ReadonlyStatusHandler.getInstance(myProject);
@@ -1320,14 +1324,17 @@ public class ChangeListManagerImpl extends ChangeListManagerEx implements Persis
return Collections.unmodifiableList(myRegisteredCommitExecutors);
}
@SuppressWarnings("removal")
@Override
public void addDirectoryToIgnoreImplicitly(@NotNull String path) {
}
@SuppressWarnings("removal")
@Override
public void setFilesToIgnore(IgnoredFileBean @NotNull ... filesToIgnore) {
}
@SuppressWarnings("removal")
@Override
public IgnoredFileBean @NotNull [] getFilesToIgnore() {
return EMPTY_ARRAY;

View File

@@ -1,12 +1,26 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.openapi.vcs.changes;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.vcs.commit.ChangesViewCommitWorkflowHandler;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.Promise;
public interface ChangesViewEx extends ChangesViewI {
/**
* @deprecated Changes no longer could be refreshed immediately, use {@link #promiseRefresh()} or {@link #scheduleRefresh()}
*/
@Deprecated
void refreshImmediately();
/**
* Immediately reset changes view and request refresh when NON_MODAL modality allows (i.e. after a plugin was unloaded or a dialog closed)
*/
@RequiresEdt
void resetViewImmediatelyAndRefreshLater();
Promise<?> promiseRefresh();
boolean isAllowExcludeFromCommit();
/**

View File

@@ -5,6 +5,7 @@ package com.intellij.openapi.vcs.changes;
import com.intellij.diagnostic.Activity;
import com.intellij.diagnostic.ActivityCategory;
import com.intellij.diagnostic.StartUpMeasurer;
import com.intellij.diagnostic.telemetry.TraceManager;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CommonActionsManager;
import com.intellij.ide.TreeExpander;
@@ -18,8 +19,7 @@ import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
@@ -45,9 +45,9 @@ import com.intellij.ui.ExperimentalUI;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.panels.Wrapper;
import com.intellij.ui.content.Content;
import com.intellij.util.Alarm;
import com.intellij.util.ModalityUiUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.concurrency.annotations.RequiresBackgroundThread;
import com.intellij.util.concurrency.annotations.RequiresEdt;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.messages.MessageBusConnection;
@@ -61,9 +61,13 @@ import com.intellij.vcs.commit.ChangesViewCommitPanel;
import com.intellij.vcs.commit.ChangesViewCommitWorkflowHandler;
import com.intellij.vcs.commit.CommitModeManager;
import com.intellij.vcs.commit.PartialCommitChangeNodeDecorator;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import org.jetbrains.annotations.CalledInAny;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.Promise;
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
@@ -87,6 +91,7 @@ import static com.intellij.openapi.vcs.changes.ui.ChangesViewContentManagerKt.is
import static com.intellij.util.containers.ContainerUtil.set;
import static com.intellij.util.ui.JBUI.Panels.simplePanel;
import static java.util.Arrays.asList;
import static org.jetbrains.concurrency.Promises.cancelledPromise;
@State(
name = "ChangesViewManager",
@@ -96,6 +101,7 @@ public class ChangesViewManager implements ChangesViewEx,
PersistentStateComponent<ChangesViewManager.State>,
Disposable {
private static final Tracer TRACER = TraceManager.INSTANCE.getTracer("vcs");
private static final String CHANGES_VIEW_PREVIEW_SPLITTER_PROPORTION = "ChangesViewManager.DETAILS_SPLITTER_PROPORTION";
@NotNull private final Project myProject;
@@ -117,7 +123,7 @@ public class ChangesViewManager implements ChangesViewEx,
public ChangesViewManager(@NotNull Project project) {
myProject = project;
ChangesViewModifier.KEY.addChangeListener(project, this::refreshImmediately, this);
ChangesViewModifier.KEY.addChangeListener(project, this::resetViewImmediatelyAndRefreshLater, this);
MessageBusConnection busConnection = project.getMessageBus().connect(this);
busConnection.subscribe(ChangesViewWorkflowManager.TOPIC, () -> updateCommitWorkflow());
@@ -250,11 +256,15 @@ public class ChangesViewManager implements ChangesViewEx,
public boolean myShowIgnored;
}
@Override
public Promise<?> promiseRefresh() {
if (myToolWindowPanel == null) return cancelledPromise();
return myToolWindowPanel.scheduleRefresh();
}
@Override
public void scheduleRefresh() {
if (myToolWindowPanel == null) return;
myToolWindowPanel.scheduleRefresh();
promiseRefresh();
}
@Override
@@ -295,7 +305,7 @@ public class ChangesViewManager implements ChangesViewEx,
@Override
public void refreshImmediately() {
if (myToolWindowPanel == null) return;
myToolWindowPanel.refreshImmediately();
myToolWindowPanel.scheduleRefreshNow();
}
@Override
@@ -308,20 +318,18 @@ public class ChangesViewManager implements ChangesViewEx,
return EditorTabDiffPreviewManager.getInstance(project).isEditorDiffPreviewAvailable();
}
public void openEditorPreview() {
if (myToolWindowPanel == null) return;
myToolWindowPanel.openEditorPreview(false);
}
public void closeEditorPreview() {
closeEditorPreview(false);
}
public void closeEditorPreview(boolean onlyIfEmpty) {
if (myToolWindowPanel == null) return;
myToolWindowPanel.closeEditorPreview(onlyIfEmpty);
}
@Override
public void resetViewImmediatelyAndRefreshLater() {
if (myToolWindowPanel != null) {
myToolWindowPanel.resetViewImmediatelyAndRefreshLater();
}
}
public static final class ChangesViewToolWindowPanel extends SimpleToolWindowPanel implements Disposable {
@NotNull private static final RegistryValue isToolbarHorizontalSetting = Registry.get("vcs.local.changes.toolbar.horizontal");
@NotNull private static final RegistryValue isOpenEditorDiffPreviewWithSingleClick =
@@ -346,9 +354,6 @@ public class ChangesViewManager implements ChangesViewEx,
@Nullable private ChangesViewCommitPanel myCommitPanel;
@Nullable private ChangesViewCommitWorkflowHandler myCommitWorkflowHandler;
@NotNull private final Alarm myTreeUpdateAlarm;
@NotNull private final Object myTreeUpdateIndicatorLock = new Object();
@NotNull private ProgressIndicator myTreeUpdateIndicator = new EmptyProgressIndicator();
private boolean myModelUpdateInProgress;
private boolean myDisposed = false;
@@ -363,7 +368,6 @@ public class ChangesViewManager implements ChangesViewEx,
MessageBusConnection busConnection = myProject.getMessageBus().connect(this);
myVcsConfiguration = VcsConfiguration.getInstance(myProject);
myTreeUpdateAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
myView = myChangesPanel.getChangesView();
myView.installPopupHandler((DefaultActionGroup)ActionManager.getInstance().getAction("ChangesViewPopupMenu"));
@@ -451,11 +455,6 @@ public class ChangesViewManager implements ChangesViewEx,
@Override
public void dispose() {
myDisposed = true;
myTreeUpdateAlarm.cancelAllRequests();
synchronized (myTreeUpdateIndicatorLock) {
myTreeUpdateIndicator.cancel();
}
}
private void setDiffPreview() {
@@ -563,13 +562,6 @@ public class ChangesViewManager implements ChangesViewEx,
return !isOpenEditorDiffPreviewWithSingleClick.asBoolean() || myVcsConfiguration.LOCAL_CHANGES_DETAILS_PREVIEW_SHOWN;
}
private void openEditorPreview(boolean focusEditor) {
if (myEditorDiffPreview == null) return;
if (!isEditorPreviewAllowed()) return;
myEditorDiffPreview.openPreview(focusEditor);
}
private void closeEditorPreview(boolean onlyIfEmpty) {
if (myEditorDiffPreview == null) return;
@@ -718,35 +710,35 @@ public class ChangesViewManager implements ChangesViewEx,
invokeLaterIfNeeded(() -> myView.setPaintBusy(b));
}
public void scheduleRefresh() {
invokeLaterIfNeeded(() -> {
if (myDisposed) return;
myTreeUpdateAlarm.cancelAllRequests();
myTreeUpdateAlarm.addRequest(() -> refreshView(), 100);
public Promise<?> scheduleRefresh() {
return scheduleRefreshWithDelay(100);
}
private final BackgroundRefresher<?> myBackgroundRefresher = new BackgroundRefresher<>(
getClass().getSimpleName() + " refresh", this);
@CalledInAny
private Promise<?> scheduleRefreshWithDelay(int delayMillis) {
setBusy(true);
return myBackgroundRefresher.requestRefresh(delayMillis, () -> {
refreshView();
return null;
}).then(result -> {
setBusy(false);
return null;
});
}
public void refreshImmediately() {
ApplicationManager.getApplication().assertIsDispatchThread();
myTreeUpdateAlarm.cancelAllRequests();
refreshView(false);
private void scheduleRefreshNow() {
scheduleRefreshWithDelay(0);
}
@RequiresBackgroundThread
private void refreshView() {
refreshView(true);
}
private void refreshView(boolean canBeCancelled) {
ProgressIndicator indicator = new EmptyProgressIndicator();
synchronized (myTreeUpdateIndicatorLock) {
myTreeUpdateIndicator.cancel();
myTreeUpdateIndicator = indicator;
}
ProgressManager.getInstance().executeProcessUnderProgress(() -> {
if (myDisposed || !myProject.isInitialized() || ApplicationManager.getApplication().isUnitTestMode()) return;
if (myDisposed || !myProject.isInitialized() || ApplicationManager.getApplication().isUnitTestMode()) return;
Span span = TRACER.spanBuilder("changes-view-refresh-background").startSpan();
try {
ChangeListManagerImpl changeListManager = ChangeListManagerImpl.getInstanceImpl(myProject);
List<LocalChangeList> changeLists = changeListManager.getChangeLists();
List<FilePath> unversionedFiles = changeListManager.getUnversionedFilesPaths();
@@ -766,26 +758,62 @@ public class ChangesViewManager implements ChangesViewEx,
treeModelBuilder.setIgnored(changeListManager.getIgnoredFilePaths());
}
invokeLaterIfNeeded(() -> {
if (myDisposed) return;
if (canBeCancelled) indicator.checkCanceled();
for (ChangesViewModifier extension : ChangesViewModifier.KEY.getExtensions(myProject)) {
for (ChangesViewModifier extension : ChangesViewModifier.KEY.getExtensions(myProject)) {
try {
extension.modifyTreeModelBuilder(treeModelBuilder);
}
DefaultTreeModel newModel = treeModelBuilder.build();
catch (Throwable t) {
Logger.getInstance(ChangesViewToolWindowPanel.class).error(t);
}
}
myModelUpdateInProgress = true;
try {
myView.updateModel(newModel);
if (myCommitWorkflowHandler != null) myCommitWorkflowHandler.synchronizeInclusion(changeLists, unversionedFiles);
}
finally {
myModelUpdateInProgress = false;
}
updatePreview(true);
DefaultTreeModel treeModel = treeModelBuilder.build();
ProgressManager.checkCanceled();
ApplicationManager.getApplication().invokeAndWait(() -> {
refreshViewOnEdt(treeModel, changeLists, unversionedFiles);
});
}, canBeCancelled ? indicator : null);
}
finally {
span.end();
}
}
/**
* Immediately reset changes view and request refresh when NON_MODAL modality allows (i.e. after a plugin was unloaded or a dialog closed)
*/
@RequiresEdt
private void resetViewImmediatelyAndRefreshLater() {
myView.setModel(new DefaultTreeModel(ChangesBrowserNode.createRoot()));
myView.setPaintBusy(true);
ApplicationManager.getApplication().invokeLater(() -> {
scheduleRefreshNow();
}, ModalityState.NON_MODAL);
}
@RequiresEdt
private void refreshViewOnEdt(@NotNull DefaultTreeModel treeModel,
@NotNull List<LocalChangeList> changeLists,
@NotNull List<FilePath> unversionedFiles) {
if (myDisposed) return;
Span span = TRACER.spanBuilder("changes-view-refresh-edt").startSpan();
try {
myModelUpdateInProgress = true;
try {
myView.updateModel(treeModel);
if (myCommitWorkflowHandler != null) myCommitWorkflowHandler.synchronizeInclusion(changeLists, unversionedFiles);
}
finally {
myModelUpdateInProgress = false;
}
updatePreview(true);
}
finally {
span.end();
}
}
public void setGrouping(@NotNull String groupingKey) {
@@ -887,7 +915,7 @@ public class ChangesViewManager implements ChangesViewEx,
@Override
public void setSelected(@NotNull AnActionEvent e, boolean state) {
myChangesViewManager.myState.myShowIgnored = state;
refreshView();
scheduleRefreshNow();
}
}
}

View File

@@ -15,25 +15,31 @@
*/
package com.intellij.openapi.vcs.changes;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.vcs.commit.ChangesViewCommitWorkflowHandler;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.Promise;
import java.util.List;
import static org.jetbrains.concurrency.Promises.resolvedPromise;
/**
* @author irengrig
*/
class DummyChangesView implements ChangesViewEx {
DummyChangesView(Project project) {
DummyChangesView() {
}
@Override
public void scheduleRefresh() {
}
@Override
public void resetViewImmediatelyAndRefreshLater() {
}
@Override
public void selectFile(VirtualFile vFile) {
}
@@ -58,6 +64,11 @@ class DummyChangesView implements ChangesViewEx {
public void refreshImmediately() {
}
@Override
public Promise<?> promiseRefresh() {
return resolvedPromise();
}
@Override
public boolean isAllowExcludeFromCommit() {
return false;

View File

@@ -93,14 +93,15 @@ public class ShowDiffFromLocalChangesActionProvider implements AnActionExtension
// this trick is essential since we are under some conditions to refresh changes;
// but we can only rely on callback after refresh
ChangeListManager.getInstance(project).invokeAfterUpdate(true, () -> {
try {
ChangesViewManager.getInstanceEx(project).refreshImmediately();
List<Change> actualChanges = loadFakeRevisions(project, changes);
resultRef.set(collectRequestProducers(project, actualChanges, unversioned, view));
}
catch (Throwable err) {
resultRef.setException(err);
}
ChangesViewManager.getInstanceEx(project).promiseRefresh().onProcessed(__ -> {
try {
List<Change> actualChanges = loadFakeRevisions(project, changes);
resultRef.set(collectRequestProducers(project, actualChanges, unversioned, view));
}
catch (Throwable err) {
resultRef.setException(err);
}
});
});
chain = new ChangeDiffRequestChain.Async() {
@@ -126,7 +127,6 @@ public class ShowDiffFromLocalChangesActionProvider implements AnActionExtension
DiffManager.getInstance().showDiff(project, chain, DiffDialogHints.DEFAULT);
}
private static boolean checkIfThereAreFakeRevisions(@NotNull Project project, @NotNull List<? extends Change> changes) {
boolean needsConversion = false;
for (Change change : changes) {

View File

@@ -0,0 +1,115 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.changes.ui
import com.intellij.openapi.Disposable
import com.intellij.openapi.progress.EmptyProgressIndicator
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Computable
import com.intellij.openapi.util.Disposer
import com.intellij.util.concurrency.AppExecutorUtil
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.concurrency.AsyncPromise
import org.jetbrains.concurrency.Promise
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
@ApiStatus.Internal
class BackgroundRefresher<T>(name: String, parentDisposable: Disposable) {
private val executor = AppExecutorUtil.createBoundedScheduledExecutorService(name, 1)
private val requestLock: Lock = ReentrantLock()
// Concurrent access is fully handled by requestLock
private var currentIndicator = EmptyProgressIndicator()
private var isDisposed = false
private var currentTask: ScheduledFuture<*>? = null
private var nextRefresh: Boolean = false
private val promisesToFulfil = mutableListOf<AsyncPromise<T>>()
init {
Disposer.register(parentDisposable, Disposable {
requestLock.withLock {
isDisposed = true
}
executor.shutdownNow()
currentIndicator.cancel()
currentTask = null
for (asyncPromise in collectPromises(force = true)) {
asyncPromise.setError(AsyncPromise.CANCELED)
}
})
}
/**
* Request refresh and return the result of the latest one.
* Promise will be resolved only when there are no more pending refreshes upon finishing next refresh.
* Current refresh may be canceled via progress indicator by the next refresh to do less obsolete computations.
*/
fun requestRefresh(delayMillis: Int, block: Computable<T>): Promise<T> = requestLock.withLock {
if (isDisposed) return AsyncPromise<T>().also { it.cancel(false) }
// Cancel queued refresh (does not affect already running one)
currentTask?.cancel(false)
// Cancel any already running refresh to save time
currentIndicator.cancel()
val promise = AsyncPromise<T>()
promisesToFulfil.add(promise)
nextRefresh = true
currentTask = executor.schedule(Runnable {
val indicator = requestLock.withLock {
nextRefresh = false
currentTask = null
val indicator = EmptyProgressIndicator()
currentIndicator = indicator
indicator
}
// If the indicator is cancelled means next request was queued or parentDisposable was terminated
try {
val value = ProgressManager.getInstance().runProcess(block, indicator)
ProgressManager.checkCanceled()
if (executor.isShutdown) {
throw ProcessCanceledException()
}
for (asyncPromise in collectPromises(force = false)) {
asyncPromise.setResult(value)
}
}
catch (t: Throwable) {
// Pass any exception even ProcessCancelledException
// If PCE was initiated by next refresh, collectPromises will return an empty list
// and promises will be intact until next refresh
for (asyncPromise in collectPromises(force = false)) {
asyncPromise.setError(t)
}
}
}, delayMillis.toLong(), TimeUnit.MILLISECONDS)
promise
}
private fun collectPromises(force: Boolean): List<AsyncPromise<T>> {
requestLock.withLock {
if (!force && (nextRefresh || isDisposed)) {
// wait for the next refresh
return emptyList()
}
else {
val promises = promisesToFulfil.toList()
promisesToFulfil.clear()
return promises
}
}
}
}

View File

@@ -32,6 +32,7 @@ import com.intellij.ui.components.JBLabel;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.util.Alarm;
import com.intellij.util.EventDispatcher;
import com.intellij.util.Futures;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.ui.AbstractLayoutManager;
@@ -45,6 +46,8 @@ import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;
import javax.swing.*;
import java.awt.*;
@@ -53,6 +56,7 @@ import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.List;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import static com.intellij.openapi.util.text.StringUtil.escapeXmlEntities;
import static com.intellij.openapi.vcs.VcsBundle.message;
@@ -258,6 +262,12 @@ public abstract class CommitChangeListDialog extends DialogWrapper implements Si
@NotNull
public abstract CommitDialogChangesBrowser getBrowser();
@NotNull
@Override
public ModalityState getModalityState() {
return ModalityState.stateForComponent(getRootPane());
}
@Override
public boolean activate() {
beforeInit();
@@ -644,8 +654,11 @@ public abstract class CommitChangeListDialog extends DialogWrapper implements Si
}
@Override
public void refreshData() {
getBrowser().updateDisplayedChangeLists();
public @NotNull Promise<?> refreshData() {
CompletableFuture<Void> future = Futures.runInEdtAsync(() -> {
getBrowser().updateDisplayedChangeLists();
});
return Promises.asPromise(future);
}
@NotNull

View File

@@ -3,6 +3,7 @@ package com.intellij.vcs.commit
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.project.Project
@@ -124,10 +125,12 @@ abstract class AbstractCommitWorkflowHandler<W : AbstractCommitWorkflow, U : Com
logCommitEvent(sessionInfo)
refreshChanges {
val commitInfo = DynamicCommitInfoImpl(commitContext, sessionInfo, ui, workflow)
workflow.continueExecution {
updateWorkflow(sessionInfo) &&
doExecuteSession(sessionInfo, commitInfo)
invokeLater(ui.modalityState) {
val commitInfo = DynamicCommitInfoImpl(commitContext, sessionInfo, ui, workflow)
workflow.continueExecution {
updateWorkflow(sessionInfo) &&
doExecuteSession(sessionInfo, commitInfo)
}
}
}
return true
@@ -189,8 +192,9 @@ abstract class AbstractCommitWorkflowHandler<W : AbstractCommitWorkflow, U : Com
protected open fun refreshChanges(callback: () -> Unit) =
ChangeListManager.getInstance(project).invokeAfterUpdateWithModal(true, VcsBundle.message("commit.progress.title")) {
ui.refreshData()
callback()
ui.refreshData().then {
callback()
}
}
override fun dispose() = Unit

View File

@@ -3,6 +3,8 @@ package com.intellij.vcs.commit
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.invokeLater
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
@@ -26,6 +28,7 @@ import com.intellij.util.ui.JBUI.Panels.simplePanel
import com.intellij.util.ui.UIUtil
import com.intellij.util.ui.tree.TreeUtil.*
import com.intellij.vcsUtil.VcsUtil.getFilePath
import org.jetbrains.concurrency.Promise
import javax.swing.JComponent
import javax.swing.SwingConstants
import kotlin.properties.Delegates.observable
@@ -108,8 +111,11 @@ class ChangesViewCommitPanel(project: Project, private val changesViewHost: Chan
}
override var editedCommit by observable<EditedCommitDetails?>(null) { _, _, newValue ->
refreshData()
newValue?.let { expand(it) }
refreshData().then {
invokeLater(ModalityState.NON_MODAL) {
newValue?.let { expand(it) }
}
}
}
override val isActive: Boolean get() = isVisible
@@ -183,7 +189,7 @@ class ChangesViewCommitPanel(project: Project, private val changesViewHost: Chan
commitMessage.setChangesSupplier(ChangeListChangesSupplier(changeLists))
}
override fun refreshData() = ChangesViewManager.getInstanceEx(project).refreshImmediately()
override fun refreshData(): Promise<*> = ChangesViewManager.getInstanceEx(project).promiseRefresh()
override fun getDisplayedChanges(): List<Change> = all(changesView).userObjects(Change::class.java)
override fun getIncludedChanges(): List<Change> = included(changesView).userObjects(Change::class.java)
@@ -208,8 +214,11 @@ class ChangesViewCommitPanel(project: Project, private val changesViewHost: Chan
val changesViewManager = ChangesViewManager.getInstance(project) as? ChangesViewManager ?: return
if (!ChangesViewManager.isEditorPreview(project)) return
refreshData()
changesViewManager.closeEditorPreview(true)
refreshData().then {
invokeLater(ModalityState.NON_MODAL) {
changesViewManager.closeEditorPreview(true)
}
}
}
override fun dispose() {

View File

@@ -7,6 +7,7 @@ import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.DataProvider
import com.intellij.openapi.actionSystem.impl.ActionButton
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.editor.colors.EditorColorsListener
import com.intellij.openapi.editor.colors.EditorColorsScheme
import com.intellij.openapi.editor.ex.EditorEx
@@ -88,6 +89,7 @@ abstract class NonModalCommitPanel(
override val commitMessageUi: CommitMessageUi get() = commitMessage
override val modalityState: ModalityState = ModalityState.NON_MODAL
override fun getComponent(): JComponent = this
override fun getPreferredFocusableComponent(): JComponent = commitMessage.editorField

View File

@@ -0,0 +1,144 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.vcs.changes.ui
import com.intellij.idea.TestFor
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.util.Disposer
import com.intellij.testFramework.ApplicationRule
import com.intellij.testFramework.DisposableRule
import com.intellij.testFramework.RuleChain
import org.junit.Assert
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.CompletableFuture
import java.util.concurrent.TimeUnit
@TestFor(classes = [BackgroundRefresher::class])
class BackgroundRefresherTest {
val disposableRule = DisposableRule()
@JvmField
@Rule
val chain = RuleChain(ApplicationRule(), disposableRule)
@Test
fun requestCancelsPreviousRequests() {
val refresher = BackgroundRefresher<Int>("T", disposableRule.disposable)
val refresh1Started = CompletableFuture<Unit>()
val result1 = refresher.requestRefresh(0) {
try {
refresh1Started.complete(Unit)
val start = System.currentTimeMillis()
while (System.currentTimeMillis() - start < 10000) {
ProgressManager.checkCanceled()
Thread.sleep(10)
}
1
}
catch (t: ProcessCanceledException) {
666
}
}
refresh1Started.get()
val result2 = refresher.requestRefresh(0) {
2
}
Assert.assertEquals(2, result2.blockingGet(5000))
Assert.assertEquals(2, result1.blockingGet(5000))
}
@Test
fun disposeLeadsToProgressCancel() {
val disposable = Disposer.newDisposable()
val refresher = BackgroundRefresher<Unit>("T", disposable)
val cancelled = CompletableFuture<Unit>()
val refreshStarted = CompletableFuture<Unit>()
refresher.requestRefresh(0) {
try {
refreshStarted.complete(Unit)
val start = System.currentTimeMillis()
while (System.currentTimeMillis() - start < 3000) {
ProgressManager.checkCanceled()
Thread.sleep(10)
}
if (Thread.interrupted()) {
cancelled.completeExceptionally(IllegalStateException("must not be interrupted"))
}
cancelled.completeExceptionally(IllegalStateException("must be cancelled"))
}
catch (t: ProcessCanceledException) {
if (Thread.interrupted()) {
cancelled.completeExceptionally(IllegalStateException("must not be interrupted"))
}
cancelled.complete(Unit)
}
}
refreshStarted.get()
Disposer.dispose(disposable)
cancelled.get(10, TimeUnit.SECONDS)
}
@Test
fun disposeLeadsToImmediateCancelResult() {
val disposable = Disposer.newDisposable()
val refresher = BackgroundRefresher<Unit>("T", disposable)
val refreshStarted = CompletableFuture<Unit>()
val refreshEnded = CompletableFuture<Unit>()
val result = refresher.requestRefresh(0) {
refreshStarted.complete(Unit)
Thread.sleep(2000)
refreshEnded.complete(Unit)
}
refreshStarted.get()
Disposer.dispose(disposable)
Assert.assertNull(result.blockingGet(10000))
Assert.assertFalse(refreshEnded.isDone)
}
@Test
fun refreshWhenDisposed() {
val disposable = Disposer.newDisposable()
val refresher = BackgroundRefresher<Any>("T", disposable)
Disposer.dispose(disposable)
val refreshWhenDisposed = refresher.requestRefresh(0) {
1
}
Assert.assertNull(refreshWhenDisposed.blockingGet(10000))
}
@Test
fun refreshReturnsException() {
val refresher = BackgroundRefresher<Any>("T", disposableRule.disposable)
val result = refresher.requestRefresh(0) {
throw IllegalStateException("some text")
}
try {
result.blockingGet(10000)
Assert.fail()
} catch (e: IllegalStateException) {
Assert.assertEquals("some text", e.message)
}
}
@Test
fun refreshReturnsCanceledException() {
val refresher = BackgroundRefresher<Any>("T", disposableRule.disposable)
val result = refresher.requestRefresh(0) {
throw ProcessCanceledException()
}
try {
result.blockingGet(10000)
Assert.fail()
} catch (_: ProcessCanceledException) {
}
}
}

View File

@@ -18,6 +18,7 @@ import git4idea.index.ContentVersion
import git4idea.index.GitFileStatus
import git4idea.index.GitStageTracker
import git4idea.index.createChange
import org.jetbrains.concurrency.resolvedPromise
import kotlin.properties.Delegates.observable
private fun GitStageTracker.State.getStaged(): Set<GitFileStatus> =
@@ -77,7 +78,7 @@ class GitStageCommitPanel(project: Project) : NonModalCommitPanel(project) {
}
override fun activate(): Boolean = true
override fun refreshData() = Unit
override fun refreshData() = resolvedPromise<Unit>()
override fun getDisplayedChanges(): List<Change> = emptyList()
override fun getIncludedChanges(): List<Change> = state.stagedChanges