/*
* Shell.java
*
* Copyright (C) 2009-12 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.console.shell;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.user.client.Command;
import com.google.inject.Inject;
import org.rstudio.core.client.BrowseCap;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.jsonrpc.RpcObjectList;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.CommandLineHistory;
import org.rstudio.studio.client.common.debugging.ErrorManager;
import org.rstudio.studio.client.common.debugging.events.UnhandledErrorEvent;
import org.rstudio.studio.client.common.debugging.model.ErrorHandlerType;
import org.rstudio.studio.client.common.shell.ShellDisplay;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.server.VoidServerRequestCallback;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.model.ClientInitState;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.ConsoleAction;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.SessionInfo;
import org.rstudio.studio.client.workbench.model.helper.StringStateValue;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.views.console.events.*;
import org.rstudio.studio.client.workbench.views.console.model.ConsoleServerOperations;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionManager;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionPopupPanel;
import org.rstudio.studio.client.workbench.views.console.shell.assist.HelpStrategy;
import org.rstudio.studio.client.workbench.views.console.shell.assist.HistoryCompletionManager;
import org.rstudio.studio.client.workbench.views.console.shell.assist.RCompletionManager;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorDisplay;
import org.rstudio.studio.client.workbench.views.environment.events.DebugModeChangedEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceEditorNative;
import java.util.ArrayList;
public class Shell implements ConsoleInputHandler,
ConsoleWriteOutputHandler,
ConsoleWriteErrorHandler,
ConsoleWritePromptHandler,
ConsoleWriteInputHandler,
ConsolePromptHandler,
ConsoleResetHistoryHandler,
ConsoleRestartRCompletedEvent.Handler,
ConsoleExecutePendingInputEvent.Handler,
SendToConsoleHandler,
DebugModeChangedEvent.Handler,
RunCommandWithDebugEvent.Handler,
UnhandledErrorEvent.Handler
{
static interface Binder extends CommandBinder<Commands, Shell>
{
}
public interface Display extends ShellDisplay
{
void onBeforeUnselected();
void onBeforeSelected();
void onSelected();
}
@Inject
public Shell(ConsoleServerOperations server,
EventBus eventBus,
Display display,
Session session,
Commands commands,
UIPrefs uiPrefs,
ErrorManager errorManager,
HelpStrategy helpStrategy)
{
super() ;
((Binder)GWT.create(Binder.class)).bind(commands, this);
server_ = server ;
eventBus_ = eventBus ;
view_ = display ;
commands_ = commands;
errorManager_ = errorManager;
input_ = view_.getInputEditorDisplay() ;
historyManager_ = new CommandLineHistory(input_);
browseHistoryManager_ = new CommandLineHistory(input_);
prefs_ = uiPrefs;
helpStrategy_ = helpStrategy;
inputAnimator_ = new ShellInputAnimator(view_.getInputEditorDisplay());
view_.setMaxOutputLines(session.getSessionInfo().getConsoleActionsLimit());
keyDownPreviewHandlers_ = new ArrayList<KeyDownPreviewHandler>() ;
keyPressPreviewHandlers_ = new ArrayList<KeyPressPreviewHandler>() ;
InputKeyDownHandler handler = new InputKeyDownHandler() ;
// This needs to be a capturing key down handler or else Ace will have
// handled the event before we had a chance to prevent it
view_.addCapturingKeyDownHandler(handler) ;
view_.addKeyPressHandler(handler) ;
eventBus.addHandler(ConsoleInputEvent.TYPE, this);
eventBus.addHandler(ConsoleWriteOutputEvent.TYPE, this);
eventBus.addHandler(ConsoleWriteErrorEvent.TYPE, this);
eventBus.addHandler(ConsoleWritePromptEvent.TYPE, this);
eventBus.addHandler(ConsoleWriteInputEvent.TYPE, this);
eventBus.addHandler(ConsolePromptEvent.TYPE, this);
eventBus.addHandler(ConsoleResetHistoryEvent.TYPE, this);
eventBus.addHandler(ConsoleRestartRCompletedEvent.TYPE, this);
eventBus.addHandler(ConsoleExecutePendingInputEvent.TYPE, this);
eventBus.addHandler(SendToConsoleEvent.TYPE, this);
eventBus.addHandler(DebugModeChangedEvent.TYPE, this);
eventBus.addHandler(RunCommandWithDebugEvent.TYPE, this);
eventBus.addHandler(UnhandledErrorEvent.TYPE, this);
final CompletionManager completionManager
= new RCompletionManager(view_.getInputEditorDisplay(),
null,
new CompletionPopupPanel(),
server,
null,
null,
null,
(DocDisplay) view_.getInputEditorDisplay(),
false);
addKeyDownPreviewHandler(completionManager) ;
addKeyPressPreviewHandler(completionManager) ;
addKeyDownPreviewHandler(new HistoryCompletionManager(
view_.getInputEditorDisplay(), server));
uiPrefs.insertMatching().bind(new CommandWithArg<Boolean>() {
public void execute(Boolean arg) {
AceEditorNative.setInsertMatching(arg);
}});
sessionInit(session);
}
private void sessionInit(Session session)
{
SessionInfo sessionInfo = session.getSessionInfo();
ClientInitState clientState = sessionInfo.getClientState();
new StringStateValue(GROUP_CONSOLE, STATE_INPUT, ClientState.TEMPORARY, clientState) {
@Override
protected void onInit(String value)
{
initialInput_ = value;
}
@Override
protected String getValue()
{
return view_.getInputEditorDisplay().getText();
}
};
JsArrayString history = sessionInfo.getConsoleHistory();
if (history != null)
setHistory(history);
RpcObjectList<ConsoleAction> actions = sessionInfo.getConsoleActions();
if (actions != null)
{
view_.playbackActions(actions);
}
if (sessionInfo.getResumed())
{
// no special UI for this (resuming session with all console
// history and other UI state preserved deemed adequate feedback)
}
}
public Display getDisplay()
{
return view_ ;
}
@Handler
void onConsoleClear()
{
// clear output
view_.clearOutput();
// notify server
server_.resetConsoleActions(new VoidServerRequestCallback());
// if we don't bounce setFocus the menu retains focus
Scheduler.get().scheduleDeferred(new ScheduledCommand() {
public void execute()
{
view_.getInputEditorDisplay().setFocus(true);
}
});
}
public void addKeyDownPreviewHandler(KeyDownPreviewHandler handler)
{
keyDownPreviewHandlers_.add(handler) ;
}
public void addKeyPressPreviewHandler(KeyPressPreviewHandler handler)
{
keyPressPreviewHandlers_.add(handler) ;
}
public void onConsoleInput(final ConsoleInputEvent event)
{
server_.consoleInput(event.getInput(),
new ServerRequestCallback<Void>() {
@Override
public void onError(ServerError error)
{
// show the error in the console then re-prompt
view_.consoleWriteError("Error: " + error.getUserMessage() + "\n");
if (lastPromptText_ != null)
consolePrompt(lastPromptText_, false);
}
});
}
public void onConsoleWriteOutput(ConsoleWriteOutputEvent event)
{
view_.consoleWriteOutput(event.getOutput()) ;
}
public void onConsoleWriteError(final ConsoleWriteErrorEvent event)
{
view_.consoleWriteError(event.getError());
}
public void onUnhandledError(UnhandledErrorEvent event)
{
if (!debugging_)
{
view_.consoleWriteExtendedError(
event.getError().getErrorMessage(),
event.getError(),
prefs_.autoExpandErrorTracebacks().getValue(),
getHistoryEntry(0));
}
}
public void onConsoleWriteInput(ConsoleWriteInputEvent event)
{
view_.consoleWriteInput(event.getInput());
}
public void onConsoleWritePrompt(ConsoleWritePromptEvent event)
{
view_.consoleWritePrompt(event.getPrompt());
}
public void onConsolePrompt(ConsolePromptEvent event)
{
String prompt = event.getPrompt().getPromptText() ;
boolean addToHistory = event.getPrompt().getAddToHistory() ;
consolePrompt(prompt, addToHistory) ;
}
private void consolePrompt(String prompt, boolean addToHistory)
{
view_.consolePrompt(prompt, true) ;
if (lastPromptText_ == null
&& initialInput_ != null
&& initialInput_.length() > 0)
{
view_.getInputEditorDisplay().setText(initialInput_);
view_.ensureInputVisible();
}
addToHistory_ = addToHistory;
resetHistoryPosition();
lastPromptText_ = prompt ;
if (restoreFocus_)
{
restoreFocus_ = false;
view_.getInputEditorDisplay().setFocus(true);
}
}
public void onConsoleResetHistory(ConsoleResetHistoryEvent event)
{
setHistory(event.getHistory());
}
@Override
public void onRestartRCompleted(ConsoleRestartRCompletedEvent event)
{
if (view_.isPromptEmpty())
eventBus_.fireEvent(new SendToConsoleEvent("", true));
focus();
}
private void processCommandEntry()
{
String commandText = view_.processCommandEntry() ;
if (addToHistory_ && (commandText.length() > 0))
addToHistory(commandText);
// fire event
eventBus_.fireEvent(new ConsoleInputEvent(commandText));
}
public void onSendToConsole(final SendToConsoleEvent event)
{
final InputEditorDisplay display = view_.getInputEditorDisplay();
// get anything already at the console
final String previousInput = StringUtil.notNull(display.getText());
// define code block we execute at finish
Command finishSendToConsole = new Command() {
@Override
public void execute()
{
if (event.shouldExecute())
{
processCommandEntry();
if (previousInput.length() > 0)
display.setText(previousInput);
}
if (!event.shouldExecute() || event.shouldFocus())
{
display.setFocus(true);
display.collapseSelection(false);
}
}
};
// do standrd finish if we aren't animating
if (!event.shouldAnimate())
{
display.clear();
display.setText(event.getCode());
finishSendToConsole.execute();
}
else
{
inputAnimator_.enque(event.getCode(), finishSendToConsole);
}
}
@Override
public void onExecutePendingInput(ConsoleExecutePendingInputEvent event)
{
// if the source view is delegating a Cmd+Enter to us then
// take it if we are focused and we have a command to enter
if (view_.getInputEditorDisplay().isFocused() &&
(view_.getInputEditorDisplay().getText().length() > 0))
{
processCommandEntry();
}
// otherwise delegate back to the source view. we do this via
// executing a command which is a bit of hack but it's a clean
// way to call code within the "current editor" (an event would
// go to all editors). another alternative would be to
// call a method on the SourceShim
else
{
commands_.executeCodeWithoutFocus().execute();
}
}
@Override
public void onDebugModeChanged(DebugModeChangedEvent event)
{
if (event.debugging())
{
view_.ensureInputVisible();
}
debugging_ = event.debugging();
}
@Override
public void onRunCommandWithDebug(final RunCommandWithDebugEvent event)
{
// Invoked from the "Rerun with Debug" command in the ConsoleError widget.
errorManager_.setDebugSessionHandlerType(
ErrorHandlerType.ERRORS_BREAK,
new ServerRequestCallback<Void>()
{
@Override
public void onResponseReceived(Void v)
{
eventBus_.fireEvent(new SendToConsoleEvent(
event.getCommand(), true));
}
@Override
public void onError(ServerError error)
{
// if we failed to set debug mode, don't rerun the command
}
});
}
private final class InputKeyDownHandler implements KeyDownHandler,
KeyPressHandler
{
public void onKeyDown(KeyDownEvent event)
{
int keyCode = event.getNativeKeyCode();
for (KeyDownPreviewHandler handler : keyDownPreviewHandlers_)
{
if (handler.previewKeyDown(event.getNativeEvent()))
{
event.preventDefault() ;
event.stopPropagation() ;
return;
}
}
if (event.getNativeKeyCode() == KeyCodes.KEY_TAB)
event.preventDefault();
int modifiers = KeyboardShortcut.getModifierValue(event.getNativeEvent());
if (event.isUpArrow() && modifiers == 0)
{
if ((input_.getCurrentLineNum() == 0) || input_.isCursorAtEnd())
{
event.preventDefault();
event.stopPropagation();
navigateHistory(-1);
}
}
else if (event.isDownArrow() && modifiers == 0)
{
if ((input_.getCurrentLineNum() == input_.getCurrentLineCount() - 1)
|| input_.isCursorAtEnd())
{
event.preventDefault();
event.stopPropagation();
navigateHistory(1);
}
}
else if (keyCode == KeyCodes.KEY_ENTER && modifiers == 0)
{
event.preventDefault();
event.stopPropagation();
restoreFocus_ = true;
processCommandEntry();
}
else if (keyCode == KeyCodes.KEY_ESCAPE && modifiers == 0)
{
event.preventDefault();
if (input_.getText().length() == 0)
{
// view_.isPromptEmpty() is to check for cases where the
// server is prompting but not at the top level. Escape
// needs to send null in those cases.
// For example, try "scan()" function
if (view_.isPromptEmpty())
{
// interrupt server
server_.interrupt(new VoidServerRequestCallback());
}
else
{
// if the input is already empty then send a console reset
// which will jump us back to the main prompt
eventBus_.fireEvent(new ConsoleInputEvent(null));
}
}
input_.clear();
}
else
{
int mod = KeyboardShortcut.getModifierValue(event.getNativeEvent());
if (mod == KeyboardShortcut.CTRL)
{
switch (keyCode)
{
case 'L':
Shell.this.onConsoleClear() ;
event.preventDefault() ;
break;
}
}
else if (mod == KeyboardShortcut.ALT)
{
switch (keyCode)
{
case 189: // hyphen
event.preventDefault();
event.stopPropagation();
input_.replaceSelection(" <- ", true);
break;
}
}
else if (
(BrowseCap.hasMetaKey() &&
(mod == (KeyboardShortcut.META + KeyboardShortcut.SHIFT))) ||
(!BrowseCap.hasMetaKey() &&
(mod == (KeyboardShortcut.CTRL + KeyboardShortcut.SHIFT))))
{
switch (keyCode)
{
case KeyCodes.KEY_M:
event.preventDefault();
event.stopPropagation();
input_.replaceSelection(" %>% ", true);
break;
}
}
}
}
public void onKeyPress(KeyPressEvent event)
{
for (KeyPressPreviewHandler handler : keyPressPreviewHandlers_)
{
if (handler.previewKeyPress(event.getCharCode()))
{
event.preventDefault() ;
event.stopPropagation() ;
return;
}
}
}
@SuppressWarnings("unused")
private boolean lastKeyCodeWasZero_;
}
private boolean isBrowsePrompt()
{
return lastPromptText_ != null && (lastPromptText_.startsWith("Browse"));
}
private void resetHistoryPosition()
{
historyManager_.resetPosition();
browseHistoryManager_.resetPosition();
}
private void addToHistory(String commandText)
{
if (isBrowsePrompt())
browseHistoryManager_.addToHistory(commandText);
else
historyManager_.addToHistory(commandText);
}
private String getHistoryEntry(int offset)
{
if (isBrowsePrompt())
return browseHistoryManager_.getHistoryEntry(offset);
else
return historyManager_.getHistoryEntry(offset);
}
private void navigateHistory(int offset)
{
if (isBrowsePrompt())
browseHistoryManager_.navigateHistory(offset);
else
historyManager_.navigateHistory(offset);
view_.ensureInputVisible();
}
public void focus()
{
input_.setFocus(true);
}
private void setHistory(JsArrayString history)
{
ArrayList<String> historyList = new ArrayList<String>(history.length());
for (int i = 0; i < history.length(); i++)
historyList.add(history.get(i));
historyManager_.setHistory(historyList);
browseHistoryManager_.resetPosition();
}
public void onBeforeUnselected()
{
view_.onBeforeUnselected();
}
public void onBeforeSelected()
{
view_.onBeforeSelected();
}
public void onSelected()
{
view_.onSelected();
}
private final ConsoleServerOperations server_ ;
private final EventBus eventBus_ ;
private final Display view_ ;
private final Commands commands_;
private final ErrorManager errorManager_;
private final InputEditorDisplay input_ ;
private final ArrayList<KeyDownPreviewHandler> keyDownPreviewHandlers_ ;
private final ArrayList<KeyPressPreviewHandler> keyPressPreviewHandlers_ ;
// indicates whether the next command should be added to history
private boolean addToHistory_ ;
private String lastPromptText_ ;
private final UIPrefs prefs_;
private final HelpStrategy helpStrategy_;
private final CommandLineHistory historyManager_;
private final CommandLineHistory browseHistoryManager_;
private final ShellInputAnimator inputAnimator_;
private String initialInput_ ;
private static final String GROUP_CONSOLE = "console";
private static final String STATE_INPUT = "input";
private boolean restoreFocus_ = true;
private boolean debugging_ = false;
}