/******************************************************************************* * Copyright (c) 2016 Bruno Medeiros and other Contributors. * 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package melnorme.lang.ide.core.engine; import static melnorme.utilbox.core.Assert.AssertNamespace.assertNotNull; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import org.eclipse.core.filebuffers.FileBuffers; import org.eclipse.core.filebuffers.IFileBuffer; import org.eclipse.core.filebuffers.ITextFileBuffer; import org.eclipse.core.filebuffers.ITextFileBufferManager; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentListener; import org.eclipse.jface.text.ISynchronizable; import melnorme.lang.ide.core.LangCore; import melnorme.lang.ide.core.engine.SourceModelManager.StructureInfo; import melnorme.lang.ide.core.engine.SourceModelManager.StructureUpdateTask; import melnorme.lang.ide.core.operations.build.BuildManager; import melnorme.lang.ide.core.utils.CoreExecutors; import melnorme.lang.ide.core.utils.DefaultBufferListener; import melnorme.lang.ide.core.utils.ResourceUtils; import melnorme.utilbox.concurrency.ICommonExecutor; import melnorme.utilbox.concurrency.CompletableResult.CompletableLatch; import melnorme.utilbox.core.fntypes.CallableX; import melnorme.utilbox.misc.Location; /** * Manager for all model reconciliations after text file buffer changes, * whether is document modifications, or file saves. */ public class DocumentReconcileManager extends AbstractAgentManager { protected final ITextFileBufferManager fbm; protected final ProjectReconcileManager projectReconciler; public DocumentReconcileManager() { this(FileBuffers.getTextFileBufferManager(), LangCore.getBuildManager()); } public DocumentReconcileManager(ITextFileBufferManager fbm, BuildManager buildMgr) { this.fbm = assertNotNull(fbm); this.projectReconciler = assertNotNull(new ProjectReconcileManager(this.executor, buildMgr)); } @Override protected ICommonExecutor init_executor() { return CoreExecutors.newExecutorTaskAgent(DocumentReconcileManager.class); } @Override protected void dispose_post() { executor.shutdownNowAndCancelAll(); } protected BuildManager getBuildManager() { return projectReconciler.buildMgr; } /* ----------------- ----------------- */ public DocumentReconcileConnection connectDocument(IDocument document, StructureInfo structureInfo) { Location location = structureInfo.getLocation(); ITextFileBuffer textFileBuffer = location == null ? null : ResourceUtils.getTextFileBuffer(fbm, location); if(textFileBuffer != null) { return new TextReconcileConnection(document, structureInfo, textFileBuffer); } return new DocumentReconcileConnection(document, structureInfo); } public class DocumentReconcileConnection { protected final IDocument document; protected final StructureInfo structureInfo; public DocumentReconcileConnection(IDocument document, StructureInfo structureInfo) { this.document = assertNotNull(document); this.structureInfo = assertNotNull(structureInfo); document.addDocumentListener(docListener); } public void disconnect() { document.removeDocumentListener(docListener); } protected final IDocumentListener docListener = new IDocumentListener() { @Override public void documentAboutToBeChanged(DocumentEvent event) { } @Override public void documentChanged(DocumentEvent event) { assertTrue(document == event.fDocument); doDocumentChanged(); } }; protected StructureUpdateTask doDocumentChanged() { return structureInfo.queueSourceUpdateTask(document.get()); } } public static IProject getProject(IFileBuffer fileBuffer) { /* TODO : improve the API to get a project, or ignore project altogheter */ IFile file= FileBuffers.getWorkspaceFileAtLocation(fileBuffer.getLocation(), true); if (file == null) { return null; } return file.getProject(); } public class TextReconcileConnection extends DocumentReconcileConnection { protected final IProject project; // Can be null protected final ITextFileBuffer textFileBuffer; protected final DirtyBufferListener fbListener; protected CompletableLatch fileSaveLatch = new CompletableLatch(); public TextReconcileConnection(IDocument document, StructureInfo structureInfo, ITextFileBuffer textFileBuffer) { super(document, structureInfo); this.textFileBuffer = assertNotNull(textFileBuffer); fbListener = new DirtyBufferListener(); fbm.addFileBufferListener(fbListener); project = getProject(textFileBuffer); } @Override public void disconnect() { super.disconnect(); fbm.removeFileBufferListener(fbListener); fileSaveLatch.setCancelledResult(); } @Override protected StructureUpdateTask doDocumentChanged() { StructureUpdateTask structureUpdateTask = structureInfo.queueSourceUpdateTask(document.get()); fileSaveLatch.setCancelledResult(); fileSaveLatch = new CompletableLatch(); if(project != null) { projectReconciler.invalidateProjectModel(project, structureUpdateTask, fileSaveLatch); } return structureUpdateTask; } public class DirtyBufferListener extends DefaultBufferListener { @Override public void dirtyStateChanged(IFileBuffer buffer, boolean isDirty) { if(buffer == textFileBuffer) { if(!isDirty) { handleDocumentSaved(); } } } /** Determine if document save was invoked under a Build action. * Uses a hack to determine this, unfortunately there is currently no other way to figure this out. * TODO: submit a bug report */ public boolean isUnderBuildAction() { StackTraceElement[] stackTrace = new Exception().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if(!stackTraceElement.getMethodName().equals("run")) { continue; } String className = stackTraceElement.getClassName(); if(className.endsWith(".BuildAction") || className.endsWith(".GlobalBuildAction")) { return true; } } return false; } protected void handleDocumentSaved() { if(!getBuildManager().autoBuildsEnablement().isEnabled() || isUnderBuildAction()) { // Cancel the current reconciliation task, because there is no point to it // A normal, full build will be performed instead. projectReconciler.cancelPendingReconciliation(project); fileSaveLatch.setCompleted(); return; } // Mark the file save as completed fileSaveLatch.setCompleted(); StructureUpdateTask structureUpdateTask = structureInfo.documentSaved(document); if(structureUpdateTask != null && project != null) { projectReconciler.invalidateProjectModel(project, structureUpdateTask, fileSaveLatch); } } } } /* ----------------- ----------------- */ public static <R> R runUnderDocumentLock(IDocument doc, CallableX<R, RuntimeException> runnable) { if(doc instanceof ISynchronizable) { ISynchronizable synchronizable = (ISynchronizable) doc; Object lockObject = synchronizable.getLockObject(); if(lockObject != null) { synchronized(lockObject) { return runnable.call(); } } } return runnable.call(); } }