From 501ad95ad5ca980ef4c6e65af1451a0d7b63cddc Mon Sep 17 00:00:00 2001 From: Tuukka Lehtonen Date: Fri, 20 Dec 2019 13:15:56 +0200 Subject: [PATCH] Limit SCL Console buffer size to 5M characters by default Low watermark limit for the buffer size can be configured from the new SCL / Console preference page. High watermark is always set to 100 80 character rows larger than the low watermark. Buffer size limiting can also be disabled/enabled entirely from the same preference page. gitlab #104 Change-Id: I8be203e60ae49fce53c39a4d251d2575a64b2543 --- bundles/org.simantics.scl.ui/plugin.xml | 20 ++ .../ui/console/AbstractCommandConsole.java | 123 ++++++++++-- .../simantics/scl/ui/console/Messages.java | 4 + .../simantics/scl/ui/console/Preferences.java | 33 +++ .../SCLConsolePreferenceInitializer.java | 25 +++ .../ui/console/SCLConsolePreferencePage.java | 188 ++++++++++++++++++ .../scl/ui/console/messages.properties | 4 + .../scl/ui/preference/SCLPreferencePage.java | 38 ++++ 8 files changed, 421 insertions(+), 14 deletions(-) create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferenceInitializer.java create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferencePage.java create mode 100644 bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/preference/SCLPreferencePage.java diff --git a/bundles/org.simantics.scl.ui/plugin.xml b/bundles/org.simantics.scl.ui/plugin.xml index bb69f55bf..b7d5c9fb2 100644 --- a/bundles/org.simantics.scl.ui/plugin.xml +++ b/bundles/org.simantics.scl.ui/plugin.xml @@ -272,4 +272,24 @@ searchResultClass="org.simantics.scl.ui.search.SCLSearchResult"> + + + + + + + + + + diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java index b00196883..a8931ecff 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/AbstractCommandConsole.java @@ -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 styleRanges = new ArrayList(); - 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(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; } diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Messages.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Messages.java index ecbb2be22..b83ff3e53 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Messages.java +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Messages.java @@ -6,6 +6,10 @@ public class Messages extends NLS { private static final String BUNDLE_NAME = "org.simantics.scl.ui.console.messages"; //$NON-NLS-1$ public static String ConsoleActions_ClearConsole; public static String ConsoleActions_InterruptCurrentCommand; + public static String SCLConsolePreferencePage_Description; + public static String SCLConsolePreferencePage_Limit_console_output_Label; + public static String SCLConsolePreferencePage_Console_buffer_size_Label; + public static String SCLConsolePreferencePage_Console_buffer_size_Error; public static String SCLConsoleView_ManageImports; public static String SCLConsoleView_RefreshAutomatically; public static String SCLConsoleView_RefreshCompleted; diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Preferences.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Preferences.java index 1d55d1fe3..6b733146b 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Preferences.java +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/Preferences.java @@ -12,6 +12,39 @@ import java.util.TreeSet; */ public final class Preferences { + /** + * Console buffer high and low water marks + */ + public static final String CONSOLE_LIMIT_CONSOLE_OUTPUT = "SCLConsole.limitConsoleOutput"; //$NON-NLS-1$ + + public static final String CONSOLE_LOW_WATER_MARK = "SCLConsole.lowWaterMark"; //$NON-NLS-1$ + + public static final String CONSOLE_HIGH_WATER_MARK = "SCLConsole.highWaterMark"; //$NON-NLS-1$ + + /** + * By default console output buffer size is limited. + */ + public static final boolean CONSOLE_LIMIT_CONSOLE_OUTPUT_DEFAULT = true; + + /** + * The console low water mark default value {@value #CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE}. + */ + public static final int CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE = 5000000; + + /** + * The console low water mark default value {@value #CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE}. + */ + public static final int CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE = highWatermarkForLow(CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE); + + /** + * The console low water mark maximum value {@value #CONSOLE_LOW_WATER_MARK_MAX_VALUE}. + */ + public static final int CONSOLE_LOW_WATER_MARK_MAX_VALUE = 10000000; + + public static int highWatermarkForLow(int low) { + return low + 80*100; + } + public static final String COMMAND_HISTORY = "COMMAND_HISTORY"; //$NON-NLS-1$ private static final String DELIMITER = "¤¤¤¤"; //$NON-NLS-1$ diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferenceInitializer.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferenceInitializer.java new file mode 100644 index 000000000..fb804eb6a --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferenceInitializer.java @@ -0,0 +1,25 @@ +package org.simantics.scl.ui.console; + +import org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer; +import org.eclipse.jface.preference.IPreferenceStore; +import org.simantics.scl.ui.Activator; + +/** + * @author Tuukka Lehtonen + * @since 1.42.0 + */ +public class SCLConsolePreferenceInitializer extends AbstractPreferenceInitializer { + + /* (non-Javadoc) + * @see org.eclipse.core.runtime.preferences.AbstractPreferenceInitializer#initializeDefaultPreferences() + */ + @Override + public void initializeDefaultPreferences() { + IPreferenceStore prefs = Activator.getInstance().getPreferenceStore(); + + prefs.setDefault(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT, Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT_DEFAULT); + prefs.setDefault(Preferences.CONSOLE_LOW_WATER_MARK, Preferences.CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE); + prefs.setDefault(Preferences.CONSOLE_HIGH_WATER_MARK, Preferences.CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE); + } + +} diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferencePage.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferencePage.java new file mode 100644 index 000000000..3f1f3073d --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/SCLConsolePreferencePage.java @@ -0,0 +1,188 @@ +package org.simantics.scl.ui.console; + +import org.eclipse.jface.preference.BooleanFieldEditor; +import org.eclipse.jface.preference.FieldEditor; +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.preference.IntegerFieldEditor; +import org.eclipse.jface.util.PropertyChangeEvent; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.simantics.scl.ui.Activator; + +/** + * A page to set the preferences for the SCL console. + * + * @author Tuukka Lehtonen + * @since 1.42.0 + * @see SCLConsolePreferenceConstants + */ +public class SCLConsolePreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + /** + * A boolean field editor that provides access to this editors boolean button. + * + * Copied from + * org.eclipse.debug.internal.ui.preferences.BooleanFieldEditor2. + */ + public class BooleanFieldEditor2 extends BooleanFieldEditor { + private Button fChangeControl; + + public BooleanFieldEditor2( + String name, + String labelText, + int style, + Composite parent) { + super(name, labelText, style, parent); + } + + @Override + public Button getChangeControl(Composite parent) { + if (fChangeControl == null) { + fChangeControl = super.getChangeControl(parent); + } + return fChangeControl; + } + } + + /** + * This class exists to provide visibility to the refreshValidState + * method and to perform more intelligent clearing of the error message. + */ + protected class ConsoleIntegerFieldEditor extends IntegerFieldEditor { + + public ConsoleIntegerFieldEditor(String name, String labelText, Composite parent) { + super(name, labelText, parent); + } + + /** + * @see org.eclipse.jface.preference.FieldEditor#refreshValidState() + */ + @Override + protected void refreshValidState() { + super.refreshValidState(); + } + + /** + * Clears the error message from the message line if the error + * message is the error message from this field editor. + */ + @Override + protected void clearErrorMessage() { + if (canClearErrorMessage()) { + super.clearErrorMessage(); + } + } + } + + private BooleanFieldEditor2 fUseBufferSize = null; + private ConsoleIntegerFieldEditor fBufferSizeEditor = null; + + /** + * Create the console page. + */ + public SCLConsolePreferencePage() { + super(GRID); + setDescription(Messages.SCLConsolePreferencePage_Description); + setPreferenceStore(Activator.getInstance().getPreferenceStore()); + } + + /** + * Create all field editors for this page + */ + @Override + public void createFieldEditors() { + fUseBufferSize = new BooleanFieldEditor2(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT, Messages.SCLConsolePreferencePage_Limit_console_output_Label, SWT.NONE, getFieldEditorParent()); + addField(fUseBufferSize); + + fBufferSizeEditor = new ConsoleIntegerFieldEditor(Preferences.CONSOLE_LOW_WATER_MARK, Messages.SCLConsolePreferencePage_Console_buffer_size_Label, getFieldEditorParent()); + addField(fBufferSizeEditor); + fBufferSizeEditor.setValidRange(1000, 10000000); + fBufferSizeEditor.setErrorMessage(Messages.SCLConsolePreferencePage_Console_buffer_size_Error); + + fUseBufferSize.getChangeControl(getFieldEditorParent()).addSelectionListener( + SelectionListener.widgetSelectedAdapter(e -> updateBufferSizeEditor())); + } + + /** + * @see IWorkbenchPreferencePage#init(IWorkbench) + */ + @Override + public void init(IWorkbench workbench) { + } + + /* (non-Javadoc) + * @see org.eclipse.jface.preference.IPreferencePage#performOk() + */ + @Override + public boolean performOk() { + boolean ok = super.performOk(); + + // update high water mark to be (about) 100 lines (100 * 80 chars) greater than low water mark + IPreferenceStore store = Activator.getInstance().getPreferenceStore(); + int low = store.getInt(Preferences.CONSOLE_LOW_WATER_MARK); + store.setValue(Preferences.CONSOLE_HIGH_WATER_MARK, Preferences.highWatermarkForLow(low)); + + return ok; + } + + /** + * @see org.eclipse.jface.preference.FieldEditorPreferencePage#initialize() + */ + @Override + protected void initialize() { + super.initialize(); + updateBufferSizeEditor(); + } + + /** + * Update enablement of buffer size editor based on enablement of 'limit + * console output' editor. + */ + protected void updateBufferSizeEditor() { + Button b = fUseBufferSize.getChangeControl(getFieldEditorParent()); + fBufferSizeEditor.getTextControl(getFieldEditorParent()).setEnabled(b.getSelection()); + fBufferSizeEditor.getLabelControl(getFieldEditorParent()).setEnabled(b.getSelection()); + } + + /** + * @see org.eclipse.jface.preference.PreferencePage#performDefaults() + */ + @Override + protected void performDefaults() { + super.performDefaults(); + updateBufferSizeEditor(); + } + + protected boolean canClearErrorMessage() { + return fBufferSizeEditor.isValid(); + } + + /** + * @see org.eclipse.jface.util.IPropertyChangeListener#propertyChange(org.eclipse.jface.util.PropertyChangeEvent) + */ + @Override + public void propertyChange(PropertyChangeEvent event) { + if (event.getProperty().equals(FieldEditor.IS_VALID)) { + boolean newValue = ((Boolean) event.getNewValue()).booleanValue(); + // If the new value is true then we must check all field editors. + // If it is false, then the page is invalid in any case. + if (newValue) { + if (fBufferSizeEditor != null && event.getSource() != fBufferSizeEditor) { + fBufferSizeEditor.refreshValidState(); + } + checkState(); + } else { + super.propertyChange(event); + } + + } else { + super.propertyChange(event); + } + } + +} diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/messages.properties b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/messages.properties index fad5ea9f2..429fe77b4 100644 --- a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/messages.properties +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/console/messages.properties @@ -1,5 +1,9 @@ ConsoleActions_ClearConsole=Clear Console ConsoleActions_InterruptCurrentCommand=Interrupt Current Command +SCLConsolePreferencePage_Description=SCL Console Settings +SCLConsolePreferencePage_Limit_console_output_Label=&Limit console output +SCLConsolePreferencePage_Console_buffer_size_Label=Console &buffer size (characters): +SCLConsolePreferencePage_Console_buffer_size_Error=Buffer size must be between 1000 and 10000000 inclusive. SCLConsoleView_ManageImports=Manage Imports SCLConsoleView_RefreshAutomatically=Refresh Automatically SCLConsoleView_RefreshCompleted=refresh completed\n diff --git a/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/preference/SCLPreferencePage.java b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/preference/SCLPreferencePage.java new file mode 100644 index 000000000..3883502ae --- /dev/null +++ b/bundles/org.simantics.scl.ui/src/org/simantics/scl/ui/preference/SCLPreferencePage.java @@ -0,0 +1,38 @@ +package org.simantics.scl.ui.preference; + +import org.eclipse.jface.preference.FieldEditorPreferencePage; +import org.eclipse.ui.IWorkbench; +import org.eclipse.ui.IWorkbenchPreferencePage; +import org.simantics.scl.ui.Activator; + +/** + * A so far blank root level SCL preference page. + * + * @author Tuukka Lehtonen + * @since 1.42.0 + */ +public class SCLPreferencePage extends FieldEditorPreferencePage implements IWorkbenchPreferencePage { + + /** + * Create the console page. + */ + public SCLPreferencePage() { + super(GRID); + setPreferenceStore(Activator.getInstance().getPreferenceStore()); + } + + /** + * Create all field editors for this page + */ + @Override + public void createFieldEditors() { + } + + /** + * @see IWorkbenchPreferencePage#init(IWorkbench) + */ + @Override + public void init(IWorkbench workbench) { + } + +} -- 2.47.1