mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-02-04 23:39:07 +07:00
vcs: refresh changes view on background
https://jetbrains.team/im/review/2L6Ks1naPkI GitOrigin-RevId: 3edd6ae3ff7c2cec8038196b6504dce5e469a5cb
This commit is contained in:
committed by
intellij-monorepo-bot
parent
a1c53e78f9
commit
3fc55552b0
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user