/*
* EditingTargetCodeExecution.java
*
* Copyright (C) 2009-16 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.source.editors;
import org.rstudio.core.client.StringUtil;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.mathjax.MathJaxUtil;
import org.rstudio.studio.client.rmarkdown.events.SendToChunkConsoleEvent;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefsAccessor;
import org.rstudio.studio.client.workbench.views.console.events.ConsoleExecutePendingInputEvent;
import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay.AnchoredSelection;
import org.rstudio.studio.client.workbench.views.source.editors.text.Scope;
import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Mode.InsertChunkInfo;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Token;
import com.google.inject.Inject;
public class EditingTargetCodeExecution
{
public interface CodeExtractor
{
String extractCode(DocDisplay docDisplay, Range range);
}
public EditingTargetCodeExecution(DocDisplay display, String docId)
{
this(null, display, docId, new CodeExtractor() {
@Override
public String extractCode(DocDisplay docDisplay, Range range)
{
return docDisplay.getCode(range.getStart(), range.getEnd());
}
});
}
public EditingTargetCodeExecution(TextEditingTarget target,
DocDisplay display,
String docId,
CodeExtractor codeExtractor)
{
target_ = target;
docDisplay_ = display;
codeExtractor_ = codeExtractor;
docId_ = docId;
inlineChunkExecutor_ = new EditingTargetInlineChunkExecution(
display, docId);
RStudioGinjector.INSTANCE.injectMembers(this);
}
@Inject
void initialize(EventBus events, UIPrefs prefs, Commands commands)
{
events_ = events;
prefs_ = prefs;
commands_ = commands;
}
public void executeSelection(boolean consoleExecuteWhenNotFocused,
boolean moveCursorAfter)
{
executeSelectionMaybeNoFocus(consoleExecuteWhenNotFocused,
moveCursorAfter,
null,
false);
}
public void executeSelection(boolean consoleExecuteWhenNotFocused,
boolean moveCursorAfter,
String functionWrapper,
boolean onlyUseConsole)
{
// when executing LaTeX in R Markdown, show a popup preview
if (executeLatex(false))
return;
// when executing inline R code, show a popup preview
if (executeInlineChunk())
return;
Range selectionRange = docDisplay_.getSelectionRange();
boolean noSelection = selectionRange.isEmpty();
if (noSelection)
{
// don't do multiline execution within Roxygen examples
int row = docDisplay_.getSelectionStart().getRow();
if (isRoxygenExampleRow(row))
{
selectionRange = Range.fromPoints(
Position.create(row, 0),
Position.create(row, docDisplay_.getLength(row)));
}
else
{
// if no selection, follow UI pref to see what to execute
selectionRange = getRangeFromBehavior(
prefs_.executionBehavior().getValue());
}
// if we failed to discover a range, bail
if (selectionRange == null)
return;
// make it harder to step off the end of a chunk
InsertChunkInfo insert = docDisplay_.getInsertChunkInfo();
if (insert != null && !StringUtil.isNullOrEmpty(insert.getValue()))
{
// get the selection we're about to execute; if it's the same as
// the last line of the chunk template, don't run it
String code = codeExtractor_.extractCode(docDisplay_, selectionRange);
String[] chunkLines = insert.getValue().split("\n");
if (!StringUtil.isNullOrEmpty(code) &&
chunkLines.length > 0 &&
code.trim() == chunkLines[chunkLines.length - 1].trim())
return;
}
}
executeRange(selectionRange, functionWrapper, onlyUseConsole);
// advance if there is no current selection
if (noSelection && moveCursorAfter)
{
moveCursorAfterExecution(selectionRange);
}
}
public void executeSelection(boolean consoleExecuteWhenNotFocused)
{
executeSelectionMaybeNoFocus(consoleExecuteWhenNotFocused,
true,
null,
false);
}
public void executeRange(Range range)
{
executeRange(range, null, false);
}
public void profileSelection()
{
// allow console a chance to execute code if we aren't focused
if (!docDisplay_.isFocused())
{
events_.fireEvent(new ConsoleExecutePendingInputEvent(
commands_.profileCodeWithoutFocus().getId()));
return;
}
executeSelection(false, false, "profvis::profvis", true);
}
private void executeSelectionMaybeNoFocus(boolean consoleExecuteWhenNotFocused,
boolean moveCursorAfter,
String functionWrapper,
boolean onlyUseConsole)
{
// allow console a chance to execute code if we aren't focused
if (consoleExecuteWhenNotFocused && !docDisplay_.isFocused())
{
events_.fireEvent(new ConsoleExecutePendingInputEvent(
commands_.executeCodeWithoutFocus().getId()));
return;
}
executeSelection(consoleExecuteWhenNotFocused, moveCursorAfter, functionWrapper, false);
}
private void executeRange(Range range, String functionWrapper, boolean onlyUseConsole)
{
String code = codeExtractor_.extractCode(docDisplay_, range);
setLastExecuted(range.getStart(), range.getEnd());
// trim intelligently
code = code.trim();
if (code.length() == 0)
code = "\n";
// strip roxygen off the beginning of lines
if (isRoxygenExampleRange(range))
{
code = code.replaceFirst("^[ \\t]*#'[ \\t]?", "");
code = code.replaceAll("\n[ \\t]*#'[ \\t]?", "\n");
}
// if we're in a chunk with in-line output, execute it there instead
if (!onlyUseConsole && docDisplay_.showChunkOutputInline())
{
Scope scope = docDisplay_.getCurrentChunk(range.getStart());
if (scope != null)
{
events_.fireEvent(new SendToChunkConsoleEvent(docId_,
scope, range));
return;
}
}
if (functionWrapper != null)
{
code = functionWrapper + "({" + code + "})";
}
// send to console
events_.fireEvent(new SendToConsoleEvent(
code,
true,
prefs_.focusConsoleAfterExec().getValue()));
}
public void executeBehavior(String executionBehavior)
{
Range range = getRangeFromBehavior(executionBehavior);
executeRange(range, null, false);
moveCursorAfterExecution(range);
}
private Range getRangeFromBehavior(String executionBehavior)
{
Range range;
// by default the range can encompass the whole document
int startRowLimit = 0;
int endRowLimit = docDisplay_.getRowCount();
// limit range to chunk if we're inside one
Scope scope = docDisplay_.getCurrentChunk();
if (scope != null)
{
startRowLimit = scope.getBodyStart().getRow();
endRowLimit = scope.getEnd().getRow() - 1;
}
if (executionBehavior == UIPrefsAccessor.EXECUTE_STATEMENT)
{
// no scope to guard region, check the document itself to find
// the region to execute
range = docDisplay_.getMultiLineExpr(
docDisplay_.getCursorPosition(), startRowLimit, endRowLimit);
}
else if (executionBehavior == UIPrefsAccessor.EXECUTE_PARAGRAPH)
{
range = docDisplay_.getParagraph(
docDisplay_.getCursorPosition(), startRowLimit, endRowLimit);
}
else
{
// single-line execution
int row = docDisplay_.getCursorPosition().getRow();
range = Range.fromPoints(
Position.create(row, 0),
Position.create(row, docDisplay_.getLength(row)));
}
return range;
}
public void executeLastCode()
{
if (lastExecutedCode_ != null)
{
String code = lastExecutedCode_.getValue();
if (code != null && code.trim().length() > 0)
{
// if in notebook mode, we want to execute the code inside the
// chunk rather than at the console
Scope scope = null;
if (docDisplay_.showChunkOutputInline())
{
scope = docDisplay_.getCurrentChunk(
lastExecutedCode_.getRange().getStart());
}
if (scope == null)
{
events_.fireEvent(new SendToConsoleEvent(code, true));
}
else
{
events_.fireEvent(new SendToChunkConsoleEvent(docId_,
scope, lastExecutedCode_.getRange()));
}
}
}
}
public void setLastExecuted(Position start, Position end)
{
detachLastExecuted();
lastExecutedCode_ = docDisplay_.createAnchoredSelection(start, end);
}
public void detachLastExecuted()
{
if (lastExecutedCode_ != null)
{
lastExecutedCode_.detach();
lastExecutedCode_ = null;
}
}
private boolean isRoxygenExampleRow(int row)
{
// walk back until we find '@examples' or a non-roxygen line
for (int i = row; i >= 0; i--)
{
String line = docDisplay_.getLine(i);
if (!isRoxygenLine(line))
return false;
if (line.matches("^\\s*#+'\\s*@example.*$"))
return true;
}
return false;
}
private boolean isRoxygenExampleRange(Range range)
{
// ensure all of the lines in the selection are within roxygen
int selStartRow = range.getStart().getRow();
int selEndRow = range.getEnd().getRow();
// ignore the last row if it's column 0
if (range.getEnd().getColumn() == 0)
selEndRow = Math.max(selEndRow-1, selStartRow);
for (int i = selStartRow; i <= selEndRow; i++)
{
if (!isRoxygenLine(docDisplay_.getLine(i)))
return false;
}
// scan backwards and look for @example
int row = selStartRow;
while (--row >= 0)
{
String line = docDisplay_.getLine(row);
// must still be within roxygen
if (!isRoxygenLine(line))
return false;
// if we are in an example block return true
if (line.matches("^\\s*#'\\s*@example.*$"))
return true;
}
// didn't find the example block
return false;
}
private boolean isRoxygenLine(String line)
{
String trimmedLine = line.trim();
return (trimmedLine.length() == 0) || trimmedLine.startsWith("#'");
}
private boolean executeLatex(boolean background)
{
// need a suitable editing target to render LaTeX chunks
if (target_ == null)
return false;
Range range = MathJaxUtil.getLatexRange(docDisplay_);
if (range == null)
return false;
target_.renderLatex(range, background);
return true;
}
private boolean executeInlineChunk()
{
if (!docDisplay_.getSelection().isEmpty())
return false;
Token token = docDisplay_.getTokenAt(docDisplay_.getCursorPosition());
if (token == null || !token.hasType("inline_r_chunk"))
return false;
// construct range to execute, trimming off the "`r ...`" boundaries
int row = docDisplay_.getCursorPosition().getRow();
int startColumn = token.getColumn() + 3;
int endColumn = token.getColumn() + token.getValue().length() - 1;
Range range = Range.create(row, startColumn, row, endColumn);
inlineChunkExecutor_.execute(range);
return true;
}
private void moveCursorAfterExecution(Range selectionRange)
{
docDisplay_.setCursorPosition(Position.create(
selectionRange.getEnd().getRow(), 0));
if (!docDisplay_.moveSelectionToNextLine(true))
docDisplay_.moveSelectionToBlankLine();
docDisplay_.scrollCursorIntoViewIfNecessary(3);
}
private final DocDisplay docDisplay_;
private final TextEditingTarget target_;
private final CodeExtractor codeExtractor_;
private final String docId_;
private final EditingTargetInlineChunkExecution inlineChunkExecutor_;
private AnchoredSelection lastExecutedCode_;
// Injected ----
private EventBus events_;
private UIPrefs prefs_;
private Commands commands_;
}