From 34912487f8dc3014f3f43cfb285dac815adda60d 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. Change-Id: Ib322afaed71b55e7dc66fb330a203520828f2c8f --- .../META-INF/MANIFEST.MF | 4 +- .../common/ui/imports/IOrganizeImportHelper.java | 101 +++++++++++ .../core/common/ui/imports/ImportOrganizer.java | 186 +++++++++++++++++++++ .../common/ui/imports/OrganizeImportsHandler.java | 84 ++++++++++ plugins/org.eclipse.etrice.core.room.ui/plugin.xml | 24 +++ .../org/eclipse/etrice/core/ui/RoomUiModule.java | 5 + .../core/ui/imports/RoomOrganizeImportHelper.java | 114 +++++++++++++ 7 files changed, 517 insertions(+), 1 deletion(-) create mode 100644 plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/IOrganizeImportHelper.java create mode 100644 plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java create mode 100644 plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/OrganizeImportsHandler.java create mode 100644 plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/imports/RoomOrganizeImportHelper.java diff --git a/plugins/org.eclipse.etrice.core.common.ui/META-INF/MANIFEST.MF b/plugins/org.eclipse.etrice.core.common.ui/META-INF/MANIFEST.MF index c434bd944..74394d092 100644 --- a/plugins/org.eclipse.etrice.core.common.ui/META-INF/MANIFEST.MF +++ b/plugins/org.eclipse.etrice.core.common.ui/META-INF/MANIFEST.MF @@ -18,7 +18,8 @@ Require-Bundle: org.eclipse.etrice.core.common;visibility:=reexport, org.eclipse.core.filesystem, org.eclipse.help, org.eclipse.xtext.xbase.lib, - org.eclipse.etrice.generator.base + org.eclipse.etrice.generator.base, + org.eclipse.xtext.xbase.ui Import-Package: org.apache.log4j, org.eclipse.xtext.xbase.lib Bundle-RequiredExecutionEnvironment: JavaSE-1.8 @@ -31,6 +32,7 @@ Export-Package: org.eclipse.etrice.core.common.ui.autoedit, org.eclipse.etrice.core.common.ui.editor.model, org.eclipse.etrice.core.common.ui.highlight, org.eclipse.etrice.core.common.ui.hover, + org.eclipse.etrice.core.common.ui.imports, org.eclipse.etrice.core.common.ui.internal, org.eclipse.etrice.core.common.ui.labeling, org.eclipse.etrice.core.common.ui.linking, diff --git a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/IOrganizeImportHelper.java b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/IOrganizeImportHelper.java new file mode 100644 index 000000000..94ca67981 --- /dev/null +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/IOrganizeImportHelper.java @@ -0,0 +1,101 @@ +/******************************************************************************* + * 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.util.List; + +import org.eclipse.emf.common.util.TreeIterator; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.etrice.core.common.base.Import; +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.ITextRegion; +import org.eclipse.xtext.util.TextRegion; + +import com.google.common.collect.Multimap; + +/** + * @author Henrik Rentz-Reichert + * + */ +public interface IOrganizeImportHelper { + + public static class ImportRegionResult { + private ITextRegion region; + private boolean needNewLine; + + public ImportRegionResult(int offset, int length, boolean needNewLine) { + super(); + this.region = new TextRegion(offset, length); + this.needNewLine = needNewLine; + } + public ITextRegion getRegion() { + return region; + } + public boolean isNeedNewLine() { + return needNewLine; + } + } + + /** + * @return a map of all {@link EClass}es that reference to be resolved objects with the corresponding {@link EReference}s + */ + Multimap getTypeReferences(); + + /** + * @param resource the Xtext resource being processed + * @return the region where the imports should go to + */ + default ImportRegionResult getImportRegion(XtextResource resource) { + EObject root = resource.getContents().get(0); + TreeIterator it = root.eAllContents(); + int begin = Integer.MAX_VALUE; + int end = 0; + while (it.hasNext()) { + EObject object = it.next(); + if (object instanceof Import) { + INode node = NodeModelUtils.findActualNodeFor(object); + if (node!=null) { + begin = Math.min(begin, node.getOffset()); + end = Math.max(end, node.getEndOffset()); + } + } + } + if (begin < end) { + return new ImportRegionResult(begin, end - begin, false); + } + else { + return null; + } + } + + /** + * @param object the object we need a qualified name for + * @return a fully qualified name for the object + */ + QualifiedName getFullyQualifiedName(EObject object); + + /** + * @param refText the reference text + * @param type the expected {@link EClass} + * @param resource the current resource + * @return a list of qualified names that resolve this reference + */ + List resolveFullyQualifiedName(String refText, EClass type, Resource resource); +} 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 new file mode 100644 index 000000000..d93dde004 --- /dev/null +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/ImportOrganizer.java @@ -0,0 +1,186 @@ +/******************************************************************************* + * 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 static com.google.common.collect.Lists.newArrayList; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.emf.common.util.TreeIterator; +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +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.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 com.google.common.collect.Multimap; +import com.google.inject.Inject; + +/** + * @author Henrik Rentz-Reichert + * + */ +public class ImportOrganizer { + + private static final int IMPORT_ALL_THRESHOLD = 3; + + @Inject + private IOrganizeImportHelper organizeImportHelper; + + @Inject + private IWhitespaceInformationProvider whitespaceInformationProvider; + + public List getOrganizedImportChanges(XtextResource resource) { + Set typeUsages = getTypeUsages(resource); + + typeUsages = simplifyImports(typeUsages); + + List changes = getOrganizedImportChanges(resource, typeUsages); + + removeNullEdits(resource, changes); + + return changes; + } + + protected Set getTypeUsages(XtextResource resource) { + Set result = new HashSet<>(); + + Multimap typeReferences = organizeImportHelper.getTypeReferences(); + EObject root = resource.getContents().get(0); + TreeIterator it = root.eAllContents(); + while (it.hasNext()) { + EObject object = it.next(); + if (typeReferences.containsKey(object.eClass())) { + Collection references = typeReferences.get(object.eClass()); + references.forEach(ref -> { + Object refObject = object.eGet(ref); + if (refObject instanceof EObject) { + 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)); + } + } + else { + boolean isQualified = false; + List nodes = NodeModelUtils.findNodesForFeature((EObject) object, ref); + if (!nodes.isEmpty()) { + // check whether this is a qualified name + if (nodes.stream().filter(node -> !node.getText().contains(".")).count()==0l) { + isQualified = true; + } + } + if (!isQualified) { + QualifiedName namespace = organizeImportHelper.getFullyQualifiedName((EObject) refObject); + if (namespace!=null) { + result.add(namespace); + } + } + } + } + }); + } + } + + // remove our own namespace + QualifiedName ownNamespace = organizeImportHelper.getFullyQualifiedName(root); + result.removeIf(fqn -> fqn.startsWith(ownNamespace)); + + return result; + } + + private Set simplifyImports(Set typeUsages) { + Map> grouped = typeUsages.stream().collect(Collectors.groupingBy(fqn->fqn.skipLast(1))); + Set result = new HashSet<>(); + grouped.forEach((namespace, names) -> { + if (names.size() > IMPORT_ALL_THRESHOLD) { + result.add(namespace.append("*")); + } + else { + result.addAll(names); + } + }); + return result; + } + + protected List getOrganizedImportChanges(XtextResource resource, Set namespaces) { + ImportRegionResult importRegionResult = organizeImportHelper.getImportRegion(resource); + ArrayList result = newArrayList(); + if (importRegionResult.getRegion()!=null) { + ArrayList sortedNamespaces = new ArrayList<>(namespaces); + Collections.sort(sortedNamespaces); + List allImportDeclarations = sortedNamespaces.stream().map(namespace -> createImport(namespace)).collect(Collectors.toList()); + String lineSeparator = getLineSeparator(resource); + String newImportSection = serializeImports(allImportDeclarations, lineSeparator); + if (importRegionResult.isNeedNewLine()) { + newImportSection += lineSeparator + lineSeparator + "\t"; + } + result.add(new ReplaceRegion(importRegionResult.getRegion(), newImportSection)); + return result; + } + else { + return result; + } + } + + protected void removeNullEdits(XtextResource resource, List changes) { + Iterator iterator = changes.iterator(); + String document = resource.getParseResult().getRootNode().getText(); + while(iterator.hasNext()) { + ReplaceRegion region = iterator.next(); + if (region.getText().equals(document.substring(region.getOffset(), region.getEndOffset()))) { + iterator.remove(); + } + } + } + + protected Import createImport(QualifiedName namespace) { + Import result = BaseFactory.eINSTANCE.createImport(); + result.setImportedNamespace(namespace.toString()); + return result; + } + + private String serializeImports(List allImportDeclarations, String newLine) { + if (allImportDeclarations.isEmpty()) { + return ""; + } + StringBuilder builder = new StringBuilder(); + allImportDeclarations.forEach(imp -> appendImport(builder, imp, newLine)); + return builder.toString().trim(); // omit first \t and trailing newLine + } + + protected void appendImport(StringBuilder builder, Import imp, String newLine) { + builder.append("\timport " + imp.getImportedNamespace() + newLine); + } + + protected String getLineSeparator(XtextResource resource) { + return whitespaceInformationProvider.getLineSeparatorInformation(resource.getURI()).getLineSeparator(); + } + +} diff --git a/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/OrganizeImportsHandler.java b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/OrganizeImportsHandler.java new file mode 100644 index 000000000..51f9d8e7b --- /dev/null +++ b/plugins/org.eclipse.etrice.core.common.ui/src/org/eclipse/etrice/core/common/ui/imports/OrganizeImportsHandler.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * 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.util.List; + +import org.apache.log4j.Logger; +import org.eclipse.core.commands.AbstractHandler; +import org.eclipse.core.commands.ExecutionEvent; +import org.eclipse.core.commands.ExecutionException; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.DocumentRewriteSession; +import org.eclipse.jface.text.DocumentRewriteSessionType; +import org.eclipse.jface.text.IDocumentExtension4; +import org.eclipse.xtext.resource.XtextResource; +import org.eclipse.xtext.ui.editor.XtextEditor; +import org.eclipse.xtext.ui.editor.model.IXtextDocument; +import org.eclipse.xtext.ui.editor.utils.EditorUtils; +import org.eclipse.xtext.util.ReplaceRegion; +import org.eclipse.xtext.util.concurrent.IUnitOfWork; +import org.eclipse.xtext.xbase.ui.imports.ReplaceConverter; + +import com.google.inject.Inject; + +/** + * @author Henrik Rentz-Reichert + * + * @see org.eclipse.xtext.xbase.ui.imports.OrganizeImportsHandler + * + */ +@SuppressWarnings("restriction") // ReplaceConverter is non-API +public class OrganizeImportsHandler extends AbstractHandler { + + private static final Logger LOG = Logger.getLogger(OrganizeImportsHandler.class); + + @Inject private ReplaceConverter replaceConverter; + + @Inject private ImportOrganizer importOrganizer; + + @Override + public Object execute(ExecutionEvent event) throws ExecutionException { + XtextEditor editor = EditorUtils.getActiveXtextEditor(event); + if (editor != null) { + final IXtextDocument document = editor.getDocument(); + doOrganizeImports(document); + } + return null; + } + + public void doOrganizeImports(final IXtextDocument document) { + List result = document.priorityReadOnly(new IUnitOfWork, XtextResource>() { + @Override + public List exec(XtextResource state) throws Exception { + return importOrganizer.getOrganizedImportChanges(state); + } + }); + if (result == null || result.isEmpty()) + return; + try { + DocumentRewriteSession session = null; + if(document instanceof IDocumentExtension4) { + session = ((IDocumentExtension4)document).startRewriteSession(DocumentRewriteSessionType.UNRESTRICTED); + } + replaceConverter.convertToTextEdit(result).apply(document); + if(session != null) { + ((IDocumentExtension4)document).stopRewriteSession(session); + } + } catch (BadLocationException e) { + LOG.error("Error organizing imports:", e); + } + } + +} diff --git a/plugins/org.eclipse.etrice.core.room.ui/plugin.xml b/plugins/org.eclipse.etrice.core.room.ui/plugin.xml index 311b0a822..fa2a92bc6 100644 --- a/plugins/org.eclipse.etrice.core.room.ui/plugin.xml +++ b/plugins/org.eclipse.etrice.core.room.ui/plugin.xml @@ -16,6 +16,7 @@ name="Room Editor"> + + + + + + + + @@ -121,6 +132,19 @@ + + + + + + + + diff --git a/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/RoomUiModule.java b/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/RoomUiModule.java index 06f9aeabb..8562599c7 100644 --- a/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/RoomUiModule.java +++ b/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/RoomUiModule.java @@ -21,11 +21,13 @@ import org.eclipse.etrice.core.common.ui.editor.folding.FoldingRegionProvider; import org.eclipse.etrice.core.common.ui.editor.model.BaseTokenTypeToPartitionMapper; import org.eclipse.etrice.core.common.ui.hover.BaseHoverDocumentationProvider; import org.eclipse.etrice.core.common.ui.hover.IKeywordHoverContentProvider; +import org.eclipse.etrice.core.common.ui.imports.IOrganizeImportHelper; import org.eclipse.etrice.core.common.ui.linking.GlobalNonPlatformURIEditorOpener; import org.eclipse.etrice.core.ui.highlight.RoomHighlightingConfiguration; import org.eclipse.etrice.core.ui.highlight.RoomSemanticHighlightingCalculator; import org.eclipse.etrice.core.ui.hover.RoomEObjectHover; import org.eclipse.etrice.core.ui.hover.RoomHoverProvider; +import org.eclipse.etrice.core.ui.imports.RoomOrganizeImportHelper; import org.eclipse.etrice.core.ui.internal.RoomActivator; import org.eclipse.etrice.core.ui.linking.RoomHyperlinkHelper; import org.eclipse.etrice.core.ui.outline.RoomOutlinePage; @@ -68,6 +70,9 @@ public class RoomUiModule extends org.eclipse.etrice.core.ui.AbstractRoomUiModul binder.bind(IEObjectDocumentationProviderExtension.class).to(MultiLineCommentDocumentationProvider.class); binder.bind(FQNPrefixMatcher.LastSegmentFinder.class).to(FQNLastSegmentFinder.class); + + // namespace provider for OrganizeImports + binder.bind(IOrganizeImportHelper.class).to(RoomOrganizeImportHelper.class); } @Override diff --git a/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/imports/RoomOrganizeImportHelper.java b/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/imports/RoomOrganizeImportHelper.java new file mode 100644 index 000000000..eaa492033 --- /dev/null +++ b/plugins/org.eclipse.etrice.core.room.ui/src/org/eclipse/etrice/core/ui/imports/RoomOrganizeImportHelper.java @@ -0,0 +1,114 @@ +/******************************************************************************* + * 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.ui.imports; + +import java.util.List; +import java.util.stream.Collectors; + +import org.eclipse.emf.ecore.EClass; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.EReference; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.etrice.core.common.base.util.ImportHelpers; +import org.eclipse.etrice.core.common.ui.imports.IOrganizeImportHelper; +import org.eclipse.etrice.core.room.RoomClass; +import org.eclipse.etrice.core.room.RoomModel; +import org.eclipse.etrice.core.room.RoomPackage; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.naming.QualifiedName; +import org.eclipse.xtext.nodemodel.ICompositeNode; +import org.eclipse.xtext.nodemodel.util.NodeModelUtils; +import org.eclipse.xtext.resource.XtextResource; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; +import com.google.inject.Inject; + +/** + * @author Henrik Rentz-Reichert + * + */ +public class RoomOrganizeImportHelper implements IOrganizeImportHelper { + + @Inject + ImportHelpers importHelpers; + + @Override + public Multimap getTypeReferences() { + Multimap result = ArrayListMultimap.create(); + RoomPackage.eINSTANCE.getEClassifiers().stream() // + .filter(EClass.class::isInstance) // + .map(EClass.class::cast) // + .forEach(cls -> { + List refs = cls.getEReferences().stream() // + .filter(ref -> !ref.isContainment() && typeNeedsImport(ref.getEReferenceType())) // + .collect(Collectors.toList()); + if (!refs.isEmpty()) { + result.putAll(cls, refs); + } + }); + return result; + } + + @Override + public QualifiedName getFullyQualifiedName(EObject object) { + if (object instanceof RoomClass) { + EObject root = EcoreUtil2.getRootContainer(object); + if (root instanceof RoomModel) { + QualifiedName result = QualifiedName.create(((RoomModel) root).getName().split("\\.")); + result = result.append(((RoomClass) object).getName()); + return result; + } + } + else if (object instanceof RoomModel) { + return QualifiedName.create(((RoomModel) object).getName().split("\\.")); + } + return null; + } + + @Override + public List resolveFullyQualifiedName(String refText, EClass type, Resource resource) { + return importHelpers.createModelPathImports(refText, resource, type, false).stream() // + .map(imp -> importHelpers.toFQN(imp)).collect(Collectors.toList()); + } + + public ImportRegionResult getImportRegion(XtextResource resource) { + ImportRegionResult importRegion = IOrganizeImportHelper.super.getImportRegion(resource); + if (importRegion==null) { + // there is no import yet: check annotation types + if (resource.getContents().get(0) instanceof RoomModel) { + RoomModel roomModel = (RoomModel) resource.getContents().get(0); + EObject placeBefore = null; + if (!roomModel.getAnnotationTypes().isEmpty()) { + placeBefore = roomModel.getAnnotationTypes().get(0); + } + else if (!roomModel.getRoomClasses().isEmpty()) { + placeBefore = roomModel.getRoomClasses().get(0); + } + if (placeBefore!=null) { + ICompositeNode node = NodeModelUtils.findActualNodeFor(placeBefore); + if (node!=null) { + return new ImportRegionResult(node.getOffset(), 0, true); + } + } + } + } + return importRegion; + } + + private boolean typeNeedsImport(EClass type) { + return RoomPackage.Literals.ROOM_CLASS.isSuperTypeOf(type); + } +} -- cgit v1.2.3