package javarepl.plugin;
/*
* Copied from IntelliJ 12 sourcecode and heavily modified
*
* Copyright 2000-2012 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.intellij.codeInsight.lookup.LookupManager;
import com.intellij.execution.process.ConsoleHistoryModel;
import com.intellij.lang.Language;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.EmptyAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.undo.UndoConstants;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actions.ContentChooser;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.util.LexerEditorHighlighter;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ex.ProjectEx;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.CharFilter;
import com.intellij.openapi.util.text.StringHash;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.CharsetToolkit;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileFactory;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.io.SafeFileOutputStream;
import com.thoughtworks.xstream.io.HierarchicalStreamReader;
import com.thoughtworks.xstream.io.xml.XppReader;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.xmlpull.v1.XmlPullParserFactory;
import org.xmlpull.v1.XmlSerializer;
import java.awt.event.KeyEvent;
import java.io.*;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
/**
* @author gregsh
*/
public class JavaREPLConsoleHistoryController {
private static final Logger LOG = Logger.getInstance("com.intellij.execution.console.ConsoleHistoryController");
private final JavaREPLLanguageConsole myConsole;
private final AnAction myHistoryNext = new MyAction(true);
private final AnAction myHistoryPrev = new MyAction(false);
private final AnAction myBrowseHistory = new MyBrowseAction();
private boolean myMultiline;
private ModelHelper myHelper;
private long myLastSaveStamp;
public JavaREPLConsoleHistoryController(@NotNull final String type,
@Nullable final String persistenceId,
@NotNull final JavaREPLLanguageConsole console,
@NotNull final ConsoleHistoryModel model) {
this(type, persistenceId, console, model, Charset.defaultCharset());
}
public JavaREPLConsoleHistoryController(@NotNull final String type,
@Nullable final String persistenceId,
@NotNull final JavaREPLLanguageConsole console,
@NotNull final ConsoleHistoryModel model,
@NotNull final Charset charset) {
String id = persistenceId == null || StringUtil.isEmpty(persistenceId) ? console.getProject().getPresentableUrl() : persistenceId;
myHelper = new ModelHelper(type, id, model, charset);
myConsole = console;
}
public boolean isMultiline() {
return myMultiline;
}
public JavaREPLConsoleHistoryController setMultiline(boolean multiline) {
myMultiline = multiline;
return this;
}
public ConsoleHistoryModel getModel() {
return myHelper.getModel();
}
public void install() {
if (myHelper.getId() != null) {
ApplicationManager.getApplication().getMessageBus().connect(myConsole).subscribe(
ProjectEx.ProjectSaved.TOPIC, new ProjectEx.ProjectSaved() {
@Override
public void saved(@NotNull final Project project) {
saveHistory();
}
});
Disposer.register(myConsole, new Disposable() {
@Override
public void dispose() {
saveHistory();
}
});
loadHistory(myHelper.getId());
}
configureActions();
myLastSaveStamp = getCurrentTimeStamp();
}
private long getCurrentTimeStamp() {
return getModel().getModificationCount() + myConsole.getEditorDocument().getModificationStamp();
}
private void configureActions() {
EmptyAction.setupAction(myHistoryNext, "Console.History.Next", null);
EmptyAction.setupAction(myHistoryPrev, "Console.History.Previous", null);
EmptyAction.setupAction(myBrowseHistory, "Console.History.Browse", null);
if (!myMultiline) {
myHistoryNext.registerCustomShortcutSet(KeyEvent.VK_UP, 0, null);
myHistoryPrev.registerCustomShortcutSet(KeyEvent.VK_DOWN, 0, null);
}
myHistoryNext.registerCustomShortcutSet(myHistoryNext.getShortcutSet(), myConsole.getCurrentEditor().getComponent());
myHistoryPrev.registerCustomShortcutSet(myHistoryPrev.getShortcutSet(), myConsole.getCurrentEditor().getComponent());
myBrowseHistory.registerCustomShortcutSet(myBrowseHistory.getShortcutSet(), myConsole.getCurrentEditor().getComponent());
}
/**
* Use this method if you decided to change the id for your console but don't want your users to loose their current histories
*
* @param id previous id id
* @return true if some text has been loaded; otherwise false
*/
public boolean loadHistory(String id) {
String prev = myHelper.getContent();
boolean result = myHelper.loadHistory(id);
String userValue = myHelper.getContent();
if (prev != userValue && userValue != null) {
setConsoleText(userValue, false, false);
}
return result;
}
private void saveHistory() {
if (myLastSaveStamp == getCurrentTimeStamp()) return;
myHelper.setContent(myConsole.getEditorDocument().getText());
myHelper.saveHistory();
myLastSaveStamp = getCurrentTimeStamp();
}
private static void cleanupOldFiles(final File dir) {
final long keep10weeks = 10 * 1000L * 60 * 60 * 24 * 7;
final long curTime = System.currentTimeMillis();
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile() && file.getName().endsWith(".hist.xml") && curTime - file.lastModified() > keep10weeks) {
file.delete();
}
}
}
}
public AnAction getHistoryNext() {
return myHistoryNext;
}
public AnAction getHistoryPrev() {
return myHistoryPrev;
}
public AnAction getBrowseHistory() {
return myBrowseHistory;
}
protected void setConsoleText(final String command, final boolean storeUserText, final boolean regularMode) {
final Editor editor = myConsole.getCurrentEditor();
final Document document = editor.getDocument();
new WriteCommandAction.Simple(myConsole.getProject()) {
@Override
public void run() {
if (storeUserText) {
myHelper.setContent(document.getText());
}
String text = StringUtil.notNullize(command);
int offset;
if (regularMode) {
if (myMultiline) {
if (text.isEmpty()) return;
int selectionStart = editor.getSelectionModel().getSelectionStart();
int selectionEnd = editor.getSelectionModel().getSelectionEnd();
int caretOffset = editor.getCaretModel().getOffset();
int line = document.getLineNumber(caretOffset);
int lineStartOffset = document.getLineStartOffset(line);
if (selectionStart == lineStartOffset) document.deleteString(selectionStart, selectionEnd);
String trimmedLine = document.getText(new TextRange(lineStartOffset, document.getLineEndOffset(line))).trim();
if (StringUtil.findFirst(trimmedLine, new CharFilter() {
@Override
public boolean accept(char ch) {
return ch == '\'' || ch == '\"' || ch == '_' || Character.isLetterOrDigit(ch);
}
}) > -1) {
text += "\n";
}
document.insertString(lineStartOffset, text);
offset = lineStartOffset;
editor.getSelectionModel().setSelection(lineStartOffset, lineStartOffset + text.length());
} else {
document.setText(text);
offset = document.getTextLength();
}
} else {
offset = 0;
try {
document.putUserData(UndoConstants.DONT_RECORD_UNDO, Boolean.TRUE);
document.setText(text);
} finally {
document.putUserData(UndoConstants.DONT_RECORD_UNDO, null);
}
}
editor.getCaretModel().moveToOffset(offset);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
}.execute();
}
private class MyAction extends AnAction {
private boolean myNext;
public MyAction(final boolean next) {
myNext = next;
getTemplatePresentation().setVisible(false);
}
@Override
public void actionPerformed(final AnActionEvent e) {
final String command;
if (myNext) {
command = getModel().getHistoryNext();
if (!myMultiline && command == null) return;
} else {
if (!myMultiline && getModel().getHistoryCursor() < 0) return;
command = ObjectUtils.chooseNotNull(getModel().getHistoryPrev(), myMultiline ? "" : StringUtil.notNullize(myHelper.getContent()));
}
setConsoleText(command, myNext && getModel().getHistoryCursor() == 0, true);
}
@Override
public void update(final AnActionEvent e) {
super.update(e);
e.getPresentation().setEnabled(myMultiline || canMoveInEditor(myNext));
}
}
private boolean canMoveInEditor(final boolean next) {
final Editor consoleEditor = myConsole.getCurrentEditor();
final Document document = consoleEditor.getDocument();
final CaretModel caretModel = consoleEditor.getCaretModel();
if (LookupManager.getActiveLookup(consoleEditor) != null) return false;
if (next) {
return document.getLineNumber(caretModel.getOffset()) == 0;
} else {
final int lineCount = document.getLineCount();
return (lineCount == 0 || document.getLineNumber(caretModel.getOffset()) == lineCount - 1) &&
StringUtil.isEmptyOrSpaces(document.getText().substring(caretModel.getOffset()));
}
}
private class MyBrowseAction extends AnAction {
@Override
public void update(final AnActionEvent e) {
e.getPresentation().setEnabled(getModel().getHistorySize() > 0);
}
@Override
public void actionPerformed(final AnActionEvent e) {
String s1 = KeymapUtil.getFirstKeyboardShortcutText(myHistoryNext);
String s2 = KeymapUtil.getFirstKeyboardShortcutText(myHistoryPrev);
String title = myConsole.getTitle() + " History" +
(StringUtil.isNotEmpty(s1) && StringUtil.isNotEmpty(s2) ? " (" + s1 + " and " + s2 + " while in editor)" : "");
final ContentChooser<String> chooser = new ContentChooser<String>(myConsole.getProject(), title, true) {
@Override
protected void removeContentAt(String content) {
getModel().removeFromHistory(content);
}
@Override
protected String getStringRepresentationFor(String content) {
return content;
}
@Override
protected List<String> getContents() {
return getModel().getHistory();
}
@Override
protected Editor createIdeaEditor(String text) {
PsiFile consoleFile = myConsole.getFile();
Language language = consoleFile.getLanguage();
Project project = consoleFile.getProject();
PsiFile psiFile = PsiFileFactory.getInstance(project).createFileFromText(
"a." + consoleFile.getFileType().getDefaultExtension(),
language,
StringUtil.convertLineSeparators(new String(text)), false, true);
VirtualFile virtualFile = psiFile.getViewProvider().getVirtualFile();
if (virtualFile instanceof LightVirtualFile) ((LightVirtualFile) virtualFile).setWritable(false);
Document document = FileDocumentManager.getInstance().getDocument(virtualFile);
EditorFactory editorFactory = EditorFactory.getInstance();
EditorEx editor = (EditorEx) editorFactory.createViewer(document, project);
editor.getSettings().setFoldingOutlineShown(false);
editor.getSettings().setLineMarkerAreaShown(false);
editor.getSettings().setIndentGuidesShown(false);
SyntaxHighlighter highlighter = SyntaxHighlighterFactory.getSyntaxHighlighter(language, project, psiFile.getViewProvider().getVirtualFile());
editor.setHighlighter(new LexerEditorHighlighter(highlighter, editor.getColorsScheme()));
return editor;
}
};
chooser.setContentIcon(null);
chooser.setSplitterOrientation(false);
chooser.setSelectedIndex(Math.max(getModel().getHistoryCursor(), 0));
chooser.show();
if (chooser.isOK()) {
setConsoleText(chooser.getSelectedText(), false, true);
}
}
}
public static class ModelHelper {
private final String myType;
private final String myId;
private final ConsoleHistoryModel myModel;
private String myContent;
@NotNull
private final Charset myCharset;
public ModelHelper(String type, String id, ConsoleHistoryModel model, @NotNull Charset charset) {
myType = type;
myId = id;
myModel = model;
myCharset = charset;
}
public ConsoleHistoryModel getModel() {
return myModel;
}
public void setContent(String userValue) {
myContent = userValue;
}
public String getId() {
return myId;
}
public String getContent() {
return myContent;
}
private String getHistoryFilePath(final String id) {
return PathManager.getSystemPath() + File.separator +
"userHistory" + File.separator +
myType + Long.toHexString(StringHash.calc(id)) + ".hist.xml";
}
public boolean loadHistory(String id) {
File file = new File(getHistoryFilePath(id));
if (!file.exists()) return false;
HierarchicalStreamReader xmlReader = null;
try {
xmlReader = new XppReader(new InputStreamReader(new FileInputStream(file), myCharset));
String text = loadHistory(xmlReader, id);
if (text != null) {
myContent = text;
return true;
}
} catch (Exception ex) {
LOG.error(ex);
} finally {
if (xmlReader != null) {
xmlReader.close();
}
}
return false;
}
private void saveHistory() {
final File file = new File(getHistoryFilePath(myId));
final File dir = file.getParentFile();
if (!dir.exists() && !dir.mkdirs() || !dir.isDirectory()) {
LOG.error("failed to create folder: " + dir.getAbsolutePath());
return;
}
OutputStream os = null;
try {
final XmlSerializer serializer = XmlPullParserFactory.newInstance().newSerializer();
try {
serializer.setProperty("http://xmlpull.org/v1/doc/properties.html#serializer-indentation", " ");
} catch (Exception e) {
// not recognized
}
serializer.setOutput(os = new SafeFileOutputStream(file), myCharset.name());
saveHistory(serializer);
} catch (Exception ex) {
LOG.error(ex);
} finally {
try {
os.close();
} catch (Exception e) {
// nothing
}
}
cleanupOldFiles(dir);
}
@Nullable
private String loadHistory(final HierarchicalStreamReader in, final String expectedId) {
if (!in.getNodeName().equals("console-history")) return null;
final String id = in.getAttribute("id");
if (!expectedId.equals(id)) return null;
final ArrayList<String> entries = new ArrayList<String>();
String consoleContent = null;
while (in.hasMoreChildren()) {
in.moveDown();
if ("history-entry".equals(in.getNodeName())) {
entries.add(in.getValue());
} else if ("console-content".equals(in.getNodeName())) {
consoleContent = in.getValue();
}
in.moveUp();
}
for (ListIterator<String> iterator = entries.listIterator(entries.size()); iterator.hasPrevious(); ) {
final String entry = iterator.previous();
getModel().addToHistory(entry);
}
return consoleContent;
}
private void saveHistory(final XmlSerializer out) throws IOException {
out.startDocument(CharsetToolkit.UTF8, null);
out.startTag(null, "console-history");
out.attribute(null, "id", myId);
for (String s : getModel().getHistory()) {
out.startTag(null, "history-entry");
out.text(s);
out.endTag(null, "history-entry");
}
String current = myContent;
if (StringUtil.isNotEmpty(current)) {
out.startTag(null, "console-content");
out.text(current);
out.endTag(null, "console-content");
}
out.endTag(null, "console-history");
out.endDocument();
}
}
}