/******************************************************************************* * 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.javaeditor; import java.util.ArrayList; import java.util.List; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.ITextInputListener; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.TextPresentation; import org.eclipse.jface.text.source.ISourceViewer; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.IWorkbenchPartSite; import org.eclipse.wst.jsdt.core.IJavaScriptElement; import org.eclipse.wst.jsdt.core.dom.ASTNode; import org.eclipse.wst.jsdt.core.dom.BooleanLiteral; import org.eclipse.wst.jsdt.core.dom.CharacterLiteral; import org.eclipse.wst.jsdt.core.dom.JavaScriptUnit; import org.eclipse.wst.jsdt.core.dom.Expression; import org.eclipse.wst.jsdt.core.dom.NumberLiteral; import org.eclipse.wst.jsdt.core.dom.RegularExpressionLiteral; import org.eclipse.wst.jsdt.core.dom.SimpleName; import org.eclipse.wst.jsdt.internal.corext.dom.GenericVisitor; import org.eclipse.wst.jsdt.internal.ui.JavaScriptPlugin; import org.eclipse.wst.jsdt.internal.ui.javaeditor.SemanticHighlightingManager.HighlightedPosition; import org.eclipse.wst.jsdt.internal.ui.javaeditor.SemanticHighlightingManager.Highlighting; import org.eclipse.wst.jsdt.internal.ui.text.java.IJavaReconcilingListener; /** * Semantic highlighting reconciler - Background thread implementation. * * */ public class SemanticHighlightingReconciler implements IJavaReconcilingListener, ITextInputListener { /** * Collects positions from the AST. */ private class PositionCollector extends GenericVisitor { /** The semantic token */ private SemanticToken fToken= new SemanticToken(); /* * @see org.eclipse.wst.jsdt.internal.corext.dom.GenericVisitor#visitNode(org.eclipse.wst.jsdt.core.dom.ASTNode) */ protected boolean visitNode(ASTNode node) { if ((node.getFlags() & ASTNode.MALFORMED) == ASTNode.MALFORMED) { retainPositions(node.getStartPosition(), node.getLength()); return false; } return true; } /* * @see org.eclipse.wst.jsdt.core.dom.ASTVisitor#visit(org.eclipse.wst.jsdt.core.dom.BooleanLiteral) */ public boolean visit(BooleanLiteral node) { return visitLiteral(node); } /* * @see org.eclipse.wst.jsdt.core.dom.ASTVisitor#visit(org.eclipse.wst.jsdt.core.dom.CharacterLiteral) */ public boolean visit(CharacterLiteral node) { return visitLiteral(node); } public boolean visit(RegularExpressionLiteral node) { return visitLiteral(node); } /* * @see org.eclipse.wst.jsdt.core.dom.ASTVisitor#visit(org.eclipse.wst.jsdt.core.dom.NumberLiteral) */ public boolean visit(NumberLiteral node) { return visitLiteral(node); } private boolean visitLiteral(Expression node) { fToken.update(node); for (int i= 0, n= fJobSemanticHighlightings.length; i < n; i++) { SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; if (fJobHighlightings[i].isEnabled() && semanticHighlighting.consumesLiteral(fToken)) { int offset= node.getStartPosition(); int length= node.getLength(); if (offset > -1 && length > 0) addPosition(offset, length, fJobHighlightings[i]); break; } } fToken.clear(); return false; } /* * @see org.eclipse.wst.jsdt.core.dom.ASTVisitor#visit(org.eclipse.wst.jsdt.core.dom.SimpleName) */ public boolean visit(SimpleName node) { fToken.update(node); for (int i= 0, n= fJobSemanticHighlightings.length; i < n; i++) { SemanticHighlighting semanticHighlighting= fJobSemanticHighlightings[i]; if (fJobHighlightings[i].isEnabled() && semanticHighlighting.consumes(fToken)) { int offset= node.getStartPosition(); int length= node.getLength(); if (offset > -1 && length > 0) addPosition(offset, length, fJobHighlightings[i]); break; } } fToken.clear(); return false; } /** * Add a position with the given range and highlighting iff it does not exist already. * @param offset The range offset * @param length The range length * @param highlighting The highlighting */ private void addPosition(int offset, int length, Highlighting highlighting) { boolean isExisting= false; // TODO: use binary search for (int i= 0, n= fRemovedPositions.size(); i < n; i++) { HighlightedPosition position= (HighlightedPosition) fRemovedPositions.get(i); if (position == null) continue; if (position.isEqual(offset, length, highlighting)) { isExisting= true; fRemovedPositions.set(i, null); fNOfRemovedPositions--; break; } } if (!isExisting) { Position position= fJobPresenter.createHighlightedPosition(offset, length, highlighting); fAddedPositions.add(position); } } /** * Retain the positions completely contained in the given range. * @param offset The range offset * @param length The range length */ private void retainPositions(int offset, int length) { // TODO: use binary search for (int i= 0, n= fRemovedPositions.size(); i < n; i++) { HighlightedPosition position= (HighlightedPosition) fRemovedPositions.get(i); if (position != null && position.isContained(offset, length)) { fRemovedPositions.set(i, null); fNOfRemovedPositions--; } } } } /** Position collector */ private PositionCollector fCollector= new PositionCollector(); /** The Java editor this semantic highlighting reconciler is installed on */ private JavaEditor fEditor; /** The source viewer this semantic highlighting reconciler is installed on */ private ISourceViewer fSourceViewer; /** The semantic highlighting presenter */ private SemanticHighlightingPresenter fPresenter; /** Semantic highlightings */ private SemanticHighlighting[] fSemanticHighlightings; /** Highlightings */ private Highlighting[] fHighlightings; /** Background job's added highlighted positions */ private List fAddedPositions= new ArrayList(); /** Background job's removed highlighted positions */ private List fRemovedPositions= new ArrayList(); /** Number of removed positions */ private int fNOfRemovedPositions; /** Background job */ private Job fJob; /** Background job lock */ private final Object fJobLock= new Object(); /** * Reconcile operation lock. * */ private final Object fReconcileLock= new Object(); /** * <code>true</code> if any thread is executing * <code>reconcile</code>, <code>false</code> otherwise. * */ private boolean fIsReconciling= false; /** The semantic highlighting presenter - cache for background thread, only valid during {@link #reconciled(JavaScriptUnit, boolean, IProgressMonitor)} */ private SemanticHighlightingPresenter fJobPresenter; /** Semantic highlightings - cache for background thread, only valid during {@link #reconciled(JavaScriptUnit, boolean, IProgressMonitor)} */ private SemanticHighlighting[] fJobSemanticHighlightings; /** Highlightings - cache for background thread, only valid during {@link #reconciled(JavaScriptUnit, boolean, IProgressMonitor)} */ private Highlighting[] fJobHighlightings; /* * @see org.eclipse.wst.jsdt.internal.ui.text.java.IJavaReconcilingListener#aboutToBeReconciled() */ public void aboutToBeReconciled() { // Do nothing } /* * @see org.eclipse.wst.jsdt.internal.ui.text.java.IJavaReconcilingListener#reconciled(JavaScriptUnit, boolean, IProgressMonitor) */ public void reconciled(JavaScriptUnit ast, boolean forced, IProgressMonitor progressMonitor) { // ensure at most one thread can be reconciling at any time synchronized (fReconcileLock) { if (fIsReconciling) return; else fIsReconciling= true; } fJobPresenter= fPresenter; fJobSemanticHighlightings= fSemanticHighlightings; fJobHighlightings= fHighlightings; try { if (fJobPresenter == null || fJobSemanticHighlightings == null || fJobHighlightings == null) return; fJobPresenter.setCanceled(progressMonitor.isCanceled()); if (ast == null || fJobPresenter.isCanceled()) return; ASTNode[] subtrees= getAffectedSubtrees(ast); if (subtrees.length == 0) return; startReconcilingPositions(); if (!fJobPresenter.isCanceled()) reconcilePositions(subtrees); TextPresentation textPresentation= null; if (!fJobPresenter.isCanceled()) textPresentation= fJobPresenter.createPresentation(fAddedPositions, fRemovedPositions); if (!fJobPresenter.isCanceled()) updatePresentation(textPresentation, fAddedPositions, fRemovedPositions); stopReconcilingPositions(); } finally { fJobPresenter= null; fJobSemanticHighlightings= null; fJobHighlightings= null; synchronized (fReconcileLock) { fIsReconciling= false; } } } /** * @param node Root node * @return Array of subtrees that may be affected by past document changes */ private ASTNode[] getAffectedSubtrees(ASTNode node) { // TODO: only return nodes which are affected by document changes - would require an 'anchor' concept for taking distant effects into account return new ASTNode[] { node }; } /** * Start reconciling positions. */ private void startReconcilingPositions() { fJobPresenter.addAllPositions(fRemovedPositions); fNOfRemovedPositions= fRemovedPositions.size(); } /** * Reconcile positions based on the AST subtrees * * @param subtrees the AST subtrees */ private void reconcilePositions(ASTNode[] subtrees) { // FIXME: remove positions not covered by subtrees for (int i= 0, n= subtrees.length; i < n; i++) subtrees[i].accept(fCollector); List oldPositions= fRemovedPositions; List newPositions= new ArrayList(fNOfRemovedPositions); for (int i= 0, n= oldPositions.size(); i < n; i ++) { Object current= oldPositions.get(i); if (current != null) newPositions.add(current); } fRemovedPositions= newPositions; } /** * Update the presentation. * * @param textPresentation the text presentation * @param addedPositions the added positions * @param removedPositions the removed positions */ private void updatePresentation(TextPresentation textPresentation, List addedPositions, List removedPositions) { Runnable runnable= fJobPresenter.createUpdateRunnable(textPresentation, addedPositions, removedPositions); if (runnable == null) return; JavaEditor editor= fEditor; if (editor == null) return; IWorkbenchPartSite site= editor.getSite(); if (site == null) return; Shell shell= site.getShell(); if (shell == null || shell.isDisposed()) return; Display display= shell.getDisplay(); if (display == null || display.isDisposed()) return; display.asyncExec(runnable); } /** * Stop reconciling positions. */ private void stopReconcilingPositions() { fRemovedPositions.clear(); fNOfRemovedPositions= 0; fAddedPositions.clear(); } /** * Install this reconciler on the given editor, presenter and highlightings. * @param editor the editor * @param sourceViewer the source viewer * @param presenter the semantic highlighting presenter * @param semanticHighlightings the semantic highlightings * @param highlightings the highlightings */ public void install(JavaEditor editor, ISourceViewer sourceViewer, SemanticHighlightingPresenter presenter, SemanticHighlighting[] semanticHighlightings, Highlighting[] highlightings) { fPresenter= presenter; fSemanticHighlightings= semanticHighlightings; fHighlightings= highlightings; fEditor= editor; fSourceViewer= sourceViewer; if (fEditor instanceof CompilationUnitEditor) { ((CompilationUnitEditor)fEditor).addReconcileListener(this); } else if (fEditor == null) { fSourceViewer.addTextInputListener(this); scheduleJob(); } } /** * Uninstall this reconciler from the editor */ public void uninstall() { if (fPresenter != null) fPresenter.setCanceled(true); if (fEditor != null) { if (fEditor instanceof CompilationUnitEditor) ((CompilationUnitEditor)fEditor).removeReconcileListener(this); else fSourceViewer.removeTextInputListener(this); fEditor= null; } fSourceViewer= null; fSemanticHighlightings= null; fHighlightings= null; fPresenter= null; } /** * Schedule a background job for retrieving the AST and reconciling the Semantic Highlighting model. */ private void scheduleJob() { final IJavaScriptElement element= fEditor.getInputJavaElement(); synchronized (fJobLock) { final Job oldJob= fJob; if (fJob != null) { fJob.cancel(); fJob= null; } if (element != null) { fJob= new Job(JavaEditorMessages.SemanticHighlighting_job) { protected IStatus run(IProgressMonitor monitor) { if (oldJob != null) { try { oldJob.join(); } catch (InterruptedException e) { JavaScriptPlugin.log(e); return Status.CANCEL_STATUS; } } if (monitor.isCanceled()) return Status.CANCEL_STATUS; JavaScriptUnit ast= JavaScriptPlugin.getDefault().getASTProvider().getAST(element, ASTProvider.WAIT_YES, monitor); reconciled(ast, false, monitor); synchronized (fJobLock) { // allow the job to be gc'ed if (fJob == this) fJob= null; } return Status.OK_STATUS; } }; fJob.setSystem(true); fJob.setPriority(Job.DECORATE); fJob.schedule(); } } } /* * @see org.eclipse.jface.text.ITextInputListener#inputDocumentAboutToBeChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument) */ public void inputDocumentAboutToBeChanged(IDocument oldInput, IDocument newInput) { synchronized (fJobLock) { if (fJob != null) { fJob.cancel(); fJob= null; } } } /* * @see org.eclipse.jface.text.ITextInputListener#inputDocumentChanged(org.eclipse.jface.text.IDocument, org.eclipse.jface.text.IDocument) */ public void inputDocumentChanged(IDocument oldInput, IDocument newInput) { if (newInput != null) scheduleJob(); } /** * Refreshes the highlighting. * * */ public void refresh() { scheduleJob(); } }