/******************************************************************************* * Copyright (c) 2012-2015 Codenvy, S.A. * 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 * * Contributors: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.jseditor.java.client.editor; import org.eclipse.che.ide.api.icon.Icon; import org.eclipse.che.ide.api.text.Position; import org.eclipse.che.ide.api.text.annotation.Annotation; import org.eclipse.che.ide.collections.Array; import org.eclipse.che.ide.collections.js.JsoArray; import org.eclipse.che.ide.ext.java.client.JavaResources; import org.eclipse.che.ide.ext.java.client.editor.JavaAnnotation; import org.eclipse.che.ide.ext.java.client.editor.JavaAnnotationUtil; import org.eclipse.che.ide.ext.java.client.editor.JavaParserWorker; import org.eclipse.che.ide.ext.java.client.editor.ProblemAnnotation; import org.eclipse.che.ide.ext.java.jdt.core.IJavaModelMarker; import org.eclipse.che.ide.ext.java.messages.ProblemLocationMessage; import org.eclipse.che.ide.ext.java.messages.WorkerProposal; import org.eclipse.che.ide.ext.java.messages.impl.MessagesImpls; import org.eclipse.che.ide.jseditor.client.annotation.QueryAnnotationsEvent; import org.eclipse.che.ide.jseditor.client.annotation.QueryAnnotationsEvent.AnnotationFilter; import org.eclipse.che.ide.jseditor.client.annotation.QueryAnnotationsEvent.QueryCallback; import org.eclipse.che.ide.jseditor.client.codeassist.CodeAssistCallback; import org.eclipse.che.ide.jseditor.client.codeassist.CompletionProposal; import org.eclipse.che.ide.jseditor.client.document.Document; import org.eclipse.che.ide.jseditor.client.quickfix.QuickAssistInvocationContext; import org.eclipse.che.ide.jseditor.client.quickfix.QuickAssistProcessor; import org.eclipse.che.ide.jseditor.client.text.LinearRange; import org.eclipse.che.ide.jseditor.client.texteditor.TextEditor; import javax.inject.Inject; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * {@link QuickAssistProcessor} for java files. */ public class JavaQuickAssistProcessor implements QuickAssistProcessor { /** The java parser worker. */ private final JavaParserWorker worker; /** The resources used for java assistants. */ private final JavaResources javaResources; @Inject public JavaQuickAssistProcessor(final JavaParserWorker worker, final JavaResources javaResources) { this.worker = worker; this.javaResources = javaResources; } @Override public void computeQuickAssistProposals(final QuickAssistInvocationContext quickAssistContext, final CodeAssistCallback callback) { final TextEditor textEditor = quickAssistContext.getTextEditor(); final Document document = textEditor.getDocument(); LinearRange tempRange; if (quickAssistContext.getLine() != null) { tempRange = document.getLinearRangeForLine(quickAssistContext.getLine()); } else { tempRange = textEditor.getSelectedLinearRange(); } final LinearRange range = tempRange; final boolean goToClosest = (range.getLength() == 0); final AnnotationFilter filter = new AnnotationFilter() { @Override public boolean accept(final Annotation annotation) { if (!(annotation instanceof JavaAnnotation)) { return false; } else { JavaAnnotation javaAnnotation = (JavaAnnotation)annotation; return (!javaAnnotation.isMarkedDeleted()) && JavaAnnotationUtil.hasCorrections(annotation); } } }; final QueryCallback queryCallback = new QueryCallback() { @Override public void respond(final Map<Annotation, Position> annotations) { final Map<Annotation, Position> problems = collectQuickFixableAnnotations(range, annotations, goToClosest); setupProposals(callback, textEditor, range, problems); } }; final QueryAnnotationsEvent event = new QueryAnnotationsEvent.Builder().withFilter(filter).withCallback(queryCallback).build(); document.getDocumentHandle().getDocEventBus().fireEvent(event); } private void setupProposals(final CodeAssistCallback callback, final TextEditor textEditor, final LinearRange range, final Map<Annotation, Position> annotations) { final JsoArray<ProblemLocationMessage> problems = JsoArray.create(); // collect problem locations and corrections from marker annotations if (annotations != null) { for (final Entry<Annotation, Position> entry : annotations.entrySet()) { final Annotation annotation = entry.getKey(); if (annotation instanceof JavaAnnotation) { final ProblemLocationMessage problemLocation = getProblemLocation((JavaAnnotation)annotation, entry.getValue()); if (problemLocation != null) { problems.add(problemLocation); } } } } worker.computeQAProposals(textEditor.getDocument().getContents(), range.getStartOffset(), range.getLength(), false, problems, textEditor.getEditorInput().getFile().getPath(), new JavaParserWorker.WorkerCallback<WorkerProposal>() { @Override public void onResult(final Array<WorkerProposal> problems) { final List<CompletionProposal> proposals = buildProposals(problems); callback.proposalComputed(proposals); } }); } private static ProblemLocationMessage getProblemLocation(final JavaAnnotation javaAnnotation, final Position position) { final int problemId = javaAnnotation.getId(); if (problemId != -1 && position != null) { final MessagesImpls.ProblemLocationMessageImpl problemLocations = MessagesImpls.ProblemLocationMessageImpl.make(); problemLocations.setOffset(position.getOffset()).setLength(position.getLength()); problemLocations.setIsError(ProblemAnnotation.ERROR_ANNOTATION_TYPE.equals(javaAnnotation.getType())); final String markerType = javaAnnotation.getMarkerType(); if (markerType != null) { problemLocations.setMarkerType(markerType); } else { problemLocations.setMarkerType(IJavaModelMarker.JAVA_MODEL_PROBLEM_MARKER); } problemLocations.setProblemId(javaAnnotation.getId()); if (javaAnnotation.getArguments() != null) { problemLocations.setProblemArguments(JsoArray.from(javaAnnotation.getArguments())); } else { problemLocations.setProblemArguments(null); } return problemLocations; } else { return null; } } private List<CompletionProposal> buildProposals(final Array<WorkerProposal> problems) { final List<CompletionProposal> proposals = new ArrayList<>(); for (final WorkerProposal problem : problems.asIterable()) { final String style = JavaCodeAssistProcessor.insertStyle(javaResources, problem.displayText()); final Icon icon = new Icon("", JavaCodeAssistProcessor.getImage(javaResources, problem.image())); final CompletionProposal proposal = new JavaCompletionProposal(problem.id(), style, icon, worker); proposals.add(proposal); } return proposals; } private static Map<Annotation, Position> collectQuickFixableAnnotations(final LinearRange lineRange, final Map<Annotation, Position> annotations, final boolean goToClosest) { if (goToClosest) { final int rangeStart = lineRange.getStartOffset(); final int rangeEnd = rangeStart + lineRange.getLength(); final ArrayList<Annotation> allAnnotations = new ArrayList<Annotation>(); int bestOffset = Integer.MAX_VALUE; for (Annotation annotation : annotations.keySet()) { if (JavaAnnotationUtil.isQuickFixableType(annotation)) { final Position pos = annotations.get(annotation); if (pos != null && isInside(pos.offset, rangeStart, rangeEnd)) { // inside our range? allAnnotations.add(annotation); bestOffset = processAnnotation(annotation, pos, lineRange.getStartOffset(), bestOffset); } } } if (bestOffset == Integer.MAX_VALUE) { return null; } final Map<Annotation, Position> result = new HashMap<>(); for (final Annotation annotation : allAnnotations) { final Position pos = annotations.get(annotation); if (isInside(bestOffset, pos.offset, pos.offset + pos.length)) { result.put(annotation, pos); } } return result; } else { final Map<Annotation, Position> result = new HashMap<>(); for (final Annotation annotation : annotations.keySet()) { if (JavaAnnotationUtil.isQuickFixableType(annotation)) { final Position pos = annotations.get(annotation); final int lineStart = lineRange.getStartOffset(); final int lineEnd = lineRange.getStartOffset() + lineRange.getLength(); if (isInside(pos.getOffset(), lineStart, lineEnd) || isInside(pos.getOffset() + pos.getLength(), lineStart, lineEnd)) { result.put(annotation, pos); } } } if (result.isEmpty()) { return null; } else { return result; } } } private static int processAnnotation(Annotation annot, Position pos, int invocationLocation, int bestOffset) { final int posBegin = pos.offset; final int posEnd = posBegin + pos.length; if (isInside(invocationLocation, posBegin, posEnd)) { // covers invocation location? return invocationLocation; } else if (bestOffset != invocationLocation) { final int newClosestPosition = computeBestOffset(posBegin, invocationLocation, bestOffset); if (newClosestPosition != -1) { if (newClosestPosition != bestOffset) { // new best if (JavaAnnotationUtil.hasCorrections(annot)) { // only jump to it if there are proposals return newClosestPosition; } } } } return bestOffset; } /** * Computes and returns the invocation offset given a new position, the initial offset and the best invocation offset found so far. * <p> * The closest offset to the left of the initial offset is the best. If there is no offset on the left, the closest on the right is the * best. * </p> * * @param newOffset the offset to llok at * @param invocationLocation the invocation location * @param bestOffset the current best offset * @return -1 is returned if the given offset is not closer or the new best offset */ private static int computeBestOffset(int newOffset, int invocationLocation, int bestOffset) { if (newOffset <= invocationLocation) { if (bestOffset > invocationLocation) { return newOffset; // closest was on the right, prefer on the left } else if (bestOffset <= newOffset) { return newOffset; // we are closer or equal } return -1; // further away } if (newOffset <= bestOffset) { return newOffset; // we are closer or equal } return -1; // further away } /** * Tells is the offset is inside the (inclusive) range defined by start-end. * * @param offset the offset * @param start the start of the range * @param end the end of the range * @return true iff offset is inside */ private static boolean isInside(int offset, int start, int end) { return offset == start || offset == end || (offset > start && offset < end); // make sure to handle 0-length ranges } }