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;
+import org.eclipse.jface.resource.FontDescriptor;
+import org.eclipse.jface.resource.FontRegistry;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.resource.LocalResourceManager;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.window.DefaultToolTip;
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.VerifyKeyListener;
-import org.eclipse.swt.events.ModifyEvent;
-import org.eclipse.swt.events.ModifyListener;
+import org.eclipse.swt.custom.StyledTextContent;
import org.eclipse.swt.events.VerifyEvent;
import org.eclipse.swt.events.VerifyListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
+import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
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
*/
public abstract class AbstractCommandConsole extends Composite {
- public static final String PLUGIN_ID = "org.simantics.scl.ui";
+ /**
+ * Use this option mask to hide and disable the console input field.
+ */
+ public static final int HIDE_INPUT = 1 << 0;
+
+ public static final String PLUGIN_ID = "org.simantics.scl.ui"; //$NON-NLS-1$
public static final int COMMAND_HISTORY_SIZE = 50;
public static final int SASH_HEIGHT = 3;
LocalResourceManager resourceManager;
-
- StyledText output;
+
+ protected final int options;
+
+ StyledText output;
Sash sash;
+ StyledText deco;
protected StyledText input;
int userInputHeight=0;
protected Color greenColor;
protected Color redColor;
-
+
+ FontRegistry fontRegistry;
+ FontDescriptor textFontDescriptor;
+ Font textFont;
+
ArrayList<String> commandHistory = new ArrayList<String>();
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;
*/
-
- public AbstractCommandConsole(Composite parent, int style) {
- super(parent, style);
+
+ public AbstractCommandConsole(Composite parent, int style, int options) {
+ super(parent, style);
+ this.options = options;
createControl();
}
-
+
@Override
public boolean setFocus() {
- return input.setFocus();
+ return input != null ? input.setFocus() : output.setFocus();
}
-
+
protected boolean canExecuteCommand() {
return true;
}
+ protected boolean hasOption(int mask) {
+ return (options & mask) != 0;
+ }
+
private void createControl() {
resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
greenColor = resourceManager.createColor(new RGB(0, 128, 0));
redColor = resourceManager.createColor(new RGB(172, 0, 0));
-
+
+ // Initialize current text font
+ fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
+ fontRegistry.addListener(fontRegistryListener);
+ FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") ); //$NON-NLS-1$
+ setTextFont(font);
+
setLayout(new FormLayout());
-
- Font textFont = new Font(getDisplay(),"Courier New",12,SWT.NONE);
// Sash
sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
// Upper
output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
output.setFont(textFont);
- {
- FormData formData = new FormData();
- formData.top = new FormAttachment(0);
- formData.bottom = new FormAttachment(sash);
- formData.left = new FormAttachment(0);
- formData.right = new FormAttachment(100);
- output.setLayoutData(formData);
- }
+ output.setLayoutData( formData(0, sash, 0, 100) );
output.addVerifyListener(new VerifyListener() {
@Override
public void verifyText(VerifyEvent e) {
if(outputModiLock)
return;
- input.append(e.text);
- input.setFocus();
- input.setCaretOffset(input.getText().length());
+ if (input != null) {
+ input.append(e.text);
+ input.setFocus();
+ input.setCaretOffset(input.getText().length());
+ }
e.doit = false;
}
});
- // Deco
- StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
+ if (hasOption(HIDE_INPUT)) {
+ sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
+ layout(true);
+ } else {
+ createInputArea();
+ }
+
+ readPreferences();
+
+ addListener(SWT.Dispose, event -> {
+ if (fontRegistry != null)
+ fontRegistry.removeListener(fontRegistryListener);
+ if (preferences != null)
+ preferences.removePreferenceChangeListener(preferenceListener);
+ try {
+ writePreferences();
+ } catch (IOException e) {
+ getLogger().error("Failed to store command history in preferences", e);
+ }
+ });
+ }
+
+ protected void createInputArea() {
+ deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
deco.setFont(textFont);
deco.setEnabled(false);
GC gc = new GC(deco);
int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
- deco.setText(">");
- {
- FormData formData = new FormData();
- formData.top = new FormAttachment(sash);
- formData.bottom = new FormAttachment(100);
- formData.left = new FormAttachment(0);
- formData.right = new FormAttachment(0, inputLeftPos);
- deco.setLayoutData(formData);
- }
-
- // Input area
- input = new StyledText(this, SWT.MULTI);
+ gc.dispose();
+ 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);
- {
- FormData formData = new FormData();
- formData.top = new FormAttachment(sash);
- formData.bottom = new FormAttachment(100);
- formData.left = new FormAttachment(0, inputLeftPos);
- formData.right = new FormAttachment(100);
- input.setLayoutData(formData);
- }
- adjustInputSize("");
- input.addVerifyKeyListener(new VerifyKeyListener() {
-
- @Override
- public void verifyKey(VerifyEvent event) {
- switch(event.keyCode) {
- case SWT.KEYPAD_CR:
- case SWT.CR:
- if((event.stateMask & SWT.CTRL) == 0) {
- if(canExecuteCommand())
- execute();
- event.doit = false;
+ input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
+ adjustInputSize(""); //$NON-NLS-1$
+ input.addVerifyKeyListener(event -> {
+ switch(event.keyCode) {
+ case SWT.KEYPAD_CR:
+ case SWT.CR:
+ if((event.stateMask & SWT.CTRL) == 0) {
+ if(canExecuteCommand())
+ execute();
+ event.doit = false;
+ }
+ break;
+ case SWT.ARROW_UP:
+ case SWT.ARROW_DOWN:
+ if((event.stateMask & SWT.CTRL) != 0) {
+ int targetHistoryPos = commandHistoryPos;
+ if(event.keyCode == SWT.ARROW_UP) {
+ if(commandHistoryPos <= 0)
+ return;
+ --targetHistoryPos;
}
- break;
- case SWT.ARROW_UP:
- case SWT.ARROW_DOWN:
- if((event.stateMask & SWT.CTRL) != 0) {
- int targetHistoryPos = commandHistoryPos;
- if(event.keyCode == SWT.ARROW_UP) {
- if(commandHistoryPos <= 0)
- return;
- --targetHistoryPos;
- }
- else {
- if(commandHistoryPos >= commandHistory.size()-1)
- return;
- ++targetHistoryPos;
- }
- setInputText(commandHistory.get(targetHistoryPos));
- commandHistoryPos = targetHistoryPos;
- event.doit = false;
+ else {
+ if(commandHistoryPos >= commandHistory.size()-1)
+ return;
+ ++targetHistoryPos;
}
- break;
-// case SWT.ESC:
-// setInputText("");
-// commandHistoryPos = commandHistory.size();
-// break;
- }
- }
-
- });
- input.addVerifyListener(new VerifyListener() {
- @Override
- public void verifyText(VerifyEvent e) {
- if(e.text.contains("\n")) {
- 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(" ");
- ++indentAmount);
- StringBuilder indent = new StringBuilder();
- indent.append('\n');
- for(int i=0;i<indentAmount;++i)
- indent.append(' ');
- e.text = e.text.replace("\n", indent);
+ setInputText(commandHistory.get(targetHistoryPos));
+ commandHistoryPos = targetHistoryPos;
+ event.doit = false;
}
+ break;
+// case SWT.ESC:
+// setInputText("");
+// commandHistoryPos = commandHistory.size();
+// break;
}
});
- input.addModifyListener(new ModifyListener() {
- @Override
- public void modifyText(ModifyEvent e) {
- adjustInputSize(input.getText());
- commandHistoryPos = commandHistory.size();
- asyncValidate();
+ input.addVerifyListener(e -> {
+ 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(" "); //$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); //$NON-NLS-1$
}
});
+ input.addModifyListener(e -> {
+ adjustInputSize(input.getText());
+ commandHistoryPos = commandHistory.size();
+ //asyncValidate();
+ });
Listener hoverListener = new Listener() {
DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
toolTipVisible = false;
}
return;
- }
- }
+ }
+ }
};
input.addListener(SWT.MouseHover, hoverListener);
input.addListener(SWT.MouseMove, hoverListener);
input.addListener(SWT.MouseExit, hoverListener);
-
- readPreferences();
-
- addListener(SWT.Dispose, new Listener() {
-
- @Override
- public void handleEvent(Event event) {
- try {
- writePreferences();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- });
-
}
-
+
+ private FormData formData(Object top, Object bottom, Object left, Object right) {
+ return formData(top, bottom, left, right, null);
+ }
+
+ private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
+ FormData d = new FormData();
+ d.top = formAttachment(top);
+ d.bottom = formAttachment(bottom);
+ d.left = formAttachment(left);
+ d.right = formAttachment(right);
+ d.height = height != null ? (Integer) height : SWT.DEFAULT;
+ return d;
+ }
+
+ private FormAttachment formAttachment(Object o) {
+ if (o == null)
+ return null;
+ if (o instanceof Control)
+ return new FormAttachment((Control) o);
+ if (o instanceof Integer)
+ return new FormAttachment((Integer) o);
+ if (o instanceof Tuple2) {
+ Tuple2 t = (Tuple2) o;
+ return new FormAttachment((Integer) t.c0, (Integer) t.c1);
+ }
+ throw new IllegalArgumentException("argument not supported: " + o); //$NON-NLS-1$
+ }
+
private int getOffsetInInput(int x, int y) {
int offset;
try {
}
public void setInputText(String text) {
+ if (input == null)
+ return;
input.setText(text);
input.setCaretOffset(text.length());
adjustInputSize(text);
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) {
};
- 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()) {
- input.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if(!input.isDisposed()) {
- validatedText = input.getText();
- validationJob.setPriority(Job.BUILD);
- validationJob.schedule();
- }
- }
- });
+ input.getDisplay().asyncExec(() -> {
+ if(!input.isDisposed()) {
+ validatedText = input.getText();
+ validationJob.setPriority(Job.BUILD);
+ validationJob.schedule();
+ }
+ });
}
return Status.OK_STATUS;
}
private void setInputHeight(int inputHeight) {
- FormData formData = new FormData();
- formData.top = new FormAttachment(100, -inputHeight);
- formData.left = new FormAttachment(0);
- formData.right = new FormAttachment(100);
- formData.height = SASH_HEIGHT;
- sash.setLayoutData(formData);
+ sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
AbstractCommandConsole.this.layout(true);
}
+ private StringBuilder outputBuffer = new StringBuilder();
+ private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
+ private AtomicBoolean outputScheduled = new AtomicBoolean(false);
+
public void appendOutput(final String text, final Color foreground, final Color background) {
- final Display display = Display.getDefault();
- if(display.isDisposed()) return;
- display.asyncExec(new Runnable() {
- @Override
- public void run() {
+ boolean scheduleOutput = false;
+ synchronized (outputBuffer) {
+ styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
+ outputBuffer.append(text);
+ scheduleOutput = outputScheduled.compareAndSet(false, true);
+ }
+ if(scheduleOutput) {
+ final Display display = Display.getDefault();
+ if(display.isDisposed()) return;
+ display.asyncExec(() -> {
if(output.isDisposed()) return;
- int pos = output.getCharCount();
- outputModiLock = true;
- output.replaceTextRange(pos, 0, text);
- outputModiLock = false;
- output.setStyleRange(new StyleRange(pos, text.length(),
- foreground, background));
+ String outputText;
+ StyleRange[] styleRangeArray;
+ synchronized(outputBuffer) {
+ outputScheduled.set(false);
+
+ outputText = outputBuffer.toString();
+ outputBuffer = new StringBuilder();
+
+ styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
+ styleRanges.clear();
+ }
+
+ 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 += insertPos;
+ output.setStyleRange(styleRange);
+ }
+
output.setCaretOffset(output.getCharCount());
output.showSelection();
- }
-
- });
+ });
+ }
}
-
- private void execute() {
+
+ private void execute() {
String command = input.getText().trim();
if(command.isEmpty())
return;
// Print it into output area
//appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
- input.setText("");
+ input.setText(""); //$NON-NLS-1$
// Execute
execute(command);
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$
}
}
}
private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
if(input.isDisposed())
return;
- input.getDisplay().asyncExec(new Runnable() {
- @Override
- public void run() {
- if(input.isDisposed())
- return;
- if(!input.getText().equals(forCommand))
- return;
- syncSetErrorAnnotations(forCommand, annotations);
- }
+ input.getDisplay().asyncExec(() -> {
+ if(input.isDisposed())
+ return;
+ if(!input.getText().equals(forCommand))
+ return;
+ syncSetErrorAnnotations(forCommand, annotations);
});
}
-
+
private boolean readPreferences() {
IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
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;
}
public void clear() {
outputModiLock = true;
- output.setText("");
+ output.setText(""); //$NON-NLS-1$
outputModiLock = false;
}
return output;
}
+ IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) );
+ }
+ };
+
+ private void setTextFont(FontDescriptor font) {
+ FontDescriptor oldFontDesc = textFontDescriptor;
+ textFont = resourceManager.createFont(font);
+ textFontDescriptor = font;
+ applyTextFont(textFont);
+
+ // Only destroy old font after the new font has been set!
+ if (oldFontDesc != null)
+ resourceManager.destroyFont(oldFontDesc);
+ }
+
+ private void applyTextFont(Font font) {
+ if (output != null)
+ output.setFont(font);
+ if (deco != null)
+ deco.setFont(font);
+ if (input != null) {
+ input.setFont(font);
+ adjustInputSize(input.getText());
+ }
+ }
+
+ public abstract Logger getLogger();
}