/*
* History.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.history;
import com.google.gwt.core.client.JsArrayNumber;
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.KeyCodes;
import com.google.gwt.event.dom.client.KeyDownEvent;
import com.google.gwt.event.dom.client.KeyDownHandler;
import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
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.inject.Inject;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.TimeBufferedCommand;
import org.rstudio.core.client.command.CommandBinder;
import org.rstudio.core.client.command.Handler;
import org.rstudio.core.client.events.HasSelectionCommitHandlers;
import org.rstudio.core.client.events.SelectionCommitEvent;
import org.rstudio.core.client.events.SelectionCommitHandler;
import org.rstudio.core.client.jsonrpc.RpcObjectList;
import org.rstudio.core.client.widget.ProgressIndicator;
import org.rstudio.core.client.widget.ProgressOperation;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.ConsoleDispatcher;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.SimpleRequestCallback;
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.workbench.WorkbenchView;
import org.rstudio.studio.client.workbench.commands.Commands;
import org.rstudio.studio.client.workbench.model.ClientState;
import org.rstudio.studio.client.workbench.model.Session;
import org.rstudio.studio.client.workbench.model.helper.StringStateValue;
import org.rstudio.studio.client.workbench.views.BasePresenter;
import org.rstudio.studio.client.workbench.views.console.events.ConsoleResetHistoryEvent;
import org.rstudio.studio.client.workbench.views.console.events.ConsoleResetHistoryHandler;
import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent;
import org.rstudio.studio.client.workbench.views.history.History.Display.Mode;
import org.rstudio.studio.client.workbench.views.history.events.FetchCommandsEvent;
import org.rstudio.studio.client.workbench.views.history.events.FetchCommandsHandler;
import org.rstudio.studio.client.workbench.views.history.events.HistoryEntriesAddedEvent;
import org.rstudio.studio.client.workbench.views.history.events.HistoryEntriesAddedHandler;
import org.rstudio.studio.client.workbench.views.history.model.HistoryEntry;
import org.rstudio.studio.client.workbench.views.history.model.HistoryServerOperations;
import org.rstudio.studio.client.workbench.views.source.events.InsertSourceEvent;
import java.util.ArrayList;
public class History extends BasePresenter implements SelectionCommitHandler<Void>,
FetchCommandsHandler
{
public interface SearchBoxDisplay extends HasValueChangeHandlers<String>
{
String getText();
public void setText(String text);
}
public interface Display extends WorkbenchView,
HasSelectionCommitHandlers<Void>
{
public enum Mode
{
Recent(0),
SearchResults(1),
CommandContext(2);
Mode(int value)
{
value_ = value;
}
public int getValue()
{
return value_;
}
private final int value_;
}
void setRecentCommands(ArrayList<HistoryEntry> commands,
boolean scrollToBottom);
void addRecentCommands(ArrayList<HistoryEntry> entries, boolean top);
int getRecentCommandsScrollPosition();
void setRecentCommandsScrollPosition(int scrollPosition);
ArrayList<Integer> getRecentCommandsSelectedRowIndexes();
int getRecentCommandsRowsDisplayed();
void truncateRecentCommands(int maxCommands);
ArrayList<String> getSelectedCommands();
ArrayList<Long> getSelectedCommandIndexes();
HandlerRegistration addFetchCommandsHandler(FetchCommandsHandler handler);
void setMoreCommands(long moreCommands);
SearchBoxDisplay getSearchBox();
Mode getMode();
void scrollToBottom();
void focusSearch();
void dismissSearchResults();
void showSearchResults(String query,
ArrayList<HistoryEntry> entries);
void showContext(String command,
ArrayList<HistoryEntry> entries,
long highlightOffset,
long highlightLength);
void dismissContext();
HasHistory getRecentCommandsWidget();
HasHistory getSearchResultsWidget();
HasHistory getCommandContextWidget();
boolean isCommandTableFocused();
}
public interface Binder extends CommandBinder<Commands, History>
{}
class SearchCommand extends TimeBufferedCommand implements ValueChangeHandler<String>
{
SearchCommand(Session session)
{
super(200);
}
@Override
protected void performAction(boolean shouldSchedulePassive)
{
final String query = searchQuery_;
if (searchQuery_ != null && searchQuery_.length() > 0)
{
server_.searchHistoryArchive(
searchQuery_, COMMAND_CHUNK_SIZE,
new SimpleRequestCallback<RpcObjectList<HistoryEntry>>()
{
@Override
public void onResponseReceived(
RpcObjectList<HistoryEntry> response)
{
if (!query.equals(searchQuery_))
return;
ArrayList<HistoryEntry> entries = toList(response);
view_.showSearchResults(query, entries);
}
});
}
}
public void onValueChange(ValueChangeEvent<String> event)
{
String query = event.getValue();
searchQuery_ = query;
if (searchQuery_.equals(""))
{
view_.dismissSearchResults();
}
else
{
nudge();
}
}
public void dismissResults()
{
view_.dismissSearchResults();
searchQuery_ = null;
}
private String searchQuery_;
}
@Inject
public History(final Display view,
HistoryServerOperations server,
final GlobalDisplay globalDisplay,
ConsoleDispatcher consoleDispatcher,
EventBus events,
final Session session,
Commands commands,
Binder binder)
{
super(view);
view_ = view;
events_ = events;
globalDisplay_ = globalDisplay;
consoleDispatcher_ = consoleDispatcher;
searchCommand_ = new SearchCommand(session);
session_ = session;
binder.bind(commands, this);
view_.addSelectionCommitHandler(this);
view_.addFetchCommandsHandler(this);
server_ = server;
events_.addHandler(ConsoleResetHistoryEvent.TYPE, new ConsoleResetHistoryHandler()
{
@Override
public void onConsoleResetHistory(ConsoleResetHistoryEvent event)
{
view_.bringToFront();
// convert to HistoryEntry
ArrayList<HistoryEntry> commands = toRecentCommandsList(
event.getHistory());
// determine entries to add
int preservedScrollPos = -1;
int startIndex = Math.max(0, commands.size() - COMMAND_CHUNK_SIZE);
// if we are updating an existing context then preserve the
// history position and the scroll position
if (event.getPreserveUIContext())
{
preservedScrollPos = view_.getRecentCommandsScrollPosition();
if (historyPosition_ < commands.size())
startIndex = new Long(historyPosition_).intValue();
}
// set recent commands
ArrayList<HistoryEntry> subList = new ArrayList<HistoryEntry>();
subList.addAll(commands.subList(startIndex, commands.size()));
boolean scrollToBottom = preservedScrollPos == -1;
setRecentCommands(subList, scrollToBottom);
// restore scroll position if requested
if (preservedScrollPos != -1)
{
final int scrollPos = preservedScrollPos;
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
public void execute()
{
view_.setRecentCommandsScrollPosition(scrollPos);
}
});
}
}
});
events_.addHandler(HistoryEntriesAddedEvent.TYPE, new HistoryEntriesAddedHandler()
{
public void onHistoryEntriesAdded(HistoryEntriesAddedEvent event)
{
view_.addRecentCommands(toList(event.getEntries()), false);
view_.truncateRecentCommands(
session_.getSessionInfo().getConsoleHistoryCapacity());
}
});
view_.getSearchBox().addValueChangeHandler(searchCommand_);
view_.getRecentCommandsWidget().getKeyTarget().addKeyDownHandler(
new KeyHandler(commands.historySendToConsole(),
commands.historySendToSource(),
null,
null));
view_.getSearchResultsWidget().getKeyTarget().addKeyDownHandler(
new KeyHandler(commands.historySendToConsole(),
commands.historySendToSource(),
commands.historyDismissResults(),
commands.historyShowContext()));
view_.getCommandContextWidget().getKeyTarget().addKeyDownHandler(
new KeyHandler(commands.historySendToConsole(),
commands.historySendToSource(),
commands.historyDismissContext(),
null));
new StringStateValue("history", "query", ClientState.TEMPORARY,
session.getSessionInfo().getClientState())
{
@Override
protected void onInit(String value)
{
if (value != null && value.length() != 0)
{
view_.getSearchBox().setText(value);
}
}
@Override
protected String getValue()
{
return view_.getSearchBox().getText();
}
};
server_.getRecentHistory(
COMMAND_CHUNK_SIZE,
new ServerRequestCallback<RpcObjectList<HistoryEntry>>()
{
@Override
public void onResponseReceived(RpcObjectList<HistoryEntry> response)
{
ArrayList<HistoryEntry> result = toRecentCommandsList(response);
setRecentCommands(result, true);
}
@Override
public void onError(ServerError error)
{
globalDisplay_.showErrorMessage("Error While Retrieving History",
error.getUserMessage());
}
});
}
private void setRecentCommands(ArrayList<HistoryEntry> commands,
boolean scrollToBottom)
{
view_.setRecentCommands(commands, scrollToBottom);
if (commands.size() > 0)
historyPosition_ = commands.get(0).getIndex();
else
historyPosition_ = 0;
view_.setMoreCommands(Math.min(historyPosition_, COMMAND_CHUNK_SIZE));
}
private class KeyHandler implements KeyDownHandler
{
private KeyHandler(Command accept,
Command shiftAccept,
Command left,
Command right)
{
this.accept_ = accept;
this.shiftAccept_ = shiftAccept;
this.left_ = left;
this.right_ = right;
}
public void onKeyDown(KeyDownEvent event)
{
if (!view_.isCommandTableFocused())
return;
boolean handled = false;
if (event.getNativeKeyCode() == KeyCodes.KEY_ENTER)
{
if (event.isShiftKeyDown())
{
if (shiftAccept_ != null)
shiftAccept_.execute();
handled = true;
}
else if (!event.isAnyModifierKeyDown())
{
if (accept_ != null)
accept_.execute();
handled = true;
}
}
else if (!event.isAnyModifierKeyDown())
{
switch (event.getNativeKeyCode())
{
case KeyCodes.KEY_ESCAPE:
case KeyCodes.KEY_LEFT:
if (left_ != null)
left_.execute();
handled = true;
break;
case KeyCodes.KEY_RIGHT:
if (right_ != null)
right_.execute();
handled = true;
break;
}
}
if (handled)
{
event.preventDefault();
event.stopPropagation();
}
}
private final Command accept_;
private final Command shiftAccept_;
private final Command left_;
private final Command right_;
}
@Override
public void onSelected()
{
super.onSelected();
if (view_.getMode() == Mode.Recent)
{
view_.scrollToBottom();
view_.focusSearch();
}
}
private String getSelectedCommands()
{
ArrayList<String> commands = view_.getSelectedCommands();
StringBuilder cmd = new StringBuilder();
for (String command : commands)
{
cmd.append(command);
cmd.append("\n");
}
String commandString = cmd.toString();
return commandString;
}
@Handler
void onHistorySendToConsole()
{
String commandString = getSelectedCommands();
commandString = StringUtil.chomp(commandString);
if (commandString.length() > 0 )
events_.fireEvent(new SendToConsoleEvent(commandString, false));
}
@Handler
void onHistorySendToSource()
{
String commandString = getSelectedCommands();
if (commandString.length() > 0)
events_.fireEvent(new InsertSourceEvent(commandString, true));
}
void onSearchHistory()
{
globalDisplay_.showErrorMessage("Message", "onSearchHistory");
}
void onLoadHistory()
{
view_.bringToFront();
consoleDispatcher_.chooseFileThenExecuteCommand("Load History",
"loadhistory");
}
void onSaveHistory()
{
view_.bringToFront();
consoleDispatcher_.saveFileAsThenExecuteCommand("Save History As",
".Rhistory",
false,
"savehistory");
}
@Handler
void onHistoryRemoveEntries()
{
// get selected indexes (bail if there is no selection)
final ArrayList<Integer> selectedRowIndexes =
view_.getRecentCommandsSelectedRowIndexes();
if (selectedRowIndexes.size() < 1)
{
globalDisplay_.showErrorMessage(
"Error",
"No history entries currently selected.");
return;
}
// bring view to front
view_.bringToFront();
globalDisplay_.showYesNoMessage(
GlobalDisplay.MSG_QUESTION,
"Confirm Remove Entries",
"Are you sure you want to remove the selected entries from " +
"the history?",
new ProgressOperation() {
public void execute(final ProgressIndicator indicator)
{
indicator.onProgress("Removing items...");
// for each selected row index we need to calculate
// the offset from the bottom
int rowCount = view_.getRecentCommandsRowsDisplayed();
JsArrayNumber bottomIndexes = (JsArrayNumber)
JsArrayNumber.createArray();
for (int i = 0; i<selectedRowIndexes.size(); i++)
bottomIndexes.push(rowCount - selectedRowIndexes.get(i) - 1);
server_.removeHistoryItems(
bottomIndexes,
new VoidServerRequestCallback(indicator));
}
},
true
);
}
@Handler
void onClearHistory()
{
view_.bringToFront();
globalDisplay_.showYesNoMessage(
GlobalDisplay.MSG_WARNING,
"Confirm Clear History",
"Are you sure you want to clear all history entries?",
new ProgressOperation() {
public void execute(final ProgressIndicator indicator)
{
indicator.onProgress("Clearing history...");
server_.clearHistory(
new VoidServerRequestCallback(indicator));
}
},
true
);
}
@Handler
void onHistoryDismissResults()
{
searchCommand_.dismissResults();
}
@Handler
void onHistoryDismissContext()
{
view_.dismissContext();
}
@Handler
void onHistoryShowContext()
{
ArrayList<Long> indexes = view_.getSelectedCommandIndexes();
if (indexes.size() != 1)
return;
final String command = view_.getSelectedCommands().get(0);
final Long min = indexes.get(0);
final long max = indexes.get(indexes.size() - 1) + 1;
final long start = Math.max(0, min - CONTEXT_LINES);
final long end = max + CONTEXT_LINES;
server_.getHistoryArchiveItems(
start,
end,
new SimpleRequestCallback<RpcObjectList<HistoryEntry>>() {
@Override
public void onResponseReceived(RpcObjectList<HistoryEntry> response)
{
ArrayList<HistoryEntry> entries = toList(response);
view_.showContext(command,
entries,
min - start,
max - min);
}
});
}
private ArrayList<HistoryEntry> toList(RpcObjectList<HistoryEntry> response)
{
ArrayList<HistoryEntry> entries = new ArrayList<HistoryEntry>();
for (int i = 0; i < response.length(); i++)
entries.add(response.get(i));
return entries;
}
private ArrayList<HistoryEntry> toRecentCommandsList(
JsArrayString jsCommands)
{
ArrayList<HistoryEntry> commands = new ArrayList<HistoryEntry>();
for (int i=0; i<jsCommands.length(); i++)
commands.add(HistoryEntry.create(i, jsCommands.get(i)));
return commands;
}
private ArrayList<HistoryEntry> toRecentCommandsList(
RpcObjectList<HistoryEntry> response)
{
ArrayList<HistoryEntry> entries = new ArrayList<HistoryEntry>();
for (int i = 0; i < response.length(); i++)
entries.add(response.get(i));
return entries;
}
public void onSelectionCommit(SelectionCommitEvent<Void> e)
{
onHistorySendToConsole();
}
public void onFetchCommands(FetchCommandsEvent event)
{
if (fetchingMoreCommands_)
return;
if (historyPosition_ == 0)
{
// This should rarely/never happen
return;
}
long startIndex = Math.max(0, historyPosition_ - COMMAND_CHUNK_SIZE);
long endIndex = historyPosition_;
server_.getHistoryItems(startIndex, endIndex,
new SimpleRequestCallback<RpcObjectList<HistoryEntry>>()
{
@Override
public void onResponseReceived(RpcObjectList<HistoryEntry> response)
{
ArrayList<HistoryEntry> entries =
toRecentCommandsList(response);
view_.addRecentCommands(entries, true);
fetchingMoreCommands_ = false;
if (response.length() > 0)
historyPosition_ = response.get(0).getIndex();
else
historyPosition_ = 0; // this shouldn't happen
view_.setMoreCommands(Math.min(historyPosition_,
COMMAND_CHUNK_SIZE));
}
@Override
public void onError(ServerError error)
{
super.onError(error);
fetchingMoreCommands_ = false;
}
});
}
// This field indicates how far into the history stream we have reached.
// When this value becomes 0, that means there is no more history to go
// fetch.
private long historyPosition_ = 0;
private static final int COMMAND_CHUNK_SIZE = 300;
private static final int CONTEXT_LINES = 50;
private boolean fetchingMoreCommands_ = false;
private final Display view_;
private final EventBus events_;
private final GlobalDisplay globalDisplay_;
private final SearchCommand searchCommand_;
private HistoryServerOperations server_;
private final Session session_;
private final ConsoleDispatcher consoleDispatcher_;
}