/******************************************************************************* * Copyright (c) 2002, 2010 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * QNX Software Systems - initial API and implementation * Dmitry Kozlov (CodeSourcery) - Build error highlighting and navigation * Andrew Gvozdev (Quoin Inc.) - Copy build log (bug 306222) * Alex Collins (Broadcom Corp.) - Global console *******************************************************************************/ package org.eclipse.cdt.internal.ui.buildconsole; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Vector; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IMarker; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentPartitioner; import org.eclipse.jface.text.IDocumentPartitionerExtension; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.ITypedRegion; import org.eclipse.jface.text.Region; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.swt.widgets.Display; import org.eclipse.ui.console.ConsolePlugin; import org.eclipse.cdt.core.ConsoleOutputStream; import org.eclipse.cdt.core.ProblemMarkerInfo; import org.eclipse.cdt.core.resources.IConsole; import org.eclipse.cdt.core.resources.ResourcesUtil; import org.eclipse.cdt.ui.CUIPlugin; import org.eclipse.cdt.internal.ui.preferences.BuildConsolePreferencePage; public class BuildConsolePartitioner implements IDocumentPartitioner, IDocumentPartitionerExtension, IConsole, IPropertyChangeListener { private IProject fProject; /** * List of partitions */ List<ITypedRegion> fPartitions = new ArrayList<ITypedRegion>(5); private int fMaxLines; /** * The stream that was last appended to */ BuildConsoleStreamDecorator fLastStream = null; BuildConsoleDocument fDocument; DocumentMarkerManager fDocumentMarkerManager; boolean killed; BuildConsoleManager fManager; /** * A queue of stream entries written to standard out and standard err. * Entries appended to the end of the queue and removed from the front. * Intentionally a vector to obtain synchronization as entries are added and * removed. */ Vector<StreamEntry> fQueue = new Vector<StreamEntry>(5); private URI fLogURI; private OutputStream fLogStream; private class StreamEntry { static public final int EVENT_APPEND = 0; static public final int EVENT_OPEN_LOG = 1; static public final int EVENT_CLOSE_LOG = 2; static public final int EVENT_OPEN_APPEND_LOG = 3; /** Identifier of the stream written to. */ private BuildConsoleStreamDecorator fStream; /** The text written */ private StringBuffer fText = null; /** Problem marker corresponding to the line of text */ private ProblemMarkerInfo fMarker; /** Type of event **/ private int eventType; public StreamEntry(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { fText = new StringBuffer(text); fStream = stream; fMarker = marker; eventType = EVENT_APPEND; } /** * This constructor is used for special events such as clear console or close log. * * @param event - kind of event. */ public StreamEntry(int event) { fText = null; fStream = null; fMarker = null; eventType = event; } /** * Returns the stream identifier */ public BuildConsoleStreamDecorator getStream() { return fStream; } public void appendText(String text) { fText.append(text); } public int size() { return fText.length(); } /** * Returns the text written */ public String getText() { return fText.toString(); } /** * Returns error marker */ public ProblemMarkerInfo getMarker() { return fMarker; } /** * Returns type of event */ public int getEventType() { return eventType; } } /** * Construct a partitioner that is not associated with a specific project */ public BuildConsolePartitioner(BuildConsoleManager manager) { this(null, manager); } public BuildConsolePartitioner(IProject project, BuildConsoleManager manager) { fProject = project; fManager = manager; fMaxLines = BuildConsolePreferencePage.buildConsoleLines(); fDocument = new BuildConsoleDocument(); fDocument.setDocumentPartitioner(this); fDocumentMarkerManager = new DocumentMarkerManager(fDocument, this); connect(fDocument); fLogURI = null; fLogStream = null; } /** * Sets the indicator that stream was opened so logging can be started. Should be called * when opening the output stream. */ public void setStreamOpened() { fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_LOG)); asyncProcessQueue(); } /** * Open the stream for appending. Must be called after a call to setStreamOpened(). * Can be used to reopen a stream for writing after it has been closed, without * emptying the log file. */ public void setStreamAppend() { fQueue.add(new StreamEntry(StreamEntry.EVENT_OPEN_APPEND_LOG)); asyncProcessQueue(); } /** * Sets the indicator that stream was closed so logging should be stopped. Should be called when * build process has finished. Note that there could still be unprocessed console * stream entries in the queue being worked on in the background. */ public void setStreamClosed() { fQueue.add(new StreamEntry(StreamEntry.EVENT_CLOSE_LOG)); asyncProcessQueue(); } /** * Adds the new text to the document. * * @param text - the text to append. * @param stream - the stream to append to. */ public void appendToDocument(String text, BuildConsoleStreamDecorator stream, ProblemMarkerInfo marker) { boolean addToQueue = true; synchronized (fQueue) { int i = fQueue.size(); if (i > 0) { StreamEntry entry = fQueue.get(i - 1); // if last stream is the same and we have not exceeded our // display write limit, append. if (entry.getStream()==stream && entry.getEventType()==StreamEntry.EVENT_APPEND && entry.getMarker()==marker && entry.size()<10000) { entry.appendText(text); addToQueue = false; } } if (addToQueue) { fQueue.add(new StreamEntry(text, stream, marker)); } } if (addToQueue) { asyncProcessQueue(); } } /** * Asynchronous processing of stream entries to append to console. * Note that all these are processed by the same thread - the user-interface thread * as of {@link Display#asyncExec(Runnable)}. */ private void asyncProcessQueue() { Runnable r = new Runnable() { public void run() { StreamEntry entry; try { entry = fQueue.remove(0); } catch (ArrayIndexOutOfBoundsException e) { return; } switch (entry.getEventType()) { case StreamEntry.EVENT_OPEN_LOG: case StreamEntry.EVENT_OPEN_APPEND_LOG: logOpen(entry.getEventType() == StreamEntry.EVENT_OPEN_APPEND_LOG); break; case StreamEntry.EVENT_APPEND: fLastStream = entry.getStream(); try { warnOfContentChange(fLastStream); if (fLastStream == null) { // special case to empty document fPartitions.clear(); fDocumentMarkerManager.clear(); fDocument.set(""); //$NON-NLS-1$ } String text = entry.getText(); if (text.length()>0) { addStreamEntryToDocument(entry); log(text); checkOverflow(); } } catch (BadLocationException e) { } break; case StreamEntry.EVENT_CLOSE_LOG: logClose(); break; } } /** * Open the log * @param append Set to true if the log should be opened for appending, false for overwriting. */ private void logOpen(boolean append) { fLogURI = fManager.getLogURI(fProject); if (fLogURI!=null) { try { IFileStore logStore = EFS.getStore(fLogURI); // Ensure the directory exists before opening the file IFileStore dir = logStore.getParent(); if (dir != null) dir.mkdir(EFS.NONE, null); int opts = append ? EFS.APPEND : EFS.NONE; fLogStream = logStore.openOutputStream(opts, null); } catch (CoreException e) { CUIPlugin.log(e); } finally { ResourcesUtil.refreshWorkspaceFiles(fLogURI); } } } private void log(String text) { if (fLogStream!=null) { try { fLogStream.write(text.getBytes()); if (fQueue.isEmpty()) { fLogStream.flush(); } } catch (IOException e) { CUIPlugin.log(e); } finally { ResourcesUtil.refreshWorkspaceFiles(fLogURI); } } } private void logClose() { if (fLogStream!=null) { try { fLogStream.close(); } catch (IOException e) { CUIPlugin.log(e); } finally { ResourcesUtil.refreshWorkspaceFiles(fLogURI); } fLogStream = null; } } }; Display display = CUIPlugin.getStandardDisplay(); if (display != null) { display.asyncExec(r); } } private void addStreamEntryToDocument(StreamEntry entry) throws BadLocationException { ProblemMarkerInfo marker = entry.getMarker(); if (marker==null) { // It is plain unmarkered console output addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(), entry.getText().length(), BuildConsolePartition.CONSOLE_PARTITION_TYPE)); } else { // this text line in entry is markered with ProblemMarkerInfo, // create special partition for it. String errorPartitionType; if (marker.severity==IMarker.SEVERITY_INFO) { errorPartitionType = BuildConsolePartition.INFO_PARTITION_TYPE; } else if (marker.severity==IMarker.SEVERITY_WARNING) { errorPartitionType = BuildConsolePartition.WARNING_PARTITION_TYPE; } else { errorPartitionType = BuildConsolePartition.ERROR_PARTITION_TYPE; } addPartition(new BuildConsolePartition(fLastStream, fDocument.getLength(), entry.getText().length(), errorPartitionType, marker)); } fDocument.replace(fDocument.getLength(), 0, entry.getText()); } void warnOfContentChange(BuildConsoleStreamDecorator stream) { if (stream != null) { ConsolePlugin.getDefault().getConsoleManager().warnOfContentChange(stream.getConsole()); } fManager.showConsole(); } public IDocument getDocument() { return fDocument; } public void setDocumentSize(int nLines) { fMaxLines = nLines; nLines = fDocument.getNumberOfLines(); checkOverflow(); } public void connect(IDocument document) { CUIPlugin.getDefault().getPreferenceStore().addPropertyChangeListener(this); } public void disconnect() { fDocument.setDocumentPartitioner(null); CUIPlugin.getDefault().getPreferenceStore().removePropertyChangeListener(this); killed = true; } public void documentAboutToBeChanged(DocumentEvent event) { } public boolean documentChanged(DocumentEvent event) { return documentChanged2(event) != null; } /** * @see org.eclipse.jface.text.IDocumentPartitioner#getLegalContentTypes() */ public String[] getLegalContentTypes() { return new String[]{BuildConsolePartition.CONSOLE_PARTITION_TYPE}; } /** * @see org.eclipse.jface.text.IDocumentPartitioner#getContentType(int) */ public String getContentType(int offset) { ITypedRegion partition = getPartition(offset); if (partition != null) { return partition.getType(); } return null; } /** * @see org.eclipse.jface.text.IDocumentPartitioner#computePartitioning(int, * int) */ public ITypedRegion[] computePartitioning(int offset, int length) { if (offset == 0 && length == fDocument.getLength()) { return fPartitions.toArray(new ITypedRegion[fPartitions.size()]); } int end = offset + length; List<ITypedRegion> list = new ArrayList<ITypedRegion>(); for (int i = 0; i < fPartitions.size(); i++) { ITypedRegion partition = fPartitions.get(i); int partitionStart = partition.getOffset(); int partitionEnd = partitionStart + partition.getLength(); if ( (offset >= partitionStart && offset <= partitionEnd) || (offset < partitionStart && end >= partitionStart)) { list.add(partition); } } return list.toArray(new ITypedRegion[list.size()]); } /** * @see org.eclipse.jface.text.IDocumentPartitioner#getPartition(int) */ public ITypedRegion getPartition(int offset) { for (int i = 0; i < fPartitions.size(); i++) { ITypedRegion partition = fPartitions.get(i); int start = partition.getOffset(); int end = start + partition.getLength(); if (offset >= start && offset < end) { return partition; } } return null; } public IRegion documentChanged2(DocumentEvent event) { String text = event.getText(); if (getDocument().getLength() == 0) { // cleared fPartitions.clear(); return new Region(0, 0); } ITypedRegion[] affectedRegions = computePartitioning(event.getOffset(), text.length()); if (affectedRegions.length == 0) { return null; } if (affectedRegions.length == 1) { return affectedRegions[0]; } int affectedLength = affectedRegions[0].getLength(); for (int i = 1; i < affectedRegions.length; i++) { ITypedRegion region = affectedRegions[i]; affectedLength += region.getLength(); } return new Region(affectedRegions[0].getOffset(), affectedLength); } /** * Checks to see if the console buffer has overflowed, and empties the * overflow if needed, updating partitions and hyperlink positions. */ protected void checkOverflow() { if (fMaxLines >= 0) { int nLines = fDocument.getNumberOfLines(); if (nLines > fMaxLines + 1) { int overflow = 0; try { overflow = fDocument.getLineOffset(nLines - fMaxLines); } catch (BadLocationException e1) { } // update partitions List<ITypedRegion> newParitions = new ArrayList<ITypedRegion>(fPartitions.size()); Iterator<ITypedRegion> partitions = fPartitions.iterator(); while (partitions.hasNext()) { ITypedRegion region = partitions.next(); if (region instanceof BuildConsolePartition) { BuildConsolePartition messageConsolePartition = (BuildConsolePartition)region; ITypedRegion newPartition = null; int offset = region.getOffset(); String type = messageConsolePartition.getType(); if (offset < overflow) { int endOffset = offset + region.getLength(); if (endOffset < overflow || BuildConsolePartition.isProblemPartitionType(type)) { // remove partition, // partitions with problem markers can't be split - remove them too } else { // split partition int length = endOffset - overflow; newPartition = messageConsolePartition.createNewPartition(0, length, type); } } else { // modify partition offset offset = messageConsolePartition.getOffset() - overflow; newPartition = messageConsolePartition.createNewPartition(offset, messageConsolePartition.getLength(), type); } if (newPartition != null) { newParitions.add(newPartition); } } } fPartitions = newParitions; fDocumentMarkerManager.moveToFirstError(); try { fDocument.replace(0, overflow, ""); //$NON-NLS-1$ } catch (BadLocationException e) { } } } } /** * Adds a new partition, combining with the previous partition if possible. */ private BuildConsolePartition addPartition(BuildConsolePartition partition) { if (fPartitions.isEmpty()) { fPartitions.add(partition); } else { int index = fPartitions.size() - 1; BuildConsolePartition last = (BuildConsolePartition)fPartitions.get(index); if (last.canBeCombinedWith(partition)) { // replace with a single partition partition = last.combineWith(partition); fPartitions.set(index, partition); } else { // different kinds - add a new parition fPartitions.add(partition); } } return partition; } public IConsole getConsole() { return this; } public void propertyChange(PropertyChangeEvent event) { if (event.getProperty() == BuildConsolePreferencePage.PREF_BUILDCONSOLE_LINES) { setDocumentSize(BuildConsolePreferencePage.buildConsoleLines()); } } public void start(final IProject project) { Display display = CUIPlugin.getStandardDisplay(); if (display != null) { display.asyncExec(new Runnable() { public void run() { fLogStream = null; fLogURI = null; fManager.startConsoleActivity(project); } }); } if (BuildConsolePreferencePage.isClearBuildConsole()) { appendToDocument("", null, null); //$NON-NLS-1$ } } public ConsoleOutputStream getOutputStream() throws CoreException { return new BuildOutputStream(this, fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_OUTPUT)); } public ConsoleOutputStream getInfoStream() throws CoreException { return new BuildOutputStream(this, fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_INFO)); } public ConsoleOutputStream getErrorStream() throws CoreException { return new BuildOutputStream(this, fManager.getStreamDecorator(BuildConsoleManager.BUILD_STREAM_TYPE_ERROR)); } /** This method is useful for future debugging and bug-fixing */ @SuppressWarnings({ "unused", "nls" }) private void printDocumentPartitioning() { System.out.println("Document partitioning: "); for (ITypedRegion tr : fPartitions) { BuildConsolePartition p = (BuildConsolePartition) tr; int start = p.getOffset(); int end = p.getOffset() + p.getLength(); String text; String isError = "U"; String type = p.getType(); if (type == BuildConsolePartition.ERROR_PARTITION_TYPE) { isError = "E"; } else if (type == BuildConsolePartition.WARNING_PARTITION_TYPE) { isError = "W"; } else if (type == BuildConsolePartition.INFO_PARTITION_TYPE) { isError = "I"; } else if (type == BuildConsolePartition.CONSOLE_PARTITION_TYPE) { isError = "C"; } try { text = fDocument.get(p.getOffset(), p.getLength()); } catch (BadLocationException e) { text = "N/A"; } if (text.endsWith("\n")) { text = text.substring(0, text.length() - 1); } System.out.println(" " + isError + " " + start + "-" + end + ":[" + text + "]"); } } /** * @return {@link URI} location of log file. */ public URI getLogURI() { return fLogURI; } IProject getProject() { return fProject; } }