/*
* Copyright (c) 2013, 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.formatter;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.dart2js.ProcessRunner;
import com.google.dart.tools.core.model.DartSdkManager;
import com.google.dart.tools.core.utilities.general.StringUtilities;
import com.google.dart.tools.core.utilities.io.FileUtilities;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.actions.DartEditorActionDefinitionIds;
import com.google.dart.tools.ui.internal.text.editor.DartEditor;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.window.IShellProvider;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.WorkspaceAction;
import org.eclipse.ui.ide.ResourceUtil;
import org.eclipse.ui.internal.editors.text.EditorsPlugin;
import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Launches the <code>dartfmt</code> process collecting stdout, stderr, and exit code information.
*/
@SuppressWarnings("restriction")
public class DartFormatter {
public static class DartFmtRunner {
public static FormattedSource format(final String source, final Point selection,
IProgressMonitor monitor) throws IOException, CoreException {
File dartfmt = DartSdkManager.getManager().getSdk().getDartFmtExecutable();
if (!dartfmt.canExecute()) {
return null;
}
if (source.length() == 0) {
FormattedSource result = new FormattedSource();
result.source = source;
return result;
}
ProcessBuilder builder = new ProcessBuilder();
List<String> args = new ArrayList<String>();
args.add(dartfmt.getPath());
if (selection != null) {
args.add(ARGS_PRESERVE_FLAG);
args.add(selection.x + ":" + selection.y);
}
if (getMaxLineLengthEnabled() && getMaxLineLength().length() > 0) {
args.add(ARGS_MAX_LINE_LEN_FLAG);
args.add(getMaxLineLength());
}
args.add(ARGS_MACHINE_FORMAT_FLAG);
builder.command(args);
builder.redirectErrorStream(true);
ProcessRunner runner = new ProcessRunner(builder) {
@Override
protected void processStarted(Process process) throws IOException {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
process.getOutputStream(),
"UTF-8"), source.length());
writer.append(source);
writer.close();
}
};
runner.runSync(monitor);
StringBuilder sb = new StringBuilder();
if (!runner.getStdOut().isEmpty()) {
sb.append(runner.getStdOut());
}
//TODO (pquitslund): better error handling
if (runner.getExitCode() != 0) {
sb.append(runner.getStdErr());
throw new IOException(sb.toString());
}
String formattedSource = sb.toString();
if (!formattedSource.startsWith("{")) {
throw new IOException(formattedSource);
}
try {
JSONObject json = new JSONObject(formattedSource);
String sourceString = (String) json.get(JSON_SOURCE_KEY);
JSONObject selectionJson = (JSONObject) json.get(JSON_SELECTION_KEY);
//TODO (pquitslund): figure out why we (occasionally) need to remove an extra trailing NEWLINE
if (sourceString.endsWith("\n\n")) {
sourceString = sourceString.substring(0, sourceString.length() - 1);
}
// prepare FormattedSource
FormattedSource result = new FormattedSource();
result.source = sourceString;
result.selectionOffset = selectionJson.getInt(JSON_OFFSET_KEY);
result.selectionLength = selectionJson.getInt(JSON_LENGTH_KEY);
// compute change
if (!sourceString.equals(source)) {
int prefixLength = StringUtilities.findCommonPrefix(source, sourceString);
int suffixLength = StringUtilities.findCommonSuffix(source, sourceString);
String prefix = source.substring(0, prefixLength);
String suffix = source.substring(source.length() - suffixLength, source.length());
int commonLength = StringUtilities.findCommonOverlap(prefix, suffix);
suffixLength -= commonLength;
result.changeOffset = prefixLength;
result.changeLength = source.length() - prefixLength - suffixLength;
int replacementEnd = sourceString.length() - suffixLength;
result.changeReplacement = sourceString.substring(prefixLength, replacementEnd);
}
// done
return result;
} catch (JSONException e) {
DartToolsPlugin.log(e);
throw new IOException(e);
}
}
}
public static class FormatFileAction extends WorkspaceAction {
private List<IFile> files = Arrays.asList(new IFile[0]);
public FormatFileAction(IShellProvider provider) {
super(provider, "Format");
setId(DartEditorActionDefinitionIds.QUICK_FORMAT);
setActionDefinitionId(DartEditorActionDefinitionIds.QUICK_FORMAT);
}
@Override
public void run() {
for (IFile file : files) {
try {
format(file, new NullProgressMonitor());
} catch (Exception e) {
DartCore.logError(e);
}
}
}
@Override
protected String getOperationMessage() {
return "Formatting;";
}
@Override
protected List<IFile> getSelectedResources() {
@SuppressWarnings("unchecked")
List<Object> res = super.getSelectedResources();
ArrayList<IFile> resources = new ArrayList<IFile>();
for (Object r : res) {
if (r instanceof IFile && DartCore.isDartLikeFileName(((IResource) r).getName())) {
resources.add((IFile) r);
}
}
return resources;
}
@Override
protected boolean updateSelection(IStructuredSelection selection) {
files = getSelectedResources();
return !files.isEmpty();
}
}
/**
* Holder for formatted source and selection info.
*/
public static class FormattedSource {
public int selectionOffset;
public int selectionLength;
public String source;
public int changeOffset;
public int changeLength;
public String changeReplacement;
}
/**
* Preference key for showing migrating print margin preferences.
*/
private final static String PRINT_MARGIN_MIGRATED = "dart-printMargin-migrated";
/**
* Preference key for showing print margin ruler.
*/
public final static String PRINT_MARGIN = "dart-printMargin";
/**
* Preference key for print margin ruler color.
*/
public final static String PRINT_MARGIN_COLOR = AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLOR;
/**
* Preference key for print margin ruler column.
*/
public final static String PRINT_MARGIN_COLUMN = "dart-printMarginColumn";
private static final String ARGS_MACHINE_FORMAT_FLAG = "-m";
private static final String ARGS_MAX_LINE_LEN_FLAG = "-l";
private static final String ARGS_PRESERVE_FLAG = "--preserve";
private static final String JSON_LENGTH_KEY = "length";
private static final String JSON_OFFSET_KEY = "offset";
private static final String JSON_SELECTION_KEY = "selection";
private static final String JSON_SOURCE_KEY = "source";
public static void ensurePrintMarginPreferencesMigrated() {
IPreferenceStore store = EditorsPlugin.getDefault().getPreferenceStore();
if (!store.getBoolean(PRINT_MARGIN_MIGRATED)) {
store.setValue(
PRINT_MARGIN_COLUMN,
store.getString(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN_COLUMN));
store.setValue(
PRINT_MARGIN,
store.getString(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_PRINT_MARGIN));
store.setValue(PRINT_MARGIN_MIGRATED, true);
}
}
/**
* Run the formatter on the given input file.
*
* @param file the source to pass to the formatter
* @param selection the selection info to pass into the formatter
* @param monitor the monitor for displaying progress
* @throws IOException if an exception was thrown during execution
* @throws CoreException if an exception occurs in file refresh
*/
public static void format(IFile file, IProgressMonitor monitor) throws IOException, CoreException {
if (file == null || DartCore.isPackagesResource(file)) {
return;
}
DartEditor editor = getDirtyEditor(file);
if (editor != null) {
// Delegate to the editor if possible
editor.doFormat();
} else {
formatFile(file, monitor);
}
}
/**
* Run the formatter on the given input source.
*
* @param source the source to pass to the formatter
* @param selection the selection info to pass into the formatter
* @param monitor the monitor for displaying progress
* @throws IOException if an exception was thrown during execution
* @throws CoreException if an exception occurs in file refresh
* @return the formatted source (or null in case formatting could not be executed)
*/
public static FormattedSource format(final String source, final Point selection,
IProgressMonitor monitor) throws IOException, CoreException {
return DartFmtRunner.format(source, selection, monitor);
}
public static String getMaxLineLength() {
return EditorsPlugin.getDefault().getPreferenceStore().getString(PRINT_MARGIN_COLUMN);
}
public static boolean getMaxLineLengthEnabled() {
return EditorsPlugin.getDefault().getPreferenceStore().getBoolean(PRINT_MARGIN);
}
public static boolean isAvailable() {
return DartSdkManager.getManager().getSdk().getDartFmtExecutable().canExecute();
}
public static void setMaxLineLength(String maxLineLength) {
EditorsPlugin.getDefault().getPreferenceStore().setValue(PRINT_MARGIN_COLUMN, maxLineLength);
}
public static void setMaxLineLengthEnabled(boolean enabled) {
EditorsPlugin.getDefault().getPreferenceStore().setValue(PRINT_MARGIN, enabled);
}
private static IEditorPart findEditor(final IWorkbenchPage activePage, final IFile file) {
final IEditorPart[] editor = new IEditorPart[1];
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
editor[0] = ResourceUtil.findEditor(activePage, file);
}
});
return editor[0];
}
private static void formatFile(IFile file, IProgressMonitor monitor)
throws UnsupportedEncodingException, CoreException, IOException {
Reader reader = new InputStreamReader(file.getContents(), file.getCharset());
String contents = FileUtilities.getContents(reader);
if (contents != null) {
FormattedSource result = format(contents, null, monitor);
if (!contents.equals(result.source)) {
InputStream stream = new ByteArrayInputStream(result.source.getBytes("UTF-8"));
file.setContents(stream, IResource.KEEP_HISTORY, monitor);
}
}
}
private static IWorkbenchPage getActivePage() {
final IWorkbenchPage[] page = new IWorkbenchPage[1];
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window != null) {
page[0] = window.getActivePage();
}
}
});
return page[0];
}
private static DartEditor getDirtyEditor(IFile file) {
IWorkbenchPage activePage = getActivePage();
if (activePage != null) {
IEditorPart editor = findEditor(activePage, file);
if (editor instanceof DartEditor) {
if (editor.isDirty()) {
return (DartEditor) editor;
}
}
}
return null;
}
}