/*******************************************************************************
* Copyright (c) 2009 Andrey Loskutov.
* 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
* Contributor: Andrey Loskutov - initial API and implementation
*******************************************************************************/
package de.loskutov.anyedit.actions;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.eclipse.core.filebuffers.FileBuffers;
import org.eclipse.core.filebuffers.ITextFileBuffer;
import org.eclipse.core.filebuffers.ITextFileBufferManager;
import org.eclipse.core.filebuffers.LocationKind;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.WorkspaceJob;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.MultiStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.text.DocumentRewriteSession;
import org.eclipse.jface.text.DocumentRewriteSessionType;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentExtension;
import org.eclipse.jface.text.IDocumentExtension4;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IActionDelegate;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.PlatformUI;
import de.loskutov.anyedit.AnyEditToolsPlugin;
import de.loskutov.anyedit.IAnyEditConstants;
import de.loskutov.anyedit.Messages;
import de.loskutov.anyedit.ui.preferences.CombinedPreferences;
import de.loskutov.anyedit.util.EclipseUtils;
import de.loskutov.anyedit.util.LineReplaceResult;
import de.loskutov.anyedit.util.TextReplaceResultSet;
@SuppressWarnings("unused")
public class ConvertAllAction extends Action implements IActionDelegate, IWorkbenchWindowActionDelegate {
protected List<IFile> selectedFiles;
protected List<IResource> selectedResources;
protected static final int MODIFIED = 1 << 0;
protected static final int SKIPPED = 1 << 1;
protected static final int ERROR = 1 << 2;
protected static final IContentType TEXT_TYPE = Platform.getContentTypeManager()
.getContentType("org.eclipse.core.runtime.text");
public ConvertAllAction() {
super();
selectedFiles = new ArrayList<IFile>();
selectedResources = new ArrayList<IResource>();
}
@Override
public void run(IAction action) {
// selectedFiles contains all files for convert.
WorkspaceJob job = new ConvertJob("Converting 'Tabs<->Spaces'", new ArrayList<IFile>(selectedFiles));
selectedResources.clear();
selectedFiles.clear();
job.schedule();
PlatformUI.getWorkbench().getProgressService().showInDialog(AnyEditToolsPlugin.getShell(), job);
}
protected static final class ConvertJob extends WorkspaceJob {
private final Shell shell;
private final Spaces spacesAction;
private final List<IFile> selectedFiles;
private final ITextFileBufferManager fbManager;
public ConvertJob(String name, List<IFile> selectedFiles) {
super(name);
this.selectedFiles = selectedFiles;
spacesAction = new Spaces();
spacesAction.setUsedOnSave(false);
shell = AnyEditToolsPlugin.getShell();
fbManager = FileBuffers.getTextFileBufferManager();
}
@Override
public IStatus runInWorkspace(IProgressMonitor monitor) {
monitor.beginTask(Messages.ConvertAll_task, selectedFiles.size());
int filesToConvert = selectedFiles.size();
IPreferenceStore preferenceStore = AnyEditToolsPlugin.getDefault()
.getPreferenceStore();
boolean saveIfDirty = preferenceStore
.getBoolean(IAnyEditConstants.SAVE_DIRTY_BUFFER);
int modified = 0;
int skipped = 0;
List<IStatus> errors = new ArrayList<IStatus>();
long start = System.currentTimeMillis();
try {
for (int i = 0; i < filesToConvert && !monitor.isCanceled(); i++) {
monitor.internalWorked(1);
IFile file = selectedFiles.get(i);
try {
int result = performAction(file, saveIfDirty, monitor);
if (result == ERROR) {
errors.add(new Status(IStatus.ERROR, AnyEditToolsPlugin.getId(),
"'Tabs<->Spaces' operation failed for file: " + file, null));
} else if (result == MODIFIED) {
modified++;
} else if (result == SKIPPED) {
skipped++;
}
} catch (CoreException e) {
errors.add(new Status(IStatus.ERROR, AnyEditToolsPlugin.getId(),
"'Tabs<->Spaces' operation failed for file: " + file, e));
}
}
} finally {
monitor.done();
long stop = System.currentTimeMillis();
long msec = (stop - start);
AnyEditToolsPlugin.logInfo("Tabs<->Spaces: modified " +
modified + " files from "
+ filesToConvert + ", ignored "
+ skipped + ", failed on: " + errors.size() + " ("
+ msec + " ms)");
}
if(errors.size() == 0){
if(monitor.isCanceled()){
AnyEditToolsPlugin.logError("'Tabs<->Spaces' operation cancelled by user", null);
return Status.CANCEL_STATUS;
}
return Status.OK_STATUS;
}
MultiStatus error = new MultiStatus(AnyEditToolsPlugin.getId(), IStatus.ERROR, "'Tabs<->Spaces' operation failed for "
+ errors.size()
+ " files. Please check log for details.", null);
for (IStatus status : errors) {
error.add(status);
}
return error;
}
private int performAction(IFile file,
boolean saveIfDirty, IProgressMonitor monitor) throws CoreException {
// set current file to action (required to get tab width from)
spacesAction.setFile(file);
CombinedPreferences preferences = spacesAction.getCombinedPreferences();
String filterPerf = preferences.getString(IAnyEditConstants.PREF_ACTIVE_FILTERS_LIST);
String[] filters = EclipseUtils.parseList(filterPerf);
String actionId;
if (spacesAction.isDefaultTabToSpaces()) {
actionId = AbstractTextAction.ACTION_ID_CONVERT_TABS;
} else {
actionId = AbstractTextAction.ACTION_ID_CONVERT_SPACES;
}
// 1) get file name. filter all excluded in preferences
if (matchFilter(file, filters)) {
return SKIPPED;
}
// 2) get content type. filter all non-text files
if (hasWrongContentType(file, monitor)) {
return SKIPPED;
}
// do the main work
return convertFile(actionId, file, saveIfDirty, monitor);
}
private int convertFile(final String actionId, IFile file, boolean saveIfDirty, IProgressMonitor monitor) throws CoreException {
int result = ERROR;
IPath fullPath = file.getFullPath();
try {
fbManager.connect(fullPath, LocationKind.IFILE, new SubProgressMonitor(monitor, 1));
monitor.subTask(fullPath.makeRelative().toString());
final ITextFileBuffer fileBuffer = fbManager.getTextFileBuffer(fullPath, LocationKind.IFILE);
if(!file.isSynchronized(IResource.DEPTH_ZERO)){
file.refreshLocal(IResource.DEPTH_ZERO, monitor);
}
// check if buffer is opened by some editor - save it first, if required
boolean wasDirty = fileBuffer.isDirty();
if (saveIfDirty && wasDirty && fileBuffer.isCommitable()) {
fileBuffer.commit(new SubProgressMonitor(monitor, 2), false);
}
// 4) perform convert in-memory
result = convertBuffer(actionId, file, fileBuffer, monitor);
if (result == MODIFIED && (!wasDirty || saveIfDirty)) {
if(fileBuffer.isShared()){
// convertBuffer() should checkout the file, but...
// because convertBuffer() operation was running in the UI thread,
// the checkout file operation task may be still incomplete
// second call to check-out file from VCS, if any
fileBuffer.validateState(monitor, shell);
}
fileBuffer.commit(new SubProgressMonitor(monitor, 2), false);
}
} finally {
// clean up - action shouldn't have old file reference
spacesAction.setFile(null);
fbManager.disconnect(fullPath, LocationKind.IFILE, new SubProgressMonitor(monitor, 1));
}
return result;
}
private int convertBuffer(String actionId, final IFile file,
ITextFileBuffer fileBuffer, IProgressMonitor monitor) throws CoreException {
final IDocument document = fileBuffer.getDocument();
final TextReplaceResultSet resultSet = spacesAction.estimateActionRange(document);
// no lines affected - return immediately
if (resultSet.getNumberOfLines() == 0) {
return SKIPPED;
}
// perform memory based replace, the result will contain all changed lines
try {
spacesAction.doTextOperation(document, actionId, resultSet);
} catch (Exception ex) {
AnyEditToolsPlugin.logError("doTextOperation() failed on: " + file, ex);
return ERROR;
}
if (!resultSet.areResultsChanged()) {
return SKIPPED;
}
int result = ERROR;
// check-out file from VCS, if any
fileBuffer.validateState(monitor, shell);
// if buffer is shared, then it means, that this operation could affect
// changes in the UI thread because of associated editors and we *must*
// to run it in the UI Thread too...
if (fileBuffer.isShared()) {
shell.getDisplay().syncExec(new Runnable() {
@Override
public void run() {
writeDocument(file, document, resultSet);
}
});
} else {
writeDocument(file, document, resultSet);
}
if (resultSet.getException() != null) {
result = ERROR;
} else {
result = MODIFIED;
}
return result;
}
static void writeDocument(IFile file, IDocument document, TextReplaceResultSet resultSet) {
int docLinesNbr = document.getNumberOfLines();
int changedLinesNbr = resultSet.getNumberOfLines();
boolean rewriteWholeDoc = changedLinesNbr >= docLinesNbr;
// some oddities with document??? prevent overflow in changedLinesNbr
if (rewriteWholeDoc) {
changedLinesNbr = docLinesNbr;
}
// this operation could affect changes in UI thread because of associated editors
final DocumentRewriteSession rewriteSession = startSequentialRewriteMode(document);
try {
for (int i = 0; i < changedLinesNbr; i++) {
LineReplaceResult trr = resultSet.get(i);
if (trr != null) {
IRegion lineInfo = document.getLineInformation(i
+ resultSet.getStartLine());
document.replace(lineInfo.getOffset() + trr.startReplaceIndex,
trr.rangeToReplace, trr.textToReplace);
}
}
} catch (Exception e) {
resultSet.setException(e);
AnyEditToolsPlugin.logError(
"Error during write document for file: " + file, e);
} finally {
stopSequentialRewriteMode(document, rewriteSession);
resultSet.clear();
}
}
@SuppressWarnings("deprecation")
private static DocumentRewriteSession startSequentialRewriteMode(IDocument document) {
if (document instanceof IDocumentExtension4) {
IDocumentExtension4 extension = (IDocumentExtension4) document;
return extension.startRewriteSession(DocumentRewriteSessionType.SEQUENTIAL);
}
if (document instanceof IDocumentExtension) {
IDocumentExtension extension = (IDocumentExtension) document;
extension.startSequentialRewrite(false);
}
return null;
}
@SuppressWarnings("deprecation")
private static void stopSequentialRewriteMode(IDocument document,
DocumentRewriteSession rewriteSession) {
if (document instanceof IDocumentExtension4) {
IDocumentExtension4 extension = (IDocumentExtension4) document;
extension.stopRewriteSession(rewriteSession);
} else if (document instanceof IDocumentExtension) {
IDocumentExtension extension = (IDocumentExtension) document;
extension.stopSequentialRewrite();
}
}
private static boolean hasWrongContentType(IFile file, IProgressMonitor monitor) {
try {
IContentDescription contentDescr = file.getContentDescription();
if (contentDescr == null) {
return true;
}
IContentType contentType = contentDescr.getContentType();
if (contentType == null) {
return true;
}
return !contentType.isKindOf(TEXT_TYPE);
} catch (CoreException e) {
AnyEditToolsPlugin.logError("Could not get content type for: " + file, e);
}
return false;
}
private static boolean matchFilter(IFile file, String[] filters) {
return EclipseUtils.matchFilter(file.getName(), filters);
}
}
@Override
public void selectionChanged(IAction action, ISelection selection) {
selectedFiles.clear();
selectedResources.clear();
if (selection instanceof IStructuredSelection) {
IStructuredSelection ssel = (IStructuredSelection) selection;
Iterator<?> iterator = ssel.iterator();
while (iterator.hasNext()) {
// by definition in plugin.xml, we are called only on IFile's
selectedFiles.add((IFile) iterator.next());
}
}
}
@Override
public void dispose() {
selectedFiles.clear();
selectedResources.clear();
}
@Override
public void init(IWorkbenchWindow window) {
// do nothing
}
}