From 38e797b73db3e87b6f734fbbfbcee7129c00ed09 Mon Sep 17 00:00:00 2001 From: Henrik Rentz-Reichert Date: Fri, 29 Nov 2019 17:30:12 +0100 Subject: Bug 546282 - [ui] Implement "organize imports" Implemented a fairly general mechanism that can be adapted easily for other models, e.g. one using the FSM part only. Conflicts: plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java Change-Id: If6f6d6039833c2e54a337b60d41be6bb20b0ed0a --- .../core/common/ui/imports/ImportOrganizer.java | 57 +++- .../ui/imports/NamespaceSelectionDialog.java | 365 +++++++++++++++++++++ .../imports/NamespaceSelectionLabelProvider.java | 33 ++ 3 files changed, 450 insertions(+), 5 deletions(-) create mode 100644 plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionDialog.java create mode 100644 plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionLabelProvider.java diff --git a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java index d93dde004..8e14e8ebb 100644 --- a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java @@ -32,13 +32,18 @@ import org.eclipse.emf.ecore.EReference; import org.eclipse.etrice.core.common.base.BaseFactory; import org.eclipse.etrice.core.common.base.Import; import org.eclipse.etrice.core.common.ui.imports.IOrganizeImportHelper.ImportRegionResult; +import org.eclipse.jface.window.Window; +import org.eclipse.swt.widgets.Display; import org.eclipse.xtext.formatting.IWhitespaceInformationProvider; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.nodemodel.INode; import org.eclipse.xtext.nodemodel.util.NodeModelUtils; import org.eclipse.xtext.resource.XtextResource; import org.eclipse.xtext.util.ReplaceRegion; +import org.eclipse.xtext.xbase.lib.Pair; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; import com.google.inject.Inject; @@ -57,7 +62,11 @@ public class ImportOrganizer { private IWhitespaceInformationProvider whitespaceInformationProvider; public List getOrganizedImportChanges(XtextResource resource) { - Set typeUsages = getTypeUsages(resource); + return getOrganizedImportChanges(resource, true); + } + + public List getOrganizedImportChanges(XtextResource resource, boolean interactive) { + Set typeUsages = getTypeUsages(resource, interactive); typeUsages = simplifyImports(typeUsages); @@ -68,9 +77,11 @@ public class ImportOrganizer { return changes; } - protected Set getTypeUsages(XtextResource resource) { + protected Set getTypeUsages(XtextResource resource, boolean interactive) { Set result = new HashSet<>(); + Multimap, QualifiedName> ambiguous = HashMultimap.create(); + Multimap typeReferences = organizeImportHelper.getTypeReferences(); EObject root = resource.getContents().get(0); TreeIterator it = root.eAllContents(); @@ -84,7 +95,14 @@ public class ImportOrganizer { if (((EObject) refObject).eIsProxy()) { List nodes = NodeModelUtils.findNodesForFeature((EObject) object, ref); if (!nodes.isEmpty()) { - result.addAll(organizeImportHelper.resolveFullyQualifiedName(nodes.get(0).getText().trim(), ref.getEReferenceType(), resource)); + String refText = nodes.get(0).getText().trim(); + List resolved = organizeImportHelper.resolveFullyQualifiedName(refText, ref.getEReferenceType(), resource); + if (resolved.size()==1) { + result.addAll(resolved); + } + else if (resolved.size()>1) { + ambiguous.putAll(new Pair(object.eClass(), refText), resolved); + } } } else { @@ -108,14 +126,43 @@ public class ImportOrganizer { } } + if (!ambiguous.isEmpty()) { + QualifiedName[][] namespaces = new QualifiedName[ambiguous.asMap().size()][]; + int i = 0; + for (Collection lists : ambiguous.asMap().values()) { + QualifiedName[] list = new QualifiedName[lists.size()]; + namespaces[i++] = lists.toArray(list); + } + result.addAll(doChooseImports(namespaces)); + } + // remove our own namespace QualifiedName ownNamespace = organizeImportHelper.getFullyQualifiedName(root); result.removeIf(fqn -> fqn.startsWith(ownNamespace)); return result; } + + protected Set doChooseImports(QualifiedName[][] openChoices) { + NamespaceSelectionLabelProvider labelProvider = new NamespaceSelectionLabelProvider(); + NamespaceSelectionDialog dialog = new NamespaceSelectionDialog(Display.getDefault().getActiveShell(), labelProvider); + dialog.setTitle("Organize Imports"); + dialog.setMessage("&Choose type to import:"); + dialog.setElements(openChoices); + Set result = new HashSet<>(); + if (dialog.open() == Window.OK) { + Object[] res = dialog.getResult(); + for (int i = 0; i < res.length; i++) { + Object[] array= (Object[]) res[i]; + if (array.length>0 && array[0] instanceof QualifiedName) { + result.add((QualifiedName) array[0]); + } + } + } + return result; + } - private Set simplifyImports(Set typeUsages) { + protected Set simplifyImports(Set typeUsages) { Map> grouped = typeUsages.stream().collect(Collectors.groupingBy(fqn->fqn.skipLast(1))); Set result = new HashSet<>(); grouped.forEach((namespace, names) -> { @@ -166,7 +213,7 @@ public class ImportOrganizer { return result; } - private String serializeImports(List allImportDeclarations, String newLine) { + protected String serializeImports(List allImportDeclarations, String newLine) { if (allImportDeclarations.isEmpty()) { return ""; } diff --git a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionDialog.java b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionDialog.java new file mode 100644 index 000000000..ceb76f6a3 --- /dev/null +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionDialog.java @@ -0,0 +1,365 @@ +/******************************************************************************* + * Copyright (c) 2019 protos software gmbh (http://www.protos.de). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * CONTRIBUTORS: + * Henrik Rentz-Reichert (initial contribution) + * + *******************************************************************************/ + +package org.eclipse.etrice.core.common.ui.imports; + +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.viewers.ILabelProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog; +import org.eclipse.ui.dialogs.FilteredList; + +/** + * @author Henrik Rentz-Reichert + * + * (initial code from org.eclipse.jdt.internal.ui.dialogs.MultiElementListSelectionDialog) + */ +public class NamespaceSelectionDialog extends AbstractElementListSelectionDialog { + + private static class Page { + private Object[] elements; + public String filter; + public boolean okState= false; + + public Page(Object[] elements) { + this.elements= elements; + } + } + + private Page[] fPages; + private int fCurrentPage; + private int fNumberOfPages; + + private Button fFinishButton; + private Button fBackButton; + private Button fNextButton; + private Button fSkipButton; + + private Label fPageInfoLabel; + private String fPageInfoMessage= "Page {0} of {1}"; + private Comparator fComparator; + + /** + * Constructs a multi-page list selection dialog. + * @param parent The parent shell + * @param renderer the label renderer. + */ + public NamespaceSelectionDialog(Shell parent, ILabelProvider renderer) { + super(parent, renderer); + } + + /** + * Sets message shown in the right top corner. Use {0} and {1} as placeholders + * for the current and the total number of pages. + * @param message the message. + */ + public void setPageInfoMessage(String message) { + fPageInfoMessage= message; + } + + /** + * Sets the elements to be displayed in the dialog. + * @param elements an array of pages holding arrays of elements + */ + public void setElements(Object[][] elements) { + fNumberOfPages= elements.length; + fPages= new Page[fNumberOfPages]; + for (int i= 0; i != fNumberOfPages; i++) + fPages[i]= new Page(elements[i]); + + initializeResult(fNumberOfPages); + } + + /* + * @see Window#open() + */ + @SuppressWarnings("unchecked") + @Override + public int open() { + List selection= getInitialElementSelections(); + if (selection == null || selection.size() != fNumberOfPages) { + setInitialSelections(new Object[fNumberOfPages]); + selection= getInitialElementSelections(); + } + + Assert.isTrue(selection.size() == fNumberOfPages); + + return super.open(); + } + + /* + * @see org.eclipse.jface.dialogs.Dialog#createDialogArea(Composite) + */ + @Override + protected Control createDialogArea(Composite parent) { + Composite contents= (Composite) super.createDialogArea(parent); + + createMessageArea(contents); + createFilterText(contents); + createFilteredList(contents); + + fCurrentPage= 0; + setPageData(); + + applyDialogFont(contents); + return contents; + } + + /* + * @see org.eclipse.jface.dialogs.Dialog#createButtonsForButtonBar(Composite) + */ + @Override + protected void createButtonsForButtonBar(Composite parent) { + fSkipButton= createButton(parent, IDialogConstants.SKIP_ID, IDialogConstants.SKIP_LABEL, false); + fBackButton= createButton(parent, IDialogConstants.BACK_ID, IDialogConstants.BACK_LABEL, false); + + // XXX: Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=279425 + boolean HAS_BUG_279425= true; + fNextButton= createButton(parent, IDialogConstants.NEXT_ID, IDialogConstants.NEXT_LABEL, !HAS_BUG_279425); + fFinishButton= createButton(parent, IDialogConstants.OK_ID, IDialogConstants.FINISH_LABEL, HAS_BUG_279425); + + createButton(parent, IDialogConstants.CANCEL_ID, IDialogConstants.CANCEL_LABEL, false); + } + + /* + * XXX: Workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=279425 + * The whole method can be removed once that bug is fixed. + * @see org.eclipse.jface.dialogs.Dialog#initializeBounds() + * @since 3.5.1 + */ + @Override + protected void initializeBounds() { + super.initializeBounds(); + fNextButton.getShell().setDefaultButton(fNextButton); + } + + /* + * @see org.eclipse.ui.dialogs.SelectionDialog#createMessageArea(Composite) + */ + @Override + protected Label createMessageArea(Composite parent) { + Composite composite= new Composite(parent, SWT.NONE); + + GridLayout layout= new GridLayout(); + layout.marginHeight= 0; + layout.marginWidth= 0; + layout.horizontalSpacing= 5; + layout.numColumns= 2; + composite.setLayout(layout); + + GridData data= new GridData(GridData.HORIZONTAL_ALIGN_FILL); + composite.setLayoutData(data); + + Label messageLabel= super.createMessageArea(composite); + + fPageInfoLabel= new Label(composite, SWT.NULL); + fPageInfoLabel.setText(getPageInfoMessage()); + + data= new GridData(GridData.HORIZONTAL_ALIGN_FILL); + data.horizontalAlignment= GridData.END; + fPageInfoLabel.setLayoutData(data); + applyDialogFont(messageLabel); + return messageLabel; + } + + /* + * @see org.eclipse.ui.dialogs.SelectionStatusDialog#computeResult() + */ + @Override + protected void computeResult() { + setResult(fCurrentPage, getSelectedElements()); + } + + /* + * @see org.eclipse.jface.dialogs.Dialog#buttonPressed(int) + */ + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.SKIP_ID) { + boolean isLastPage= fCurrentPage == fNumberOfPages - 1 ? true : false; + turnPage(true, true); + if (isLastPage) { + buttonPressed(IDialogConstants.OK_ID); + } + } else if (buttonId == IDialogConstants.BACK_ID) { + turnPage(false, false); + } else if (buttonId == IDialogConstants.NEXT_ID) { + turnPage(true, false); + } else { + super.buttonPressed(buttonId); + } + } + + /** + * @see AbstractElementListSelectionDialog#handleDefaultSelected() + */ + @Override + protected void handleDefaultSelected() { + if (validateCurrentSelection()) { + if (fCurrentPage == fNumberOfPages - 1) { + buttonPressed(IDialogConstants.OK_ID); + } else { + buttonPressed(IDialogConstants.NEXT_ID); + } + } + } + + /** + * @see AbstractElementListSelectionDialog#updateButtonsEnableState(IStatus) + */ + @Override + protected void updateButtonsEnableState(IStatus status) { + boolean isOK= !status.matches(IStatus.ERROR); + fPages[fCurrentPage].okState= isOK; + + boolean isAllOK= isOK; + for (int i= 0; i != fNumberOfPages; i++) + isAllOK = isAllOK && fPages[i].okState; + + fFinishButton.setEnabled(isAllOK); + + boolean nextButtonEnabled= isOK && (fCurrentPage < fNumberOfPages - 1); + + fNextButton.setEnabled(nextButtonEnabled); + fBackButton.setEnabled(fCurrentPage != 0); + + // since there will always be a default selection - see FilteredList.TableUpdateJob.defaultSelect() + fSkipButton.setEnabled(true); + + if (nextButtonEnabled) { + getShell().setDefaultButton(fNextButton); + } else if (isAllOK) { + getShell().setDefaultButton(fFinishButton); + } + } + + private void turnPage(boolean toNextPage, boolean skipSelection) { + Page page= fPages[fCurrentPage]; + + // store filter + String filter= getFilter(); + if (filter == null) + filter= ""; //$NON-NLS-1$ + page.filter= filter; + + if (skipSelection) { + setSelection(null); + } + + // store selection + Object[] selectedElements= getSelectedElements(); + @SuppressWarnings("unchecked") + List list= getInitialElementSelections(); + list.set(fCurrentPage, selectedElements); + + // store result + setResult(fCurrentPage, selectedElements); + + if (toNextPage) { + if (fCurrentPage + 1 >= fNumberOfPages) + return; + + fCurrentPage++; + } else { + if (fCurrentPage - 1 < 0) + return; + + fCurrentPage--; + } + + if (fPageInfoLabel != null && !fPageInfoLabel.isDisposed()) + fPageInfoLabel.setText(getPageInfoMessage()); + + setPageData(); + + validateCurrentSelection(); + } + + private void setPageData() { + Page page= fPages[fCurrentPage]; + + // 1. set elements + setListElements(page.elements); + + // 2. apply filter + String filter= page.filter; + if (filter == null) + filter= ""; //$NON-NLS-1$ + setFilter(filter); + + // 3. select elements + Object[] selectedElements= (Object[]) getInitialElementSelections().get(fCurrentPage); + setSelection(selectedElements); + fFilteredList.setFocus(); + } + + private String getPageInfoMessage() { + if (fPageInfoMessage == null) + return ""; //$NON-NLS-1$ + + Object[] args= new Object[] { Integer.toString(fCurrentPage + 1), Integer.toString(fNumberOfPages) }; + return MessageFormat.format(fPageInfoMessage, args); + } + + private void initializeResult(int length) { + List result= new ArrayList<>(length); + for (int i= 0; i != length; i++) + result.add(null); + + setResult(result); + } + + /** + * Gets the current Page. + * @return Returns a int + */ + public int getCurrentPage() { + return fCurrentPage; + } + + /** + * Set the Comparator used to sort + * the elements in the List. + * + * @param comparator the comparator to use, not null. + */ + public void setComparator(Comparator comparator) { + fComparator= comparator; + if (fFilteredList != null) + fFilteredList.setComparator(fComparator); + } + + @Override + protected FilteredList createFilteredList(Composite parent) { + FilteredList filteredList= super.createFilteredList(parent); + if (fComparator != null) { + filteredList.setComparator(fComparator); + } + return filteredList; + } +} diff --git a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionLabelProvider.java b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionLabelProvider.java new file mode 100644 index 000000000..1ea6a404c --- /dev/null +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/NamespaceSelectionLabelProvider.java @@ -0,0 +1,33 @@ +/******************************************************************************* + * Copyright (c) 2019 protos software gmbh (http://www.protos.de). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * CONTRIBUTORS: + * Henrik Rentz-Reichert (initial contribution) + * + *******************************************************************************/ + +package org.eclipse.etrice.core.common.ui.imports; + +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.xtext.naming.QualifiedName; + +/** + * @author Henrik Rentz-Reichert + * + */ +public class NamespaceSelectionLabelProvider extends LabelProvider { + @Override + public String getText(Object element) { + if (element instanceof QualifiedName) { + return element.toString(); + } + else return super.getText(element); + } + +} -- cgit v1.2.3