/******************************************************************************* * Copyright (c) 2012-2017 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.ext.java.client.editor; import com.google.common.base.Optional; import org.eclipse.che.ide.api.editor.EditorAgent; import org.eclipse.che.ide.api.editor.annotation.QueryAnnotationsEvent; import org.eclipse.che.ide.api.editor.codeassist.CodeAssistCallback; import org.eclipse.che.ide.api.editor.codeassist.CompletionProposal; import org.eclipse.che.ide.api.editor.document.Document; import org.eclipse.che.ide.api.editor.link.HasLinkedMode; import org.eclipse.che.ide.api.editor.quickfix.QuickAssistInvocationContext; import org.eclipse.che.ide.api.editor.quickfix.QuickAssistProcessor; import org.eclipse.che.ide.api.editor.text.LinearRange; import org.eclipse.che.ide.api.editor.text.Position; import org.eclipse.che.ide.api.editor.text.annotation.Annotation; import org.eclipse.che.ide.api.editor.texteditor.TextEditor; import org.eclipse.che.ide.api.resources.Project; import org.eclipse.che.ide.api.resources.Resource; import org.eclipse.che.ide.api.resources.VirtualFile; import org.eclipse.che.ide.dto.DtoFactory; import org.eclipse.che.ide.ext.java.client.JavaResources; import org.eclipse.che.ide.ext.java.client.action.ProposalAction; import org.eclipse.che.ide.ext.java.client.refactoring.RefactoringUpdater; import org.eclipse.che.ide.ext.java.shared.dto.Problem; import org.eclipse.che.ide.ext.java.shared.dto.ProposalPresentation; import org.eclipse.che.ide.ext.java.shared.dto.Proposals; import org.eclipse.che.ide.rest.AsyncRequestCallback; import org.eclipse.che.ide.rest.DtoUnmarshallerFactory; import org.eclipse.che.ide.rest.Unmarshallable; import org.eclipse.che.ide.util.loging.Log; import javax.inject.Inject; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import static org.eclipse.che.ide.ext.java.client.editor.JavaCodeAssistProcessor.insertStyle; import static org.eclipse.che.ide.ext.java.client.util.JavaUtil.resolveFQN; /** * {@link QuickAssistProcessor} for java files. */ public class JavaQuickAssistProcessor implements QuickAssistProcessor { private final JavaCodeAssistClient client; /** The resources used for java assistants. */ private final JavaResources javaResources; private final Map<String, ProposalAction> proposalActions; private final DtoUnmarshallerFactory unmarshallerFactory; private final DtoFactory dtoFactory; private final RefactoringUpdater refactoringUpdater; private final EditorAgent editorAgent; @Inject public JavaQuickAssistProcessor(final JavaCodeAssistClient client, final JavaResources javaResources, Map<String, ProposalAction> proposalActions, DtoUnmarshallerFactory unmarshallerFactory, DtoFactory dtoFactory, RefactoringUpdater refactoringUpdater, EditorAgent editorAgent) { this.client = client; this.javaResources = javaResources; this.proposalActions = proposalActions; this.unmarshallerFactory = unmarshallerFactory; this.dtoFactory = dtoFactory; this.refactoringUpdater = refactoringUpdater; this.editorAgent = editorAgent; } @Override public void computeQuickAssistProposals(final QuickAssistInvocationContext quickAssistContext, final CodeAssistCallback callback) { final TextEditor textEditor = quickAssistContext.getTextEditor(); final Document document = textEditor.getDocument(); LinearRange tempRange; tempRange = textEditor.getSelectedLinearRange(); final LinearRange range = tempRange; final boolean goToClosest = (range.getLength() == 0); final QueryAnnotationsEvent.AnnotationFilter filter = new QueryAnnotationsEvent.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 QueryAnnotationsEvent.QueryCallback queryCallback = new QueryAnnotationsEvent.QueryCallback() { @Override public void respond(final Map<Annotation, Position> annotations) { List<Problem> problems = new ArrayList<>(); /*final Map<Annotation, Position> problems =*/ int offset = collectQuickFixableAnnotations(range, document, annotations, goToClosest, problems); if (offset != range.getStartOffset()) { TextEditor presenter = ((TextEditor)textEditor); presenter.getCursorModel().setCursorPosition(offset); } setupProposals(callback, textEditor, offset, problems); } }; final QueryAnnotationsEvent event = new QueryAnnotationsEvent.Builder().withFilter(filter).withCallback(queryCallback).build(); document.getDocumentHandle().getDocEventBus().fireEvent(event); } private void showProposals(final CodeAssistCallback callback, final Proposals responds, TextEditor editor) { List<ProposalPresentation> presentations = responds.getProposals(); final List<CompletionProposal> proposals = new ArrayList<>(presentations.size()); HasLinkedMode linkedEditor = editor instanceof HasLinkedMode ? (HasLinkedMode)editor : null; for (ProposalPresentation proposal : presentations) { CompletionProposal completionProposal; String actionId = proposal.getActionId(); if (actionId != null) { ProposalAction action = proposalActions.get(actionId); completionProposal = new ActionCompletionProposal(insertStyle(javaResources, proposal.getDisplayString()), actionId, action, JavaCodeAssistProcessor.getIcon(proposal.getImage()) ); } else { completionProposal = new JavaCompletionProposal(proposal.getIndex(), insertStyle(javaResources, proposal.getDisplayString()), JavaCodeAssistProcessor.getIcon(proposal.getImage()), client, responds.getSessionId(), linkedEditor, refactoringUpdater, editorAgent); } proposals.add(completionProposal); } callback.proposalComputed(proposals); } private void setupProposals(final CodeAssistCallback callback, final TextEditor textEditor, final int offset, final List<Problem> annotations) { final VirtualFile file = textEditor.getEditorInput().getFile(); if (file instanceof Resource) { final Optional<Project> project = ((Resource)file).getRelatedProject(); Unmarshallable<Proposals> unmarshaller = unmarshallerFactory.newUnmarshaller(Proposals.class); client.computeAssistProposals(project.get().getLocation().toString(), resolveFQN(file), offset, annotations, new AsyncRequestCallback<Proposals>(unmarshaller) { @Override protected void onSuccess(Proposals proposals) { showProposals(callback, proposals, textEditor); } @Override protected void onFailure(Throwable throwable) { Log.error(JavaCodeAssistProcessor.class, throwable); } }); } } private int collectQuickFixableAnnotations(final LinearRange lineRange, Document document, final Map<Annotation, Position> annotations, final boolean goToClosest, List<Problem> resultingProblems) { int invocationLocation = lineRange.getStartOffset(); if (goToClosest) { LinearRange line = document.getLinearRangeForLine(document.getPositionFromIndex(lineRange.getStartOffset()).getLine()); int rangeStart = line.getStartOffset(); int rangeEnd = rangeStart + line.getLength(); ArrayList<Position> allPositions = new ArrayList<>(); List<JavaAnnotation> allAnnotations = new ArrayList<>(); int bestOffset = Integer.MAX_VALUE; for (Annotation problem : annotations.keySet()) { if (problem instanceof JavaAnnotation) { JavaAnnotation ann = ((JavaAnnotation)problem); Position pos = annotations.get(problem); if (pos != null && isInside(pos.offset, rangeStart, rangeEnd)) { // inside our range? allAnnotations.add(ann); allPositions.add(pos); bestOffset = processAnnotation(problem, pos, invocationLocation, bestOffset); } } } if (bestOffset == Integer.MAX_VALUE) { return invocationLocation; } for (int i = 0; i < allPositions.size(); i++) { Position pos = allPositions.get(i); if (isInside(bestOffset, pos.offset, pos.offset + pos.length)) { resultingProblems.add(createProblem(allAnnotations.get(i), pos)); } } return bestOffset; } else { for (Annotation problem : annotations.keySet()) { Position pos = annotations.get(problem); if (pos != null && isInside(invocationLocation, pos.offset, pos.offset + pos.length)) { resultingProblems.add(createProblem((JavaAnnotation)problem, pos)); } } return invocationLocation; } } private Problem createProblem(JavaAnnotation javaAnnotation, Position pos) { Problem problem = dtoFactory.createDto(Problem.class); //server use only this fields problem.setID(javaAnnotation.getId()); problem.setError(javaAnnotation.isError()); problem.setArguments(Arrays.asList(javaAnnotation.getArguments())); problem.setSourceStart(pos.getOffset()); //TODO I don't know why but in that place source end is bugger on 1 char problem.setSourceEnd(pos.getOffset() + pos.getLength() - 1); return problem; } 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 } }