/******************************************************************************* * Copyright (c) 2019 Microsoft Corporation and others. * 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: * Microsoft Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.jdt.ls.core.internal.handlers; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.stream.Stream; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.jdt.core.ICompilationUnit; import org.eclipse.jdt.core.IJavaProject; import org.eclipse.jdt.core.ISourceRange; import org.eclipse.jdt.core.JavaModelException; import org.eclipse.jdt.core.Signature; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.core.dom.Name; import org.eclipse.jdt.core.dom.SimpleName; import org.eclipse.jdt.core.dom.rewrite.ASTRewrite; import org.eclipse.jdt.core.dom.rewrite.ImportRewrite; import org.eclipse.jdt.core.manipulation.CodeStyleConfiguration; import org.eclipse.jdt.core.manipulation.ImportReferencesCollector; import org.eclipse.jdt.core.manipulation.OrganizeImportsOperation; import org.eclipse.jdt.core.search.TypeNameMatch; import org.eclipse.jdt.internal.corext.codemanipulation.ContextSensitiveImportRewriteContext; import org.eclipse.jdt.internal.corext.dom.IASTSharedValues; import org.eclipse.jdt.internal.corext.refactoring.util.RefactoringASTParser; import org.eclipse.jdt.internal.corext.util.JavaModelUtil; import org.eclipse.jdt.ls.core.internal.JDTUtils; import org.eclipse.jdt.ls.core.internal.JSONUtility; import org.eclipse.jdt.ls.core.internal.JavaClientConnection; import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin; import org.eclipse.jdt.ls.core.internal.JobHelpers; import org.eclipse.jdt.ls.core.internal.corrections.SimilarElementsRequestor; import org.eclipse.jdt.ls.core.internal.preferences.PreferenceManager; import org.eclipse.jdt.ls.core.internal.text.correction.SourceAssistProcessor; import org.eclipse.lsp4j.CodeActionParams; import org.eclipse.lsp4j.Range; import org.eclipse.lsp4j.WorkspaceEdit; import org.eclipse.text.edits.DeleteEdit; import org.eclipse.text.edits.InsertEdit; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.TextEdit; import com.google.gson.Gson; public final class OrganizeImportsHandler { public static final String CLIENT_COMMAND_ID_CHOOSEIMPORTS = "java.action.organizeImports.chooseImports"; public static TextEdit organizeImports(ICompilationUnit unit, Function<ImportSelection[], ImportCandidate[]> chooseImports) { if (unit == null) { return null; } RefactoringASTParser astParser = new RefactoringASTParser(IASTSharedValues.SHARED_AST_LEVEL); CompilationUnit astRoot = astParser.parse(unit, true); OrganizeImportsOperation op = new OrganizeImportsOperation(unit, astRoot, true, false, true, (TypeNameMatch[][] openChoices, ISourceRange[] ranges) -> { List<ImportSelection> selections = new ArrayList<>(); for (int i = 0; i < openChoices.length; i++) { ImportCandidate[] candidates = Stream.of(openChoices[i]).map((choice) -> new ImportCandidate(choice)).toArray(ImportCandidate[]::new); Range range = null; try { range = JDTUtils.toRange(unit, ranges[i].getOffset(), ranges[i].getLength()); } catch (JavaModelException e) { range = JDTUtils.newRange(); } // TODO Sort the ambiguous candidates based on a relevance score. selections.add(new ImportSelection(candidates, range)); } ImportCandidate[] chosens = chooseImports.apply(selections.toArray(new ImportSelection[0])); if (chosens == null) { return null; } Map<String, TypeNameMatch> typeMaps = new HashMap<>(); Stream.of(openChoices).flatMap(x -> Arrays.stream(x)).forEach(x -> { typeMaps.put(x.getFullyQualifiedName() + "@" + x.hashCode(), x); }); return Stream.of(chosens).filter(chosen -> chosen != null && typeMaps.containsKey(chosen.id)).map(chosen -> typeMaps.get(chosen.id)).toArray(TypeNameMatch[]::new); }); try { JobHelpers.waitForJobs(DocumentLifeCycleHandler.DOCUMENT_LIFE_CYCLE_JOBS, new NullProgressMonitor()); TextEdit edit = op.createTextEdit(null); // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=283287 TextEdit staticEdit = wrapStaticImports(edit, astRoot, unit); if (staticEdit.getChildrenSize() == 0) { return null; } return staticEdit; } catch (OperationCanceledException | CoreException e) { JavaLanguageServerPlugin.logException("Failed to resolve organize imports source action", e); } return null; } public static TextEdit wrapStaticImports(TextEdit edit, CompilationUnit root, ICompilationUnit unit) throws MalformedTreeException, CoreException { String[] favourites = PreferenceManager.getPrefs(unit.getResource()).getJavaCompletionFavoriteMembers(); if (favourites.length == 0) { return edit; } IJavaProject project = unit.getJavaProject(); if (JavaModelUtil.is50OrHigher(project)) { List<SimpleName> typeReferences = new ArrayList<>(); List<SimpleName> staticReferences = new ArrayList<>(); ImportReferencesCollector.collect(root, project, null, typeReferences, staticReferences); if (staticReferences.isEmpty()) { return edit; } ImportRewrite importRewrite = CodeStyleConfiguration.createImportRewrite(root, true); AST ast = root.getAST(); ASTRewrite astRewrite = ASTRewrite.create(ast); for (SimpleName node : staticReferences) { addImports(root, unit, favourites, importRewrite, ast, astRewrite, node, true); addImports(root, unit, favourites, importRewrite, ast, astRewrite, node, false); } TextEdit staticEdit = importRewrite.rewriteImports(null); if (staticEdit != null && staticEdit.getChildrenSize() > 0) { TextEdit lastStatic = staticEdit.getChildren()[staticEdit.getChildrenSize() - 1]; if (lastStatic instanceof DeleteEdit) { if (edit.getChildrenSize() > 0) { TextEdit last = edit.getChildren()[edit.getChildrenSize() - 1]; if (last instanceof DeleteEdit && lastStatic.getOffset() == last.getOffset() && lastStatic.getLength() == last.getLength()) { edit.removeChild(last); } } } TextEdit firstStatic = staticEdit.getChildren()[0]; if (firstStatic instanceof InsertEdit) { if (edit.getChildrenSize() > 0) { TextEdit firstEdit = edit.getChildren()[0]; if (firstEdit instanceof InsertEdit) { if (areEqual((InsertEdit) firstEdit, (InsertEdit) firstStatic)) { edit.removeChild(firstEdit); } } } } try { staticEdit.addChild(edit); return staticEdit; } catch (MalformedTreeException e) { JavaLanguageServerPlugin.logException("Failed to resolve static organize imports source action", e); } } } return edit; } private static boolean areEqual(InsertEdit edit1, InsertEdit edit2) { if (edit1 != null && edit2 != null) { return edit1.getOffset() == edit2.getOffset() && edit1.getLength() == edit2.getLength() && edit1.getText().equals(edit2.getText()); } return false; } private static void addImports(CompilationUnit root, ICompilationUnit unit, String[] favourites, ImportRewrite importRewrite, AST ast, ASTRewrite astRewrite, SimpleName node, boolean isMethod) throws JavaModelException { String name = node.getIdentifier(); String[] imports = SimilarElementsRequestor.getStaticImportFavorites(unit, name, isMethod, favourites); if (imports.length > 1) { // See https://github.com/redhat-developer/vscode-java/issues/1472 return; } for (int i = 0; i < imports.length; i++) { String curr = imports[i]; String qualifiedTypeName = Signature.getQualifier(curr); String res = importRewrite.addStaticImport(qualifiedTypeName, name, isMethod, new ContextSensitiveImportRewriteContext(root, node.getStartPosition(), importRewrite)); int dot = res.lastIndexOf('.'); if (dot != -1) { String usedTypeName = importRewrite.addImport(qualifiedTypeName); Name newName = ast.newQualifiedName(ast.newName(usedTypeName), ast.newSimpleName(name)); astRewrite.replace(node, newName, null); } } } public static WorkspaceEdit organizeImports(JavaClientConnection connection, CodeActionParams params) { String uri = params.getTextDocument().getUri(); final ICompilationUnit unit = JDTUtils.resolveCompilationUnit(params.getTextDocument().getUri()); if (unit == null) { return null; } TextEdit edit = organizeImports(unit, (selections) -> { Object commandResult = connection.executeClientCommand(CLIENT_COMMAND_ID_CHOOSEIMPORTS, uri, selections); String json = commandResult == null ? null : new Gson().toJson(commandResult); return JSONUtility.toModel(json, ImportCandidate[].class); }); return SourceAssistProcessor.convertToWorkspaceEdit(unit, edit); } public static class ImportCandidate { public String fullyQualifiedName; public String id; public ImportCandidate() { } public ImportCandidate(TypeNameMatch typeMatch) { fullyQualifiedName = typeMatch.getFullyQualifiedName(); id = typeMatch.getFullyQualifiedName() + "@" + typeMatch.hashCode(); } } public static class ImportSelection { public ImportCandidate[] candidates; public Range range; public ImportSelection(ImportCandidate[] candidates, Range range) { this.candidates = candidates; this.range = range; } } }