org.rstudio.studio.client.workbench.views.console.shell.Shell.java Source code

Java tutorial

Introduction

Here is the source code for org.rstudio.studio.client.workbench.views.console.shell.Shell.java

Source

/*
 * 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.KeyboardHelper;
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.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) {
        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;

        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(),
                true);
        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) {
                    if (KeyboardHelper.isHyphenKeycode(keyCode)) {
                        event.preventDefault();
                        event.stopPropagation();
                        input_.replaceSelection(" <- ", true);
                    }
                } 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 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;
}