| /* |
| * Copyright (c) 2012, the Dart project authors. |
| * |
| * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except |
| * in compliance with the License. You may obtain a copy of the License at |
| * |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Unless required by applicable law or agreed to in writing, software distributed under the License |
| * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express |
| * or implied. See the License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.dart.tools.ui.internal.text.dart; |
| |
| import com.google.common.collect.Lists; |
| import com.google.dart.engine.ast.CompilationUnit; |
| import com.google.dart.engine.context.AnalysisContext; |
| import com.google.dart.engine.context.AnalysisException; |
| import com.google.dart.engine.source.Source; |
| import com.google.dart.engine.utilities.instrumentation.Instrumentation; |
| import com.google.dart.engine.utilities.instrumentation.InstrumentationBuilder; |
| import com.google.dart.server.UpdateContentConsumer; |
| import com.google.dart.server.generated.types.AddContentOverlay; |
| import com.google.dart.server.generated.types.ChangeContentOverlay; |
| import com.google.dart.server.generated.types.RemoveContentOverlay; |
| import com.google.dart.server.generated.types.SourceEdit; |
| import com.google.dart.tools.core.DartCore; |
| import com.google.dart.tools.core.DartCoreDebug; |
| import com.google.dart.tools.core.analysis.model.AnalysisEvent; |
| import com.google.dart.tools.core.analysis.model.AnalysisListener; |
| import com.google.dart.tools.core.analysis.model.ContextManager; |
| import com.google.dart.tools.core.analysis.model.ResolvedEvent; |
| import com.google.dart.tools.core.analysis.model.ResolvedHtmlEvent; |
| import com.google.dart.tools.core.internal.builder.AnalysisManager; |
| import com.google.dart.tools.core.internal.builder.AnalysisWorker; |
| import com.google.dart.tools.core.internal.model.DartIgnoreManager; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.jface.text.DocumentEvent; |
| import org.eclipse.jface.text.DocumentRewriteSessionEvent; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IDocumentExtension4; |
| import org.eclipse.jface.text.IDocumentListener; |
| import org.eclipse.jface.text.IDocumentRewriteSessionListener; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.reconciler.DirtyRegion; |
| import org.eclipse.jface.text.reconciler.IReconciler; |
| import org.eclipse.jface.text.reconciler.IReconcilingStrategy; |
| import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension; |
| import org.eclipse.swt.events.DisposeEvent; |
| import org.eclipse.swt.events.DisposeListener; |
| |
| import java.io.IOException; |
| import java.util.HashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| public class DartReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension { |
| |
| /** |
| * The editor containing the document with source to be reconciled (not {@code null}). |
| */ |
| private final DartReconcilingEditor editor; |
| |
| /** |
| * The manager used by the receiver to request background analysis (not {@code null}). |
| */ |
| private final AnalysisManager analysisManager; |
| |
| /** |
| * The ignore manager used by the receiver to determine if a source should be analyzed. |
| */ |
| private final DartIgnoreManager ignoreManager; |
| |
| /** |
| * The document with source to be reconciled. This is set by the {@link IReconciler} and should |
| * not be {@code null}. |
| */ |
| private IDocument document; |
| |
| /** |
| * The {@link IDocumentExtension4} for {@link #document}. |
| */ |
| private IDocumentExtension4 document4; |
| |
| /** |
| * Synchronize against this field before accessing {@link #dirtyRegion} |
| */ |
| private final Object lock = new Object(); |
| |
| /** |
| * The region of source that has changed and needs to be reconciled, or empty if analysis of this |
| * file is up to date, or {@code null} if the entire file needs to be reconciled. Synchronize |
| * against {@link #lock} before accessing this field. |
| */ |
| private DartReconcilingRegion dirtyRegion = DartReconcilingRegion.EMPTY; |
| |
| /** |
| * The contents of the document the last time it was updated, or null if this file is up to date. |
| * Synchronize against {@link #lock} before accessing this field. |
| */ |
| private String codeAsOfLastUpdate = null; |
| |
| /** |
| * A flag indicating whether an "overlay" has been already added for this file. |
| */ |
| private boolean isOverlayAdded = false; |
| |
| /** |
| * The modification stamp of the document which was sent to the server. |
| */ |
| private long lastSentStamp = -1; |
| |
| /** |
| * The modification stamp of the document which was confirmed by the server. |
| */ |
| private long lastConfirmedStamp = -1; |
| |
| /** |
| * Listen for analysis results for the source being edited and update the editor. |
| */ |
| private final AnalysisListener analysisListener = new AnalysisListener() { |
| |
| @Override |
| public void complete(AnalysisEvent event) { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| if (event.getContext().equals(context)) { |
| applyResolvedUnit(); |
| } |
| } |
| |
| @Override |
| public void resolved(ResolvedEvent event) { |
| if (event.getContext().equals(editor.getInputAnalysisContext())) { |
| if (event.getSource().equals(editor.getInputSource())) { |
| applyAnalysisResult(event.getUnit()); |
| } |
| } |
| } |
| |
| @Override |
| public void resolvedHtml(ResolvedHtmlEvent event) { |
| // ignored |
| } |
| }; |
| |
| /** |
| * Listen for changes to the source to clear the cached AST and record the last modification time. |
| */ |
| private final IDocumentListener documentListener = new IDocumentListener() { |
| @Override |
| public void documentAboutToBeChanged(DocumentEvent event) { |
| } |
| |
| @Override |
| public void documentChanged(DocumentEvent event) { |
| // Record the source region that has changed |
| String newText = event.getText(); |
| int newLength = newText != null ? newText.length() : 0; |
| synchronized (lock) { |
| if (dirtyRegion != null) { |
| dirtyRegion = dirtyRegion.add(event.getOffset(), event.getLength(), newLength); |
| } |
| codeAsOfLastUpdate = document.get(); |
| } |
| editor.applyResolvedUnit(null); |
| |
| // Start analysis immediately if "." pressed to improve code completion response |
| if (newText.endsWith(".")) { |
| reconcile(); |
| } |
| } |
| }; |
| |
| /** |
| * Reconciler usually waits for some time, like 500 ms, before actually performing reconciliation, |
| * so that it happens when user stops typing. But when we apply a Quick Fix/Assist or a |
| * refactoring, we should apply changes as soon as possible, because these changes form their own |
| * complete unit of work. |
| */ |
| private final IDocumentRewriteSessionListener rewriteSessionListener = new IDocumentRewriteSessionListener() { |
| @Override |
| public void documentRewriteSessionChanged(DocumentRewriteSessionEvent event) { |
| if (event.getChangeType() == DocumentRewriteSessionEvent.SESSION_STOP) { |
| reconcile(); |
| } |
| } |
| }; |
| |
| /** |
| * Construct a new instance for the specified editor. |
| * |
| * @param editor the editor (not {@code null}) |
| */ |
| public DartReconcilingStrategy(DartReconcilingEditor editor) { |
| this(editor, AnalysisManager.getInstance(), null); |
| } |
| |
| /** |
| * Construct a new instance for the specified editor. |
| * |
| * @param editor the editor (not {@code null}) |
| * @param analysisManager the analysis manager (not {@code null}) |
| * @param ignoreManager the ignore manager (not {@code null}) |
| */ |
| public DartReconcilingStrategy(DartReconcilingEditor editor, AnalysisManager analysisManager, |
| DartIgnoreManager ignoreManager) { |
| |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| this.editor = editor; |
| this.analysisManager = null; |
| this.ignoreManager = DartIgnoreManager.getInstance(); |
| } else { |
| this.editor = editor; |
| this.analysisManager = analysisManager; |
| if (ignoreManager == null) { |
| this.ignoreManager = DartCore.getIgnoreManager(); |
| } else { |
| this.ignoreManager = ignoreManager; |
| } |
| AnalysisWorker.addListener(analysisListener); |
| } |
| editor.setDartReconcilingStrategy(this); |
| |
| // Cleanup the receiver when editor is closed |
| editor.addViewerDisposeListener(new DisposeListener() { |
| @Override |
| public void widgetDisposed(DisposeEvent e) { |
| dispose(); |
| } |
| }); |
| } |
| |
| /** |
| * Cleanup when the editor is closed. |
| */ |
| public void dispose() { |
| if (document != null) { |
| document.removeDocumentListener(documentListener); |
| document4.removeDocumentRewriteSessionListener(rewriteSessionListener); |
| } |
| AnalysisWorker.removeListener(analysisListener); |
| // clear the cached source content to ensure the source will be read from disk |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| removeOverlay(); |
| } else { |
| sourceChanged(null); |
| } |
| } |
| |
| /** |
| * Return {@code true} if there are sent content changes that have not been processed by the |
| * server yet. |
| */ |
| public boolean hasPendingContentChanges() { |
| return lastConfirmedStamp != document4.getModificationStamp(); |
| } |
| |
| @Override |
| public void initialReconcile() { |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| return; |
| } |
| if (!applyResolvedUnit()) { |
| Source source = editor.getInputSource(); |
| if (source != null && ignoreManager.isAnalyzed(source.getFullName())) { |
| try { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| if (context != null && source != null) { |
| // TODO (danrubel): Push this into background analysis |
| // once AnalysisWorker notifies listeners when units are parsed before resolved |
| CompilationUnit unit = context.parseCompilationUnit(source); |
| editor.applyResolvedUnit(unit); |
| performAnalysisInBackground(); |
| } |
| } catch (AnalysisException e) { |
| if (!(e.getCause() instanceof IOException)) { |
| DartCore.logError("Parse failed for " + editor.getTitle(), e); |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Activates reconciling of the current dirty region. |
| */ |
| public void reconcile() { |
| InstrumentationBuilder instrumentation = Instrumentation.builder("DartReconcilingStrategy-reconcile"); |
| try { |
| instrumentation.data("Name", editor.getTitle()); |
| DartReconcilingRegion region; |
| String code; |
| synchronized (lock) { |
| region = dirtyRegion; |
| code = codeAsOfLastUpdate; |
| dirtyRegion = DartReconcilingRegion.EMPTY; |
| codeAsOfLastUpdate = null; |
| } |
| if (region == null) { |
| instrumentation.data("Length", code.length()); |
| sourceChanged(code); |
| } else if (!region.isEmpty()) { |
| instrumentation.data("Offset", region.getOffset()); |
| instrumentation.data("OldLength", region.getOldLength()); |
| instrumentation.data("NewLength", region.getNewLength()); |
| sourceChanged(code, region.getOffset(), region.getOldLength(), region.getNewLength()); |
| } |
| } finally { |
| instrumentation.log(); |
| } |
| } |
| |
| @Override |
| public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) { |
| reconcile(); |
| } |
| |
| @Override |
| public void reconcile(IRegion partition) { |
| reconcile(); |
| } |
| |
| public void saved() { |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| reconcile(); |
| removeOverlay(); |
| } else { |
| // We don't use overlays with the Java based analyzer. |
| } |
| } |
| |
| /** |
| * Cache the document and add document changed and analysis result listeners. |
| */ |
| @Override |
| public void setDocument(IDocument document) { |
| IDocument oldDocument = this.document; |
| IDocumentExtension4 oldDocument4 = this.document4; |
| if (oldDocument != null) { |
| oldDocument.removeDocumentListener(documentListener); |
| oldDocument4.removeDocumentRewriteSessionListener(rewriteSessionListener); |
| } |
| this.document = document; |
| this.document4 = (IDocumentExtension4) document; |
| this.lastSentStamp = document4.getModificationStamp(); |
| this.lastConfirmedStamp = document4.getModificationStamp(); |
| document.addDocumentListener(documentListener); |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| document4.addDocumentRewriteSessionListener(rewriteSessionListener); |
| } |
| } |
| |
| @Override |
| public void setProgressMonitor(IProgressMonitor monitor) { |
| } |
| |
| /** |
| * Adds overlay for this file. |
| */ |
| private void addOverlay(String code) { |
| AddContentOverlay change = new AddContentOverlay(code); |
| updateFileContent(change); |
| isOverlayAdded = true; |
| } |
| |
| /** |
| * Apply analysis results only if there are no pending source changes. |
| */ |
| private void applyAnalysisResult(CompilationUnit unit) { |
| if (unit == null) { |
| return; |
| } |
| synchronized (lock) { |
| if (dirtyRegion == null || !dirtyRegion.isEmpty()) { |
| return; |
| } |
| } |
| editor.applyResolvedUnit(unit); |
| } |
| |
| /** |
| * Get the resolved compilation unit from the editor's analysis context and apply that unit. |
| * |
| * @return {@code true} if a resolved unit was obtained and applied |
| */ |
| private boolean applyResolvedUnit() { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| Source source = editor.getInputSource(); |
| if (context != null && source != null) { |
| Source[] libraries = context.getLibrariesContaining(source); |
| if (libraries != null && libraries.length > 0) { |
| // TODO (danrubel): Handle multiple libraries gracefully |
| CompilationUnit unit = context.getResolvedCompilationUnit(source, libraries[0]); |
| if (unit != null) { |
| applyAnalysisResult(unit); |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Return the {@link ContextManager} to use for this editor. |
| */ |
| private ContextManager getContextManager() { |
| ContextManager manager = editor.getInputProject(); |
| if (manager == null) { |
| manager = DartCore.getProjectManager(); |
| } |
| return manager; |
| } |
| |
| /** |
| * Start background analysis of the context containing the source being edited. |
| */ |
| private void performAnalysisInBackground() { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| if (context != null) { |
| ContextManager manager = getContextManager(); |
| analysisManager.performAnalysisInBackground(manager, context); |
| } |
| } |
| |
| private void removeOverlay() { |
| if (isOverlayAdded) { |
| RemoveContentOverlay change = new RemoveContentOverlay(); |
| updateFileContent(change); |
| isOverlayAdded = false; |
| } |
| } |
| |
| /** |
| * Schedules the source change notification and analysis. |
| * |
| * @param code the new source code or {@code null} if the source should be pulled from disk. Will |
| * never be {@code null} when analysis server is in use. |
| */ |
| private void sourceChanged(String code) { |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| if (!isOverlayAdded) { |
| addOverlay(code); |
| } else { |
| AddContentOverlay change = new AddContentOverlay(code); |
| updateFileContent(change); |
| } |
| } else { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| Source source = editor.getInputSource(); |
| if (context != null && source != null) { |
| ContextManager manager = getContextManager(); |
| DartUpdateSourceHelper.getInstance().updateFast( |
| analysisManager, |
| manager, |
| context, |
| source, |
| code); |
| } |
| } |
| } |
| |
| /** |
| * Schedules the source change notification and analysis. |
| * |
| * @param code the new source code or {@code null} if the source should be pulled from disk |
| * @param offset the offset into the current contents |
| * @param oldLength the number of characters in the original contents that were replaced |
| * @param newLength the number of characters in the replacement text |
| */ |
| private void sourceChanged(String code, int offset, int oldLength, int newLength) { |
| if (DartCoreDebug.ENABLE_ANALYSIS_SERVER) { |
| if (document4.getModificationStamp() != lastSentStamp) { |
| if (!isOverlayAdded) { |
| addOverlay(code); |
| } else { |
| List<SourceEdit> sourceEdits = Lists.newArrayList(); |
| String replacement = code.substring(offset, offset + newLength); |
| sourceEdits.add(new SourceEdit(offset, oldLength, replacement, null)); |
| ChangeContentOverlay change = new ChangeContentOverlay(sourceEdits); |
| updateFileContent(change); |
| } |
| } |
| } else { |
| AnalysisContext context = editor.getInputAnalysisContext(); |
| Source source = editor.getInputSource(); |
| if (context != null && source != null) { |
| ContextManager manager = getContextManager(); |
| DartUpdateSourceHelper.getInstance().updateFast( |
| analysisManager, |
| manager, |
| context, |
| source, |
| code, |
| offset, |
| oldLength, |
| newLength); |
| } |
| } |
| } |
| |
| private void updateFileContent(Object change) { |
| String file = editor.getInputFilePath(); |
| if (file != null) { |
| final long documentStamp = document4.getModificationStamp(); |
| lastSentStamp = documentStamp; |
| Map<String, Object> files = new HashMap<String, Object>(); |
| files.put(file, change); |
| DartCore.getAnalysisServer().analysis_updateContent(files, new UpdateContentConsumer() { |
| @Override |
| public void onResponse() { |
| lastConfirmedStamp = documentStamp; |
| } |
| }); |
| } |
| } |
| } |