/*******************************************************************************
* Copyright (c) 2015, 2015 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:
* 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 static melnorme.utilbox.core.CoreUtil.areEqual;
import org.eclipse.jface.text.IDocument;
import melnorme.lang.ide.core.LangCore;
import melnorme.lang.ide.core.engine.DocumentReconcileManager.DocumentReconcileConnection;
import melnorme.lang.tooling.LocationKey;
import melnorme.lang.tooling.structure.SourceFileStructure;
import melnorme.lang.utils.concurrency.ConcurrentlyDerivedData;
import melnorme.lang.utils.concurrency.ConcurrentlyDerivedData.DataUpdateTask;
import melnorme.lang.utils.concurrency.SynchronizedEntryMap;
import melnorme.utilbox.concurrency.OperationCancellation;
import melnorme.utilbox.core.CommonException;
import melnorme.utilbox.core.fntypes.CommonResult;
import melnorme.utilbox.fields.ListenerListHelper;
import melnorme.utilbox.misc.Location;
import melnorme.utilbox.ownership.IDisposable;
import melnorme.utilbox.ownership.StrictDisposable;
/**
* The SourceModelManager keeps track of text document changes, and updates derived models, such as:
*
* - Source module parse-analysis and structure-info (possibly with problem markers update).
* - Possible persist document changes to files in filesystem, or update a semantic engine buffers.
*
*/
public abstract class SourceModelManager extends AbstractAgentManager {
protected final DocumentReconcileManager reconcileMgr;
public SourceModelManager() {
this(new DocumentReconcileManager(), new ProblemMarkerUpdater());
}
public SourceModelManager(DocumentReconcileManager reconcileMgr, ProblemMarkerUpdater problemUpdater) {
this.reconcileMgr = assertNotNull(reconcileMgr);
asOwner().bind(reconcileMgr);
if(problemUpdater != null) {
problemUpdater.install(this);
}
}
/* ----------------- ----------------- */
protected final SynchronizedEntryMap<LocationKey, StructureInfo> infosMap =
new SynchronizedEntryMap<LocationKey, StructureInfo>() {
@Override
protected StructureInfo createEntry(LocationKey key) {
return new StructureInfo(key);
}
};
/** @return the {@link SourceFileStructure} currently stored under given key. Can be null. */
public StructureInfo getStoredStructureInfo(LocationKey key) {
return infosMap.getEntryOrNull(key);
}
/**
* Connect given structure listener to structure under given key,
* and begin structure updates using given document as input.
*
* If a previous listener was already connected, but with a different document,
* an unmanaged {@link StructureInfo} will be returned
*
* @return non-null. The {@link StructureInfo} resulting from given connection.
*/
public StructureModelRegistration connectStructureUpdates(LocationKey key, IDocument document,
IStructureModelListener structureListener) {
assertNotNull(key);
assertNotNull(document);
assertNotNull(structureListener);
log.println("connectStructureUpdates: " + key);
StructureInfo structureInfo = infosMap.getEntry(key);
boolean connected = structureInfo.connectDocument(document, structureListener);
if(!connected) {
// Special case: this key has already been connected to, but with a different document.
// As such, return a unmanaged StructureInfo
structureInfo = new StructureInfo(key);
connected = structureInfo.connectDocument(document, structureListener);
}
assertTrue(connected);
assertTrue(areEqual(structureInfo.getLocation(), key.getLocation()));
return new StructureModelRegistration(structureInfo, structureListener);
}
public class StructureModelRegistration extends StrictDisposable {
public final StructureInfo structureInfo;
protected final IStructureModelListener structureListener;
public StructureModelRegistration(StructureInfo structureInfo, IStructureModelListener structureListener) {
this.structureInfo = assertNotNull(structureInfo);
this.structureListener = assertNotNull(structureListener);
}
@Override
protected void disposeDo() {
log.println("disconnectStructureUpdates: " + structureInfo.getKey2());
structureInfo.disconnectFromDocument(structureListener);
}
}
/* ----------------- ----------------- */
public class StructureInfo extends ConcurrentlyDerivedData<CommonResult<SourceFileStructure>, StructureInfo> {
protected final LocationKey key2;
protected IDocument document = null;
protected DocumentReconcileConnection reconcileConnection = null;
public StructureInfo(LocationKey key) {
super();
this.key2 = assertNotNull(key);
}
public final LocationKey getKey2() {
return key2;
}
/** @return the file location if source is based on a file, null otherwise. */
public Location getLocation() {
return key2.getLocation();
}
@Override
protected void internalSetData(CommonResult<SourceFileStructure> newData) {
if(newData == null) {
newData = new CommonResult<>(null);
}
super.internalSetData(newData);
}
@Override
public CommonResult<SourceFileStructure> getStoredData() {
return assertNotNull(super.getStoredData());
}
public synchronized boolean hasConnectedListeners() {
return connectedListeners.getListeners().size() > 0;
}
protected synchronized boolean connectDocument(IDocument newDocument, IStructureModelListener listener) {
if(document == null) {
document = newDocument;
queueSourceUpdateTask(document.get());
reconcileConnection = reconcileMgr.connectDocument(document, this);
}
else if(document != newDocument) {
return false;
}
connectedListeners.addListener(listener);
return true;
}
protected synchronized void disconnectFromDocument(IStructureModelListener structureListener) {
connectedListeners.removeListener(structureListener);
if(!hasConnectedListeners()) {
reconcileConnection.disconnect();
reconcileConnection = null;
document = null;
queueUpdateTask(createDisconnectTask(this));
}
}
protected StructureUpdateTask queueSourceUpdateTask(final String source) {
StructureUpdateTask updateTask = createUpdateTask(this, source);
queueUpdateTask(updateTask);
return updateTask;
}
public synchronized StructureUpdateTask documentSaved(IDocument document) {
// need to recheck, the underlying document might have changed
if(document != this.document) {
return null;
}
StructureUpdateTask updateTask = createUpdateTask_forFileSave(this, document.get());
if(updateTask != null) {
queueUpdateTask(updateTask);
}
return updateTask;
}
protected synchronized void queueUpdateTask(StructureUpdateTask updateTask) {
setUpdateTask(updateTask);
executor.submitTask(updateTask);
}
@Override
protected void doHandleDataChanged() {
super.doHandleDataChanged();
notifyStructureChanged(this, globalListeners);
}
}
/* ----------------- ----------------- */
/**
* Create an update task for the given structureInfo, due to a document change.
* @param source the new source of the document being listened to.
*/
protected abstract StructureUpdateTask createUpdateTask(StructureInfo structureInfo, String source);
/**
* Similar to {@link #createUpdateTask(StructureInfo, String)} but only for when the file buffer
* is saved to disk. Default is to return null, which ignores this event.
*/
@SuppressWarnings("unused")
protected StructureUpdateTask createUpdateTask_forFileSave(StructureInfo structureInfo, String source) {
return null;
}
/**
* Create an update task for when the last listener of given structureInfo disconnects.
*/
protected DisconnectUpdatesTask createDisconnectTask(StructureInfo structureInfo) {
return new DisconnectUpdatesTask(structureInfo);
}
public static abstract class StructureUpdateTask extends DataUpdateTask<CommonResult<SourceFileStructure>> {
protected final StructureInfo structureInfo;
public StructureUpdateTask(StructureInfo structureInfo) {
super(structureInfo, structureInfo.getKey2().toString());
this.structureInfo = structureInfo;
}
@Override
protected void handleRuntimeException(RuntimeException e) {
LangCore.logInternalError(e);
}
@Override
protected final CommonResult<SourceFileStructure> createNewData() throws OperationCancellation {
try {
return new CommonResult<>(doCreateNewData());
} catch(CommonException e) {
return new CommonResult<>(null, e);
}
}
protected abstract SourceFileStructure doCreateNewData() throws CommonException, OperationCancellation;
}
public static class DisconnectUpdatesTask extends StructureUpdateTask {
public DisconnectUpdatesTask(StructureInfo structureInfo) {
super(structureInfo);
}
@Override
protected SourceFileStructure doCreateNewData() throws OperationCancellation {
Location location = structureInfo.getLocation();
if(location != null) {
handleDisconnectForLocation(location);
} else {
handleDisconnectForNoLocation();
}
throw new OperationCancellation();
}
@SuppressWarnings("unused")
protected void handleDisconnectForLocation(Location location) {
}
//@SuppressWarnings("unused")
protected void handleDisconnectForNoLocation() {
}
}
/* ----------------- Listeners ----------------- */
protected final ListenerListHelper<IStructureModelListener> globalListeners = new ListenerListHelper<>();
public IDisposable addListener(IStructureModelListener listener) {
assertNotNull(listener);
globalListeners.addListener(listener);
return () -> removeListener(listener);
}
public void removeListener(IStructureModelListener listener) {
globalListeners.removeListener(listener);
}
}