blob: a20b8c706ff25833c4a6ad655308191592f2e2b2 [file] [log] [blame]
package com.intellij.execution.console;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.ide.util.PropertiesComponent;
import com.intellij.lang.Language;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.VisualPosition;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.ex.DocumentBulkUpdateListener;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.RangeHighlighterEx;
import com.intellij.openapi.editor.impl.EditorComponentImpl;
import com.intellij.openapi.editor.impl.EditorImpl;
import com.intellij.openapi.editor.impl.SoftWrapModelImpl;
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
import com.intellij.openapi.editor.impl.softwrap.mapping.SoftWrapApplianceManager;
import com.intellij.openapi.editor.markup.CustomHighlighterRenderer;
import com.intellij.openapi.editor.markup.HighlighterLayer;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Conditions;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.testFramework.LightVirtualFile;
import com.intellij.ui.components.JBLayeredPane;
import com.intellij.util.Consumer;
import com.intellij.util.PairFunction;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
/**
* @experimental
*/
public final class LanguageConsoleBuilder {
@Nullable
private LanguageConsoleView consoleView;
@Nullable
private Condition<LanguageConsoleImpl> executionEnabled = Conditions.alwaysTrue();
@Nullable
private PairFunction<VirtualFile, Project, PsiFile> psiFileFactory;
@Nullable
private BaseConsoleExecuteActionHandler executeActionHandler;
@Nullable
private String historyType;
@Nullable
private GutterContentProvider gutterContentProvider;
private boolean oneLineInput;
private String processInputStateKey;
// todo to be removed
public LanguageConsoleBuilder(@SuppressWarnings("NullableProblems") @NotNull LanguageConsoleView consoleView) {
this.consoleView = consoleView;
}
public LanguageConsoleBuilder() {
}
public LanguageConsoleBuilder processHandler(@NotNull ProcessHandler processHandler) {
//noinspection deprecation
executionEnabled = new ProcessBackedExecutionEnabledCondition(processHandler);
return this;
}
public LanguageConsoleBuilder executionEnabled(@NotNull Condition<LanguageConsoleImpl> condition) {
executionEnabled = condition;
return this;
}
/**
* @see {@link com.intellij.psi.PsiCodeFragment}
*/
public LanguageConsoleBuilder psiFileFactory(@NotNull PairFunction<VirtualFile, Project, PsiFile> value) {
psiFileFactory = value;
return this;
}
@NotNull
public LanguageConsoleBuilder initActions(@NotNull BaseConsoleExecuteActionHandler executeActionHandler, @NotNull String historyType) {
if (consoleView == null) {
this.executeActionHandler = executeActionHandler;
this.historyType = historyType;
}
else {
doInitAction(consoleView, executeActionHandler, historyType);
}
return this;
}
private void doInitAction(@NotNull LanguageConsoleView consoleView, @NotNull BaseConsoleExecuteActionHandler executeActionHandler, @NotNull String historyType) {
ConsoleExecuteAction action = new ConsoleExecuteAction(consoleView, executeActionHandler, executionEnabled);
action.registerCustomShortcutSet(action.getShortcutSet(), consoleView.getConsole().getConsoleEditor().getComponent());
new ConsoleHistoryController(historyType, null, consoleView.getConsole(), executeActionHandler.getConsoleHistoryModel()).install();
}
/**
* todo This API doesn't look good, but it is much better than force client to know low-level details
*/
public static Pair<AnAction, ConsoleHistoryController> registerExecuteAction(@NotNull LanguageConsoleImpl console,
@NotNull final Consumer<String> executeActionHandler,
@NotNull String historyType,
@Nullable String historyPersistenceId,
@Nullable Condition<LanguageConsoleImpl> enabledCondition) {
ConsoleExecuteAction.ConsoleExecuteActionHandler handler = new ConsoleExecuteAction.ConsoleExecuteActionHandler(true) {
@Override
void doExecute(@NotNull String text, @NotNull LanguageConsoleImpl console, @Nullable LanguageConsoleView consoleView) {
executeActionHandler.consume(text);
}
};
ConsoleExecuteAction action = new ConsoleExecuteAction(console, handler, enabledCondition);
action.registerCustomShortcutSet(action.getShortcutSet(), console.getConsoleEditor().getComponent());
ConsoleHistoryController historyController = new ConsoleHistoryController(historyType, historyPersistenceId, console, handler.getConsoleHistoryModel());
historyController.install();
return new Pair<AnAction, ConsoleHistoryController>(action, historyController);
}
public LanguageConsoleBuilder gutterContentProvider(@Nullable GutterContentProvider value) {
gutterContentProvider = value;
return this;
}
/**
* @see {@link com.intellij.openapi.editor.ex.EditorEx#setOneLineMode(boolean)}
*/
@SuppressWarnings("UnusedDeclaration")
public LanguageConsoleBuilder oneLineInput() {
oneLineInput(true);
return this;
}
/**
* @see {@link com.intellij.openapi.editor.ex.EditorEx#setOneLineMode(boolean)}
*/
public LanguageConsoleBuilder oneLineInput(boolean value) {
oneLineInput = value;
return this;
}
@NotNull
public LanguageConsoleBuilder processInputStateKey(@Nullable String value) {
processInputStateKey = value;
return this;
}
@NotNull
public LanguageConsoleView build(@NotNull Project project, @NotNull Language language) {
GutteredLanguageConsole console = new GutteredLanguageConsole(language.getDisplayName() + " Console", project, language, gutterContentProvider, psiFileFactory);
if (oneLineInput) {
console.getConsoleEditor().setOneLineMode(true);
}
LanguageConsoleViewImpl consoleView = new LanguageConsoleViewImpl(console, true);
if (executeActionHandler != null) {
assert historyType != null;
doInitAction(consoleView, executeActionHandler, historyType);
}
if (processInputStateKey != null) {
assert executeActionHandler != null;
if (PropertiesComponent.getInstance().getBoolean(processInputStateKey, false)) {
executeActionHandler.useProcessStdIn = true;
DaemonCodeAnalyzer daemonCodeAnalyzer = DaemonCodeAnalyzer.getInstance(consoleView.getProject());
daemonCodeAnalyzer.setHighlightingEnabled(consoleView.getConsole().getFile(), false);
}
consoleView.addCustomConsoleAction(new UseConsoleInputAction(processInputStateKey));
}
console.initComponents();
return consoleView;
}
@Deprecated
/**
* @deprecated Don't use it directly!
* Will be private in IDEA >13
*/
public static class ProcessBackedExecutionEnabledCondition implements Condition<LanguageConsoleImpl> {
private final ProcessHandler myProcessHandler;
public ProcessBackedExecutionEnabledCondition(ProcessHandler myProcessHandler) {
this.myProcessHandler = myProcessHandler;
}
@Override
public boolean value(LanguageConsoleImpl console) {
return !myProcessHandler.isProcessTerminated();
}
}
private final static class GutteredLanguageConsole extends LanguageConsoleImpl {
private final GutterContentProvider gutterContentProvider;
@Nullable
private final PairFunction<VirtualFile, Project, PsiFile> psiFileFactory;
public GutteredLanguageConsole(@NotNull String title,
@NotNull Project project,
@NotNull Language language,
@Nullable GutterContentProvider gutterContentProvider,
@Nullable PairFunction<VirtualFile, Project, PsiFile> psiFileFactory) {
super(project, title, new LightVirtualFile(title, language, ""), false, psiFileFactory);
setShowSeparatorLine(false);
this.gutterContentProvider = gutterContentProvider == null ? new BasicGutterContentProvider() : gutterContentProvider;
this.psiFileFactory = psiFileFactory;
}
@Override
boolean isHistoryViewerForceAdditionalColumnsUsage() {
return false;
}
@Override
int getMinHistoryLineCount() {
return 1;
}
@NotNull
@Override
protected PsiFile createFile(@NotNull LightVirtualFile virtualFile, @NotNull Document document, @NotNull Project project) {
if (psiFileFactory == null) {
return super.createFile(virtualFile, document, project);
}
else {
return psiFileFactory.fun(virtualFile, project);
}
}
@Override
protected void setupEditorDefault(@NotNull EditorEx editor) {
super.setupEditorDefault(editor);
if (editor == getConsoleEditor()) {
return;
}
final ConsoleGutterComponent lineStartGutter = new ConsoleGutterComponent(editor, gutterContentProvider, true);
final ConsoleGutterComponent lineEndGutter = new ConsoleGutterComponent(editor, gutterContentProvider, false);
editor.getSoftWrapModel().forceAdditionalColumnsUsage();
((SoftWrapModelImpl)editor.getSoftWrapModel()).getApplianceManager().setWidthProvider(new SoftWrapApplianceManager.VisibleAreaWidthProvider() {
@Override
public int getVisibleAreaWidth() {
int guttersWidth = lineEndGutter.getPreferredSize().width + lineStartGutter.getPreferredSize().width;
EditorEx editor = getHistoryViewer();
return editor.getScrollingModel().getVisibleArea().width - guttersWidth;
}
});
editor.setHorizontalScrollbarVisible(true);
JLayeredPane layeredPane = new JBLayeredPane() {
@Override
public Dimension getPreferredSize() {
Dimension editorSize = getEditorComponent().getPreferredSize();
return new Dimension(lineStartGutter.getPreferredSize().width + editorSize.width, editorSize.height);
}
@Override
public Dimension getMinimumSize() {
Dimension editorSize = getEditorComponent().getMinimumSize();
return new Dimension(lineStartGutter.getPreferredSize().width + editorSize.width, editorSize.height);
}
@Override
public void doLayout() {
EditorComponentImpl editor = getEditorComponent();
int w = getWidth();
int h = getHeight();
int lineStartGutterWidth = lineStartGutter.getPreferredSize().width;
lineStartGutter.setBounds(0, 0, lineStartGutterWidth + gutterContentProvider.getLineStartGutterOverlap(editor.getEditor()), h);
editor.setBounds(lineStartGutterWidth, 0, w - lineStartGutterWidth, h);
int lineEndGutterWidth = lineEndGutter.getPreferredSize().width;
lineEndGutter.setBounds(lineStartGutterWidth + (w - lineEndGutterWidth - editor.getEditor().getScrollPane().getVerticalScrollBar().getWidth()), 0, lineEndGutterWidth, h);
}
@NotNull
private EditorComponentImpl getEditorComponent() {
for (int i = getComponentCount() - 1; i >= 0; i--) {
Component component = getComponent(i);
if (component instanceof EditorComponentImpl) {
return (EditorComponentImpl)component;
}
}
throw new IllegalStateException();
}
};
layeredPane.add(lineStartGutter, JLayeredPane.PALETTE_LAYER);
JScrollPane scrollPane = editor.getScrollPane();
layeredPane.add(scrollPane.getViewport().getView(), JLayeredPane.DEFAULT_LAYER);
layeredPane.add(lineEndGutter, JLayeredPane.PALETTE_LAYER);
scrollPane.setViewportView(layeredPane);
GutterUpdateScheduler gutterUpdateScheduler = new GutterUpdateScheduler(lineStartGutter, lineEndGutter);
getProject().getMessageBus().connect(this).subscribe(DocumentBulkUpdateListener.TOPIC, gutterUpdateScheduler);
editor.getDocument().addDocumentListener(gutterUpdateScheduler);
}
@Override
protected void doAddPromptToHistory() {
gutterContentProvider.beforeEvaluate(getHistoryViewer());
}
private final class GutterUpdateScheduler extends DocumentAdapter implements DocumentBulkUpdateListener {
private final ConsoleGutterComponent lineStartGutter;
private final ConsoleGutterComponent lineEndGutter;
private Task gutterSizeUpdater;
private RangeHighlighter lineSeparatorPainter;
private final CustomHighlighterRenderer renderer = new CustomHighlighterRenderer() {
@Override
public void paint(@NotNull Editor editor, @NotNull RangeHighlighter highlighter, @NotNull Graphics g) {
Rectangle clip = g.getClipBounds();
int lineHeight = editor.getLineHeight();
int startLine = clip.y / lineHeight;
int endLine = Math.min(((clip.y + clip.height) / lineHeight) + 1, ((EditorImpl)editor).getVisibleLineCount());
if (startLine >= endLine) {
return;
}
// workaround - editor ask us to paint line 4-6, but we should draw line for line 3 (startLine - 1) also, otherwise it will be not rendered
int actualStartLine = startLine == 0 ? 0 : startLine - 1;
int y = (actualStartLine + 1) * lineHeight;
g.setColor(editor.getColorsScheme().getColor(EditorColors.INDENT_GUIDE_COLOR));
for (int visualLine = actualStartLine; visualLine < endLine; visualLine++) {
if (gutterContentProvider.isShowSeparatorLine(editor.visualToLogicalPosition(new VisualPosition(visualLine, 0)).line, editor)) {
g.drawLine(clip.x, y, clip.x + clip.width, y);
}
y += lineHeight;
}
}
};
public GutterUpdateScheduler(@NotNull ConsoleGutterComponent lineStartGutter, @NotNull ConsoleGutterComponent lineEndGutter) {
this.lineStartGutter = lineStartGutter;
this.lineEndGutter = lineEndGutter;
// console view can invoke markupModel.removeAllHighlighters(), so, we must be aware of it
getHistoryViewer().getMarkupModel().addMarkupModelListener(GutteredLanguageConsole.this, new MarkupModelListener.Adapter() {
@Override
public void beforeRemoved(@NotNull RangeHighlighterEx highlighter) {
if (lineSeparatorPainter == highlighter) {
lineSeparatorPainter = null;
}
}
});
}
private void addLineSeparatorPainterIfNeed() {
if (lineSeparatorPainter != null) {
return;
}
RangeHighlighter highlighter = getHistoryViewer().getMarkupModel().addRangeHighlighter(0, getDocument().getTextLength(), HighlighterLayer.ADDITIONAL_SYNTAX, null, HighlighterTargetArea.EXACT_RANGE);
highlighter.setGreedyToRight(true);
highlighter.setCustomRenderer(renderer);
lineSeparatorPainter = highlighter;
}
private DocumentEx getDocument() {
return getHistoryViewer().getDocument();
}
@Override
public void documentChanged(DocumentEvent event) {
DocumentEx document = getDocument();
if (document.isInBulkUpdate()) {
return;
}
if (document.getTextLength() > 0) {
addLineSeparatorPainterIfNeed();
int startDocLine = document.getLineNumber(event.getOffset());
int endDocLine = document.getLineNumber(event.getOffset() + event.getNewLength());
if (event.getOldLength() > event.getNewLength() || startDocLine != endDocLine || StringUtil.indexOf(event.getOldFragment(), '\n') != -1) {
updateGutterSize(startDocLine, endDocLine);
}
}
else if (event.getOldLength() > 0) {
documentCleared();
}
}
private void documentCleared() {
gutterSizeUpdater = null;
lineEndGutter.documentCleared();
gutterContentProvider.documentCleared(getHistoryViewer());
}
@Override
public void updateStarted(@NotNull Document document) {
}
@Override
public void updateFinished(@NotNull Document document) {
if (getDocument().getTextLength() == 0) {
documentCleared();
}
else {
addLineSeparatorPainterIfNeed();
updateGutterSize(0, Integer.MAX_VALUE);
}
}
private void updateGutterSize(int start, int end) {
if (gutterSizeUpdater != null) {
gutterSizeUpdater.start = Math.min(start, gutterSizeUpdater.start);
gutterSizeUpdater.end = Math.max(end, gutterSizeUpdater.end);
return;
}
gutterSizeUpdater = new Task(start, end);
SwingUtilities.invokeLater(gutterSizeUpdater);
}
private final class Task implements Runnable {
private int start;
private int end;
public Task(int start, int end) {
this.start = start;
this.end = end;
}
@Override
public void run() {
if (!getHistoryViewer().isDisposed()) {
lineStartGutter.updateSize(start, end);
lineEndGutter.updateSize(start, end);
}
gutterSizeUpdater = null;
}
}
}
}
}