/* * 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.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.dartlang.analysis.server.protocol.AddContentOverlay; import org.dartlang.analysis.server.protocol.ChangeContentOverlay; import org.dartlang.analysis.server.protocol.RemoveContentOverlay; import org.dartlang.analysis.server.protocol.SourceEdit; 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; } }); } } }