/******************************************************************************* * Copyright (c) 2000, 2008 IBM Corporation and others. * 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: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.wst.jsdt.internal.ui.text.correction; import java.util.Iterator; import org.eclipse.compare.rangedifferencer.IRangeComparator; import org.eclipse.compare.rangedifferencer.RangeDifference; import org.eclipse.compare.rangedifferencer.RangeDifferencer; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Status; import org.eclipse.jface.dialogs.ErrorDialog; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.Document; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITextViewer; import org.eclipse.jface.text.contentassist.ICompletionProposalExtension2; import org.eclipse.jface.text.contentassist.IContextInformation; import org.eclipse.jface.text.link.ILinkedModeListener; import org.eclipse.jface.text.link.LinkedModeModel; import org.eclipse.jface.text.link.LinkedModeUI; import org.eclipse.jface.text.link.LinkedPosition; import org.eclipse.jface.text.link.LinkedPositionGroup; import org.eclipse.jface.text.link.ProposalPosition; import org.eclipse.jface.text.link.LinkedModeUI.ExitFlags; import org.eclipse.ltk.core.refactoring.Change; import org.eclipse.ltk.core.refactoring.DocumentChange; import org.eclipse.ltk.core.refactoring.TextChange; import org.eclipse.ltk.core.refactoring.TextFileChange; import org.eclipse.swt.events.VerifyEvent; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.text.edits.MalformedTreeException; import org.eclipse.text.edits.MultiTextEdit; import org.eclipse.text.edits.TextEdit; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.texteditor.ITextEditor; import org.eclipse.ui.texteditor.link.EditorLinkedModeUI; import org.eclipse.wst.jsdt.core.IJavaScriptUnit; import org.eclipse.wst.jsdt.core.JavaScriptModelException; import org.eclipse.wst.jsdt.internal.corext.codemanipulation.StubUtility; import org.eclipse.wst.jsdt.internal.corext.fix.LinkedProposalModel; import org.eclipse.wst.jsdt.internal.corext.fix.LinkedProposalPositionGroup; import org.eclipse.wst.jsdt.internal.corext.refactoring.changes.CompilationUnitChange; import org.eclipse.wst.jsdt.internal.corext.util.Resources; import org.eclipse.wst.jsdt.internal.corext.util.Strings; import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin; import org.eclipse.wst.jsdt.internal.ui.JavaUIStatus; import org.eclipse.wst.jsdt.internal.ui.compare.JavaTokenComparator; import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorHighlightingSynchronizer; import org.eclipse.wst.jsdt.internal.ui.javaeditor.EditorUtility; import org.eclipse.wst.jsdt.internal.ui.javaeditor.JavaEditor; import org.eclipse.wst.jsdt.internal.ui.util.ExceptionHandler; import org.eclipse.wst.jsdt.ui.JavaScriptUI; import org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposal; /** * A proposal for quick fixes and quick assist that work on a single compilation unit. * Either a {@link TextChange text change} is directly passed in the constructor or method * {@link #addEdits(IDocument, TextEdit)} is overridden to provide the text edits that are * applied to the document when the proposal is evaluated. * <p> * The proposal takes care of the preview of the changes as proposal information. * </p> * */ public class CUCorrectionProposal extends ChangeCorrectionProposal { private IJavaScriptUnit fCompilationUnit; private LinkedProposalModel fLinkedProposalModel; /** * Constructs a correction proposal working on a compilation unit with a given text change * * @param name the name that is displayed in the proposal selection dialog. * @param cu the compilation unit on that the change works. * @param change the change that is executed when the proposal is applied or <code>null</code> * if implementors override {@link #addEdits(IDocument, TextEdit)} to provide * the text edits or {@link #createTextChange()} to provide a text change. * @param relevance the relevance of this proposal. * @param image the image that is displayed for this proposal or <code>null</code> if no * image is desired. */ public CUCorrectionProposal(String name, IJavaScriptUnit cu, TextChange change, int relevance, Image image) { super(name, change, relevance, image); if (cu == null) { throw new IllegalArgumentException("Compilation unit must not be null"); //$NON-NLS-1$ } fCompilationUnit= cu; fLinkedProposalModel= null; } /** * Constructs a correction proposal working on a compilation unit. * <p>Users have to override {@link #addEdits(IDocument, TextEdit)} to provide * the text edits or {@link #createTextChange()} to provide a text change. * </p> * * @param name The name that is displayed in the proposal selection dialog. * @param cu The compilation unit on that the change works. * @param relevance The relevance of this proposal. * @param image The image that is displayed for this proposal or <code>null</code> if no * image is desired. */ protected CUCorrectionProposal(String name, IJavaScriptUnit cu, int relevance, Image image) { this(name, cu, null, relevance, image); } /** * Called when the {@link CompilationUnitChange} is initialized. Subclasses can override to * add text edits to the root edit of the change. Implementors must not access the proposal, * e.g getting the change. * <p>The default implementation does not add any edits</p> * * @param document content of the underlying compilation unit. To be accessed read only. * @param editRoot The root edit to add all edits to * @throws CoreException can be thrown if adding the edits is failing. */ protected void addEdits(IDocument document, TextEdit editRoot) throws CoreException { if (false) { throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, "Implementors can throw an exception", null)); //$NON-NLS-1$ } } protected LinkedProposalModel getLinkedProposalModel() { if (fLinkedProposalModel == null) { fLinkedProposalModel= new LinkedProposalModel(); } return fLinkedProposalModel; } protected void setLinkedProposalModel(LinkedProposalModel model) { fLinkedProposalModel= model; } /* * @see ICompletionProposal#getAdditionalProposalInfo() */ public String getAdditionalProposalInfo() { StringBuffer buf= new StringBuffer(); try { TextChange change= getTextChange(); IDocument previewContent= change.getPreviewDocument(new NullProgressMonitor()); String currentConentString= change.getCurrentContent(new NullProgressMonitor()); /* * Do not change the type of those local variables. We use Object * here in order to prevent loading of the Compare plug-in at load * time of this class. */ Object leftSide= new JavaTokenComparator(previewContent.get()); Object rightSide= new JavaTokenComparator(currentConentString); RangeDifference[] differences= RangeDifferencer.findRanges((IRangeComparator)leftSide, (IRangeComparator)rightSide); for (int i= 0; i < differences.length; i++) { RangeDifference curr= differences[i]; int start= ((JavaTokenComparator)leftSide).getTokenStart(curr.leftStart()); int end= ((JavaTokenComparator)leftSide).getTokenStart(curr.leftEnd()); if (curr.kind() == RangeDifference.CHANGE && curr.leftLength() > 0) { buf.append("<b>"); //$NON-NLS-1$ appendContent(previewContent, start, end, buf, false); buf.append("</b>"); //$NON-NLS-1$ } else if (curr.kind() == RangeDifference.NOCHANGE) { appendContent(previewContent, start, end, buf, true); } } } catch (CoreException e) { JavaScriptPlugin.log(e); } catch (BadLocationException e) { JavaScriptPlugin.log(e); } return buf.toString(); } private final int surroundLines= 1; private void appendContent(IDocument text, int startOffset, int endOffset, StringBuffer buf, boolean surroundLinesOnly) throws BadLocationException { int startLine= text.getLineOfOffset(startOffset); int endLine= text.getLineOfOffset(endOffset); boolean dotsAdded= false; if (surroundLinesOnly && startOffset == 0) { // no surround lines for the top no-change range startLine= Math.max(endLine - surroundLines, 0); buf.append("...<br>"); //$NON-NLS-1$ dotsAdded= true; } for (int i= startLine; i <= endLine; i++) { if (surroundLinesOnly) { if ((i - startLine > surroundLines) && (endLine - i > surroundLines)) { if (!dotsAdded) { buf.append("...<br>"); //$NON-NLS-1$ dotsAdded= true; } else if (endOffset == text.getLength()) { return; // no surround lines for the bottom no-change range } continue; } } IRegion lineInfo= text.getLineInformation(i); int start= lineInfo.getOffset(); int end= start + lineInfo.getLength(); int from= Math.max(start, startOffset); int to= Math.min(end, endOffset); String content= text.get(from, to - from); if (surroundLinesOnly && (from == start) && Strings.containsOnlyWhitespaces(content)) { continue; // ignore empty lines except when range started in the middle of a line } for (int k= 0; k < content.length(); k++) { char ch= content.charAt(k); if (ch == '<') { buf.append("<"); //$NON-NLS-1$ } else if (ch == '>') { buf.append(">"); //$NON-NLS-1$ } else { buf.append(ch); } } if (to == end && to != endOffset) { // new line when at the end of the line, and not end of range buf.append("<br>"); //$NON-NLS-1$ } } } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposal#apply(org.eclipse.jface.text.IDocument) */ public void apply(IDocument document) { try { IJavaScriptUnit unit= getCompilationUnit(); IEditorPart part= null; if (unit.getResource().exists()) { boolean canEdit= performValidateEdit(unit); if (!canEdit) { return; } part= EditorUtility.isOpenInEditor(unit); if (part == null) { part= JavaScriptUI.openInEditor(unit); if (part != null) { document= JavaScriptUI.getDocumentProvider().getDocument(part.getEditorInput()); } } IWorkbenchPage page= JavaScriptPlugin.getActivePage(); if (page != null && part != null) { page.bringToTop(part); } if (part != null) { part.setFocus(); } } performChange(part, document); } catch (CoreException e) { ExceptionHandler.handle(e, CorrectionMessages.CUCorrectionProposal_error_title, CorrectionMessages.CUCorrectionProposal_error_message); } } private boolean performValidateEdit(IJavaScriptUnit unit) { IStatus status= Resources.makeCommittable(unit.getResource(), JavaScriptPlugin.getActiveWorkbenchShell()); if (!status.isOK()) { String label= CorrectionMessages.CUCorrectionProposal_error_title; String message= CorrectionMessages.CUCorrectionProposal_error_message; ErrorDialog.openError(JavaScriptPlugin.getActiveWorkbenchShell(), label, message, status); return false; } return true; } /* (non-Javadoc) * @see org.eclipse.wst.jsdt.internal.ui.text.correction.ChangeCorrectionProposal#performChange(org.eclipse.jface.text.IDocument, org.eclipse.ui.IEditorPart) */ protected void performChange(IEditorPart part, IDocument document) throws CoreException { try { super.performChange(part, document); if (part == null) { return; } if (fLinkedProposalModel != null) { if (fLinkedProposalModel.hasLinkedPositions() && part instanceof JavaEditor) { // enter linked mode ITextViewer viewer= ((JavaEditor) part).getViewer(); enterLinkedMode(viewer, part); } else if (part instanceof ITextEditor) { LinkedProposalPositionGroup.PositionInformation endPosition= fLinkedProposalModel.getEndPosition(); if (endPosition != null) { // select a result int pos= endPosition.getOffset() + endPosition.getLength(); ((ITextEditor) part).selectAndReveal(pos, 0); } } } } catch (BadLocationException e) { throw new CoreException(JavaUIStatus.createError(IStatus.ERROR, e)); } } private void enterLinkedMode(ITextViewer viewer, IEditorPart editor) throws BadLocationException { IDocument document= viewer.getDocument(); LinkedModeModel model= new LinkedModeModel(); boolean added= false; Iterator iterator= fLinkedProposalModel.getPositionGroupIterator(); while (iterator.hasNext()) { LinkedProposalPositionGroup curr= (LinkedProposalPositionGroup) iterator.next(); LinkedPositionGroup group= new LinkedPositionGroup(); LinkedProposalPositionGroup.PositionInformation[] positions= curr.getPositions(); if (positions.length > 0) { LinkedProposalPositionGroup.Proposal[] linkedModeProposals= curr.getProposals(); if (linkedModeProposals.length <= 1) { for (int i= 0; i < positions.length; i++) { LinkedProposalPositionGroup.PositionInformation pos= positions[i]; if (pos.getOffset() != -1) { group.addPosition(new LinkedPosition(document, pos.getOffset(), pos.getLength(), pos.getSequenceRank())); } } } else { LinkedPositionProposalImpl[] proposalImpls= new LinkedPositionProposalImpl[linkedModeProposals.length]; for (int i= 0; i < linkedModeProposals.length; i++) { proposalImpls[i]= new LinkedPositionProposalImpl(linkedModeProposals[i], model); } for (int i= 0; i < positions.length; i++) { LinkedProposalPositionGroup.PositionInformation pos= positions[i]; if (pos.getOffset() != -1) { group.addPosition(new ProposalPosition(document, pos.getOffset(), pos.getLength(), pos.getSequenceRank(), proposalImpls)); } } } model.addGroup(group); added= true; } } model.forceInstall(); if (editor instanceof JavaEditor) { model.addLinkingListener(new EditorHighlightingSynchronizer((JavaEditor) editor)); } if (added) { // only set up UI if there are any positions set LinkedModeUI ui= new EditorLinkedModeUI(model, viewer); LinkedProposalPositionGroup.PositionInformation endPosition= fLinkedProposalModel.getEndPosition(); if (endPosition != null && endPosition.getOffset() != -1) { ui.setExitPosition(viewer, endPosition.getOffset() + endPosition.getLength(), 0, Integer.MAX_VALUE); } else { int cursorPosition= viewer.getSelectedRange().x; if (cursorPosition != 0) { ui.setExitPosition(viewer, cursorPosition, 0, Integer.MAX_VALUE); } } ui.setExitPolicy(new LinkedModeExitPolicy()); ui.enter(); IRegion region= ui.getSelectedRegion(); viewer.setSelectedRange(region.getOffset(), region.getLength()); viewer.revealRange(region.getOffset(), region.getLength()); } } /** * Creates the text change for this proposal. * This method is only called once and only when no text change has been passed in * {@link #CUCorrectionProposal(String, IJavaScriptUnit, TextChange, int, Image)}. * * @return returns the created text change. * @throws CoreException thrown if the creation of the text change failed. */ protected TextChange createTextChange() throws CoreException { IJavaScriptUnit cu= getCompilationUnit(); String name= getName(); TextChange change; if (!cu.getResource().exists()) { String source; try { source= cu.getSource(); } catch (JavaScriptModelException e) { JavaScriptPlugin.log(e); source= new String(); // empty } Document document= new Document(source); document.setInitialLineDelimiter(StubUtility.getLineDelimiterUsed(cu)); change= new DocumentChange(name, document); } else { CompilationUnitChange cuChange = new CompilationUnitChange(name, cu); cuChange.setSaveMode(TextFileChange.LEAVE_DIRTY); change= cuChange; } TextEdit rootEdit= new MultiTextEdit(); change.setEdit(rootEdit); // initialize text change IDocument document= change.getCurrentDocument(new NullProgressMonitor()); addEdits(document, rootEdit); return change; } /* (non-Javadoc) * @see org.eclipse.wst.jsdt.internal.ui.text.correction.ChangeCorrectionProposal#createChange() */ protected final Change createChange() throws CoreException { return createTextChange(); // make sure that only text changes are allowed here } /** * Gets the text change that is invoked when the change is applied. * * @return returns the text change that is invoked when the change is applied. * @throws CoreException throws an exception if accessing the change failed */ public final TextChange getTextChange() throws CoreException { return (TextChange) getChange(); } /** * The compilation unit on that the change works. * * @return the compilation unit on that the change works. */ public final IJavaScriptUnit getCompilationUnit() { return fCompilationUnit; } /** * Creates a preview of the content of the compilation unit after applying the change. * * @return returns the preview of the changed compilation unit. * @throws CoreException thrown if the creation of the change failed. */ public String getPreviewContent() throws CoreException { return getTextChange().getPreviewContent(new NullProgressMonitor()); } /* (non-Javadoc) * @see java.lang.Object#toString() */ public String toString() { try { return getPreviewContent(); } catch (CoreException e) { } return super.toString(); } private static class LinkedModeExitPolicy implements LinkedModeUI.IExitPolicy { public ExitFlags doExit(LinkedModeModel model, VerifyEvent event, int offset, int length) { if (event.character == '=') { return new ExitFlags(ILinkedModeListener.EXIT_ALL, true); } return null; } } private static class LinkedPositionProposalImpl implements ICompletionProposalExtension2, IJavaCompletionProposal { private final LinkedProposalPositionGroup.Proposal fProposal; private final LinkedModeModel fLinkedPositionModel; public LinkedPositionProposalImpl(LinkedProposalPositionGroup.Proposal proposal, LinkedModeModel model) { fProposal= proposal; fLinkedPositionModel= model; } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#apply(org.eclipse.jface.text.ITextViewer, char, int, int) */ public void apply(ITextViewer viewer, char trigger, int stateMask, int offset) { IDocument doc= viewer.getDocument(); LinkedPosition position= fLinkedPositionModel.findPosition(new LinkedPosition(doc, offset, 0)); if (position != null) { try { try { TextEdit edit= fProposal.computeEdits(offset, position, trigger, stateMask, fLinkedPositionModel); if (edit != null) { edit.apply(position.getDocument(), 0); } } catch (MalformedTreeException e) { throw new CoreException(new Status(IStatus.ERROR, JavaScriptUI.ID_PLUGIN, IStatus.ERROR, "Unexpected exception applying edit", e)); //$NON-NLS-1$ } catch (BadLocationException e) { throw new CoreException(new Status(IStatus.ERROR, JavaScriptUI.ID_PLUGIN, IStatus.ERROR, "Unexpected exception applying edit", e)); //$NON-NLS-1$ } } catch (CoreException e) { JavaScriptPlugin.log(e); } } } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposal#getDisplayString() */ public String getDisplayString() { return fProposal.getDisplayString(); } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposal#getImage() */ public Image getImage() { return fProposal.getImage(); } /* (non-Javadoc) * @see org.eclipse.wst.jsdt.ui.text.java.IJavaCompletionProposal#getRelevance() */ public int getRelevance() { return fProposal.getRelevance(); } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposal#apply(org.eclipse.jface.text.IDocument) */ public void apply(IDocument document) { // not called } /* (non-Javadoc) * @see org.eclipse.jface.text.contentassist.ICompletionProposal#getAdditionalProposalInfo() */ public String getAdditionalProposalInfo() { return fProposal.getAdditionalProposalInfo(); } public Point getSelection(IDocument document) { return null; } public IContextInformation getContextInformation() { return null; } public void selected(ITextViewer viewer, boolean smartToggle) {} public void unselected(ITextViewer viewer) {} /* * @see org.eclipse.jface.text.contentassist.ICompletionProposalExtension2#validate(org.eclipse.jface.text.IDocument, int, org.eclipse.jface.text.DocumentEvent) */ public boolean validate(IDocument document, int offset, DocumentEvent event) { // ignore event String insert= getDisplayString(); int off; LinkedPosition pos= fLinkedPositionModel.findPosition(new LinkedPosition(document, offset, 0)); if (pos != null) { off= pos.getOffset(); } else { off= Math.max(0, offset - insert.length()); } int length= offset - off; if (offset <= document.getLength()) { try { String content= document.get(off, length); if (insert.startsWith(content)) return true; } catch (BadLocationException e) { JavaScriptPlugin.log(e); // and ignore and return false } } return false; } } }