/******************************************************************************* * Copyright (c) 2012-2017 Codenvy, S.A. * 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: * Codenvy, S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.che.ide.api.editor.reconciler; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.inject.assistedinject.Assisted; import com.google.inject.assistedinject.AssistedInject; import org.eclipse.che.ide.api.editor.EditorInput; import org.eclipse.che.ide.api.editor.document.Document; import org.eclipse.che.ide.api.editor.document.DocumentHandle; import org.eclipse.che.ide.api.editor.events.DocumentChangeEvent; import org.eclipse.che.ide.api.editor.partition.DocumentPartitioner; import org.eclipse.che.ide.api.editor.text.Region; import org.eclipse.che.ide.api.editor.text.RegionImpl; import org.eclipse.che.ide.api.editor.text.TypedRegion; import org.eclipse.che.ide.api.editor.texteditor.TextEditor; import org.eclipse.che.ide.util.loging.Log; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Default implementation of {@link Reconciler}. * Also this implementation provide autosave function. * Autosave will performed before 'reconcile'. */ /* Maybe this class not proper place for autosave function, but for this issue: https://jira.codenvycorp.com/browse/IDEX-2099 we need to save file content before 'reconcile'. */ public class ReconcilerWithAutoSave implements Reconciler { private static final int DELAY = 5000; private final Map<String, ReconcilingStrategy> strategies; private final String partition; private final DocumentPartitioner partitioner; private DirtyRegionQueue dirtyRegionQueue; private DocumentHandle documentHandle; private TextEditor editor; private boolean autoSaveEnabled = true; private final Timer autoSaveTimer = new Timer() { @Override public void run() { save(); } }; @AssistedInject public ReconcilerWithAutoSave(@Assisted final String partition, @Assisted final DocumentPartitioner partitioner) { this.partition = partition; strategies = new HashMap<>(); this.partitioner = partitioner; } private void reconcilerDocumentChanged() { for (String key : strategies.keySet()) { ReconcilingStrategy reconcilingStrategy = strategies.get(key); reconcilingStrategy.setDocument(documentHandle.getDocument()); } autoSaveTimer.cancel(); autoSaveTimer.schedule(DELAY); } @Override public void install(TextEditor editor) { this.editor = editor; this.dirtyRegionQueue = new DirtyRegionQueue(); reconcilerDocumentChanged(); } @Override public void uninstall() { autoSaveTimer.cancel(); for (ReconcilingStrategy strategy : strategies.values()) { strategy.closeReconciler(); } } private void save() { if (autoSaveEnabled) { if (editor.isDirty()) { editor.doSave(new AsyncCallback<EditorInput>() { @Override public void onFailure(Throwable throwable) { Log.error(ReconcilerWithAutoSave.class, throwable); } @Override public void onSuccess(EditorInput editorInput) { processNextRegion(); } }); return; } } processNextRegion(); } private void processNextRegion() { final DirtyRegion region = dirtyRegionQueue.removeNextDirtyRegion(); process(region); } /** * Processes a dirty region. If the dirty region is <code>null</code> the whole document is consider being dirty. The dirty region is * partitioned by the document and each partition is handed over to a reconciling strategy registered for the partition's content type. * * @param dirtyRegion the dirty region to be processed */ protected void process(final DirtyRegion dirtyRegion) { Region region = dirtyRegion; if (region == null) { region = new RegionImpl(0, getDocument().getContents().length()); } final List<TypedRegion> regions = computePartitioning(region.getOffset(), region.getLength()); for (final TypedRegion r : regions) { final ReconcilingStrategy strategy = getReconcilingStrategy(r.getType()); if (strategy == null) { continue; } if (dirtyRegion != null) { strategy.reconcile(dirtyRegion, r); } else { strategy.reconcile(r); } } } /** * Computes and returns the partitioning for the given region of the input document of the reconciler's connected text viewer. * * @param offset the region offset * @param length the region length * @return the computed partitioning */ private List<TypedRegion> computePartitioning(final int offset, final int length) { return partitioner.computePartitioning(offset, length); } /** * Returns the input document of the text view this reconciler is installed on. * * @return the reconciler document */ protected Document getDocument() { return documentHandle.getDocument(); } /** * Creates a dirty region for a document event and adds it to the queue. * * @param event the document event for which to create a dirty region */ private void createDirtyRegion(final DocumentChangeEvent event) { if (event.getLength() == 0 && event.getText() != null) { // Insert dirtyRegionQueue.addDirtyRegion(new DirtyRegion(event.getOffset(), event.getText().length(), DirtyRegion.INSERT, event.getText())); } else if (event.getText() == null || event.getText().length() == 0) { // Remove dirtyRegionQueue.addDirtyRegion(new DirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.REMOVE, null)); } else { // Replace (Remove + Insert) dirtyRegionQueue.addDirtyRegion(new DirtyRegion(event.getOffset(), event.getLength(), DirtyRegion.REMOVE, null)); dirtyRegionQueue.addDirtyRegion(new DirtyRegion(event.getOffset(), event.getText().length(), DirtyRegion.INSERT, event.getText())); } } @Override public ReconcilingStrategy getReconcilingStrategy(final String contentType) { return strategies.get(contentType); } @Override public String getDocumentPartitioning() { return partition; } @Override public void addReconcilingStrategy(final String contentType, final ReconcilingStrategy strategy) { strategies.put(contentType, strategy); } @Override public void onDocumentChange(final DocumentChangeEvent event) { if (documentHandle == null || !documentHandle.isSameAs(event.getDocument())) { return; } createDirtyRegion(event); autoSaveTimer.cancel(); autoSaveTimer.schedule(DELAY); } @Override public DocumentHandle getDocumentHandle() { return this.documentHandle; } @Override public void setDocumentHandle(final DocumentHandle handle) { this.documentHandle = handle; } public boolean isAutoSaveEnabled() { return autoSaveEnabled; } public void disableAutoSave() { autoSaveEnabled = false; autoSaveTimer.cancel(); } public void enableAutoSave() { autoSaveEnabled = true; autoSaveTimer.schedule(DELAY); } }