/*
* Copyright (c) 2012 Sam Harwell, Tunnel Vision Laboratories LLC
* All rights reserved.
*
* The source code of this document is proprietary work, and is not licensed for
* distribution. For information about licensing, contact Sam Harwell at:
* sam@tunnelvisionlabs.com
*/
package org.tvl.goworks.editor.go.formatting;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import org.antlr.netbeans.util.NotificationIcons;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.extexecution.ExternalProcessBuilder;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.editor.indent.spi.Context;
import org.netbeans.modules.editor.indent.spi.ExtraLock;
import org.netbeans.modules.editor.indent.spi.ReformatTask;
import org.openide.awt.NotificationDisplayer;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Utilities;
import org.tvl.goworks.editor.GoEditorKit;
/**
*
* @author Sam Harwell
*/
public class GoReformatTask implements ReformatTask {
// -J-Dorg.tvl.goworks.editor.go.formatting.GoReformatTask.level=FINE
private static final Logger LOGGER = Logger.getLogger(GoReformatTask.class.getName());
private final Context context;
private GoReformatTask(Context context) {
this.context = context;
}
public static String reformat(String text, GoCodeStyle style) {
return reformat(text, style, style.getTextLimitWidth());
}
public static String reformat(String text, GoCodeStyle style, int rightMargin) {
return runGofmt(text);
}
public static String runGofmt(String text) {
return runGofmt(text, true, 8);
}
public static String runGofmt(String text, boolean useTabs, int tabWidth) {
File goroot = new File(System.getenv("GOROOT"));
if (!goroot.isDirectory()) {
displayError("The GOROOT environment variable does not point to an accessible Go installation.");
throw new UnsupportedOperationException("Couldn't determine GOROOT.");
}
FileObject gorootObject = FileUtil.toFileObject(goroot);
if (gorootObject == null || !gorootObject.isFolder()) {
throw new UnsupportedOperationException("Couldn't determine GOROOT.");
}
FileObject binFolder = gorootObject.getFileObject("bin");
if (binFolder == null || !binFolder.isFolder()) {
displayError(String.format("The Go installation directory environment variable does not contain a 'bin' directory. Expected: %s%sbin", gorootObject.getPath(), File.separatorChar));
throw new UnsupportedOperationException("Couldn't determine Go bin directory.");
}
FileObject executable = binFolder.getFileObject("gofmt", "");
if (executable == null && Utilities.isWindows()) {
executable = binFolder.getFileObject("gofmt", "exe");
}
if (executable == null || !executable.isData()) {
String extension = Utilities.isWindows() ? ".exe" : "";
String expected = gorootObject.getPath() + File.separator + "bin" + File.separator + "gofmt" + extension;
if (File.separatorChar != '/') {
expected = expected.replace('/', File.separatorChar);
}
displayError(String.format("Couldn't find the Go tool. Expected: %s", expected));
throw new UnsupportedOperationException("Couldn't find the Go tool.");
}
List<String> args = new ArrayList<>();
args.add("-tabs=" + useTabs);
args.add("-tabwidth=" + tabWidth);
ExternalProcessBuilder nativeProcessBuilder = new ExternalProcessBuilder(executable.getPath());
for (String arg : args) {
nativeProcessBuilder = nativeProcessBuilder.addArgument(arg);
}
try {
Process process = nativeProcessBuilder.call();
final InputStream inputStream = process.getInputStream();
final InputStream errorStream = process.getErrorStream();
try (OutputStream outputStream = process.getOutputStream()) {
outputStream.write(text.getBytes());
}
ExecutorService ioService = Executors.newFixedThreadPool(2);
Future<String> resultFuture = ioService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
try {
return new java.util.Scanner(inputStream).useDelimiter("\\A").next();
} catch (NoSuchElementException ex) {
return "";
}
}
});
Future<String> errorFuture = ioService.submit(new Callable<String>() {
@Override
public String call() throws Exception {
try {
return new java.util.Scanner(errorStream).useDelimiter("\\A").next();
} catch (NoSuchElementException ex) {
return "";
}
}
});
try {
String result = resultFuture.get();
String error = errorFuture.get();
process.waitFor();
if (error != null && !error.isEmpty()) {
return null;
}
return result;
} catch (ExecutionException | InterruptedException ex) {
return null;
}
} catch (IOException ex) {
LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex);
return null;
}
}
@Override
public void reformat() throws BadLocationException {
if (context.indentRegions().size() > 1) {
throw new UnsupportedOperationException("The reformatter currently only supports one region per call.");
}
for (final Context.Region region : context.indentRegions()) {
final String original = context.document().getText(region.getStartOffset(), region.getEndOffset() - region.getStartOffset());
if (original == null || original.isEmpty()) {
continue;
}
final String formatted = runGofmt(original);
if (formatted == null || formatted.equals(original)) {
continue;
}
Runnable applyer = new Runnable() {
@Override
public void run() {
try {
context.document().remove(region.getStartOffset(), region.getEndOffset() - region.getStartOffset());
context.document().insertString(region.getStartOffset(), formatted, null);
} catch (BadLocationException ex) {
LOGGER.log(Level.WARNING, ex.getLocalizedMessage(), ex);
throw new RuntimeException(ex);
}
}
};
if (context.document() instanceof BaseDocument) {
((BaseDocument)context.document()).runAtomicAsUser(applyer);
} else {
applyer.run();
}
}
}
@Override
public ExtraLock reformatLock() {
return null;
}
private static void displayError(String message) {
NotificationDisplayer.getDefault().notify("Error executing gofmt", NotificationIcons.ERROR, message, null);
}
@MimeRegistration(mimeType=GoEditorKit.GO_MIME_TYPE, service=ReformatTask.Factory.class)
public static final class FactoryImpl implements Factory {
@Override
public ReformatTask createTask(Context context) {
return new GoReformatTask(context);
}
}
}