blob: f5882d92bcb45b74b3da6c4fdd03c256f32ff6c5 [file] [log] [blame]
/*
* Copyright 2000-2009 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.
*/
package com.intellij.diagnostic.logging;
import com.intellij.execution.filters.TextConsoleBuilder;
import com.intellij.execution.filters.TextConsoleBuilderFactory;
import com.intellij.execution.impl.ConsoleBuffer;
import com.intellij.execution.impl.ConsoleViewImpl;
import com.intellij.execution.process.ProcessAdapter;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.FilterComponent;
import com.intellij.util.Alarm;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.util.List;
/**
* @author Eugene.Kudelevsky
*/
public abstract class LogConsoleBase extends AdditionalTabComponent implements LogConsole, LogFilterListener {
private static final Logger LOG = Logger.getInstance("com.intellij.diagnostic.logging.LogConsoleImpl");
@NonNls public static final String APPLYING_FILTER_TITLE = "Applying filter...";
private boolean myDisposed;
private ConsoleView myConsole;
private final LightProcessHandler myProcessHandler = new LightProcessHandler();
private ReaderThread myReaderThread;
private StringBuffer myOriginalDocument = null;
private String myLineUnderSelection = null;
private int myLineOffset = -1;
private LogContentPreprocessor myContentPreprocessor;
private final Project myProject;
private String myTitle = null;
private boolean myWasInitialized;
private final JPanel myTopComponent = new JPanel(new BorderLayout());
private ActionGroup myActions;
private final boolean myBuildInActions;
private LogFilterModel myModel;
private final List<LogConsoleListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final List<? extends LogFilter> myFilters;
private FilterComponent myFilter = new FilterComponent("LOG_FILTER_HISTORY", 5) {
@Override
public void filter() {
final Task.Backgroundable task = new Task.Backgroundable(myProject, APPLYING_FILTER_TITLE) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
myModel.updateCustomFilter(getFilter());
}
};
ProgressManager.getInstance().run(task);
}
};
private JPanel mySearchComponent;
private JComboBox myLogFilterCombo;
private JPanel myTextFilterWrapper;
public LogConsoleBase(@NotNull Project project, @Nullable Reader reader, String title, final boolean buildInActions, LogFilterModel model) {
this(project, reader, title, buildInActions, model, GlobalSearchScope.allScope(project));
}
public LogConsoleBase(@NotNull Project project, @Nullable Reader reader, String title, final boolean buildInActions, LogFilterModel model,
@NotNull GlobalSearchScope scope) {
super(new BorderLayout());
myProject = project;
myTitle = title;
myModel = model;
myFilters = myModel.getLogFilters();
myReaderThread = new ReaderThread(reader);
myBuildInActions = buildInActions;
TextConsoleBuilder builder = TextConsoleBuilderFactory.getInstance().createBuilder(project, scope);
myConsole = builder.getConsole();
myConsole.attachToProcess(myProcessHandler);
myDisposed = false;
myModel.addFilterListener(this);
}
@Override
public void setFilterModel(LogFilterModel model) {
if (myModel != null) {
myModel.removeFilterListener(this);
}
myModel = model;
myModel.addFilterListener(this);
}
@Override
public LogFilterModel getFilterModel() {
return myModel;
}
@Override
public LogContentPreprocessor getContentPreprocessor() {
return myContentPreprocessor;
}
@Override
public void setContentPreprocessor(final LogContentPreprocessor contentPreprocessor) {
myContentPreprocessor = contentPreprocessor;
}
@Nullable
protected BufferedReader updateReaderIfNeeded(@Nullable BufferedReader reader) throws IOException {
return reader;
}
@SuppressWarnings({"NonStaticInitializer"})
private JComponent createToolbar() {
String customFilter = myModel.getCustomFilter();
myFilter.reset();
myFilter.setSelectedItem(customFilter != null ? customFilter : "");
new AnAction() {
{
registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_TAB, InputEvent.SHIFT_DOWN_MASK)),
LogConsoleBase.this);
}
@Override
public void actionPerformed(final AnActionEvent e) {
myFilter.requestFocusInWindow();
}
};
if (myBuildInActions) {
final JComponent tbComp =
ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, getOrCreateActions(), true).getComponent();
myTopComponent.add(tbComp, BorderLayout.CENTER);
myTopComponent.add(getSearchComponent(), BorderLayout.EAST);
}
return myTopComponent;
}
public ActionGroup getOrCreateActions() {
if (myActions != null) return myActions;
DefaultActionGroup group = new DefaultActionGroup();
final AnAction[] actions = getConsoleNotNull().createConsoleActions();
for (AnAction action : actions) {
group.add(action);
}
group.addSeparator();
/*for (final LogFilter filter : filters) {
group.add(new ToggleAction(filter.getName(), filter.getName(), filter.getIcon()) {
public boolean isSelected(AnActionEvent e) {
return prefs.isFilterSelected(filter);
}
public void setSelected(AnActionEvent e, boolean state) {
prefs.setFilterSelected(filter, state);
}
});
}*/
myActions = group;
return myActions;
}
@Override
public void onFilterStateChange(final LogFilter filter) {
filterConsoleOutput();
}
@Override
public void onTextFilterChange() {
filterConsoleOutput();
}
@Override
@NotNull
public JComponent getComponent() {
if (!myWasInitialized) {
myWasInitialized = true;
add(getConsoleNotNull().getComponent(), BorderLayout.CENTER);
add(createToolbar(), BorderLayout.NORTH);
}
return this;
}
public abstract boolean isActive();
public void activate() {
final ReaderThread readerThread = myReaderThread;
if (readerThread == null) {
return;
}
if (isActive() && !readerThread.myRunning) {
resetLogFilter();
myFilter.setSelectedItem(myModel.getCustomFilter());
readerThread.startRunning();
ApplicationManager.getApplication().executeOnPooledThread(readerThread);
}
else if (!isActive() && readerThread.myRunning) {
readerThread.stopRunning();
}
}
public void stateChanged(final ChangeEvent e) {
activate();
}
@NotNull
@Override
public String getTabTitle() {
return myTitle;
}
@Override
public void dispose() {
myModel.removeFilterListener(this);
stopRunning(false);
synchronized (this) {
myDisposed = true;
if (myConsole != null) {
Disposer.dispose(myConsole);
myConsole = null;
}
}
if (myFilter != null) {
myFilter.dispose();
myFilter = null;
}
myOriginalDocument = null;
}
private void stopRunning(boolean checkActive) {
if (!checkActive) {
fireLoggingWillBeStopped();
}
final ReaderThread readerThread = myReaderThread;
if (readerThread != null && readerThread.myReader != null) {
if (!checkActive) {
readerThread.stopRunning();
try {
readerThread.myReader.close();
}
catch (IOException e) {
LOG.warn(e);
}
readerThread.myReader = null;
myReaderThread = null;
}
else {
try {
final BufferedReader reader = readerThread.myReader;
while (reader != null && reader.ready()) {
addMessage(reader.readLine());
}
}
catch (IOException ignore) {}
stopRunning(false);
}
}
}
protected synchronized void addMessage(final String text) {
if (text == null) return;
if (myContentPreprocessor != null) {
final List<LogFragment> fragments = myContentPreprocessor.parseLogLine(text + "\n");
myOriginalDocument = getOriginalDocument();
for (LogFragment fragment : fragments) {
myProcessHandler.notifyTextAvailable(fragment.getText(), fragment.getOutputType());
if (myOriginalDocument != null) {
myOriginalDocument.append(fragment.getText());
}
}
}
else {
final LogFilterModel.MyProcessingResult processingResult = myModel.processLine(text);
if (processingResult.isApplicable()) {
final Key key = processingResult.getKey();
if (key != null) {
final String messagePrefix = processingResult.getMessagePrefix();
if (messagePrefix != null) {
myProcessHandler.notifyTextAvailable(messagePrefix, key);
}
myProcessHandler.notifyTextAvailable(text + "\n", key);
}
}
myOriginalDocument = getOriginalDocument();
if (myOriginalDocument != null) {
myOriginalDocument.append(text).append("\n");
}
}
}
public void attachStopLogConsoleTrackingListener(final ProcessHandler process) {
if (process != null) {
final ProcessAdapter stopListener = new ProcessAdapter() {
@Override
public void processTerminated(final ProcessEvent event) {
process.removeProcessListener(this);
stopRunning(true);
}
};
process.addProcessListener(stopListener);
}
}
public StringBuffer getOriginalDocument() {
if (myOriginalDocument == null) {
final Editor editor = getEditor();
if (editor != null) {
myOriginalDocument = new StringBuffer(editor.getDocument().getText());
}
} else {
if (ConsoleBuffer.useCycleBuffer()) {
final int toRemove = myOriginalDocument.length() - ConsoleBuffer.getCycleBufferSize();
if (toRemove > 0) {
myOriginalDocument.delete(0, toRemove);
}
}
}
return myOriginalDocument;
}
@Nullable
private Editor getEditor() {
final ConsoleView console = getConsole();
return console != null ? CommonDataKeys.EDITOR.getData((DataProvider) console) : null;
}
private void filterConsoleOutput() {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
computeSelectedLineAndFilter();
}
});
}
private synchronized void computeSelectedLineAndFilter() {
// we have to do this in dispatch thread, because ConsoleViewImpl can flush something to document otherwise
myOriginalDocument = getOriginalDocument();
if (myOriginalDocument != null) {
final Editor editor = getEditor();
LOG.assertTrue(editor != null);
final Document document = editor.getDocument();
final int caretOffset = editor.getCaretModel().getOffset();
myLineUnderSelection = null;
myLineOffset = -1;
if (caretOffset > -1 && caretOffset < document.getTextLength()) {
int line;
try {
line = document.getLineNumber(caretOffset);
}
catch (IllegalStateException e) {
throw new IllegalStateException("document.length=" + document.getTextLength() + ", caret offset = " + caretOffset + "; " + e.getMessage(), e);
}
if (line > -1 && line < document.getLineCount()) {
final int startOffset = document.getLineStartOffset(line);
myLineUnderSelection = document.getText().substring(startOffset, document.getLineEndOffset(line));
myLineOffset = caretOffset - startOffset;
}
}
}
ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
@Override
public void run() {
doFilter();
}
});
}
private synchronized void doFilter() {
if (myDisposed) {
return;
}
final ConsoleView console = getConsoleNotNull();
console.clear();
myModel.processingStarted();
final String[] lines = myOriginalDocument.toString().split("\n");
int offset = 0;
boolean caretPositioned = false;
for (String line : lines) {
final int printed = printMessageToConsole(line);
if (printed > 0) {
if (!caretPositioned) {
if (Comparing.strEqual(myLineUnderSelection, line)) {
caretPositioned = true;
offset += myLineOffset != -1 ? myLineOffset : 0;
}
else {
offset += printed;
}
}
}
}
// we need this, because, document can change before actual scrolling, so offset may be already not at the end
if (caretPositioned) {
console.scrollTo(offset);
}
else {
((ConsoleViewImpl)console).requestScrollingToEnd();
}
}
private int printMessageToConsole(String line) {
final ConsoleView console = getConsoleNotNull();
if (myContentPreprocessor != null) {
List<LogFragment> fragments = myContentPreprocessor.parseLogLine(line + '\n');
for (LogFragment fragment : fragments) {
ConsoleViewContentType consoleViewType = ConsoleViewContentType.getConsoleViewType(fragment.getOutputType());
if (consoleViewType != null) {
console.print(fragment.getText(), consoleViewType);
}
}
return line.length() + 1;
}
else {
final LogFilterModel.MyProcessingResult processingResult = myModel.processLine(line);
if (processingResult.isApplicable()) {
final Key key = processingResult.getKey();
if (key != null) {
ConsoleViewContentType type = ConsoleViewContentType.getConsoleViewType(key);
if (type != null) {
final String messagePrefix = processingResult.getMessagePrefix();
if (messagePrefix != null) {
console.print(messagePrefix, type);
}
console.print(line + "\n", type);
return (messagePrefix != null ? messagePrefix.length() : 0) + line.length() + 1;
}
}
}
return 0;
}
}
@Nullable
public synchronized ConsoleView getConsole() {
return myConsole;
}
/**
* A shortcut for "getConsole()+assert console != null"
* Use this method when you are sure that console must not be null.
* If we get the assertion then it is a time to revisit logic of caller ;)
*/
@NotNull
private synchronized ConsoleView getConsoleNotNull() {
final ConsoleView console = getConsole();
assert console != null: "it looks like console has been disposed";
return console;
}
@Override
public ActionGroup getToolbarActions() {
return getOrCreateActions();
}
@Override
public String getToolbarPlace() {
return ActionPlaces.UNKNOWN;
}
@Override
@Nullable
public JComponent getToolbarContextComponent() {
final ConsoleView console = getConsole();
return console == null ? null : console.getComponent();
}
@Override
public JComponent getPreferredFocusableComponent() {
return getConsoleNotNull().getPreferredFocusableComponent();
}
public String getTitle() {
return myTitle;
}
public synchronized void clear() {
getConsoleNotNull().clear();
myOriginalDocument = null;
}
@Override
public JComponent getSearchComponent() {
myLogFilterCombo.setModel(new DefaultComboBoxModel(myFilters.toArray(new LogFilter[myFilters.size()])));
resetLogFilter();
myLogFilterCombo.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
final LogFilter filter = (LogFilter)myLogFilterCombo.getSelectedItem();
final Task.Backgroundable task = new Task.Backgroundable(myProject, APPLYING_FILTER_TITLE) {
@Override
public void run(@NotNull ProgressIndicator indicator) {
myModel.selectFilter(filter);
}
};
ProgressManager.getInstance().run(task);
}
});
myTextFilterWrapper.removeAll();
myTextFilterWrapper.add(getTextFilterComponent());
return mySearchComponent;
}
private void resetLogFilter() {
for (LogFilter filter : myFilters) {
if (myModel.isFilterSelected(filter)) {
if (myLogFilterCombo.getSelectedItem() != filter) {
myLogFilterCombo.setSelectedItem(filter);
break;
}
}
}
}
@NotNull
protected Component getTextFilterComponent() {
return myFilter;
}
@Override
public boolean isContentBuiltIn() {
return myBuildInActions;
}
public void writeToConsole(String text, Key outputType) {
myProcessHandler.notifyTextAvailable(text, outputType);
}
public void addListener(LogConsoleListener listener) {
myListeners.add(listener);
}
private void fireLoggingWillBeStopped() {
for (LogConsoleListener listener : myListeners) {
listener.loggingWillBeStopped();
}
}
private static class LightProcessHandler extends ProcessHandler {
@Override
protected void destroyProcessImpl() {
throw new UnsupportedOperationException();
}
@Override
protected void detachProcessImpl() {
throw new UnsupportedOperationException();
}
@Override
public boolean detachIsDefault() {
return false;
}
@Override
@Nullable
public OutputStream getProcessInput() {
return null;
}
}
private class ReaderThread implements Runnable {
private BufferedReader myReader;
private boolean myRunning = false;
private final Alarm myAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, LogConsoleBase.this);
public ReaderThread(@Nullable Reader reader) {
myReader = reader != null ? new BufferedReader(reader) : null;
}
@Override
public void run() {
if (myReader == null) return;
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (myRunning) {
try {
myReader = updateReaderIfNeeded(myReader);
int i = 0;
while (i++ < 1000) {
final BufferedReader reader = myReader;
if (myRunning && reader != null && reader.ready()) {
addMessage(reader.readLine());
}
else {
break;
}
}
}
catch (IOException e) {
LOG.info(e);
addMessage("I/O Error" + (e.getMessage() != null ? ": " + e.getMessage() : ""));
return;
}
}
if (myAlarm.isDisposed()) return;
myAlarm.addRequest(this, 100);
}
};
if (myAlarm.isDisposed()) return;
myAlarm.addRequest(runnable, 10);
}
public void startRunning() {
myRunning = true;
}
public void stopRunning() {
myRunning = false;
synchronized (this) {
notifyAll();
}
}
}
}