blob: 3df7bbd523dd03f0dea962a2933f8a52a200b614 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.r.launching;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IMarker;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.variables.IDynamicVariable;
import org.eclipse.core.variables.IStringVariable;
import org.eclipse.debug.core.DebugPlugin;
import org.eclipse.debug.core.IStatusHandler;
import org.eclipse.debug.ui.IDebugUIConstants;
import org.eclipse.jface.text.AbstractDocument;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.statushandlers.StatusManager;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.status.Status;
import org.eclipse.statet.jcommons.status.StatusException;
import org.eclipse.statet.jcommons.status.eplatform.EStatusUtils;
import org.eclipse.statet.jcommons.ts.core.ToolRunnable;
import org.eclipse.statet.ecommons.preferences.core.Preference.BooleanPref;
import org.eclipse.statet.ecommons.preferences.core.util.PreferenceUtils;
import org.eclipse.statet.ecommons.text.TextUtil;
import org.eclipse.statet.ecommons.variables.core.DynamicVariable;
import org.eclipse.statet.ecommons.variables.core.StringVariable;
import org.eclipse.statet.ecommons.variables.core.VariableText;
import org.eclipse.statet.ecommons.variables.core.VariableText.LocationProcessor;
import org.eclipse.statet.internal.r.debug.ui.RControllerCodeLaunchConnector;
import org.eclipse.statet.internal.r.debug.ui.RLaunchingMessages;
import org.eclipse.statet.internal.r.debug.ui.launcher.RCodeLaunchRegistry;
import org.eclipse.statet.internal.r.debug.ui.launcher.RCodeLaunchRegistry.ContentHandler.FileCommand;
import org.eclipse.statet.ltk.model.core.elements.ISourceUnit;
import org.eclipse.statet.ltk.model.core.elements.IWorkspaceSourceUnit;
import org.eclipse.statet.nico.core.runtime.ToolController;
import org.eclipse.statet.nico.core.runtime.ToolWorkspace;
import org.eclipse.statet.r.console.core.AbstractRController;
import org.eclipse.statet.r.core.RUtil;
import org.eclipse.statet.r.core.model.IRLangSourceElement;
import org.eclipse.statet.r.core.model.IRSourceUnit;
import org.eclipse.statet.r.core.model.IRWorkspaceSourceUnit;
import org.eclipse.statet.r.core.rsource.ast.GenericVisitor;
import org.eclipse.statet.r.core.rsource.ast.RAstNode;
import org.eclipse.statet.r.nico.IRModelSrcref;
import org.eclipse.statet.r.ui.RUI;
/**
* Provides methods to submit code to R
*
* The methods use the code launch connector selected in the preferences
* and therefore it supports external consoles too
* (in contrast to direct usage of new {@link AbstractRController}).
*/
public final class RCodeLaunching {
public static final String SUBMIT_SELECTION_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitSelectionToR"; //$NON-NLS-1$
public static final String SUBMIT_SELECTION_GOTOCONSOLE_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitSelectionToR_GotoConsole"; //$NON-NLS-1$
public static final String SUBMIT_SELECTION_PASTEOUTPUT_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitSelectionToR_PasteOutput"; //$NON-NLS-1$
public static final String SUBMIT_UPTO_SELECTION_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitUptoSelectionToR"; //$NON-NLS-1$
public static final String SUBMIT_FILEVIACOMMAND_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitFileToRViaCommand"; //$NON-NLS-1$
public static final String SUBMIT_FILEVIACOMMAND_GOTOCONSOLE_COMMAND_ID = "org.eclipse.statet.r.commands.SubmitFileToRViaCommand_GotoConsole"; //$NON-NLS-1$
public static final String FILE_COMMAND_ID_PARAMTER_ID = "fileCommandId"; //$NON-NLS-1$
private static final IStringVariable FILE_NAME_VARIABLE = new StringVariable("resource_loc", "The complete path of the source file"); //$NON-NLS-1$
private static final IStringVariable FILE_ENCODING_VARIABLE = new StringVariable("resource_encoding", "The encoding of the source file"); //$NON-NLS-1$
private static final IStringVariable ECHO_ENABLED_VARIABLE = new StringVariable("echo", "If echo is enabled"); //$NON-NLS-1$
private static final IStatus STATUS_PROMPTER = new org.eclipse.core.runtime.Status(IStatus.INFO, IDebugUIConstants.PLUGIN_ID, 200, "", null); //$NON-NLS-1$
private static final IStatus STATUS_SAVE = new org.eclipse.core.runtime.Status(IStatus.INFO, DebugPlugin.getUniqueIdentifier(), 222, "", null); //$NON-NLS-1$
public static class SourceRegion implements IRModelSrcref, IAdaptable {
private static final String MARKER_TYPE = "org.eclipse.core.resources.marker";
private static class ElementSearcher extends GenericVisitor {
private List<IRLangSourceElement> fList;
@Override
public void visitNode(final RAstNode node) throws InvocationTargetException {
final List<Object> attachments= node.getAttachments();
for (final Object attachment : attachments) {
if (attachment instanceof IRLangSourceElement) {
if (fList == null) {
fList= new ArrayList<>();
}
fList.add((IRLangSourceElement) attachment);
}
}
if (fList == null) {
super.visitNode(node);
}
}
}
private final IRSourceUnit fSourceUnit;
private RAstNode fNode;
private List<IRLangSourceElement> fElements;
private final AbstractDocument fDocument;
private int fBeginOffset = -1;
private int fBeginLine = -1;
private int fBeginColumn = -1;
private int fEndOffset = -1;
private int fEndLine = -1;
private int fEndColumn = -1;
private String fCode;
private IMarker fMarker;
public SourceRegion(final IRSourceUnit file, final AbstractDocument document) {
fSourceUnit = file;
fDocument = document;
}
@Override
public IRSourceUnit getFile() {
return fSourceUnit;
}
public void setNode(final RAstNode node) {
fNode = node;
}
@Override
public List<IRLangSourceElement> getElements() {
if (fElements == null) {
if (fNode != null) {
final ElementSearcher searcher = new ElementSearcher();
try {
searcher.visitNode(fNode);
fElements = searcher.fList;
}
catch (final InvocationTargetException e) {}
}
if (fElements == null) {
fElements = Collections.emptyList();
}
}
return fElements;
}
public void setCode(final String code) {
fCode = code;
}
public void setBegin(final int offset) throws BadLocationException {
fBeginOffset = offset;
fBeginLine = fDocument.getLineOfOffset(fBeginOffset);
fBeginColumn = TextUtil.getColumn(fDocument, fBeginOffset, fBeginLine, 8);
}
@Override
public boolean hasBeginDetail() {
return (fBeginLine >= 0 && fBeginColumn >= 0);
}
@Override
public int getOffset() {
return fBeginOffset;
}
@Override
public int getFirstLine() {
return fBeginLine;
}
@Override
public int getFirstColumn() {
return fBeginColumn;
}
public void setEnd(final int offset) throws BadLocationException {
fEndOffset = offset;
fEndLine = fDocument.getLineOfOffset(fEndOffset);
fEndColumn = TextUtil.getColumn(fDocument, fEndOffset-1, fEndLine, 8);
}
@Override
public boolean hasEndDetail() {
return (fEndLine >= 0 && fEndColumn >= 0);
}
@Override
public int getLength() {
return fEndOffset-fBeginOffset;
}
@Override
public int getLastLine() {
return fEndLine;
}
@Override
public int getLastColumn() {
return fEndColumn;
}
void installMarker() {
if (fSourceUnit instanceof IRWorkspaceSourceUnit) {
final IResource resource = ((IRWorkspaceSourceUnit) fSourceUnit).getResource();
try {
fMarker = resource.createMarker(MARKER_TYPE);
fMarker.setAttribute(IMarker.CHAR_START, fBeginOffset);
fMarker.setAttribute(IMarker.CHAR_END, fEndOffset);
}
catch (final CoreException e) {
StatusManager.getManager().handle(new org.eclipse.core.runtime.Status(IStatus.ERROR, RUI.BUNDLE_ID, 0,
"An error occurred when creating code position marker.", e));
}
}
}
void disposeMarker() {
if (this.fMarker != null) {
try {
this.fMarker.delete();
this.fMarker = null;
}
catch (final CoreException e) {
StatusManager.getManager().handle(new org.eclipse.core.runtime.Status(IStatus.ERROR, RUI.BUNDLE_ID, 0,
"An error occurred when removing code position marker.", e));
}
}
}
@Override
public <T> T getAdapter(final Class<T> adapterType) {
if (adapterType == IMarker.class) {
return (T) this.fMarker;
}
return null;
}
}
public static void gotoRConsole() throws CoreException {
final IRCodeSubmitConnector connector = RCodeLaunchRegistry.getDefault().getConnector();
connector.gotoConsole();
}
public static String getFileCommand(final String id) {
final FileCommand fileCommand = RCodeLaunchRegistry.getDefault().getFileCommand(id);
if (fileCommand != null) {
return fileCommand.getCurrentCommand();
}
return null;
}
public static String getPreferredFileCommand(final String contentType) {
final FileCommand fileCommand = RCodeLaunchRegistry.getDefault().getContentFileCommand(contentType);
return fileCommand.getCurrentCommand();
}
public static ICodeSubmitContentHandler getCodeSubmitContentHandler(final String contentType) {
return RCodeLaunchRegistry.getDefault().getContentHandler(contentType);
}
/**
* Runs a file related command in R.
* Use this method only, if you don't have an IFile or IPath object for your file
* (e.g. file on webserver).
* <p>
* The pattern ${file} in command string is replaced by the path of
* the specified file.</p>
*
* @param command the command, (at moment) should be single line.
* @param file the file.
* @throws CoreException if running failed.
*/
public static void runFileUsingCommand(final String command, final URI fileURI,
final ISourceUnit su, final String encoding, final boolean gotoConsole) throws CoreException {
if (su instanceof IWorkspaceSourceUnit && su.getResource() instanceof IFile) {
final IFile file = (IFile) ((IWorkspaceSourceUnit) su).getResource();
// save files
final IProject project = file.getProject();
if (project != null) {
final IProject[] referencedProjects = project.getReferencedProjects();
final IProject[] allProjects = new IProject[referencedProjects.length+1];
allProjects[0] = project;
System.arraycopy(referencedProjects, 0, allProjects, 1, referencedProjects.length);
if (!saveBeforeLaunch(allProjects)) {
return;
}
}
}
final IRCodeSubmitConnector connector = RCodeLaunchRegistry.getDefault().getConnector();
IFileStore fileStore = null;
try {
fileStore = EFS.getStore(fileURI);
}
catch (final CoreException e) {
fileStore = null;
}
if (fileStore != null && connector instanceof RControllerCodeLaunchConnector) {
final IFileStore store = fileStore;
((RControllerCodeLaunchConnector) connector).submit(new RControllerCodeLaunchConnector.CommandsCreator() {
@Override
public Status submitTo(final ToolController controller) {
final ToolWorkspace workspace = controller.getWorkspaceData();
try {
final String path = workspace.toToolPath(store);
final String code = resolveVariables(command, path, encoding);
final ToolRunnable runnable = new SubmitFileViaCommandRunnable(
PlatformUI.getWorkbench().getSharedImages().getImage(ISharedImages.IMG_OBJ_FILE),
"Run '" + store.toString() + "'", // TODO custom image and label
code, su );
final Status status = controller.getTool().getQueue().add(runnable);
if (status.getSeverity() != Status.OK) {
runnable.changed(ToolRunnable.BEING_ABANDONED, null);
}
return status;
}
catch (final StatusException e) {
return e.getStatus();
}
catch (final CoreException e) {
return EStatusUtils.convert(e.getStatus());
}
}
}, gotoConsole);
}
else {
String path = null;
try {
if (EFS.getLocalFileSystem().equals(EFS.getFileSystem(fileURI.getScheme()))) {
path = EFS.getLocalFileSystem().getStore(fileURI).toString();
}
} catch (final CoreException e) {
}
if (path == null) {
path = fileURI.toString();
}
final String code = resolveVariables(command, path, encoding);
connector.submit(Collections.singletonList(code), gotoConsole);
}
}
private static String resolveVariables(final String command, final String path, final String encoding)
throws CoreException {
final List<IDynamicVariable> variables = new ArrayList<>();
variables.add(new DynamicVariable(FILE_NAME_VARIABLE) {
@Override
public String getValue(final String argument) throws CoreException {
return RUtil.escapeCompletely(path);
}
});
variables.add(new DynamicVariable(FILE_ENCODING_VARIABLE) {
@Override
public String getValue(final String argument) throws CoreException {
return encoding != null ? RUtil.escapeCompletely(encoding) : "unknown";
}
});
variables.add(new DynamicVariable(ECHO_ENABLED_VARIABLE) {
@Override
public String getValue(final String argument) throws CoreException {
final Boolean echo = PreferenceUtils.getInstancePrefs().getPreferenceValue(
RCodeLaunching.ECHO_ENABLED_PREF);
return (echo != null && echo.booleanValue()) ?
"TRUE" : "FALSE"; //$NON-NLS-1$ //$NON-NLS-2$
}
});
final VariableText text = new VariableText(command, variables, true);
text.performFinalStringSubstitution(new LocationProcessor() {
@Override
public String process(final String path) throws CoreException {
return RUtil.escapeCompletely(path);
}
});
return text.getText();
}
private static boolean saveBeforeLaunch(final IProject[] projects) throws CoreException {
IStatusHandler prompter = null;
prompter = DebugPlugin.getDefault().getStatusHandler(STATUS_PROMPTER);
if (prompter != null) {
return ((Boolean) prompter.handleStatus(STATUS_SAVE,
new Object[] { null, projects } )).booleanValue();
}
return true;
}
public static boolean runRCodeDirect(final List<String> lines, final boolean gotoConsole,
final IProgressMonitor monitor) throws CoreException {
if (monitor != null) {
monitor.subTask(RLaunchingMessages.RCodeLaunch_SubmitCode_task);
}
final IRCodeSubmitConnector connector = RCodeLaunchRegistry.getDefault().getConnector();
return connector.submit(lines, gotoConsole);
}
public static boolean runRCodeDirect(final List<SourceRegion> codeRegions,
final boolean gotoConsole) throws CoreException {
final IRCodeSubmitConnector connector = RCodeLaunchRegistry.getDefault().getConnector();
if (connector instanceof RControllerCodeLaunchConnector) {
return ((RControllerCodeLaunchConnector) connector).submit(
new RControllerCodeLaunchConnector.CommandsCreator() {
@Override
public Status submitTo(final ToolController controller) {
final ToolRunnable[] runnables = new ToolRunnable[codeRegions.size()];
final List<String> lines = new ArrayList<>();
for (int i = 0; i < runnables.length; i++) {
final SourceRegion region = codeRegions.get(i);
lines.clear();
TextUtil.addLines(region.fCode, lines);
runnables[i] = new SubmitEntireCommandRunnable(
lines.toArray(new String[lines.size()]), region);
}
final Status status = controller.getTool().getQueue().add(
ImCollections.newList(runnables) );
if (status.getSeverity() != Status.OK) {
for (int i = 0; i < runnables.length; i++) {
runnables[i].changed(ToolRunnable.BEING_ABANDONED, null);
}
}
return status;
}
}, gotoConsole);
}
final List<String> lines = new ArrayList<>(codeRegions.size()*2);
for (int i = 0; i < codeRegions.size(); i++) {
TextUtil.addLines(codeRegions.get(i).fCode, lines);
}
return runRCodeDirect(lines, gotoConsole, null);
}
public static final BooleanPref ECHO_ENABLED_PREF = new BooleanPref(
RRunDebugPreferenceConstants.ROOT_QUALIFIER + "/codelaunch", "echo.enabled" );
}