diff options
author | Matthias Sohn | 2018-02-13 07:38:55 +0000 |
---|---|---|
committer | Gerrit Code Review @ Eclipse.org | 2018-02-13 07:38:55 +0000 |
commit | 29140317b95679445b78fb51cb2a6506c9e5e36a (patch) | |
tree | a8d9a0b8425a3648ed22ebe49c6eb17cbd10ed57 | |
parent | 7231e69ff2d1b809f5b754a0bb778765d0b23bbd (diff) | |
parent | ffa3b9913f7de7974e8aa6e5609ee1ab55c4c3fe (diff) | |
download | egit-29140317b95679445b78fb51cb2a6506c9e5e36a.tar.gz egit-29140317b95679445b78fb51cb2a6506c9e5e36a.tar.xz egit-29140317b95679445b78fb51cb2a6506c9e5e36a.zip |
Merge changes from topic 'push-branch-wizard'
* changes:
Include local branch name in proposals
Prevent NPE in RefContentProposal.appendObjectSummary
Prevent MissingObjectException being logged in ref content proposal
Asynchronous content proposals for upstream refs in PushBranchPage
Generalize UIUtils.addContentProposalToText a bit more
Make the PushWizardDialog a NonBlockingWizardDialog
Remove extra progress popup
Extract the asynchronous future from FetchGerritChangePage
19 files changed, 966 insertions, 429 deletions
diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java index e38f1a1543..98c8f078eb 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/UIUtils.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2015 SAP AG and others. + * Copyright (c) 2010, 2018 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -18,6 +18,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.function.Function; import java.util.regex.Pattern; import java.util.regex.PatternSyntaxException; @@ -33,7 +34,6 @@ import org.eclipse.egit.ui.internal.RepositorySaveableFilter; import org.eclipse.egit.ui.internal.UIIcons; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.components.RefContentProposal; -import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.bindings.Trigger; import org.eclipse.jface.bindings.TriggerSequence; @@ -47,6 +47,7 @@ import org.eclipse.jface.fieldassist.ControlDecoration; import org.eclipse.jface.fieldassist.FieldDecorationRegistry; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalProvider; +import org.eclipse.jface.fieldassist.IControlContentAdapter; import org.eclipse.jface.fieldassist.TextContentAdapter; import org.eclipse.jface.resource.FontRegistry; import org.eclipse.jface.resource.ImageDescriptor; @@ -61,6 +62,7 @@ import org.eclipse.jface.text.hyperlink.IHyperlinkDetector; import org.eclipse.jface.viewers.AbstractTreeViewer; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jgit.annotations.Nullable; import org.eclipse.jgit.lib.Ref; import org.eclipse.jgit.lib.Repository; import org.eclipse.osgi.util.NLS; @@ -214,6 +216,58 @@ public class UIUtils { } /** + * A {@link ContentProposalAdapter} with a <em>public</em> + * {@link #openProposalPopup()} method. + */ + public static class ExplicitContentProposalAdapter + extends ContentProposalAdapter { + + /** + * Construct a content proposal adapter that can assist the user with + * choosing content for the field. + * + * @param control + * the control for which the adapter is providing content + * assist. May not be {@code null}. + * @param controlContentAdapter + * the {@link IControlContentAdapter} used to obtain and + * update the control's contents as proposals are accepted. + * May not be {@code null}. + * @param proposalProvider + * the {@link IContentProposalProvider}> used to obtain + * content proposals for this control. + * @param keyStroke + * the keystroke that will invoke the content proposal popup. + * If this value is {@code null}, then proposals will be + * activated automatically when any of the auto activation + * characters are typed. + * @param autoActivationCharacters + * characters that trigger auto-activation of content + * proposal. If specified, these characters will trigger + * auto-activation of the proposal popup, regardless of + * whether an explicit invocation keyStroke was specified. If + * this parameter is {@code null}, then only a specified + * keyStroke will invoke content proposal. If this parameter + * is {@code null} and the keyStroke parameter is + * {@code null}, then all alphanumeric characters will + * auto-activate content proposal. + */ + public ExplicitContentProposalAdapter(Control control, + IControlContentAdapter controlContentAdapter, + IContentProposalProvider proposalProvider, + KeyStroke keyStroke, char[] autoActivationCharacters) { + super(control, controlContentAdapter, proposalProvider, keyStroke, + autoActivationCharacters); + } + + @Override + public void openProposalPopup() { + // Make this method accessible + super.openProposalPopup(); + } + } + + /** * @param id * see {@link FontRegistry#get(String)} * @return the font @@ -465,11 +519,17 @@ public class UIUtils { * the repository * @param refListProvider * provides the {@link Ref}s to show in the proposal + * @param upstream + * {@code true} if the candidates provided by the + * {@code refListProvider} are from an upstream repository + * @return the content proposal adapter set on the {@code textField} */ - public static final void addRefContentProposalToText(Text textField, + public static final ExplicitContentProposalAdapter addRefContentProposalToText( + Text textField, Repository repository, - IContentProposalCandidateProvider<Ref> refListProvider) { - UIUtils.<Ref> addContentProposalToText(textField, + IContentProposalCandidateProvider<Ref> refListProvider, + boolean upstream) { + return UIUtils.<Ref> addContentProposalToText(textField, refListProvider, (pattern, ref) -> { String shortenedName = Repository .shortenRefName(ref.getName()); @@ -478,8 +538,9 @@ public class UIUtils { && !pattern.matcher(shortenedName).matches()) { return null; } - return new RefContentProposal(repository, ref); - }, UIText.UIUtils_StartTypingForRemoteRefMessage, + return new RefContentProposal(repository, ref, upstream); + }, null, + UIText.UIUtils_StartTypingForRemoteRefMessage, UIText.UIUtils_PressShortcutForRemoteRefMessage); } @@ -497,20 +558,32 @@ public class UIUtils { * @param factory * {@link IContentProposalFactory} to use to create proposals * from candidates + * @param patternProvider + * to convert the current text of the field into a pattern + * suitable for filtering the candidates. If {@code null}, a + * default pattern is constructed using + * {@link #createProposalPattern(String)}. * @param startTypingMessage * hover message if no content assist key binding is active * @param shortcutMessage * hover message if a content assist key binding is active, * should have a "{0}" placeholder that will be filled by the * appropriate keystroke + * @return the content proposal adapter set on the {@code textField} */ - public static final <T> void addContentProposalToText(Text textField, + public static final <T> ExplicitContentProposalAdapter addContentProposalToText( + Text textField, IContentProposalCandidateProvider<T> candidateProvider, - IContentProposalFactory<T> factory, String startTypingMessage, + IContentProposalFactory<T> factory, + Function<String, Pattern> patternProvider, + String startTypingMessage, String shortcutMessage) { KeyStroke stroke = UIUtils .getKeystrokeOfBestActiveBindingFor(IWorkbenchCommandConstants.EDIT_CONTENT_ASSIST); if (stroke == null) { + if (startTypingMessage == null) { + return null; + } addBulbDecorator(textField, startTypingMessage); } else { addBulbDecorator(textField, @@ -521,16 +594,19 @@ public class UIUtils { public IContentProposal[] getProposals(String contents, int position) { List<IContentProposal> resultList = new ArrayList<>(); - Pattern pattern = createProposalPattern(contents); Collection<? extends T> candidates = candidateProvider .getCandidates(); - if (candidates != null) { - for (final T candidate : candidates) { - IContentProposal proposal = factory.getProposal(pattern, - candidate); - if (proposal != null) { - resultList.add(proposal); - } + if (candidates == null) { + return null; + } + Pattern pattern = patternProvider != null + ? patternProvider.apply(contents) + : createProposalPattern(contents); + for (final T candidate : candidates) { + IContentProposal proposal = factory.getProposal(pattern, + candidate); + if (proposal != null) { + resultList.add(proposal); } } return resultList.toArray(new IContentProposal[resultList @@ -538,12 +614,13 @@ public class UIUtils { } }; - ContentProposalAdapter adapter = new ContentProposalAdapter(textField, - new TextContentAdapter(), cp, stroke, + ExplicitContentProposalAdapter adapter = new ExplicitContentProposalAdapter( + textField, new TextContentAdapter(), cp, stroke, UIUtils.VALUE_HELP_ACTIVATIONCHARS); // set the acceptance style to always replace the complete content adapter.setProposalAcceptanceStyle( ContentProposalAdapter.PROPOSAL_REPLACE); + return adapter; } /** diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java index 902a667ef2..782f948579 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/UIText.java @@ -143,6 +143,12 @@ public class UIText extends NLS { public static String AddToIndexCommand_addingFilesFailed; /** */ + public static String AsynchronousRefProposalProvider_FetchingRemoteRefsMessage; + + /** */ + public static String AsynchronousRefProposalProvider_ShowingProposalsJobName; + + /** */ public static String RemoveFromIndexAction_removingFiles; /** */ @@ -1556,6 +1562,9 @@ public class UIText extends NLS { public static String RefContentProposal_errorReadingObject; /** */ + public static String RefContentProposal_newRemoteObject; + + /** */ public static String RefContentProposal_tag; /** */ @@ -1568,6 +1577,9 @@ public class UIText extends NLS { public static String RefContentProposal_unknownObject; /** */ + public static String RefContentProposal_unknownRemoteObject; + + /** */ public static String ReflogView_DateColumnHeader; /** */ @@ -2852,9 +2864,6 @@ public class UIText extends NLS { public static String FetchGerritChangePage_CreatingTagTaskName; /** */ - public static String FetchGerritChangePage_FetchingRemoteRefsMessage; - - /** */ public static String FetchGerritChangePage_FetchingTaskName; /** */ @@ -2882,9 +2891,6 @@ public class UIText extends NLS { public static String FetchGerritChangePage_PageTitle; /** */ - public static String FetchGerritChangePage_ShowingProposalsJobName; - - /** */ public static String FetchGerritChangePage_SuggestedRefNamePattern; /** */ diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousListOperation.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousListOperation.java new file mode 100644 index 0000000000..d6340840bb --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousListOperation.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.ui.internal.components; + +import java.lang.reflect.InvocationTargetException; +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.Collection; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.egit.core.op.ListRemoteOperation; +import org.eclipse.egit.ui.Activator; +import org.eclipse.egit.ui.UIPreferences; +import org.eclipse.egit.ui.internal.UIText; +import org.eclipse.egit.ui.internal.dialogs.CancelableFuture; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.URIish; + +/** + * A {@link ListRemoteOperation} that is run asynchronously as a + * {@link CancelableFuture}. + * + * @param <T> + * result type + */ +public abstract class AsynchronousListOperation<T> + extends CancelableFuture<Collection<T>> { + + private final Repository repository; + + private final String uriText; + + private ListRemoteOperation listOp; + + /** + * Creates a new {@link AsynchronousListOperation}. + * + * @param repository + * local repository for which to run the operation + * @param uriText + * upstream URI + */ + public AsynchronousListOperation(Repository repository, String uriText) { + this.repository = repository; + this.uriText = uriText; + } + + @Override + protected String getJobTitle() { + return MessageFormat.format( + UIText.AsynchronousRefProposalProvider_FetchingRemoteRefsMessage, + uriText); + } + + @Override + protected void prepareRun() throws InvocationTargetException { + try { + listOp = new ListRemoteOperation(repository, new URIish(uriText), + Activator.getDefault().getPreferenceStore() + .getInt(UIPreferences.REMOTE_CONNECTION_TIMEOUT)); + } catch (URISyntaxException e) { + throw new InvocationTargetException(e); + } + } + + @Override + protected void run(IProgressMonitor monitor) + throws InterruptedException, InvocationTargetException { + listOp.run(monitor); + set(convert(listOp.getRemoteRefs())); + } + + /** + * Transforms the {@link Ref}s obtained into the final objects. May just + * return the input if the generic type T is Ref, or may post-process the + * results as appropriate. + * + * @param refs + * obtained from the upstream repository + * @return final result + */ + protected abstract Collection<T> convert(Collection<Ref> refs); + + @Override + protected void done() { + listOp = null; + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousRefProposalProvider.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousRefProposalProvider.java new file mode 100644 index 0000000000..e52de02e9e --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/AsynchronousRefProposalProvider.java @@ -0,0 +1,176 @@ +/******************************************************************************* + * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.ui.internal.components; + +import java.lang.reflect.InvocationTargetException; +import java.text.MessageFormat; +import java.util.Collection; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.egit.ui.UIUtils.ExplicitContentProposalAdapter; +import org.eclipse.egit.ui.UIUtils.IContentProposalCandidateProvider; +import org.eclipse.egit.ui.internal.UIText; +import org.eclipse.egit.ui.internal.dialogs.CancelableFuture; +import org.eclipse.egit.ui.internal.dialogs.NonBlockingWizardDialog; +import org.eclipse.jface.operation.IRunnableWithProgress; +import org.eclipse.jface.wizard.IWizardContainer; +import org.eclipse.jgit.lib.Ref; +import org.eclipse.swt.SWTException; +import org.eclipse.swt.widgets.Text; +import org.eclipse.ui.progress.WorkbenchJob; + +/** + * An {@link IContentProposalCandidateProvider} that is intended to be used with + * an asynchronous {@link CancelableFuture} to get proposals in the background. + * <p> + * Note that is must be used with an {@link ExplicitContentProposalAdapter}, + * which must be made known to it via + * {@link #setContentProposalAdapter(ExplicitContentProposalAdapter) + * setContentProposalAdapter()}. + */ +public class AsynchronousRefProposalProvider + implements IContentProposalCandidateProvider<Ref> { + + private final IWizardContainer container; + + private final Text textField; + + private final Supplier<String> uriProvider; + + private final Function<String, CancelableFuture<Collection<Ref>>> listProvider; + + private ExplicitContentProposalAdapter contentProposer; + + /** + * Creates a new {@link AsynchronousRefProposalProvider}. Because this is + * supposed to run truly asynchronously, typically in a + * {@link NonBlockingWizardDialog}, it needs to know the text field it + * belongs to and also the URI it was run for. It opens the proposals only + * if both are in a state where it still makes sense to show the proposals. + * + * @param container + * this candidate provider will be run for + * @param uriProvider + * a function returning the upstream URI to get proposals from + * @param textField + * this candidate provider belongs to + * @param listProvider + * a function that provides the CancelableFuture used to obtain + * the upstream refs + */ + public AsynchronousRefProposalProvider( + IWizardContainer container, Text textField, + Supplier<String> uriProvider, + Function<String, CancelableFuture<Collection<Ref>>> listProvider) { + this.container = container; + this.textField = textField; + this.uriProvider = uriProvider; + this.listProvider = listProvider; + } + + /** + * Makes the content proposal adapter known to this candidate provider. This + * is needed to be able to open the proposal popup asynchronously. If set to + * {@code null}, proposals will not be opened. + * + * @param adapter + * to set + */ + public void setContentProposalAdapter( + ExplicitContentProposalAdapter adapter) { + contentProposer = adapter; + } + + @Override + public Collection<? extends Ref> getCandidates() { + String uri = uriProvider.get(); + if (uri == null) { + return null; + } + CancelableFuture<Collection<Ref>> list = listProvider.apply(uri); + try { + if (!list.isFinished()) { + IRunnableWithProgress operation = monitor -> { + monitor.beginTask(MessageFormat.format( + UIText.AsynchronousRefProposalProvider_FetchingRemoteRefsMessage, + uri), IProgressMonitor.UNKNOWN); + Collection<Ref> result = list.get(); + if (monitor.isCanceled()) { + return; + } + // If we get here, the ChangeList future is done. + if (result == null || result.isEmpty()) { + // Don't bother if we didn't get any results + return; + } + // If we do have results now, open the proposals. + Job showProposals = new WorkbenchJob( + UIText.AsynchronousRefProposalProvider_ShowingProposalsJobName) { + + @Override + public boolean shouldRun() { + return super.shouldRun() && contentProposer != null; + } + + @Override + public IStatus runInUIThread( + IProgressMonitor uiMonitor) { + // But only if we're not disposed, the focus is + // still (or again) in the Change field, and the uri + // is still the same + try { + if (container instanceof NonBlockingWizardDialog) { + // Otherwise the dialog was blocked anyway, + // and focus will be restored + if (textField.isDisposed() + || !textField.isVisible() + || textField != textField + .getDisplay() + .getFocusControl()) { + return Status.CANCEL_STATUS; + } + String uriNow = uriProvider.get(); + if (!uriNow.equals(uri)) { + return Status.CANCEL_STATUS; + } + } + contentProposer.openProposalPopup(); + } catch (SWTException e) { + // Disposed already + return Status.CANCEL_STATUS; + } finally { + uiMonitor.done(); + } + return Status.OK_STATUS; + } + + }; + showProposals.schedule(); + }; + if (container instanceof NonBlockingWizardDialog) { + NonBlockingWizardDialog dialog = (NonBlockingWizardDialog) container; + dialog.run(operation, () -> list + .cancel(CancelableFuture.CancelMode.ABANDON)); + } else { + container.run(true, true, operation); + } + return null; + } + return list.get(); + } catch (InterruptedException | InvocationTargetException e) { + return null; + } + } + +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefContentProposal.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefContentProposal.java index d332d61006..ad4a3d3139 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefContentProposal.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefContentProposal.java @@ -14,6 +14,7 @@ import java.io.IOException; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.jface.fieldassist.IContentProposal; +import org.eclipse.jgit.errors.MissingObjectException; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectLoader; @@ -57,12 +58,14 @@ public class RefContentProposal implements IContentProposal { private static void appendObjectSummary(final StringBuilder sb, final String type, final PersonIdent author, final String message) { sb.append(type); - sb.append(" "); //$NON-NLS-1$ - sb.append(UIText.RefContentProposal_by); - sb.append(" "); //$NON-NLS-1$ - sb.append(author.getName()); - sb.append("\n"); //$NON-NLS-1$ - sb.append(author.getWhen()); + if (author != null) { + sb.append(" "); //$NON-NLS-1$ + sb.append(UIText.RefContentProposal_by); + sb.append(" "); //$NON-NLS-1$ + sb.append(author.getName()); + sb.append("\n"); //$NON-NLS-1$ + sb.append(author.getWhen()); + } sb.append("\n\n"); //$NON-NLS-1$ final int newLine = message.indexOf('\n'); final int last = (newLine != -1 ? newLine : message.length()); @@ -76,6 +79,12 @@ public class RefContentProposal implements IContentProposal { private final ObjectId objectId; /** + * Whether the ref is an upstream ref. For upstream refs, it's OK to have a + * missing object; it just means we haven't fetched yet. + */ + private final boolean upstream; + + /** * Create content proposal for specified ref. * * @param repo @@ -84,9 +93,11 @@ public class RefContentProposal implements IContentProposal { * @param ref * ref being a content proposal. May have null or locally * non-existent object id. + * @param upstream + * {@code true} if the ref comes from an upstream repository */ - public RefContentProposal(final Repository repo, final Ref ref) { - this(repo, ref.getName(), ref.getObjectId()); + public RefContentProposal(Repository repo, Ref ref, boolean upstream) { + this(repo, ref.getName(), ref.getObjectId(), upstream); } /** @@ -100,12 +111,15 @@ public class RefContentProposal implements IContentProposal { * @param objectId * object being pointed by this ref name. May be null or locally * non-existent object. + * @param upstream + * {@code true} if the ref comes from an upstream repository */ - public RefContentProposal(final Repository repo, final String refName, - final ObjectId objectId) { + public RefContentProposal(Repository repo, String refName, + ObjectId objectId, boolean upstream) { this.db = repo; this.refName = refName; this.objectId = objectId; + this.upstream = upstream; } @Override @@ -120,10 +134,23 @@ public class RefContentProposal implements IContentProposal { @Override public String getDescription() { - if (objectId == null) + if (objectId == null) { return null; + } else if (upstream && objectId.equals(ObjectId.zeroId())) { + return refName + '\n' + UIText.RefContentProposal_newRemoteObject; + } try (ObjectReader reader = db.newObjectReader()) { - final ObjectLoader loader = reader.open(objectId); + ObjectLoader loader = null; + try { + loader = reader.open(objectId); + } catch (MissingObjectException e) { + if (upstream) { + return refName + '\n' + objectId.abbreviate(7).name() + + " - " //$NON-NLS-1$ + + UIText.RefContentProposal_unknownRemoteObject; + } + throw e; + } final StringBuilder sb = new StringBuilder(); sb.append(refName); sb.append('\n'); @@ -157,7 +184,8 @@ public class RefContentProposal implements IContentProposal { return sb.toString(); } catch (IOException e) { Activator.logError(NLS.bind( - UIText.RefContentProposal_errorReadingObject, objectId), e); + UIText.RefContentProposal_errorReadingObject, objectId, + refName), e); return null; } } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefSpecPanel.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefSpecPanel.java index 6375beef66..e29d69a30d 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefSpecPanel.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/components/RefSpecPanel.java @@ -1736,13 +1736,15 @@ public class RefSpecPanel { } }); set.addAll(refs); - if (HEAD != null) + if (HEAD != null) { set.add(HEAD); + } final List<RefContentProposal> result = new ArrayList<>( set.size()); - for (final Ref r : set) - result.add(new RefContentProposal(localDb, r)); + for (final Ref r : set) { + result.add(new RefContentProposal(localDb, r, HEAD == null)); + } return result; } @@ -1817,27 +1819,33 @@ public class RefSpecPanel { // contents contains wildcards // check if contents can be safely added as wildcard spec - if (isValidRefExpression(contents)) - result.add(new RefContentProposal(localDb, contents, null)); + if (isValidRefExpression(contents)) { + result.add(new RefContentProposal(localDb, contents, null, + true)); + } // let's expand wildcards final String regex = ".*" //$NON-NLS-1$ + contents.replace("*", ".*").replace("?", ".?") + ".*"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ //$NON-NLS-5$ final Pattern p = Pattern.compile(regex); - for (final RefContentProposal prop : proposals) - if (p.matcher(prop.getContent()).matches()) + for (final RefContentProposal prop : proposals) { + if (p.matcher(prop.getContent()).matches()) { result.add(prop); + } + } } else { - for (final RefContentProposal prop : proposals) - if (prop.getContent().contains(contents)) + for (final RefContentProposal prop : proposals) { + if (prop.getContent().contains(contents)) { result.add(prop); + } + } if (tryResolvingLocally && result.isEmpty()) { final ObjectId id = tryResolveLocalRef(contents); - if (id != null) - result - .add(new RefContentProposal(localDb, contents, - id)); + if (id != null) { + result.add(new RefContentProposal(localDb, contents, id, + false)); + } } } return result.toArray(new IContentProposal[0]); diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CancelableFuture.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CancelableFuture.java new file mode 100644 index 0000000000..d7786d774d --- /dev/null +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/CancelableFuture.java @@ -0,0 +1,326 @@ +/******************************************************************************* + * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + *******************************************************************************/ +package org.eclipse.egit.ui.internal.dialogs; + +import java.lang.reflect.InvocationTargetException; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.egit.ui.Activator; + +/** + * A {@code CancelableFuture} is a "Future" using Eclipse jobs to asynchronously + * perform long tasks. The {@link #get() get()} method blocks until the result + * is available or the future is canceled. The first call to {@link #get() + * get()} starts a background job running the {@link #run(IProgressMonitor) + * run()} operation, which is supposed to call {@link #set(Object) set()} to set + * its result. Pre-fetching is possible by calling {@link #start()} directly. + * <p> + * The main differences to a {@link java.util.concurrent.FutureTask} are: + * </p> + * <ul> + * <li>This class is not that much optimized.</li> + * <li>Cancellation behaves differently. A {@code FutureTask} that is canceled + * before completed ignores any result that might still arrive, and always says + * it's been canceled. A {@code CancelableFuture} accepts a result even after + * having been canceled, so a subsequent {@link #get()} may get that result. In + * both, cancellation wakes up all waiting calls to {@link #get()}.</li> + * <li>A {@code CancelableFuture} automatically runs the task in a background + * job, whereas a {@code FutureTask} would need an extra + * {@link java.util.concurrent.ExecutorService ExecutorService} to + * asynchronously run its task.</li> + * <li>This class uses Eclipse {@link Job}s instead of Threads directly.</li> + * </ul> + * + * @param <V> + * the type of the result value + */ +public abstract class CancelableFuture<V> { + + /** + * Determines how to cancel a not yet completed future. Irrespective of the + * mechanism, the job may actually terminate normally, and subsequent calls + * to {@link CancelableFuture#get() get()} may return a result. + */ + public static enum CancelMode { + /** + * Tries to cancel the job, which may decide to ignore the request. + * Callers to {@link CancelableFuture#get() get()} will remain blocked + * until the job terminates. + */ + CANCEL, + /** + * Tries to cancel the job, which may decide to ignore the request. + * Outstanding {@link CancelableFuture#get() get()} calls will be woken + * up and may throw {@link InterruptedException} or return a result if + * the job terminated in the meantime. + */ + ABANDON, + /** + * Tries to cancel the job, and if that doesn't succeed immediately, + * interrupts the job's thread. Outstanding calls to + * {@link CancelableFuture#get() get()} will be woken up and may throw + * {@link InterruptedException} or return a result if the job terminated + * in the meantime. + */ + INTERRUPT + } + + private static enum State { + PRISTINE, SCHEDULED, CANCELING, INTERRUPT, CANCELED, DONE + } + + private State state = State.PRISTINE; + + private V result; + + private InterruptibleJob job; + + /** + * Tries to cancel the future. See {@link CancelMode} for semantics. + * + * @param cancellation + * {@link CancelMode} defining how to cancel. If {@code null} + * defaults to {@link CancelMode#CANCEL}. + * + * @return {@code true} if the future was canceled (its job is not running + * anymore), {@code false} otherwise. + */ + public synchronized boolean cancel(CancelMode cancellation) { + CancelMode mode = cancellation == null ? CancelMode.CANCEL + : cancellation; + switch (state) { + case PRISTINE: + finish(false); + return true; + case SCHEDULED: + state = State.CANCELING; + boolean canceled = job.cancel(); + if (canceled) { + state = State.CANCELED; + } else if (mode == CancelMode.INTERRUPT) { + interrupt(); + } else if (mode == CancelMode.ABANDON) { + notifyAll(); + } + return canceled; + case CANCELING: + // cancel(CANCEL|ABANDON) was called before. + if (mode == CancelMode.INTERRUPT) { + interrupt(); + } else if (mode == CancelMode.ABANDON) { + notifyAll(); + } + return false; + case INTERRUPT: + if (mode != CancelMode.CANCEL) { + notifyAll(); + } + return false; + case CANCELED: + return true; + default: + return false; + } + } + + /** + * Tells whether or not the future's background job is still running. + * + * @return {@code true} if the future's background job isn't running anymore + * (was canceled or terminated normally) + */ + public synchronized boolean isFinished() { + return state == State.CANCELED || state == State.DONE; + } + + /** + * Tells whether the future completed normally. + * + * @return {@code true} if the future completed normally, {@code false} + * otherwise + */ + public synchronized boolean isDone() { + return state == State.DONE; + } + + /** + * Retrieves the result. If the result is not yet available, the method + * blocks until it is or {@link #cancel(CancelMode)} is called with + * {@link CancelMode#ABANDON} or {@link CancelMode#INTERRUPT}. + * + * @return the result, which may be {@code null} if the future was canceled + * @throws InterruptedException + * if waiting was interrupted + * @throws InvocationTargetException + * if the future's job cannot be created + */ + public synchronized V get() + throws InterruptedException, InvocationTargetException { + switch (state) { + case DONE: + case CANCELED: + return result; + case PRISTINE: + start(); + return get(); + default: + wait(); + if (state == State.CANCELING || state == State.INTERRUPT) { + // canceled with ABANDON or INTERRUPT + throw new InterruptedException(); + } + return get(); + } + } + + private synchronized void finish(boolean done) { + state = done ? State.DONE : State.CANCELED; + job = null; + try { + done(); + } finally { + // We're done, wake up all outstanding get() calls. + notifyAll(); + } + } + + private synchronized void interrupt() { + state = State.INTERRUPT; + job.interrupt(); + notifyAll(); // Abandon outstanding get() calls + } + + /** + * Sets the future's result. + * + * @param value + * to use as the result + */ + protected void set(V value) { + result = value; + } + + /** + * Called by {@link #start()} before the background job is scheduled. + * Subclasses may override to initialize data before the job starts. + * + * @throws InvocationTargetException + * on errors + */ + protected void prepareRun() throws InvocationTargetException { + // Default does nothing + } + + /** + * Obtain a job title for the background job executing the future's task. + * + * @return the title + */ + protected String getJobTitle() { + return ""; //$NON-NLS-1$ + } + + /** + * Performs the future's task. + * + * @param monitor + * for progress reporting and cancellation + * @throws InterruptedException + * if canceled + * @throws InvocationTargetException + * on other errors + */ + protected abstract void run(IProgressMonitor monitor) + throws InterruptedException, InvocationTargetException; + + /** + * Called when the future is done, i.e., either completed normally, was + * canceled, or failed to start. Subclasses may override to do clean-up. + */ + protected void done() { + // Default does nothing + } + + /** + * On the first call, starts a background job to fetch the result. + * Subsequent calls do nothing and return immediately. Before creating and + * scheduling the job, {@link #prepareRun()} is called. + * + * @throws InvocationTargetException + * propagated from {@link #prepareRun()} + */ + public synchronized void start() throws InvocationTargetException { + if (job != null || state != State.PRISTINE) { + return; + } + try { + prepareRun(); + } catch (InvocationTargetException e) { + finish(false); + throw e; + } + job = new InterruptibleJob(getJobTitle()) { + + @Override + protected IStatus run(IProgressMonitor monitor) { + try { + CancelableFuture.this.run(monitor); + return Status.OK_STATUS; + } catch (InterruptedException e) { + return Status.CANCEL_STATUS; + } catch (InvocationTargetException e) { + synchronized (CancelableFuture.this) { + if (state == State.CANCELING + || state == State.INTERRUPT) { + return Status.CANCEL_STATUS; + } + } + return Activator.createErrorStatus(e.getLocalizedMessage(), + e); + } catch (RuntimeException e) { + return Activator.createErrorStatus(e.getLocalizedMessage(), + e); + } + } + + }; + job.addJobChangeListener(new JobChangeAdapter() { + + @Override + public void done(IJobChangeEvent event) { + IStatus status = event.getResult(); + finish(status != null && status.isOK()); + } + + }); + job.setUser(false); + job.setSystem(true); + state = State.SCHEDULED; + job.schedule(); + } + + private static abstract class InterruptibleJob extends Job { + + public InterruptibleJob(String name) { + super(name); + } + + public void interrupt() { + Thread thread = getThread(); + if (thread != null) { + thread.interrupt(); + } + } + } +} diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/NonBlockingWizardDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/NonBlockingWizardDialog.java index 033ccc0a11..d8605f5c04 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/NonBlockingWizardDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/dialogs/NonBlockingWizardDialog.java @@ -85,26 +85,17 @@ public class NonBlockingWizardDialog extends MinimumSizeWizardDialog { } /** - * If {@code fork} is {@code true}, this implementation does <em>not</em> - * block but schedules a true background job. Such background jobs are - * queued and will execute one after another. They are canceled when the - * current wizard page changes, or when the dialog closes. - * </p> - */ - @Override - public void run(boolean fork, boolean cancelable, - IRunnableWithProgress runnable) - throws InvocationTargetException, InterruptedException { - if (!fork) { - super.run(fork, cancelable, runnable); - } - run(runnable, null); - } - - /** * Runs the given {@code runnable} in a background job, reporting progress * through the dialog's progress monitor, if any, and invoking * {@code onCancel} if the job is canceled. + * <p> + * The dialog is <em>not</em> made inactive while the job runs. + * </p> + * <p> + * Such background jobs are queued and will execute one after another. They + * are canceled when the current wizard page changes, or when the dialog + * closes. + * </p> * * @param runnable * to run diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchDestinationPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchDestinationPage.java index a22e9de2b8..58c849b76b 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchDestinationPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchDestinationPage.java @@ -94,7 +94,7 @@ public class FetchDestinationPage extends WizardPage { GridDataFactory.fillDefaults().grab(true, false).applyTo( destinationText); UIUtils.addRefContentProposalToText(sourceText, repository, - () -> getRemoteRefs()); + () -> getRemoteRefs(), true); force = new Button(main, SWT.CHECK); force.setText(UIText.FetchDestinationPage_ForceCheckbox); diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchGerritChangePage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchGerritChangePage.java index 0df477cedc..cad680e295 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchGerritChangePage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchGerritChangePage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2017 SAP AG and others. + * Copyright (c) 2010, 2018 SAP AG and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -26,7 +26,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.regex.Matcher; @@ -41,37 +40,32 @@ import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; -import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.egit.core.internal.gerrit.GerritUtil; import org.eclipse.egit.core.op.CreateLocalBranchOperation; -import org.eclipse.egit.core.op.ListRemoteOperation; import org.eclipse.egit.core.op.TagOperation; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.JobFamilies; import org.eclipse.egit.ui.UIPreferences; import org.eclipse.egit.ui.UIUtils; +import org.eclipse.egit.ui.UIUtils.ExplicitContentProposalAdapter; import org.eclipse.egit.ui.internal.ActionUtils; import org.eclipse.egit.ui.internal.UIText; import org.eclipse.egit.ui.internal.ValidationUtils; import org.eclipse.egit.ui.internal.branch.BranchOperationUI; +import org.eclipse.egit.ui.internal.components.AsynchronousListOperation; import org.eclipse.egit.ui.internal.components.BranchNameNormalizer; import org.eclipse.egit.ui.internal.dialogs.AbstractBranchSelectionDialog; import org.eclipse.egit.ui.internal.dialogs.BranchEditDialog; import org.eclipse.egit.ui.internal.dialogs.NonBlockingWizardDialog; import org.eclipse.egit.ui.internal.gerrit.GerritDialogSettings; -import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.dialogs.Dialog; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.IInputValidator; import org.eclipse.jface.dialogs.IPageChangeProvider; import org.eclipse.jface.dialogs.IPageChangedListener; import org.eclipse.jface.dialogs.PageChangedEvent; -import org.eclipse.jface.fieldassist.ContentProposalAdapter; import org.eclipse.jface.fieldassist.IContentProposal; -import org.eclipse.jface.fieldassist.IContentProposalProvider; -import org.eclipse.jface.fieldassist.TextContentAdapter; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.operation.IRunnableWithProgress; import org.eclipse.jface.resource.JFaceResources; @@ -110,7 +104,6 @@ import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Text; -import org.eclipse.ui.IWorkbenchCommandConstants; import org.eclipse.ui.actions.ActionFactory; import org.eclipse.ui.progress.WorkbenchJob; @@ -542,7 +535,7 @@ public class FetchGerritChangePage extends WizardPage { private void preFetch(ChangeList list) { try { - list.fetch(); + list.start(); } catch (InvocationTargetException e) { Activator.handleError(e.getLocalizedMessage(), e.getCause(), true); } @@ -724,22 +717,26 @@ public class FetchGerritChangePage extends WizardPage { } ChangeList list = changeRefs.get(uriCombo.getText()); if (list != null && list.isDone()) { - if (change.getPatchSetNumber() != null) { - if (!list.getResult().contains(change)) { - setErrorMessage( - UIText.FetchGerritChangePage_UnknownChangeRefMessage); - return; - } - } else { - Change fromGerrit = findHighestPatchSet( - list.getResult(), - change.getChangeNumber().intValue()); - if (fromGerrit == null) { - setErrorMessage(NLS.bind( - UIText.FetchGerritChangePage_NoSuchChangeMessage, - change.getChangeNumber())); - return; + try { + if (change.getPatchSetNumber() != null) { + if (!list.get().contains(change)) { + setErrorMessage( + UIText.FetchGerritChangePage_UnknownChangeRefMessage); + return; + } + } else { + Change fromGerrit = findHighestPatchSet(list.get(), + change.getChangeNumber().intValue()); + if (fromGerrit == null) { + setErrorMessage(NLS.bind( + UIText.FetchGerritChangePage_NoSuchChangeMessage, + change.getChangeNumber())); + return; + } } + } catch (InterruptedException + | InvocationTargetException e) { + // Ignore: since we're done, this should never occur } } } else { @@ -768,7 +765,7 @@ public class FetchGerritChangePage extends WizardPage { IWizardContainer container = getContainer(); IRunnableWithProgress operation = monitor -> { monitor.beginTask(MessageFormat.format( - UIText.FetchGerritChangePage_FetchingRemoteRefsMessage, + UIText.AsynchronousRefProposalProvider_FetchingRemoteRefsMessage, uriText), IProgressMonitor.UNKNOWN); Collection<Change> result = list.get(); if (monitor.isCanceled()) { @@ -781,7 +778,7 @@ public class FetchGerritChangePage extends WizardPage { } // If we do have results now, open the proposals. Job showProposals = new WorkbenchJob( - UIText.FetchGerritChangePage_ShowingProposalsJobName) { + UIText.AsynchronousRefProposalProvider_ShowingProposalsJobName) { @Override public boolean shouldRun() { @@ -1007,7 +1004,7 @@ public class FetchGerritChangePage extends WizardPage { throws OperationCanceledException { if (changeList != null) { monitor.subTask(NLS.bind( - UIText.FetchGerritChangePage_FetchingRemoteRefsMessage, + UIText.AsynchronousRefProposalProvider_FetchingRemoteRefsMessage, uri)); Collection<Change> changes; try { @@ -1132,72 +1129,29 @@ public class FetchGerritChangePage extends WizardPage { private ExplicitContentProposalAdapter addRefContentProposalToText( final Text textField) { - KeyStroke stroke = UIUtils - .getKeystrokeOfBestActiveBindingFor(IWorkbenchCommandConstants.EDIT_CONTENT_ASSIST); - if (stroke != null) { - UIUtils.addBulbDecorator(textField, NLS.bind( - UIText.FetchGerritChangePage_ContentAssistTooltip, - stroke.format())); - } - IContentProposalProvider cp = new IContentProposalProvider() { - - @Override - public IContentProposal[] getProposals(String contents, int position) { - Collection<Change> proposals; - try { - proposals = getRefsForContentAssist(contents); - } catch (InvocationTargetException e) { - Activator.handleError(e.getMessage(), e, true); - return null; - } catch (InterruptedException e) { - return null; - } - - if (proposals == null) { - return null; - } - List<IContentProposal> resultList = new ArrayList<>(); - String input = contents; - Matcher matcher = GERRIT_CHANGE_REF_PATTERN.matcher(contents); - if (matcher.find()) { - input = matcher.group(2); - } - Pattern pattern = UIUtils.createProposalPattern(input); - for (final Change ref : proposals) { - if (pattern != null && !pattern - .matcher(ref.getChangeNumber().toString()) - .matches()) { - continue; - } - resultList.add(new ChangeContentProposal(ref)); - } - return resultList - .toArray(new IContentProposal[resultList.size()]); + return UIUtils.addContentProposalToText(textField, () -> { + try { + return getRefsForContentAssist(textField.getText()); + } catch (InvocationTargetException e) { + Activator.handleError(e.getMessage(), e, true); + return null; + } catch (InterruptedException e) { + return null; } - }; - - ExplicitContentProposalAdapter adapter = new ExplicitContentProposalAdapter( - textField, cp, stroke); - // set the acceptance style to always replace the complete content - adapter.setProposalAcceptanceStyle(ContentProposalAdapter.PROPOSAL_REPLACE); - return adapter; - } - - private static class ExplicitContentProposalAdapter - extends ContentProposalAdapter { - - public ExplicitContentProposalAdapter(Control control, - IContentProposalProvider proposalProvider, - KeyStroke keyStroke) { - super(control, new TextContentAdapter(), proposalProvider, - keyStroke, null); - } - - @Override - public void openProposalPopup() { - // Make this method accessible - super.openProposalPopup(); - } + }, (pattern, ref) -> { + if (pattern == null || pattern + .matcher(ref.getChangeNumber().toString()).matches()) { + return new ChangeContentProposal(ref); + } + return null; + }, s -> { + String input = s; + Matcher matcher = GERRIT_CHANGE_REF_PATTERN.matcher(input); + if (matcher.find()) { + input = matcher.group(2); + } + return UIUtils.createProposalPattern(input); + }, null, UIText.FetchGerritChangePage_ContentAssistTooltip); } final static class Change implements Comparable<Change> { @@ -1340,254 +1294,26 @@ public class FetchGerritChangePage extends WizardPage { } /** - * A {@code ChangeList} is a "Future", loading the list of change refs - * asynchronously from the remote repository. The {@link ChangeList#get() - * get()} method blocks until the result is available or the future is - * canceled. Pre-fetching is possible by calling {@link ChangeList#fetch()} - * directly. + * A {@code ChangeList} loads the list of change refs asynchronously from + * the remote repository. */ - private static class ChangeList { - - /** - * Determines how to cancel a not-yet-completed future. Irrespective of - * the mechanism, the job may actually terminate normally, and - * subsequent calls to get() may return a result. - */ - public static enum CancelMode { - /** - * Tries to cancel the job, which may decide to ignore the request. - * Callers to get() will remain blocked until the job terminates. - */ - CANCEL, - /** - * Tries to cancel the job, which may decide to ignore the request. - * Outstanding get() calls will be woken up and may throw - * InterruptedException or return a result if the job terminated in - * the meantime. - */ - ABANDON, - /** - * Tries to cancel the job, and if that doesn't succeed immediately, - * interrupts the job's thread. Outstanding calls to get() will be - * woken up and may throw InterruptedException or return a result if - * the job terminated in the meantime. - */ - INTERRUPT - } - - private static enum State { - PRISTINE, SCHEDULED, CANCELING, INTERRUPT, CANCELED, DONE - } - - private final Repository repository; - - private final String uriText; - - private State state = State.PRISTINE; - - private Set<Change> result; - - private InterruptibleJob job; + private static class ChangeList extends AsynchronousListOperation<Change> { public ChangeList(Repository repository, String uriText) { - this.repository = repository; - this.uriText = uriText; - } - - /** - * Tries to cancel the future. {@code cancel(false)} tries a normal job - * cancellation, which may or may not terminated the job (it may decide - * not to react to cancellation requests). - * - * @param cancellation - * {@link CancelMode} defining how to cancel - * - * @return {@code true} if the future was canceled (its job is not - * running anymore), {@code false} otherwise. - */ - public synchronized boolean cancel(CancelMode cancellation) { - CancelMode mode = cancellation == null ? CancelMode.CANCEL - : cancellation; - switch (state) { - case PRISTINE: - finish(false); - return true; - case SCHEDULED: - state = State.CANCELING; - boolean canceled = job.cancel(); - if (canceled) { - state = State.CANCELED; - } else if (mode == CancelMode.INTERRUPT) { - interrupt(); - } else if (mode == CancelMode.ABANDON) { - notifyAll(); - } - return canceled; - case CANCELING: - // cancel(CANCEL|ABANDON) was called before. - if (mode == CancelMode.INTERRUPT) { - interrupt(); - } else if (mode == CancelMode.ABANDON) { - notifyAll(); - } - return false; - case INTERRUPT: - if (mode != CancelMode.CANCEL) { - notifyAll(); - } - return false; - case CANCELED: - return true; - default: - return false; - } - } - - public synchronized boolean isFinished() { - return state == State.CANCELED || state == State.DONE; - } - - public synchronized boolean isDone() { - return state == State.DONE; - } - - /** - * Retrieves the result. If the result is not yet available, the method - * blocks until it is or {@link #cancel(CancelMode)} is called with - * {@link CancelMode#ABANDON} or {@link CancelMode#INTERRUPT}. - * - * @return the result, which may be {@code null} if the future was - * canceled - * @throws InterruptedException - * if waiting was interrupted - * @throws InvocationTargetException - * if the future's job cannot be created - */ - public synchronized Collection<Change> get() - throws InterruptedException, InvocationTargetException { - switch (state) { - case DONE: - case CANCELED: - return result; - case PRISTINE: - fetch(); - return get(); - default: - wait(); - if (state == State.CANCELING || state == State.INTERRUPT) { - // canceled with ABANDON or INTERRUPT - throw new InterruptedException(); - } - return get(); - } - } - - public synchronized Collection<Change> getResult() { - if (isFinished()) { - return result; - } - throw new IllegalStateException( - "Fetching change list is not finished"); //$NON-NLS-1$ - } - - private synchronized void finish(boolean done) { - state = done ? State.DONE : State.CANCELED; - job = null; - notifyAll(); // We're done, wake up all outstanding get() calls - } - - private synchronized void interrupt() { - state = State.INTERRUPT; - job.interrupt(); - notifyAll(); // Abandon outstanding get() calls - } - - /** - * On the first call, starts a background job to fetch the result. - * Subsequent calls do nothing and return immediately. - * - * @throws InvocationTargetException - * if starting the job fails - */ - public synchronized void fetch() throws InvocationTargetException { - if (job != null || state != State.PRISTINE) { - return; - } - ListRemoteOperation listOp; - try { - listOp = new ListRemoteOperation(repository, - new URIish(uriText), - Activator.getDefault().getPreferenceStore().getInt( - UIPreferences.REMOTE_CONNECTION_TIMEOUT)); - } catch (URISyntaxException e) { - finish(false); - throw new InvocationTargetException(e); - } - job = new InterruptibleJob(MessageFormat.format( - UIText.FetchGerritChangePage_FetchingRemoteRefsMessage, - uriText)) { - - @Override - protected IStatus run(IProgressMonitor monitor) { - try { - listOp.run(monitor); - } catch (InterruptedException e) { - return Status.CANCEL_STATUS; - } catch (InvocationTargetException e) { - synchronized (ChangeList.this) { - if (state == State.CANCELING - || state == State.INTERRUPT) { - // JGit may report a TransportException when the - // thread is interrupted. Let's just pretend we - // canceled before. Also, if the user canceled - // already, he's not interested in errors - // anymore. - return Status.CANCEL_STATUS; - } - } - return Activator - .createErrorStatus(e.getLocalizedMessage(), e); - } - List<Change> changes = new ArrayList<>(); - for (Ref ref : listOp.getRemoteRefs()) { - Change change = Change.fromRef(ref.getName()); - if (change != null) { - changes.add(change); - } - } - Collections.sort(changes, Collections.reverseOrder()); - result = new LinkedHashSet<>(changes); - return Status.OK_STATUS; - } - - }; - job.addJobChangeListener(new JobChangeAdapter() { - - @Override - public void done(IJobChangeEvent event) { - IStatus status = event.getResult(); - finish(status != null && status.isOK()); - } - - }); - job.setUser(false); - job.setSystem(true); - state = State.SCHEDULED; - job.schedule(); + super(repository, uriText); } - private static abstract class InterruptibleJob extends Job { - - public InterruptibleJob(String name) { - super(name); - } - - public void interrupt() { - Thread thread = getThread(); - if (thread != null) { - thread.interrupt(); + @Override + protected Collection<Change> convert(Collection<Ref> refs) { + List<Change> changes = new ArrayList<>(); + for (Ref ref : refs) { + Change change = Change.fromRef(ref.getName()); + if (change != null) { + changes.add(change); } } + Collections.sort(changes, Collections.reverseOrder()); + return new LinkedHashSet<>(changes); } } } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchSourcePage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchSourcePage.java index f1ff6c2cea..a78c532db2 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchSourcePage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/fetch/FetchSourcePage.java @@ -89,7 +89,7 @@ public class FetchSourcePage extends WizardPage { }); GridDataFactory.fillDefaults().grab(true, false).applyTo(sourceText); UIUtils.addRefContentProposalToText(sourceText, repository, - () -> getRemoteRefs()); + () -> getRemoteRefs(), true); checkPage(); setControl(main); } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/gerrit/GerritConfigurationPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/gerrit/GerritConfigurationPage.java index 985b61dc28..c5dce09919 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/gerrit/GerritConfigurationPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/gerrit/GerritConfigurationPage.java @@ -317,7 +317,8 @@ class GerritConfigurationPage extends WizardPage { return null; } return new ContentProposal(refName); - }, UIText.GerritConfigurationPage_BranchTooltipStartTyping, + }, null, + UIText.GerritConfigurationPage_BranchTooltipStartTyping, UIText.GerritConfigurationPage_BranchTooltipHover); } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/pull/PullWizardPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/pull/PullWizardPage.java index 02b2eadce4..ec80bb6abc 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/pull/PullWizardPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/pull/PullWizardPage.java @@ -172,7 +172,7 @@ public class PullWizardPage extends WizardPage { .getRefsForContentAssist(false, true); } return Collections.emptyList(); - }); + }, true); remoteBranchNameText.setText(getSuggestedBranchName()); remoteBranchNameText.addModifyListener(new ModifyListener() { @Override diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchPage.java index eb9626bfda..6ca9b14e42 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchPage.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2016 Robin Stocker <robin@nibor.org> and others. + * Copyright (c) 2013, 2018 Robin Stocker <robin@nibor.org> and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at @@ -8,29 +8,36 @@ package org.eclipse.egit.ui.internal.push; import java.io.IOException; +import java.lang.reflect.InvocationTargetException; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.eclipse.egit.core.internal.Utils; import org.eclipse.egit.core.op.CreateLocalBranchOperation; import org.eclipse.egit.ui.Activator; import org.eclipse.egit.ui.UIUtils; +import org.eclipse.egit.ui.internal.CommonUtils; import org.eclipse.egit.ui.internal.UIIcons; import org.eclipse.egit.ui.internal.UIText; +import org.eclipse.egit.ui.internal.components.AsynchronousListOperation; +import org.eclipse.egit.ui.internal.components.AsynchronousRefProposalProvider; import org.eclipse.egit.ui.internal.components.BranchNameNormalizer; -import org.eclipse.egit.ui.internal.components.RefContentAssistProvider; import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo; import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.IRemoteSelectionListener; import org.eclipse.egit.ui.internal.components.RemoteSelectionCombo.SelectionType; import org.eclipse.egit.ui.internal.components.UpstreamConfigComponent; import org.eclipse.egit.ui.internal.components.UpstreamConfigComponent.UpstreamConfigSelectionListener; +import org.eclipse.egit.ui.internal.dialogs.CancelableFuture; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.GridLayoutFactory; @@ -43,7 +50,9 @@ import org.eclipse.jgit.lib.BranchConfig; import org.eclipse.jgit.lib.BranchConfig.BranchRebaseMode; import org.eclipse.jgit.lib.Constants; import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectIdRef; import org.eclipse.jgit.lib.Ref; +import org.eclipse.jgit.lib.Ref.Storage; import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.StoredConfig; import org.eclipse.jgit.revwalk.RevCommit; @@ -91,8 +100,6 @@ public class PushBranchPage extends WizardPage { private Text remoteBranchNameText; - private RefContentAssistProvider assist; - private BranchRebaseMode upstreamConfig; private UpstreamConfigComponent upstreamConfigComponent; @@ -104,6 +111,8 @@ public class PushBranchPage extends WizardPage { private Set<Resource> disposables = new HashSet<>(); + private Map<String, FutureRefs> refs = new HashMap<>(); + /** * Create the page. * @@ -161,6 +170,12 @@ public class PushBranchPage extends WizardPage { @Override public void createControl(Composite parent) { + parent.addDisposeListener(event -> { + for (CancelableFuture<Collection<Ref>> l : refs.values()) { + l.cancel(CancelableFuture.CancelMode.INTERRUPT); + } + refs.clear(); + }); try { this.remoteConfigs = RemoteConfig.getAllRemoteConfigs(repository.getConfig()); Collections.sort(remoteConfigs, new Comparator<RemoteConfig>() { @@ -288,15 +303,30 @@ public class PushBranchPage extends WizardPage { GridDataFactory.fillDefaults().grab(true, false).span(2, 1) .applyTo(remoteBranchNameText); remoteBranchNameText.setText(getSuggestedBranchName()); - UIUtils.addRefContentProposalToText(remoteBranchNameText, - this.repository, () -> { - if (PushBranchPage.this.assist != null) { - return PushBranchPage.this.assist - .getRefsForContentAssist(false, true); + AsynchronousRefProposalProvider candidateProvider = new AsynchronousRefProposalProvider( + getContainer(), remoteBranchNameText, () -> { + RemoteConfig config = remoteSelectionCombo + .getSelectedRemote(); + if (config == null) { + return null; } - return Collections.emptyList(); + List<URIish> uris = config.getURIs(); + if (uris == null || uris.isEmpty()) { + return null; + } + return uris.get(0).toString(); + }, uri -> { + FutureRefs list = refs.get(uri); + if (list == null) { + list = new FutureRefs(repository, uri, + getLocalBranchName()); + refs.put(uri, list); + } + return list; }); - + candidateProvider.setContentProposalAdapter( + UIUtils.addRefContentProposalToText(remoteBranchNameText, + this.repository, candidateProvider, true)); if (this.ref != null) { upstreamConfigComponent = new UpstreamConfigComponent(inputPanel, SWT.NONE); @@ -464,6 +494,13 @@ public class PushBranchPage extends WizardPage { } } + private String getLocalBranchName() { + if (ref != null && !ref.getName().startsWith(Constants.R_REMOTES)) { + return Repository.shortenRefName(ref.getName()); + } + return null; + } + private String getSuggestedBranchName() { if (ref != null && !ref.getName().startsWith(Constants.R_REMOTES)) { StoredConfig config = repository.getConfig(); @@ -483,9 +520,22 @@ public class PushBranchPage extends WizardPage { private void setRefAssist(RemoteConfig config) { if (config != null && config.getURIs().size() > 0) { - this.assist = new RefContentAssistProvider( - PushBranchPage.this.repository, config.getURIs().get(0), - getShell()); + String uriText = config.getURIs().get(0).toString(); + FutureRefs list = refs.get(uriText); + if (list == null) { + list = new FutureRefs(repository, uriText, + getLocalBranchName()); + refs.put(uriText, list); + preFetch(list); + } + } + } + + private void preFetch(FutureRefs list) { + try { + list.start(); + } catch (InvocationTargetException e) { + Activator.handleError(e.getLocalizedMessage(), e.getCause(), true); } } @@ -523,4 +573,51 @@ public class PushBranchPage extends WizardPage { for (Resource disposable : this.disposables) disposable.dispose(); } + + /** + * {@code FutureRefs} are loaded asynchronously from the upstream + * repository. + */ + private static class FutureRefs extends AsynchronousListOperation<Ref> { + + private final String localBranchName; + + public FutureRefs(Repository repository, String uriText, + String localBranchName) { + super(repository, uriText); + this.localBranchName = localBranchName; + } + + @Override + protected Collection<Ref> convert(Collection<Ref> refs) { + List<Ref> filtered = new ArrayList<>(); + String localFullName = localBranchName != null + ? Constants.R_HEADS + localBranchName : null; + boolean localBranchFound = false; + // Restrict to branches + for (Ref ref : refs) { + String name = ref.getName(); + if (name.startsWith(Constants.R_HEADS)) { + filtered.add(ref); + if (localFullName != null + && localFullName.equalsIgnoreCase(name)) { + localBranchFound = true; + } + } + } + // Sort them + Collections.sort(filtered, CommonUtils.REF_ASCENDING_COMPARATOR); + // Add a new remote ref for localBranchName in front if it doesn't + // exist + if (localFullName != null && !localBranchFound) { + List<Ref> newRefs = new ArrayList<>(filtered.size() + 1); + newRefs.add(new ObjectIdRef.Unpeeled(Storage.NEW, localFullName, + ObjectId.zeroId())); + newRefs.addAll(filtered); + filtered = newRefs; + } + return filtered; + } + } + } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchWizard.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchWizard.java index c48f346dc3..b9f214314e 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchWizard.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushBranchWizard.java @@ -105,6 +105,7 @@ public class PushBranchWizard extends Wizard { } }; + setNeedsProgressMonitor(true); setDefaultPageImageDescriptor(UIIcons.WIZBAN_PUSH); } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushToGerritPage.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushToGerritPage.java index d2ec499abe..034b86efba 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushToGerritPage.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushToGerritPage.java @@ -510,7 +510,7 @@ public class PushToGerritPage extends WizardPage { return null; } return new ContentProposal(refName); - }, UIText.PushToGerritPage_ContentProposalStartTypingText, + }, null, UIText.PushToGerritPage_ContentProposalStartTypingText, UIText.PushToGerritPage_ContentProposalHoverText); } } diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushWizardDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushWizardDialog.java index d3897d2083..c1712bd7c9 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushWizardDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/PushWizardDialog.java @@ -8,9 +8,9 @@ package org.eclipse.egit.ui.internal.push; import org.eclipse.egit.ui.internal.UIText; +import org.eclipse.egit.ui.internal.dialogs.NonBlockingWizardDialog; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.wizard.IWizard; -import org.eclipse.jface.wizard.WizardDialog; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Shell; @@ -18,7 +18,7 @@ import org.eclipse.swt.widgets.Shell; /** * A dialog dedicated to {@link PushBranchWizard}, customizing button labels */ -public class PushWizardDialog extends WizardDialog { +public class PushWizardDialog extends NonBlockingWizardDialog { /** * @param parentShell diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/RefSpecDialog.java b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/RefSpecDialog.java index 6eafd866d8..403aa1f28e 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/RefSpecDialog.java +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/push/RefSpecDialog.java @@ -159,7 +159,8 @@ public class RefSpecDialog extends TitleAndImageDialog { }); // content assist for source UIUtils.addRefContentProposalToText(sourceText, repo, - () -> assistProvider.getRefsForContentAssist(true, pushMode)); + () -> assistProvider.getRefsForContentAssist(true, pushMode), + !pushMode); // suggest remote tracking branch if (!pushMode) { @@ -198,7 +199,8 @@ public class RefSpecDialog extends TitleAndImageDialog { }); // content assist for destination UIUtils.addRefContentProposalToText(destinationText, repo, - () -> assistProvider.getRefsForContentAssist(false, pushMode)); + () -> assistProvider.getRefsForContentAssist(false, pushMode), + pushMode); // force update forceButton = new Button(main, SWT.CHECK); diff --git a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties index 06a20e4012..9d37794af5 100644 --- a/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties +++ b/org.eclipse.egit.ui/src/org/eclipse/egit/ui/internal/uitext.properties @@ -56,6 +56,8 @@ AddRemoteWizard_Title=Add Remote AddSubmoduleWizard_WindowTitle=Add Submodule AddToIndexAction_addingFiles=Adding Files to Index AddToIndexCommand_addingFilesFailed=Adding files failed +AsynchronousRefProposalProvider_FetchingRemoteRefsMessage=Fetching remote refs from {0} +AsynchronousRefProposalProvider_ShowingProposalsJobName=Showing proposals RemoveFromIndexAction_removingFiles=Removing file from Index BlameInformationControl_Author=Author: {0} <{1}> {2} BlameInformationControl_Commit=Commit {0} @@ -537,11 +539,13 @@ RefContentProposal_blob=blob RefContentProposal_branch=branch RefContentProposal_by=by RefContentProposal_commit=commit -RefContentProposal_errorReadingObject=Unable to read object {0} for content proposal assistance +RefContentProposal_errorReadingObject=Unable to read object {0} for content proposal assistance (ref = {1}) +RefContentProposal_newRemoteObject=New object will be created at the remote repository RefContentProposal_tag=tag RefContentProposal_trackingBranch=tracking branch RefContentProposal_tree=tree RefContentProposal_unknownObject=locally unknown object +RefContentProposal_unknownRemoteObject=locally unknown object: upstream is ahead of local repository and hasn't been fetched yet ReflogView_DateColumnHeader=Date ReflogView_ErrorOnLoad=Loading the reflog encountered an error; see the Error Log for more information ReflogView_ErrorOnOpenCommit=Error opening commit @@ -960,7 +964,6 @@ FetchGerritChangePage_ContentAssistDescription=Patch set {0} of change {1} FetchGerritChangePage_ContentAssistTooltip=Press {0} to see a filtered list of changes FetchGerritChangePage_CreatingBranchTaskName=Creating branch FetchGerritChangePage_CreatingTagTaskName=Creating tag -FetchGerritChangePage_FetchingRemoteRefsMessage=Fetching remote refs from {0} FetchGerritChangePage_FetchingTaskName=Fetching change {0} FetchGerritChangePage_GeneratedTagMessage=Generated for Gerrit change {0} FetchGerritChangePage_GetChangeTaskName=Get change from Gerrit @@ -970,7 +973,6 @@ FetchGerritChangePage_MissingChangeMessage=Please provide a change FetchGerritChangePage_NoSuchChangeMessage=The Gerrit server has no change number {0} FetchGerritChangePage_PageMessage=Please select a Gerrit URI and change to fetch FetchGerritChangePage_PageTitle=Fetch a change from Gerrit into repository {0} -FetchGerritChangePage_ShowingProposalsJobName=Showing proposals FetchGerritChangePage_SuggestedRefNamePattern=change/{0}/{1} FetchGerritChangePage_TagNameText=Tag &name: FetchGerritChangePage_TagRadio=Create and check out a &tag |