]> gerrit.simantics Code Review - simantics/platform.git/blobdiff - bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java
Limit SCL Console buffer size to 5M characters by default
[simantics/platform.git] / bundles / org.simantics.scl.ui / src / org / simantics / scl / ui / console / AbstractCommandConsole.java
index b0019688386fcbbf8f7d019dad052630c75410d0..a8931ecffa22dbf57be5054b60022b2cbb624a55 100644 (file)
@@ -2,12 +2,16 @@ package org.simantics.scl.ui.console;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Deque;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.core.runtime.IProgressMonitor;
 import org.eclipse.core.runtime.IStatus;
 import org.eclipse.core.runtime.Status;
 import org.eclipse.core.runtime.jobs.Job;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
 import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.jface.preference.IPersistentPreferenceStore;
 import org.eclipse.jface.preference.IPreferenceStore;
@@ -22,6 +26,7 @@ import org.eclipse.jface.window.ToolTip;
 import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.StyleRange;
 import org.eclipse.swt.custom.StyledText;
+import org.eclipse.swt.custom.StyledTextContent;
 import org.eclipse.swt.events.VerifyEvent;
 import org.eclipse.swt.events.VerifyListener;
 import org.eclipse.swt.graphics.Color;
@@ -67,7 +72,7 @@ public abstract class AbstractCommandConsole extends Composite {
 
     protected final int options;
 
-    StyledText output;
+       StyledText output;
     Sash sash;
     StyledText deco;
     protected StyledText input;
@@ -86,7 +91,39 @@ public abstract class AbstractCommandConsole extends Composite {
     int commandHistoryPos = 0;
     
     boolean outputModiLock = false;
-    
+
+    boolean limitConsoleOutput;
+
+    /**
+     * The amount of buffered characters to adjust {@link #output} to when trimming
+     * the console buffer after its length has exceeded {@link #highWatermark}.
+     */
+    int lowWatermark;
+    /**
+     * The maximum amount of buffered characters allowed in {@link #output} before
+     * the buffer is pruned to under {@link #lowWatermark} characters.
+     */
+    int highWatermark;
+
+    /**
+     * The console preference scope listened to.
+     */
+    IEclipsePreferences preferences;
+
+    /**
+     * The console preference listener.
+     */
+    IPreferenceChangeListener preferenceListener = e -> {
+        String k = e.getKey();
+        if (Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT.equals(k)) {
+            limitConsoleOutput = preferences.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT, Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT_DEFAULT);
+        } else if (Preferences.CONSOLE_LOW_WATER_MARK.equals(k)) {
+            lowWatermark = preferences.getInt(Preferences.CONSOLE_LOW_WATER_MARK, Preferences.CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE);
+        } else if (Preferences.CONSOLE_HIGH_WATER_MARK.equals(k)) {
+            highWatermark = preferences.getInt(Preferences.CONSOLE_HIGH_WATER_MARK, Preferences.CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE);
+        }
+    };
+
     /*
     Shell tip = null;
     Label label = null;
@@ -169,10 +206,12 @@ public abstract class AbstractCommandConsole extends Composite {
         addListener(SWT.Dispose, event -> {
             if (fontRegistry != null)
                 fontRegistry.removeListener(fontRegistryListener);
+            if (preferences != null) 
+                preferences.removePreferenceChangeListener(preferenceListener);
             try {
                 writePreferences();
             } catch (IOException e) {
-                e.printStackTrace();
+                getLogger().error("Failed to store command history in preferences", e);
             }
         });
     }
@@ -426,15 +465,16 @@ public abstract class AbstractCommandConsole extends Composite {
 
     private StringBuilder outputBuffer = new StringBuilder();
     private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
-    private volatile boolean outputScheduled = false;
+    private AtomicBoolean outputScheduled = new AtomicBoolean(false);
 
     public void appendOutput(final String text, final Color foreground, final Color background) {
+        boolean scheduleOutput = false;
         synchronized (outputBuffer) {
             styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
             outputBuffer.append(text);
+            scheduleOutput = outputScheduled.compareAndSet(false, true);
         }
-        if(!outputScheduled) {
-            outputScheduled = true;
+        if(scheduleOutput) {
             final Display display = Display.getDefault();
             if(display.isDisposed()) return;
             display.asyncExec(() -> {
@@ -442,7 +482,7 @@ public abstract class AbstractCommandConsole extends Composite {
                 String outputText;
                 StyleRange[] styleRangeArray;
                 synchronized(outputBuffer) {
-                    outputScheduled = false;
+                    outputScheduled.set(false);
 
                     outputText = outputBuffer.toString();
                     outputBuffer = new StringBuilder();
@@ -450,14 +490,62 @@ public abstract class AbstractCommandConsole extends Composite {
                     styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
                     styleRanges.clear();
                 }
-                int pos = output.getCharCount();
 
-                outputModiLock = true;
-                output.replaceTextRange(pos, 0, outputText);
-                outputModiLock = false;
+                int addedLength = outputText.length();
+                int currentLength = output.getCharCount();
+                int insertPos = currentLength;
+                int newLength = insertPos + addedLength;
+
+                if (limitConsoleOutput && newLength > highWatermark) {
+                    // Test for corner case: buffer overflows and more text is incoming than fits low watermark
+                    if (addedLength > lowWatermark) {
+                        // Prune the new input text first if it is too large to fit in the buffer even on its own to be < lowWatermark
+                        int removedCharacters = addedLength - lowWatermark;
+
+                        outputText = outputText.substring(removedCharacters);
+                        addedLength = outputText.length();
+                        newLength = insertPos + addedLength;
+
+                        // Prune new incoming style ranges also
+                        int firstStyleRangeToCopy = 0;
+                        for (int i = 0; i < styleRangeArray.length; ++i, ++firstStyleRangeToCopy) {
+                            StyleRange sr = styleRangeArray[i];
+                            if ((sr.start + sr.length) > removedCharacters) {
+                                if (sr.start < removedCharacters)
+                                    sr.start = removedCharacters;
+                                break;
+                            }
+                        }
+                        styleRangeArray = Arrays.copyOfRange(styleRangeArray, firstStyleRangeToCopy, styleRangeArray.length);
+                        for (StyleRange sr : styleRangeArray)
+                            sr.start -= removedCharacters;
+                    }
+
+                    int minimallyRemoveFromBegin = Math.min(currentLength, newLength - lowWatermark);
+
+                    // Find the next line change to prune the text until then
+                    StyledTextContent content = output.getContent();
+                    int lineCount = content.getLineCount();
+                    int lastRemovedLine = content.getLineAtOffset(minimallyRemoveFromBegin);
+                    int removeUntilOffset = lastRemovedLine >= (lineCount-1)
+                            ? currentLength
+                            : content.getOffsetAtLine(lastRemovedLine + 1);
+
+                    insertPos -= removeUntilOffset;
+
+                    outputModiLock = true;
+                    output.replaceTextRange(0, removeUntilOffset, "");
+                    output.replaceTextRange(insertPos, 0, outputText);
+                    outputModiLock = false;
+                } else {
+                    // Buffer does not need to be pruned, just append at end
+                    outputModiLock = true;
+                    output.replaceTextRange(insertPos, 0, outputText);
+                    outputModiLock = false;
+                }
 
-                for(StyleRange styleRange : styleRangeArray) {
-                    styleRange.start += pos;
+                for (StyleRange styleRange : styleRangeArray) {
+                    styleRange.start += insertPos;
                     output.setStyleRange(styleRange);
                 }
 
@@ -536,7 +624,7 @@ public abstract class AbstractCommandConsole extends Composite {
             syncSetErrorAnnotations(forCommand, annotations);
         });
     }
-    
+
     private boolean readPreferences() {
         
         IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
@@ -547,6 +635,13 @@ public abstract class AbstractCommandConsole extends Composite {
         commandHistory = new ArrayList<String>(recentImportPaths);
         commandHistoryPos = commandHistory.size();
 
+        limitConsoleOutput = store.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT);
+        lowWatermark = store.getInt(Preferences.CONSOLE_LOW_WATER_MARK);
+        highWatermark = store.getInt(Preferences.CONSOLE_HIGH_WATER_MARK);
+
+        preferences = InstanceScope.INSTANCE.getNode(PLUGIN_ID);
+        preferences.addPreferenceChangeListener(preferenceListener);
+
         return true;
     }