]> 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 a293eaee1008c97ddda906211c2be6a240e0d130..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;
@@ -43,6 +48,7 @@ import org.eclipse.swt.widgets.Sash;
 import org.eclipse.ui.PlatformUI;
 import org.eclipse.ui.preferences.ScopedPreferenceStore;
 import org.simantics.scl.runtime.tuple.Tuple2;
+import org.slf4j.Logger;
 
 /**
  * A console with input and output area that can be embedded
@@ -56,7 +62,7 @@ public abstract class AbstractCommandConsole extends Composite {
      */
     public static final int HIDE_INPUT = 1 << 0;
 
-    public static final String PLUGIN_ID = "org.simantics.scl.ui";
+    public static final String PLUGIN_ID = "org.simantics.scl.ui"; //$NON-NLS-1$
 
     public static final int COMMAND_HISTORY_SIZE = 50;
     
@@ -66,7 +72,7 @@ public abstract class AbstractCommandConsole extends Composite {
 
     protected final int options;
 
-    StyledText output;
+       StyledText output;
     Sash sash;
     StyledText deco;
     protected StyledText input;
@@ -85,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;
@@ -118,7 +156,7 @@ public abstract class AbstractCommandConsole extends Composite {
         // Initialize current text font
         fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
         fontRegistry.addListener(fontRegistryListener);
-        FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") );
+        FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") ); //$NON-NLS-1$
         setTextFont(font);
 
         setLayout(new FormLayout());
@@ -168,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);
             }
         });
     }
@@ -183,14 +223,14 @@ public abstract class AbstractCommandConsole extends Composite {
         GC gc = new GC(deco);
         int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
         gc.dispose();
-        deco.setText(">");
+        deco.setText(">"); //$NON-NLS-1$
         deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
 
         // Input area
         input = new StyledText(this, SWT.MULTI);
         input.setFont(textFont);
         input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
-        adjustInputSize("");
+        adjustInputSize(""); //$NON-NLS-1$
         input.addVerifyKeyListener(event -> {
             switch(event.keyCode) {
             case SWT.KEYPAD_CR:
@@ -227,19 +267,19 @@ public abstract class AbstractCommandConsole extends Composite {
             }
         });
         input.addVerifyListener(e -> {
-            if(e.text.contains("\n")) {
+            if(e.text.contains("\n")) { //$NON-NLS-1$
                 int lineId = input.getLineAtOffset(e.start);
                 int lineOffset = input.getOffsetAtLine(lineId);
                 int indentAmount;
                 for(indentAmount=0;
                         lineOffset+indentAmount < input.getCharCount() && 
-                        input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
+                        input.getTextRange(lineOffset+indentAmount, 1).equals(" "); //$NON-NLS-1$
                         ++indentAmount);
                 StringBuilder indent = new StringBuilder();
                 indent.append('\n');
                 for(int i=0;i<indentAmount;++i)
                     indent.append(' ');
-                e.text = e.text.replace("\n", indent);
+                e.text = e.text.replace("\n", indent); //$NON-NLS-1$
             }
         });
         input.addModifyListener(e -> {
@@ -335,7 +375,7 @@ public abstract class AbstractCommandConsole extends Composite {
             Tuple2 t = (Tuple2) o;
             return new FormAttachment((Integer) t.c0, (Integer) t.c1);
         }
-        throw new IllegalArgumentException("argument not supported: " + o);
+        throw new IllegalArgumentException("argument not supported: " + o); //$NON-NLS-1$
     }
 
     private int getOffsetInInput(int x, int y) {
@@ -365,7 +405,7 @@ public abstract class AbstractCommandConsole extends Composite {
     
     String validatedText;
     
-    Job validationJob = new Job("SCL input validation") {
+    Job validationJob = new Job("SCL input validation") { //$NON-NLS-1$
 
         @Override
         protected IStatus run(IProgressMonitor monitor) {
@@ -376,7 +416,7 @@ public abstract class AbstractCommandConsole extends Composite {
         
     };
     
-    Job preValidationJob = new Job("SCL input validation") {
+    Job preValidationJob = new Job("SCL input validation") { //$NON-NLS-1$
         @Override
         protected IStatus run(IProgressMonitor monitor) {
             if(!input.isDisposed()) {
@@ -425,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(() -> {
@@ -441,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();
@@ -449,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);
                 }
 
@@ -482,7 +571,7 @@ public abstract class AbstractCommandConsole extends Composite {
         
         // Print it into output area
         //appendOutput("> " + command.replace("\n", "\n  ") + "\n", greenColor, null);
-        input.setText("");
+        input.setText(""); //$NON-NLS-1$
         
         // Execute
         execute(command);
@@ -519,8 +608,7 @@ public abstract class AbstractCommandConsole extends Composite {
                 range.start = 0;
                 range.length = 1;
                 input.setStyleRange(range);
-                System.err.println("The following error message didn't have a proper location:");
-                System.err.println(annotation.description);
+                getLogger().error("The following error message didn't have a proper location: {}", annotation.description, e); //$NON-NLS-1$
             }
         }
     }
@@ -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;
     }
 
@@ -566,7 +661,7 @@ public abstract class AbstractCommandConsole extends Composite {
     
     public void clear() {
         outputModiLock = true;
-        output.setText("");
+        output.setText(""); //$NON-NLS-1$
         outputModiLock = false;
     }
 
@@ -603,4 +698,5 @@ public abstract class AbstractCommandConsole extends Composite {
         }
     }
 
+    public abstract Logger getLogger();
 }