/*
* TextEditingTargetNotebook.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.text.rmd;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.Debug;
import org.rstudio.core.client.JsArrayUtil;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.command.AppCommand;
import org.rstudio.core.client.dom.DomUtils;
import org.rstudio.core.client.layout.FadeOutAnimation;
import org.rstudio.core.client.widget.Operation;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.DeferredInitCompletedEvent;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.application.events.InterruptStatusEvent;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.dependencies.DependencyManager;
import org.rstudio.studio.client.rmarkdown.events.ChunkPlotRefreshFinishedEvent;
import org.rstudio.studio.client.rmarkdown.events.ChunkPlotRefreshedEvent;
import org.rstudio.studio.client.rmarkdown.events.RmdChunkOutputEvent;
import org.rstudio.studio.client.rmarkdown.events.RmdChunkOutputFinishedEvent;
import org.rstudio.studio.client.rmarkdown.events.SendToChunkConsoleEvent;
import org.rstudio.studio.client.rmarkdown.model.NotebookDoc;
import org.rstudio.studio.client.rmarkdown.model.NotebookDocQueue;
import org.rstudio.studio.client.rmarkdown.model.NotebookQueueUnit;
import org.rstudio.studio.client.rmarkdown.model.RMarkdownServerOperations;
import org.rstudio.studio.client.rmarkdown.model.RmdChunkOptions;
import org.rstudio.studio.client.rmarkdown.model.RmdChunkOutput;
import org.rstudio.studio.client.rmarkdown.model.RmdChunkOutputUnit;
import org.rstudio.studio.client.rmarkdown.model.RmdEditorOptions;
import org.rstudio.studio.client.rmarkdown.model.YamlFrontMatter;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.console.model.ConsoleServerOperations;
import org.rstudio.studio.client.workbench.views.source.Source;
import org.rstudio.studio.client.workbench.views.source.SourceWindowManager;
import org.rstudio.studio.client.workbench.views.source.editors.text.ChunkOutputWidget;
import org.rstudio.studio.client.workbench.views.source.editors.text.ChunkRowExecState;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.PinnedLineWidget;
import org.rstudio.studio.client.workbench.views.source.editors.text.Scope;
import org.rstudio.studio.client.workbench.views.source.editors.text.ScopeList;
import org.rstudio.studio.client.workbench.views.source.editors.text.ScopeList.ScopePredicate;
import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTarget;
import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetChunks;
import org.rstudio.studio.client.workbench.views.source.editors.text.TextEditingTargetScopeHelper;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.LineWidget;
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.events.RenderFinishedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ScopeTreeReadyEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ChunkSatelliteCacheEditorStyleEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ChunkSatelliteCloseAllWindowEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ChunkSatelliteCodeExecutingEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.ChunkSatelliteWindowOpenedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.EditorThemeStyleChangedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.rmd.events.InterruptChunkEvent;
import org.rstudio.studio.client.workbench.views.source.events.ChunkChangeEvent;
import org.rstudio.studio.client.workbench.views.source.events.ChunkContextChangeEvent;
import org.rstudio.studio.client.workbench.views.source.events.SaveFileEvent;
import org.rstudio.studio.client.workbench.views.source.events.SaveFileHandler;
import org.rstudio.studio.client.workbench.views.source.events.SourceDocAddedEvent;
import org.rstudio.studio.client.workbench.views.source.model.DirtyState;
import org.rstudio.studio.client.workbench.views.source.model.DocUpdateSentinel;
import org.rstudio.studio.client.workbench.views.source.model.SourceDocument;
import org.rstudio.studio.client.workbench.views.source.model.SourceServerOperations;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Display;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.FocusEvent;
import com.google.gwt.event.dom.client.FocusHandler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.Widget;
import com.google.inject.Inject;
import com.google.inject.Provider;
public class TextEditingTargetNotebook
implements EditorThemeStyleChangedEvent.Handler,
RmdChunkOutputEvent.Handler,
RmdChunkOutputFinishedEvent.Handler,
ChunkPlotRefreshedEvent.Handler,
ChunkPlotRefreshFinishedEvent.Handler,
SendToChunkConsoleEvent.Handler,
ChunkChangeEvent.Handler,
ChunkContextChangeEvent.Handler,
ResizeHandler,
InterruptStatusEvent.Handler,
DeferredInitCompletedEvent.Handler,
ScopeTreeReadyEvent.Handler,
PinnedLineWidget.Host,
SourceDocAddedEvent.Handler,
RenderFinishedEvent.Handler,
ChunkSatelliteWindowOpenedEvent.Handler
{
public TextEditingTargetNotebook(final TextEditingTarget editingTarget,
TextEditingTargetChunks chunks,
TextEditingTarget.Display editingDisplay,
DocDisplay docDisplay,
DirtyState dirtyState,
DocUpdateSentinel docUpdateSentinel,
SourceDocument document,
ArrayList<HandlerRegistration> releaseOnDismiss,
DependencyManager dependencyManager)
{
docDisplay_ = docDisplay;
docUpdateSentinel_ = docUpdateSentinel;
dirtyState_ = dirtyState;
releaseOnDismiss_ = releaseOnDismiss;
notebookDoc_ = document.getNotebookDoc();
initialChunkDefs_ = JsArrayUtil.deepCopy(notebookDoc_.getChunkDefs());
outputs_ = new HashMap<String, ChunkOutputUi>();
satelliteChunkRequestIds_ = new ArrayList<String>();
setupCrc32_ = docUpdateSentinel_.getProperty(LAST_SETUP_CRC32);
editingTarget_ = editingTarget;
chunks_ = chunks;
editingDisplay_ = editingDisplay;
scopeHelper_ = new TextEditingTargetScopeHelper(docDisplay_);
dependencyManager_ = dependencyManager;
RStudioGinjector.INSTANCE.injectMembers(this);
releaseOnDismiss.add(docDisplay_.addEditorFocusHandler(new FocusHandler()
{
@Override
public void onFocus(FocusEvent arg0)
{
if (queuedResize_ != null)
{
onResize(queuedResize_);
queuedResize_ = null;
}
}
}));
// listen for future changes to the preference and sync accordingly
releaseOnDismiss.add(
docUpdateSentinel_.addPropertyValueChangeHandler(CHUNK_OUTPUT_TYPE,
new ValueChangeHandler<String>()
{
@Override
public void onValueChange(ValueChangeEvent<String> event)
{
// propagate to YAML
String yaml = RmdEditorOptions.set(
YamlFrontMatter.getFrontMatter(docDisplay_),
CHUNK_OUTPUT_TYPE, event.getValue());
YamlFrontMatter.applyFrontMatter(docDisplay_, yaml);
// change the output mode in the document
changeOutputMode(event.getValue());
}
}));
// when the width of the outline changes, treat it as a resize
ValueChangeHandler<String> outlineWidthHandler =
new ValueChangeHandler<String>()
{
@Override
public void onValueChange(ValueChangeEvent<String> event)
{
onResize(null);
}
};
releaseOnDismiss.add(
docUpdateSentinel_.addPropertyValueChangeHandler(
TextEditingTarget.DOC_OUTLINE_SIZE,
outlineWidthHandler));
releaseOnDismiss.add(
docUpdateSentinel_.addPropertyValueChangeHandler(
TextEditingTarget.DOC_OUTLINE_VISIBLE,
outlineWidthHandler));
releaseOnDismiss.add(docDisplay_.addValueChangeHandler(
new ValueChangeHandler<Void>()
{
@Override
public void onValueChange(ValueChangeEvent<Void> arg0)
{
// check the setup chunk's CRC32 next time we run a chunk
validateSetupChunk_ = true;
// if the change happened in one of our scopes, clean up gutter
// indicators for that scope (debounce this so it doesn't fire on
// every keystroke)
cleanErrorGutter_.schedule(250);
}
}));
// rendering of chunk output line widgets (we wait until after the first
// render to ensure that ace places the line widgets correctly)
renderReg_ = docDisplay_.addRenderFinishedHandler(this);
releaseOnDismiss_.add(editingTarget_.addInterruptChunkHandler(new InterruptChunkEvent.Handler()
{
@Override
public void onInterruptChunk(InterruptChunkEvent event)
{
int row = event.getRow();
String chunkId = getRowChunkId(row);
// just interrupt R if we have no chunk id for some reason (shouldn't happen)
if (StringUtil.isNullOrEmpty(chunkId))
{
RStudioGinjector.INSTANCE.getApplicationInterrupt().interruptR(null);
return;
}
// interrupt this chunk's execution
server_.interruptChunk(
editingTarget_.getId(),
chunkId,
new ServerRequestCallback<Void>()
{
@Override
public void onResponseReceived(Void response)
{
RStudioGinjector.INSTANCE.getApplicationInterrupt().interruptR(null);
}
@Override
public void onError(ServerError error)
{
Debug.logError(error);
RStudioGinjector.INSTANCE.getApplicationInterrupt().interruptR(null);
}
});
}
}));
}
@Inject
public void initialize(EventBus events,
RMarkdownServerOperations server,
ConsoleServerOperations console,
SourceServerOperations source,
Session session,
UIPrefs prefs,
Commands commands,
Provider<SourceWindowManager> pSourceWindowManager,
DependencyManager dependencyManager)
{
events_ = events;
server_ = server;
source_ = source;
session_ = session;
prefs_ = prefs;
commands_ = commands;
pSourceWindowManager_ = pSourceWindowManager;
queue_ = new NotebookQueueState(docDisplay_, editingTarget_,
docUpdateSentinel_, server, events, this);
releaseOnDismiss_.add(
events_.addHandler(RmdChunkOutputEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(RmdChunkOutputFinishedEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(ChunkPlotRefreshedEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(ChunkPlotRefreshFinishedEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(SendToChunkConsoleEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(ChunkChangeEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(ChunkContextChangeEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(InterruptStatusEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(DeferredInitCompletedEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(SourceDocAddedEvent.TYPE, this));
releaseOnDismiss_.add(
events_.addHandler(ChunkSatelliteWindowOpenedEvent.TYPE, this));
// subscribe to global rmd output inline preference and sync
// again when it changes
releaseOnDismiss_.add(
prefs_.showRmdChunkOutputInline().addValueChangeHandler(
new ValueChangeHandler<Boolean>() {
@Override
public void onValueChange(ValueChangeEvent<Boolean> event)
{
syncOutputMode();
}
}));
// set up HTML rendering on save
htmlRenderer_ = new NotebookHtmlRenderer(docDisplay_,
editingTarget_, editingDisplay_, docUpdateSentinel_,
server_, events_, dependencyManager);
releaseOnDismiss_.add(docDisplay_.addSaveCompletedHandler(htmlRenderer_));
// set up preference sync on save
releaseOnDismiss_.add(docDisplay_.addSaveCompletedHandler(
new SaveFileHandler()
{
@Override
public void onSaveFile(SaveFileEvent event)
{
// propagate output preference from YAML into doc preference
String frontMatter = YamlFrontMatter.getFrontMatter(docDisplay_);
if (!StringUtil.isNullOrEmpty(frontMatter))
{
// if the YAML mode was manually changed to be different than
// the doc mode, set the doc mode appropriately
String yamlMode = RmdEditorOptions.getString(frontMatter,
CHUNK_OUTPUT_TYPE, null);
if (!StringUtil.isNullOrEmpty(yamlMode))
{
String docMode = docUpdateSentinel_.getProperty(
CHUNK_OUTPUT_TYPE, yamlMode);
if (yamlMode != docMode)
docUpdateSentinel_.setProperty(CHUNK_OUTPUT_TYPE,
yamlMode);
}
}
}
}));
}
public void onActivate()
{
// remember that we haven't maximized the pane in this session
maximizedPane_ = false;
// listen for clicks on notebook progress UI
registerProgressHandlers();
// propagate output preference from YAML into doc preference
String frontMatter = YamlFrontMatter.getFrontMatter(docDisplay_);
if (!StringUtil.isNullOrEmpty(frontMatter))
{
String mode = RmdEditorOptions.getString(frontMatter,
CHUNK_OUTPUT_TYPE, null);
if (!StringUtil.isNullOrEmpty(mode))
{
docUpdateSentinel_.setProperty(CHUNK_OUTPUT_TYPE, mode);
}
}
}
public void executeChunks(final String jobDesc,
final List<ChunkExecUnit> chunks)
{
if (queue_.isExecuting())
{
RStudioGinjector.INSTANCE.getGlobalDisplay().showErrorMessage(
jobDesc + ": Chunks Currently Executing",
"RStudio cannot execute '" + jobDesc + "' because this " +
"notebook is already executing code. Interrupt R, or wait " +
"for execution to complete.");
return;
}
docUpdateSentinel_.withSavedDoc(new Command()
{
@Override
public void execute()
{
queue_.executeChunks(jobDesc, chunks);
}
});
}
public void executeChunk(final Scope chunk)
{
// maximize the source pane if we haven't yet this session
if (!maximizedPane_ &&
prefs_.hideConsoleOnChunkExecute().getValue())
{
pSourceWindowManager_.get().maximizeSourcePaneIfNecessary();
maximizedPane_ = true;
}
docUpdateSentinel_.withSavedDoc(new Command()
{
@Override
public void execute()
{
// if this isn't the setup chunk, ensure the setup chunk is executed
// by creating a job that runs both chunks
if (!isSetupChunkScope(chunk) &&
needsSetupChunkExecuted())
{
List<ChunkExecUnit> chunks = new ArrayList<ChunkExecUnit>();
chunks.add(new ChunkExecUnit(getSetupChunkScope(),
NotebookQueueUnit.EXEC_MODE_BATCH));
chunks.add(new ChunkExecUnit(chunk,
NotebookQueueUnit.EXEC_MODE_SINGLE));
queue_.executeChunks("Run Chunks", chunks);
}
else
{
queue_.executeChunk(new ChunkExecUnit(chunk,
NotebookQueueUnit.EXEC_MODE_SINGLE));
}
}
});
}
public void manageCommands()
{
boolean inlineOutput = docDisplay_.showChunkOutputInline();
commands_.restartRClearOutput().setEnabled(inlineOutput);
commands_.restartRClearOutput().setVisible(inlineOutput);
commands_.restartRRunAllChunks().setEnabled(inlineOutput);
commands_.restartRRunAllChunks().setVisible(inlineOutput);
commands_.notebookCollapseAllOutput().setEnabled(inlineOutput);
commands_.notebookCollapseAllOutput().setVisible(inlineOutput);
commands_.notebookExpandAllOutput().setEnabled(inlineOutput);
commands_.notebookExpandAllOutput().setVisible(inlineOutput);
commands_.notebookClearOutput().setEnabled(inlineOutput);
commands_.notebookClearOutput().setVisible(inlineOutput);
commands_.notebookClearAllOutput().setEnabled(inlineOutput);
commands_.notebookClearAllOutput().setVisible(inlineOutput);
commands_.notebookToggleExpansion().setEnabled(inlineOutput);
commands_.notebookToggleExpansion().setVisible(inlineOutput);
editingDisplay_.setNotebookUIVisible(inlineOutput);
}
// Command handlers --------------------------------------------------------
//
// Notebook-specific commands are received on the parent text editing target
// and then dispatched here (the logic to bind commands to the active editor
// exists only in the text editing target)
public void onNotebookCollapseAllOutput()
{
setAllExpansionStates(ChunkOutputWidget.COLLAPSED);
}
public void onNotebookExpandAllOutput()
{
setAllExpansionStates(ChunkOutputWidget.EXPANDED);
}
public void onNotebookClearOutput()
{
// find the current chunk
Scope chunk = docDisplay_.getCurrentChunk();
clearChunkOutput(chunk);
}
public void onNotebookToggleExpansion()
{
String chunkId = getCurrentChunkId();
if (chunkId == null || !outputs_.containsKey(chunkId))
return;
ChunkOutputWidget widget = outputs_.get(chunkId).getOutputWidget();
widget.setExpansionState(
widget.getExpansionState() == ChunkOutputWidget.COLLAPSED ?
ChunkOutputWidget.EXPANDED : ChunkOutputWidget.COLLAPSED);
}
public void clearChunkOutput(Scope chunk)
{
if (chunk == null)
{
// no-op if cursor is not inside a chunk
return;
}
String chunkId = getRowChunkId(chunk.getPreamble().getRow());
if (chunkId == null)
{
// no-op if we don't have known output
return;
}
events_.fireEvent(new ChunkChangeEvent(docUpdateSentinel_.getId(),
chunkId, "", 0, ChunkChangeEvent.CHANGE_REMOVE));
}
public void onNotebookClearAllOutput()
{
if (!queue_.isExecuting())
{
// no chunks running, just clean everything up
removeAllChunks();
return;
}
else
{
RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage(
GlobalDisplay.MSG_INFO,
"Chunks Currently Running",
"Output can't be cleared because there are still chunks " +
"running. Do you want to interrupt them?",
false,
new Operation()
{
@Override
public void execute()
{
if (commands_.interruptR().isEnabled())
commands_.interruptR().execute();
clearChunkExecQueue();
removeAllChunks();
}
},
null,
null,
"Interrupt and Clear Output",
"Cancel",
false);
}
}
public void onRestartRRunAllChunks()
{
restartThenExecute(commands_.executeAllCode());
}
public void onRestartRClearOutput()
{
restartThenExecute(commands_.notebookClearAllOutput());
}
public String getChunkCode(Scope chunk)
{
return docDisplay_.getCode(
chunk.getBodyStart(),
Position.create(chunk.getEnd().getRow(), 0));
}
public int getCommitMode()
{
if (editingTarget_.isRmdNotebook())
{
// notebooks always play chunks as uncommitted
return MODE_UNCOMMITTED;
}
else if (dirtyState_.getValue())
{
// uncommitted R Markdown files also play chunks as uncommitted
return MODE_UNCOMMITTED;
}
// everything else plays directly to committed
return MODE_COMMITTED;
}
// Event handlers ----------------------------------------------------------
@Override
public void onEditorThemeStyleChanged(EditorThemeStyleChangedEvent event)
{
// update cached style
editorStyle_ = event.getStyle();
ChunkOutputWidget.cacheEditorStyle(
editorStyle_.getColor(),
editorStyle_.getBackgroundColor(),
DomUtils.extractCssValue("ace_editor", "color")
);
for (ChunkOutputUi output: outputs_.values())
{
output.getOutputWidget().applyCachedEditorStyle();
}
events_.fireEvent(
new ChunkSatelliteCacheEditorStyleEvent(
docUpdateSentinel_.getId(),
editorStyle_.getColor(),
editorStyle_.getBackgroundColor(),
DomUtils.extractCssValue("ace_editor", "color")
)
);
// update if currently executing
if (queue_.isExecuting())
{
NotebookQueueUnit unit = queue_.executingUnit();
if (unit != null)
{
setChunkExecuting(unit.getChunkId(), unit.getExecMode(),
unit.getExecScope());
}
}
}
@Override
public void onSendToChunkConsole(final SendToChunkConsoleEvent event)
{
// not for our doc
if (event.getDocId() != docUpdateSentinel_.getId())
return;
// execute setup chunk first if necessary
if (needsSetupChunkExecuted() && !isSetupChunkScope(event.getScope()))
{
List<ChunkExecUnit> chunks = new ArrayList<ChunkExecUnit>();
chunks.add(new ChunkExecUnit(getSetupChunkScope(),
NotebookQueueUnit.EXEC_MODE_BATCH));
chunks.add(new ChunkExecUnit(event.getScope(), event.getRange(),
NotebookQueueUnit.EXEC_MODE_SINGLE, event.getExecScope()));
queue_.executeChunks("Run Chunks", chunks);
}
else
{
queue_.executeChunk(
new ChunkExecUnit(event.getScope(), event.getRange(),
NotebookQueueUnit.EXEC_MODE_SINGLE, event.getExecScope()));
}
}
@Override
public void onRmdChunkOutput(RmdChunkOutputEvent event)
{
// ignore if not targeted at this document
if (event.getOutput().getDocId() != docUpdateSentinel_.getId())
return;
// if nothing at all was returned, this means the chunk doesn't exist on
// the server, so clean it up here.
if (event.getOutput().isEmpty() && !queue_.isExecuting())
{
events_.fireEvent(new ChunkChangeEvent(
docUpdateSentinel_.getId(), event.getOutput().getChunkId(),
event.getOutput().getRequestId(), 0,
ChunkChangeEvent.CHANGE_REMOVE));
return;
}
String chunkId = event.getOutput().getChunkId();
// ignore requests performed to initialize satellite chunks
if (satelliteChunkRequestIds_.contains(event.getOutput().getRequestId()))
return;
// if this is the currently executing chunk and it has an error...
NotebookQueueUnit unit = queue_.executingUnit();
if (unit != null &&
unit.getChunkId() == event.getOutput().getChunkId() &&
event.getOutput().getType() == RmdChunkOutput.TYPE_SINGLE_UNIT &&
event.getOutput().getUnit().getType() ==
RmdChunkOutputUnit.TYPE_ERROR)
{
// draw the error
Scope scope = getChunkScope(unit.getChunkId());
if (scope != null)
{
int offset = scope.getBodyStart().getRow();
List<Integer> lines = unit.getExecutingLines();
for (Integer line: lines)
docDisplay_.setChunkLineExecState(line + offset, line + offset,
ChunkRowExecState.LINE_ERROR);
}
// don't execute any more chunks if this chunk's options includes
// error = FALSE
if (!outputs_.containsKey(chunkId) ||
!outputs_.get(chunkId).getOptions().error())
{
clearChunkExecQueue();
}
}
// show output in matching chunk
if (outputs_.containsKey(chunkId))
{
// by default, ensure chunks are visible if we aren't replaying them
// from the cache
boolean ensureVisible = !event.getOutput().isReplay();
int mode = queue_.getChunkExecMode(chunkId);
// no need to make chunks visible in batch mode
if (ensureVisible && mode == NotebookQueueUnit.EXEC_MODE_BATCH)
ensureVisible = false;
outputs_.get(chunkId).getOutputWidget()
.showChunkOutput(event.getOutput(), mode,
NotebookQueueUnit.EXEC_SCOPE_PARTIAL,
!queue_.isChunkExecuting(chunkId),
ensureVisible);
}
}
@Override
public void onRmdChunkOutputFinished(RmdChunkOutputFinishedEvent event)
{
// ignore if not targeted at this document
if (event.getData().getDocId() != docUpdateSentinel_.getId())
return;
boolean ensureVisible = true;
RmdChunkOutputFinishedEvent.Data data = event.getData();
// ignore requests performed to initialize satellite chunks
if (satelliteChunkRequestIds_.contains(event.getData().getRequestId()))
return;
// clean up execution state
cleanChunkExecState(event.getData().getChunkId());
ensureVisible = queue_.getChunkExecMode(data.getChunkId()) ==
NotebookQueueUnit.EXEC_MODE_SINGLE;
if (outputs_.containsKey(data.getChunkId()))
{
ChunkOutputUi output = outputs_.get(data.getChunkId());
if (isSetupChunkScope(output.getScope()))
{
writeSetupCrc32(getChunkCrc32(output.getScope()));
if (output.hasErrors())
{
ensureVisible = true;
validateSetupChunk_ = true;
}
else
{
writeSetupCrc32(getChunkCrc32(output.getScope()));
}
}
}
if (data.getType() == RmdChunkOutputFinishedEvent.TYPE_REPLAY &&
data.getRequestId() == Integer.toHexString(requestId_))
{
state_ = STATE_INITIALIZED;
}
else if (data.getType() == RmdChunkOutputFinishedEvent.TYPE_INTERACTIVE &&
data.getDocId() == docUpdateSentinel_.getId())
{
if (outputs_.containsKey(data.getChunkId()))
{
outputs_.get(data.getChunkId()).getOutputWidget()
.onOutputFinished(ensureVisible, data.getScope());
// set dirty state if necessary
setDirtyState();
}
}
}
@Override
public void onChunkPlotRefreshFinished(ChunkPlotRefreshFinishedEvent event)
{
// ignore replays that are not targeting this instance
if (currentPlotsReplayId_ != event.getData().getReplayId())
return;
currentPlotsReplayId_ = null;
// ignore if targeted at another document
if (event.getData().getDocId() != docUpdateSentinel_.getId())
return;
lastPlotWidth_ = event.getData().getWidth();
// clean up flag
resizingPlotsRemote_ = false;
// mark any plots as no longer queued for resize
for (ChunkOutputUi output: outputs_.values())
{
output.getOutputWidget().setPlotPending(false);
}
}
@Override
public void onChunkPlotRefreshed(ChunkPlotRefreshedEvent event)
{
// ignore replays that are not targeting this instance
if (currentPlotsReplayId_ != event.getData().getReplayId())
return;
// ignore if targeted at another document
if (event.getData().getDocId() != docUpdateSentinel_.getId())
return;
// find chunk containing plot and push the new plot in
String chunkId = event.getData().getChunkId();
if (outputs_.containsKey(chunkId))
outputs_.get(chunkId).getOutputWidget().updatePlot(
event.getData().getPlotUrl());
}
@Override
public void onChunkChange(ChunkChangeEvent event)
{
if (event.getDocId() != docUpdateSentinel_.getId())
return;
switch(event.getChangeType())
{
case ChunkChangeEvent.CHANGE_CREATE:
createChunkOutput(ChunkDefinition.create(event.getRow(),
1, true, ChunkOutputWidget.EXPANDED, RmdChunkOptions.create(),
event.getDocId(), event.getChunkId(),
getKnitrChunkLabel(event.getRow(), docDisplay_,
new ScopeList(docDisplay_))));
break;
case ChunkChangeEvent.CHANGE_REMOVE:
removeChunk(event.getChunkId(), event.getRequestId());
break;
}
}
@Override
public void onChunkContextChange(ChunkContextChangeEvent event)
{
contextId_ = event.getContextId();
if (docDisplay_.isRendered())
{
// if the doc is already up, clean it out and replace the contents
removeAllChunks();
populateChunkDefs(event.getChunkDefs());
}
else
{
// otherwise, just queue up for when we do render
initialChunkDefs_ = event.getChunkDefs();
}
}
@Override
public void onResize(ResizeEvent event)
{
// queue resize rather than processing it right away if we're not the
// active document--in addition to being wasteful we're likely to compute
// incorrect sizes
if (!editingTarget_.isActiveDocument())
{
queuedResize_ = event;
return;
}
// lightly debounce local resizes since they're somewhat expensive
resizePlotsLocal_.schedule(50);
// heavily debounce remote resizes since they're very expensive
// (this actually spins up a separate R process to re-render all the
// plots at the new resolution)
resizePlotsRemote_.schedule(500);
for (ChunkOutputUi output: outputs_.values())
{
// throwing exceptions during resize breaks most of the UI and this
// invokes Javascript from a package downstream, so tolerate
// (and log) exceptions
try
{
output.getOutputWidget().onResize();
}
catch(Exception e)
{
Debug.logException(e);
}
}
}
@Override
public void onChunkSatelliteWindowOpened(ChunkSatelliteWindowOpenedEvent event)
{
String docId = event.getDocId();
String chunkId = event.getChunkId();
if (docId != docUpdateSentinel_.getId())
return;
events_.fireEvent(
new ChunkSatelliteCacheEditorStyleEvent(
docId,
editorStyle_.getColor(),
editorStyle_.getBackgroundColor(),
DomUtils.extractCssValue("ace_editor", "color")
)
);
refreshSatelliteChunk(chunkId);
}
@Override
public void onRenderFinished(RenderFinishedEvent event)
{
// single shot rendering of output line widgets (we wait until after the
// first render to ensure that ace places the line widgets correctly)
if (initialChunkDefs_ != null)
{
for (int i = 0; i < initialChunkDefs_.length(); i++)
{
createChunkOutput(initialChunkDefs_.get(i));
}
// if we got chunk content, load initial chunk output from server --
// note that some outputs need the rmarkdown package to render, so
// update that silently if needed
if (initialChunkDefs_.length() > 0)
{
dependencyManager_.withRMarkdown("R Notebook",
null, new CommandWithArg<Boolean>()
{
@Override
public void execute(Boolean arg)
{
loadInitialChunkOutput();
}
});
}
initialChunkDefs_ = null;
// sync to editor style changes
editingTarget_.addEditorThemeStyleChangedHandler(
TextEditingTargetNotebook.this);
// read and/or set initial render width
lastPlotWidth_ = notebookDoc_.getChunkRenderedWidth();
if (lastPlotWidth_ == 0)
{
lastPlotWidth_ = getPlotWidth();
}
}
// remove render handler
renderReg_.removeHandler();
}
@Override
public void onInterruptStatus(InterruptStatusEvent event)
{
if (event.getStatus() != InterruptStatusEvent.INTERRUPT_INITIATED)
return;
// when the user interrupts R, clear any pending chunk executions
clearChunkExecQueue();
// clear currently executing chunk
cleanCurrentExecChunk();
}
@Override
public void onDeferredInitCompleted(DeferredInitCompletedEvent event)
{
// if we had recorded a run of the setup chunk prior to restart, clear
// it
if (!StringUtil.isNullOrEmpty(setupCrc32_))
writeSetupCrc32("");
// clean execution state
clearChunkExecQueue();
cleanCurrentExecChunk();
// run any queued command
if (postRestartCommand_ != null)
{
if (postRestartCommand_.isEnabled())
postRestartCommand_.execute();
postRestartCommand_ = null;
return;
}
}
@Override
public void onLineWidgetAdded(LineWidget widget)
{
// no action necessary; this just lets us know that a chunk output has
// been attached to the DOM
}
@Override
public void onLineWidgetRemoved(LineWidget widget)
{
for (ChunkOutputUi output: outputs_.values())
{
// ignore moving widgets -- ACE doesn't have a way to move a line
// widget from one row to another, but we occasionally need to do this
// to keep the output pinned to the end of the chunk
if (output.moving())
continue;
if (output.getLineWidget() == widget && !output.moving())
{
// save scope and widget
int terminalLine = widget.getRow() - 1;
Scope scope = docDisplay_.getCurrentChunk(Position.create(
terminalLine, 1));
ChunkOutputWidget outputWidget = output.getOutputWidget();
// clean up old widget
output.remove();
outputs_.remove(output.getChunkId());
// if the scope is still at the terminal line, then this widget was
// deleted over-aggressively by Ace (this can happen when e.g.
// removing the final line of a chunk). create a new linewidget in
// place of the old one (but re-use the output widget so the output
// is identical)
if (scope != null &&
scope.getEnd().getRow() == terminalLine)
{
ChunkDefinition def = (ChunkDefinition)widget.getData();
def.setRow(terminalLine);
widget.setRow(terminalLine);
ChunkOutputUi newOutput = new ChunkOutputUi(
docUpdateSentinel_.getId(),
docDisplay_, def, this, outputWidget);
outputs_.put(output.getChunkId(), newOutput);
return;
}
break;
}
}
}
@Override
public void onSourceDocAdded(SourceDocAddedEvent e)
{
if (e.getDoc().getId() != docUpdateSentinel_.getId())
return;
// when interactively adding a new notebook, we maximize the source pane
if (e.getMode() == Source.OPEN_INTERACTIVE &&
editingTarget_.isActiveDocument() &&
editingTarget_.isRmdNotebook())
{
pSourceWindowManager_.get().maximizeSourcePaneIfNecessary();
maximizedPane_ = true;
}
}
@Override
public void onScopeTreeReady(ScopeTreeReadyEvent event)
{
Scope thisScope = event.getCurrentScope();
// initialization
if (lastStart_ == null && lastEnd_ == null)
{
if (thisScope != null)
{
lastStart_ = Position.create(thisScope.getBodyStart());
lastEnd_ = Position.create(thisScope.getEnd());
}
return;
}
// if the cursor is in the same scope as it was before and that
// scope hasn't changed, there's no need to revalidate all the chunk
// scopes (this is purely an optimization to avoid revalidating
// constantly, since scope tree ready events fire as the user types
// and we don't want to do proportional work on the UI thread if we can
// avoid it)
if (thisScope != null)
{
Position thisStart = thisScope.getBodyStart();
Position thisEnd = thisScope.getEnd();
if (((lastStart_ == null && thisStart == null) ||
(lastStart_ != null && lastStart_.compareTo(thisStart) == 0)) &&
((lastEnd_ == null && thisEnd == null) ||
(lastEnd_ != null && lastEnd_.compareTo(thisEnd) == 0)))
{
return;
}
lastStart_ = Position.create(thisScope.getBodyStart());
lastEnd_ = Position.create(thisScope.getEnd());
}
else
{
lastStart_ = null;
lastEnd_ = null;
}
for (ChunkOutputUi output: outputs_.values())
{
Scope scope = output.getScope();
// if the scope associated with this output no longer looks like a
// valid chunk scope, or is considerably out of sync with the widget,
// remove the widget
if (scope == null || !scope.isChunk() ||
scope.getBodyStart() == null || scope.getEnd() == null ||
scope.getEnd().getRow() - output.getCurrentRow() > 1)
{
events_.fireEvent(new ChunkChangeEvent(
docUpdateSentinel_.getId(), output.getChunkId(), "", 0,
ChunkChangeEvent.CHANGE_REMOVE));
}
}
}
public static String getKnitrChunkLabel(int row, DocDisplay display,
ScopeList scopes)
{
// find the chunk at this row
Scope chunk = display.getCurrentChunk(Position.create(row, 0));
if (chunk == null)
return "";
// if it has a name, just return it
String label = chunk.getChunkLabel();
if (!StringUtil.isNullOrEmpty(label))
return label;
// label the first unlabeled chunk as unlabeled-chunk-1, the next as
// unlabeled-chunk-2, etc.
int pos = 1;
for (Scope curScope: scopes)
{
if (!curScope.isChunk())
continue;
if (curScope.getPreamble().getRow() ==
chunk.getPreamble().getRow())
break;
if (StringUtil.isNullOrEmpty(curScope.getChunkLabel()))
pos++;
}
return "unnamed-chunk-" + pos;
}
public String getRowChunkId(int preambleRow)
{
// find the chunk corresponding to the row
for (ChunkOutputUi output: outputs_.values())
{
if (output.getScope().getPreamble().getRow() == preambleRow)
return output.getChunkId();
}
// no row mapped -- how about the setup chunk?
Scope setupScope = getSetupChunkScope();
if (setupScope != null &&
setupScope.getPreamble().getRow() == preambleRow)
{
return SETUP_CHUNK_ID;
}
return null;
}
public Scope getChunkScope(String chunkId)
{
if (chunkId == SETUP_CHUNK_ID)
{
return getSetupChunkScope();
}
else if (outputs_.containsKey(chunkId))
{
return outputs_.get(chunkId).getScope();
}
return null;
}
public Scope getSetupChunkScope()
{
ScopeList scopes = new ScopeList(docDisplay_);
return scopes.findFirst(new ScopePredicate()
{
@Override
public boolean test(Scope scope)
{
return isSetupChunkScope(scope);
}
});
}
public void setChunkExecuting(String chunkId, int mode, int execScope)
{
// let the chunk widget know it's started executing
if (outputs_.containsKey(chunkId))
{
ChunkOutputUi output = outputs_.get(chunkId);
// expand the chunk if it's in a fold
Scope scope = output.getScope();
if (scope != null)
{
docDisplay_.unfold(Range.fromPoints(scope.getPreamble(),
scope.getEnd()));
}
events_.fireEvent(
new ChunkSatelliteCodeExecutingEvent(
docUpdateSentinel_.getId(),
chunkId,
mode,
NotebookQueueUnit.EXEC_SCOPE_PARTIAL
)
);
output.getOutputWidget().setCodeExecuting(mode, execScope);
// scroll the widget into view if it's a single-shot exec
if (mode == NotebookQueueUnit.EXEC_MODE_SINGLE)
output.ensureVisible();
}
// draw UI on chunk
Scope chunk = getChunkScope(chunkId);
if (chunk != null)
{
setChunkState(chunk, ChunkContextToolbar.STATE_EXECUTING);
}
}
public void cleanChunkExecState(String chunkId)
{
Scope chunk = getChunkScope(chunkId);
if (chunk != null)
{
docDisplay_.setChunkLineExecState(
chunk.getBodyStart().getRow(),
chunk.getEnd().getRow(),
ChunkRowExecState.LINE_RESTING);
setChunkState(chunk, ChunkContextToolbar.STATE_RESTING);
}
}
public void setChunkState(Scope chunk, int state)
{
chunks_.setChunkState(chunk.getPreamble().getRow(), state);
}
public static boolean isSetupChunkScope(Scope scope)
{
if (!scope.isChunk())
return false;
if (scope.getChunkLabel() == null)
return false;
return scope.getChunkLabel().toLowerCase() == "setup";
}
public void dequeueChunk(int preambleRow)
{
queue_.dequeueChunk(preambleRow);
}
public void setOutputOptions(String chunkId, RmdChunkOptions options)
{
if (outputs_.containsKey(chunkId))
{
outputs_.get(chunkId).setOptions(options);
}
}
public int getPlotWidth()
{
// subtract some space to account for padding; ensure the plot doesn't
// grow arbitrarily large. note that this value must total the amount of
// space outside the element (here, 2 * (10px margin + 1px border)); since
// we stretch the plot to fit the space it will scale in unpredictable
// ways if it doesn't fit exactly
return Math.min(Math.max(docDisplay_.getPixelWidth() - 22,
ChunkOutputUi.MIN_PLOT_WIDTH),
ChunkOutputUi.MAX_PLOT_WIDTH);
}
public void cleanScopeErrorState(Scope scope)
{
// this can be called on a timer, so ensure the scope is still valid
if (scope == null ||
scope.getBodyStart() == null ||
scope.getEnd() == null)
return;
docDisplay_.setChunkLineExecState(
scope.getBodyStart().getRow(),
scope.getEnd().getRow(),
ChunkRowExecState.LINE_NONE);
}
public void onDismiss()
{
closeAllSatelliteChunks();
}
// set the output mode based on the global pref (or our local
// override of it, if any)
public void syncOutputMode()
{
String outputType = docUpdateSentinel_.getProperty(CHUNK_OUTPUT_TYPE);
if (!StringUtil.isNullOrEmpty(outputType) && outputType != "undefined")
{
// if the document property is set, apply it directly
docDisplay_.setShowChunkOutputInline(
outputType == CHUNK_OUTPUT_INLINE);
}
else
{
// otherwise, use the global preference to set the value
docDisplay_.setShowChunkOutputInline(
docDisplay_.getModeId() == "mode/rmarkdown" &&
RStudioGinjector.INSTANCE.getUIPrefs()
.showRmdChunkOutputInline().getValue());
}
// watch for scope tree changes if showing output inline
if (docDisplay_.showChunkOutputInline() && scopeTreeReg_ == null)
{
scopeTreeReg_ = docDisplay_.addScopeTreeReadyHandler(this);
}
else if (!docDisplay_.showChunkOutputInline() && scopeTreeReg_ != null)
{
scopeTreeReg_.removeHandler();
scopeTreeReg_ = null;
}
}
// Private methods --------------------------------------------------------
private void closeAllSatelliteChunks()
{
String docId = docUpdateSentinel_.getId();
events_.fireEvent(new ChunkSatelliteCloseAllWindowEvent(docId));
}
private void restartThenExecute(AppCommand command)
{
if (commands_.restartR().isEnabled())
{
postRestartCommand_ = command;
commands_.restartR().execute();
}
}
private void cleanCurrentExecChunk()
{
String chunkId = queue_.getExecutingChunkId();
if (chunkId == null)
return;
if (outputs_.containsKey(chunkId))
{
outputs_.get(chunkId)
.getOutputWidget().onOutputFinished(false,
NotebookQueueUnit.EXEC_SCOPE_PARTIAL);
}
cleanChunkExecState(chunkId);
}
private void loadInitialChunkOutput()
{
if (state_ != STATE_NONE)
return;
state_ = STATE_INITIALIZING;
requestId_ = nextRequestId_++;
server_.refreshChunkOutput(
docUpdateSentinel_.getPath(),
docUpdateSentinel_.getId(),
contextId_,
Integer.toHexString(requestId_),
"",
new ServerRequestCallback<NotebookDocQueue>()
{
@Override
public void onResponseReceived(NotebookDocQueue queue)
{
if (queue != null)
queue_.setQueue(queue);
}
@Override
public void onError(ServerError error)
{
Debug.logError(error);
}
});
}
// look for a line widget associated with the given chunk ID (used to find
// orphans)
private LineWidget getLineWidget(String chunkId)
{
JsArray<LineWidget> lineWidgets = docDisplay_.getLineWidgets();
for (int i = 0; i < lineWidgets.length(); i++)
{
LineWidget w = lineWidgets.get(i);
if (w.getType() == ChunkDefinition.LINE_WIDGET_TYPE)
{
ChunkDefinition def = w.getData();
if (def.getChunkId() == chunkId)
return w;
}
}
return null;
}
// NOTE: this implements chunk removal locally; prefer firing a
// ChunkChangeEvent if you're removing a chunk so appropriate hooks are
// invoked elsewhere
private void removeChunk(final String chunkId, final String requestId)
{
// ignore if this chunk is currently executing
if (queue_.isChunkExecuting(chunkId))
return;
final ChunkOutputUi output = outputs_.get(chunkId);
if (output == null)
{
// this case is unexpected; it means that a chunk we don't know about
// was removed. look for an orphaned line widget matching the chunk ID
// in case our output map is out of sync.
LineWidget w = getLineWidget(chunkId);
if (w != null)
{
docDisplay_.removeLineWidget(w);
if (w.getElement() != null)
{
w.getElement().getStyle().setDisplay(Display.NONE);
w.getElement().removeFromParent();
}
}
return;
}
// remove any errors in the gutter associated with this chunk
cleanScopeErrorState(output.getScope());
ArrayList<Widget> widgets = new ArrayList<Widget>();
widgets.add(output.getOutputWidget());
FadeOutAnimation anim = new FadeOutAnimation(widgets, new Command()
{
@Override
public void execute()
{
// physically remove chunk output
output.remove();
outputs_.remove(chunkId);
// mark doc dirty if interactive (this is not undoable)
if (StringUtil.isNullOrEmpty(requestId))
setDirtyState();
}
});
anim.run(400);
}
private void removeAllChunks()
{
for (ChunkOutputUi output: outputs_.values())
{
// clean any error state still attached to the output's scope
cleanScopeErrorState(output.getScope());
output.remove();
}
closeAllSatelliteChunks();
outputs_.clear();
}
private void changeOutputMode(String mode)
{
docDisplay_.setShowChunkOutputInline(mode == CHUNK_OUTPUT_INLINE);
// manage commands
manageCommands();
// if we don't have any inline output, we're done
if (outputs_.size() == 0 || mode != CHUNK_OUTPUT_CONSOLE)
return;
// if we do have inline output, offer to clean it up
RStudioGinjector.INSTANCE.getGlobalDisplay().showYesNoMessage(
GlobalDisplay.MSG_QUESTION,
"Remove Inline Chunk Output",
"Do you want to clear all the existing chunk output from your " +
"notebook?", false,
new Operation()
{
@Override
public void execute()
{
removeAllChunks();
}
},
new Operation()
{
@Override
public void execute()
{
// no action necessary
}
},
null,
"Remove Output",
"Keep Output",
false);
}
private void populateChunkDefs(JsArray<ChunkDefinition> defs)
{
for (int i = 0; i < defs.length(); i++)
{
createChunkOutput(defs.get(i));
}
}
private void createChunkOutput(ChunkDefinition def)
{
outputs_.put(def.getChunkId(),
new ChunkOutputUi(docUpdateSentinel_.getId(), docDisplay_,
def, this));
}
private boolean needsSetupChunkExecuted()
{
// ignore if disabled
if (!prefs_.autoRunSetupChunk().getValue())
return false;
// ignore if setup chunk currently running or already in the execution
// queue
if (queue_.isChunkExecuting(SETUP_CHUNK_ID) ||
queue_.isChunkQueued(SETUP_CHUNK_ID))
{
return false;
}
// no reason to do work if we don't need to re-validate the setup chunk
if (!validateSetupChunk_ && !StringUtil.isNullOrEmpty(setupCrc32_))
return false;
validateSetupChunk_ = false;
// find the setup chunk
Scope setupScope = getSetupChunkScope();
if (setupScope != null)
{
// make sure there's some code to execute
Range range = scopeHelper_.getSweaveChunkInnerRange(setupScope);
if (range.getStart().isEqualTo(range.getEnd()))
return false;
String crc32 = getChunkCrc32(setupScope);
// compare with previously known hash; if it differs, re-run the
// setup chunk
if (crc32 != setupCrc32_)
{
// push it into the execution queue
return true;
}
}
return false;
}
private String getChunkCrc32(Scope chunk)
{
// extract the body of the chunk
String code = getChunkCode(chunk);
// hash the body and prefix with the virtual session ID (so all
// hashes are automatically invalidated when the session changes)
return session_.getSessionInfo().getSessionId() +
StringUtil.crc32(code);
}
private void writeSetupCrc32(String crc32)
{
setupCrc32_ = crc32;
docUpdateSentinel_.setProperty(LAST_SETUP_CRC32, crc32);
}
private void setAllExpansionStates(int state)
{
for (ChunkOutputUi output: outputs_.values())
{
output.getOutputWidget().setExpansionState(state);
}
}
private void clearChunkExecQueue()
{
if (queue_ != null)
queue_.clear();
}
private Timer cleanErrorGutter_ = new Timer()
{
@Override
public void run()
{
// get the doc's current scope and see if it matches any of our
// known chunk outputs
Scope current = docDisplay_.getCurrentChunk();
if (current == null)
return;
for (ChunkOutputUi output: outputs_.values())
{
if (output.getScope() == null)
continue;
if (output.getScope().getPreamble() == current.getPreamble())
{
cleanScopeErrorState(current);
return;
}
}
}
};
private Timer resizePlotsLocal_ = new Timer()
{
@Override
public void run()
{
for (ChunkOutputUi output: outputs_.values())
{
output.getOutputWidget().syncHeight(false, false);
}
}
};
private Timer resizePlotsRemote_ = new Timer()
{
@Override
public void run()
{
// avoid reentrancy
if (resizingPlotsRemote_)
return;
// avoid unnecessary work
final int plotWidth = getPlotWidth();
if (plotWidth == lastPlotWidth_)
return;
// we want to resize the visible plots first, so provide the server
// with the id of the first visible chunk as a cue
int row = docDisplay_.getFirstVisibleRow();
Integer min = null;
String chunkId = "";
boolean hasPlots = false;
for (ChunkOutputUi output: outputs_.values())
{
int delta = Math.abs(output.getCurrentRow() - row);
if (min == null || delta < min)
{
min = delta;
chunkId = output.getChunkId();
}
hasPlots = hasPlots || output.getOutputWidget().hasPlots();
}
// if no widgets have plots, don't bother with resize
if (!hasPlots)
return;
// make the request
server_.replayNotebookPlots(docUpdateSentinel_.getId(),
chunkId,
plotWidth,
0,
new ServerRequestCallback<String>()
{
@Override
public void onResponseReceived(String replayId)
{
// server returns empty in the case wherein there's already
// a resize RPC in process (could be from e.g. another
// notebook in this session)
if (replayId == null || replayId.isEmpty())
return;
currentPlotsReplayId_ = replayId;
// don't replay a request for this width again
lastPlotWidth_ = plotWidth;
// mark all plots as queued for resize
for (ChunkOutputUi output: outputs_.values())
output.getOutputWidget().setPlotPending(true);
resizingPlotsRemote_ = true;
}
@Override
public void onError(ServerError error)
{
currentPlotsReplayId_ = null;
Debug.logError(error);
}
});
}
};
private void registerProgressHandlers()
{
// register click callback if necessary
if (progressClickReg_ == null)
{
progressClickReg_ = editingTarget_.getStatusBar()
.addProgressClickHandler(new ClickHandler()
{
@Override
public void onClick(ClickEvent arg0)
{
String chunkId = queue_.getExecutingChunkId();
if (chunkId != null &&
outputs_.containsKey(chunkId))
{
outputs_.get(chunkId).ensureVisible();
}
}
});
releaseOnDismiss_.add(progressClickReg_);
}
if (progressCancelReg_ == null)
{
progressCancelReg_ = editingTarget_.getStatusBar()
.addProgressCancelHandler(new Command()
{
@Override
public void execute()
{
// interrupt R if it's busy
if (commands_.interruptR().isEnabled())
commands_.interruptR().execute();
// don't execute any more chunks
clearChunkExecQueue();
}
});
releaseOnDismiss_.add(progressCancelReg_);
}
}
private void setDirtyState()
{
if (getCommitMode() == MODE_UNCOMMITTED && !dirtyState_.getValue())
{
// mark the document dirty (if it isn't already) since it now
// contains notebook cache changes that haven't been committed
dirtyState_.markDirty(false);
source_.setSourceDocumentDirty(
docUpdateSentinel_.getId(), true,
new VoidServerRequestCallback());
}
}
private void refreshSatelliteChunk(String chunkId)
{
requestId_ = nextRequestId_++;
satelliteChunkRequestIds_.add(Integer.toHexString(requestId_));
server_.refreshChunkOutput(
docUpdateSentinel_.getPath(),
docUpdateSentinel_.getId(),
contextId_,
Integer.toHexString(requestId_),
chunkId,
new ServerRequestCallback<NotebookDocQueue>()
{
@Override
public void onResponseReceived(NotebookDocQueue queue)
{
if (queue != null)
queue_.setQueue(queue);
}
@Override
public void onError(ServerError error)
{
Debug.logError(error);
}
});
}
private String getCurrentChunkId()
{
Scope chunk = docDisplay_.getCurrentChunk();
if (chunk == null)
return null;
return getRowChunkId(chunk.getPreamble().getRow());
}
private JsArray<ChunkDefinition> initialChunkDefs_;
private HashMap<String, ChunkOutputUi> outputs_;
private ArrayList<String> satelliteChunkRequestIds_;
private HandlerRegistration progressClickReg_;
private HandlerRegistration scopeTreeReg_;
private HandlerRegistration progressCancelReg_;
private Position lastStart_;
private Position lastEnd_;
private AppCommand postRestartCommand_;
private final DocDisplay docDisplay_;
private final DocUpdateSentinel docUpdateSentinel_;
private final TextEditingTarget editingTarget_;
private final TextEditingTarget.Display editingDisplay_;
private final TextEditingTargetChunks chunks_;
private final DirtyState dirtyState_;
private final NotebookDoc notebookDoc_;
private final TextEditingTargetScopeHelper scopeHelper_;
private final DependencyManager dependencyManager_;
private final HandlerRegistration renderReg_;
ArrayList<HandlerRegistration> releaseOnDismiss_;
private Session session_;
private Provider<SourceWindowManager> pSourceWindowManager_;
private UIPrefs prefs_;
private Commands commands_;
private NotebookHtmlRenderer htmlRenderer_;
private RMarkdownServerOperations server_;
private SourceServerOperations source_;
private EventBus events_;
private NotebookQueueState queue_;
private Style editorStyle_;
private static int nextRequestId_ = 0;
private int requestId_ = 0;
private String contextId_ = "";
private ResizeEvent queuedResize_ = null;
private boolean validateSetupChunk_ = false;
private String setupCrc32_ = "";
private int lastPlotWidth_ = 0;
private boolean resizingPlotsRemote_ = false;
private boolean maximizedPane_ = false;
private int state_ = STATE_NONE;
private String currentPlotsReplayId_ = null;
// no chunk state
private final static int STATE_NONE = 0;
// synchronizing chunk state from server
private final static int STATE_INITIALIZING = 0;
// chunk state synchronized
private final static int STATE_INITIALIZED = 1;
private final static String LAST_SETUP_CRC32 = "last_setup_crc32";
public final static String SETUP_CHUNK_ID = "csetup_chunk";
// stored document properties/values
public final static String CHUNK_OUTPUT_TYPE = "chunk_output_type";
public final static String CHUNK_OUTPUT_INLINE = "inline";
public final static String CHUNK_OUTPUT_CONSOLE = "console";
public final static String CONTENT_PREVIEW_ENABLED =
"content_preview_enabled";
public final static String CONTENT_PREVIEW_INLINE =
"content_preview_inline";
public final static int MODE_COMMITTED = 0;
public final static int MODE_UNCOMMITTED = 1;
}