1 package org.simantics.scl.ui.console;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Arrays;
6 import java.util.Deque;
7 import java.util.concurrent.atomic.AtomicBoolean;
9 import org.eclipse.core.runtime.IProgressMonitor;
10 import org.eclipse.core.runtime.IStatus;
11 import org.eclipse.core.runtime.Status;
12 import org.eclipse.core.runtime.jobs.Job;
13 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
14 import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
15 import org.eclipse.core.runtime.preferences.InstanceScope;
16 import org.eclipse.jface.preference.IPersistentPreferenceStore;
17 import org.eclipse.jface.preference.IPreferenceStore;
18 import org.eclipse.jface.resource.FontDescriptor;
19 import org.eclipse.jface.resource.FontRegistry;
20 import org.eclipse.jface.resource.JFaceResources;
21 import org.eclipse.jface.resource.LocalResourceManager;
22 import org.eclipse.jface.util.IPropertyChangeListener;
23 import org.eclipse.jface.util.PropertyChangeEvent;
24 import org.eclipse.jface.window.DefaultToolTip;
25 import org.eclipse.jface.window.ToolTip;
26 import org.eclipse.swt.SWT;
27 import org.eclipse.swt.custom.StyleRange;
28 import org.eclipse.swt.custom.StyledText;
29 import org.eclipse.swt.custom.StyledTextContent;
30 import org.eclipse.swt.events.VerifyEvent;
31 import org.eclipse.swt.events.VerifyListener;
32 import org.eclipse.swt.graphics.Color;
33 import org.eclipse.swt.graphics.Font;
34 import org.eclipse.swt.graphics.FontData;
35 import org.eclipse.swt.graphics.GC;
36 import org.eclipse.swt.graphics.Point;
37 import org.eclipse.swt.graphics.RGB;
38 import org.eclipse.swt.graphics.Rectangle;
39 import org.eclipse.swt.layout.FormAttachment;
40 import org.eclipse.swt.layout.FormData;
41 import org.eclipse.swt.layout.FormLayout;
42 import org.eclipse.swt.widgets.Composite;
43 import org.eclipse.swt.widgets.Control;
44 import org.eclipse.swt.widgets.Display;
45 import org.eclipse.swt.widgets.Event;
46 import org.eclipse.swt.widgets.Listener;
47 import org.eclipse.swt.widgets.Sash;
48 import org.eclipse.ui.PlatformUI;
49 import org.eclipse.ui.preferences.ScopedPreferenceStore;
50 import org.simantics.scl.runtime.tuple.Tuple2;
51 import org.slf4j.Logger;
54 * A console with input and output area that can be embedded
55 * into any editor or view.
56 * @author Hannu Niemistö
58 public abstract class AbstractCommandConsole extends Composite {
61 * Use this option mask to hide and disable the console input field.
63 public static final int HIDE_INPUT = 1 << 0;
65 public static final String PLUGIN_ID = "org.simantics.scl.ui"; //$NON-NLS-1$
67 public static final int COMMAND_HISTORY_SIZE = 50;
69 public static final int SASH_HEIGHT = 3;
71 LocalResourceManager resourceManager;
73 protected final int options;
78 protected StyledText input;
80 int userInputHeight=0;
83 protected Color greenColor;
84 protected Color redColor;
86 FontRegistry fontRegistry;
87 FontDescriptor textFontDescriptor;
90 ArrayList<String> commandHistory = new ArrayList<String>();
91 int commandHistoryPos = 0;
93 boolean outputModiLock = false;
95 boolean limitConsoleOutput;
98 * The amount of buffered characters to adjust {@link #output} to when trimming
99 * the console buffer after its length has exceeded {@link #highWatermark}.
103 * The maximum amount of buffered characters allowed in {@link #output} before
104 * the buffer is pruned to under {@link #lowWatermark} characters.
109 * The console preference scope listened to.
111 IEclipsePreferences preferences;
114 * The console preference listener.
116 IPreferenceChangeListener preferenceListener = e -> {
117 String k = e.getKey();
118 if (Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT.equals(k)) {
119 limitConsoleOutput = preferences.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT, Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT_DEFAULT);
120 } else if (Preferences.CONSOLE_LOW_WATER_MARK.equals(k)) {
121 lowWatermark = preferences.getInt(Preferences.CONSOLE_LOW_WATER_MARK, Preferences.CONSOLE_LOW_WATER_MARK_DEFAULT_VALUE);
122 } else if (Preferences.CONSOLE_HIGH_WATER_MARK.equals(k)) {
123 highWatermark = preferences.getInt(Preferences.CONSOLE_HIGH_WATER_MARK, Preferences.CONSOLE_HIGH_WATER_MARK_DEFAULT_VALUE);
132 public AbstractCommandConsole(Composite parent, int style, int options) {
133 super(parent, style);
134 this.options = options;
139 public boolean setFocus() {
140 return input != null ? input.setFocus() : output.setFocus();
143 protected boolean canExecuteCommand() {
147 protected boolean hasOption(int mask) {
148 return (options & mask) != 0;
151 private void createControl() {
152 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
153 greenColor = resourceManager.createColor(new RGB(0, 128, 0));
154 redColor = resourceManager.createColor(new RGB(172, 0, 0));
156 // Initialize current text font
157 fontRegistry = PlatformUI.getWorkbench().getThemeManager().getCurrentTheme().getFontRegistry();
158 fontRegistry.addListener(fontRegistryListener);
159 FontDescriptor font = FontDescriptor.createFrom( fontRegistry.getFontData("org.simantics.scl.consolefont") ); //$NON-NLS-1$
162 setLayout(new FormLayout());
165 sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
166 sash.addListener(SWT.Selection, new Listener () {
167 public void handleEvent(Event e) {
168 Rectangle bounds = AbstractCommandConsole.this.getBounds();
169 int max = bounds.y + bounds.height;
171 userInputHeight = max-e.y;
173 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
174 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
175 setInputHeight(actualInputHeight);
180 output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
181 output.setFont(textFont);
182 output.setLayoutData( formData(0, sash, 0, 100) );
183 output.addVerifyListener(new VerifyListener() {
185 public void verifyText(VerifyEvent e) {
189 input.append(e.text);
191 input.setCaretOffset(input.getText().length());
197 if (hasOption(HIDE_INPUT)) {
198 sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
206 addListener(SWT.Dispose, event -> {
207 if (fontRegistry != null)
208 fontRegistry.removeListener(fontRegistryListener);
209 if (preferences != null)
210 preferences.removePreferenceChangeListener(preferenceListener);
213 } catch (IOException e) {
214 getLogger().error("Failed to store command history in preferences", e);
219 protected void createInputArea() {
220 deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
221 deco.setFont(textFont);
222 deco.setEnabled(false);
223 GC gc = new GC(deco);
224 int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
226 deco.setText(">"); //$NON-NLS-1$
227 deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
230 input = new StyledText(this, SWT.MULTI);
231 input.setFont(textFont);
232 input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
233 adjustInputSize(""); //$NON-NLS-1$
234 input.addVerifyKeyListener(event -> {
235 switch(event.keyCode) {
238 if((event.stateMask & SWT.CTRL) == 0) {
239 if(canExecuteCommand())
246 if((event.stateMask & SWT.CTRL) != 0) {
247 int targetHistoryPos = commandHistoryPos;
248 if(event.keyCode == SWT.ARROW_UP) {
249 if(commandHistoryPos <= 0)
254 if(commandHistoryPos >= commandHistory.size()-1)
258 setInputText(commandHistory.get(targetHistoryPos));
259 commandHistoryPos = targetHistoryPos;
265 // commandHistoryPos = commandHistory.size();
269 input.addVerifyListener(e -> {
270 if(e.text.contains("\n")) { //$NON-NLS-1$
271 int lineId = input.getLineAtOffset(e.start);
272 int lineOffset = input.getOffsetAtLine(lineId);
275 lineOffset+indentAmount < input.getCharCount() &&
276 input.getTextRange(lineOffset+indentAmount, 1).equals(" "); //$NON-NLS-1$
278 StringBuilder indent = new StringBuilder();
280 for(int i=0;i<indentAmount;++i)
282 e.text = e.text.replace("\n", indent); //$NON-NLS-1$
285 input.addModifyListener(e -> {
286 adjustInputSize(input.getText());
287 commandHistoryPos = commandHistory.size();
290 Listener hoverListener = new Listener() {
292 DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
295 boolean toolTipVisible = false;
298 public void handleEvent(Event e) {
300 case SWT.MouseHover: {
301 int offset = getOffsetInInput(e.x, e.y);
305 min = Integer.MIN_VALUE;
306 max = Integer.MAX_VALUE;
307 StringBuilder description = new StringBuilder();
308 boolean first = true;
309 for(ErrorAnnotation annotation : errorAnnotations) {
310 if(annotation.start <= offset && annotation.end > offset) {
311 min = Math.max(min, annotation.start);
312 max = Math.max(min, annotation.end);
316 description.append('\n');
317 description.append(annotation.description);
321 if(min != Integer.MIN_VALUE) {
322 Rectangle bounds = input.getTextBounds(min, max-1);
323 toolTip.setText(description.toString());
324 toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
325 toolTipVisible = true;
331 int offset = getOffsetInInput(e.x, e.y);
332 if(offset < min || offset >= max) {
334 toolTipVisible = false;
342 toolTipVisible = false;
348 input.addListener(SWT.MouseHover, hoverListener);
349 input.addListener(SWT.MouseMove, hoverListener);
350 input.addListener(SWT.MouseExit, hoverListener);
353 private FormData formData(Object top, Object bottom, Object left, Object right) {
354 return formData(top, bottom, left, right, null);
357 private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
358 FormData d = new FormData();
359 d.top = formAttachment(top);
360 d.bottom = formAttachment(bottom);
361 d.left = formAttachment(left);
362 d.right = formAttachment(right);
363 d.height = height != null ? (Integer) height : SWT.DEFAULT;
367 private FormAttachment formAttachment(Object o) {
370 if (o instanceof Control)
371 return new FormAttachment((Control) o);
372 if (o instanceof Integer)
373 return new FormAttachment((Integer) o);
374 if (o instanceof Tuple2) {
375 Tuple2 t = (Tuple2) o;
376 return new FormAttachment((Integer) t.c0, (Integer) t.c1);
378 throw new IllegalArgumentException("argument not supported: " + o); //$NON-NLS-1$
381 private int getOffsetInInput(int x, int y) {
384 offset = input.getOffsetAtLocation(new Point(x, y));
385 } catch(IllegalArgumentException e) {
388 if(offset == input.getText().length())
390 else if(offset > 0) {
391 Rectangle rect = input.getTextBounds(offset, offset);
392 if(!rect.contains(x, y))
398 public void setInputText(String text) {
402 input.setCaretOffset(text.length());
403 adjustInputSize(text);
406 String validatedText;
408 Job validationJob = new Job("SCL input validation") { //$NON-NLS-1$
411 protected IStatus run(IProgressMonitor monitor) {
412 String text = validatedText;
413 asyncSetErrorAnnotations(text, validate(text));
414 return Status.OK_STATUS;
419 Job preValidationJob = new Job("SCL input validation") { //$NON-NLS-1$
421 protected IStatus run(IProgressMonitor monitor) {
422 if(!input.isDisposed()) {
423 input.getDisplay().asyncExec(() -> {
424 if(!input.isDisposed()) {
425 validatedText = input.getText();
426 validationJob.setPriority(Job.BUILD);
427 validationJob.schedule();
432 return Status.OK_STATUS;
436 private void asyncValidate() {
437 if(!input.getText().equals(errorAnnotationsForCommand)) {
438 preValidationJob.cancel();
439 preValidationJob.setPriority(Job.BUILD);
440 preValidationJob.schedule(500);
444 private static int rowCount(String text) {
446 for(int i=0;i<text.length();++i)
447 if(text.charAt(i)=='\n')
452 private void adjustInputSize(String text) {
453 int lineHeight = input.getLineHeight();
454 int height = rowCount(text)*lineHeight+SASH_HEIGHT;
455 if(height != minInputHeight) {
456 minInputHeight = height;
457 setInputHeight(Math.max(minInputHeight, userInputHeight));
461 private void setInputHeight(int inputHeight) {
462 sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
463 AbstractCommandConsole.this.layout(true);
466 private StringBuilder outputBuffer = new StringBuilder();
467 private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
468 private AtomicBoolean outputScheduled = new AtomicBoolean(false);
470 public void appendOutput(final String text, final Color foreground, final Color background) {
471 boolean scheduleOutput = false;
472 synchronized (outputBuffer) {
473 styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
474 outputBuffer.append(text);
475 scheduleOutput = outputScheduled.compareAndSet(false, true);
478 final Display display = Display.getDefault();
479 if(display.isDisposed()) return;
480 display.asyncExec(() -> {
481 if(output.isDisposed()) return;
483 StyleRange[] styleRangeArray;
484 synchronized(outputBuffer) {
485 outputScheduled.set(false);
487 outputText = outputBuffer.toString();
488 outputBuffer = new StringBuilder();
490 styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
494 int addedLength = outputText.length();
495 int currentLength = output.getCharCount();
496 int insertPos = currentLength;
497 int newLength = insertPos + addedLength;
499 if (limitConsoleOutput && newLength > highWatermark) {
500 // Test for corner case: buffer overflows and more text is incoming than fits low watermark
501 if (addedLength > lowWatermark) {
502 // Prune the new input text first if it is too large to fit in the buffer even on its own to be < lowWatermark
503 int removedCharacters = addedLength - lowWatermark;
505 outputText = outputText.substring(removedCharacters);
506 addedLength = outputText.length();
507 newLength = insertPos + addedLength;
509 // Prune new incoming style ranges also
510 int firstStyleRangeToCopy = 0;
511 for (int i = 0; i < styleRangeArray.length; ++i, ++firstStyleRangeToCopy) {
512 StyleRange sr = styleRangeArray[i];
513 if ((sr.start + sr.length) > removedCharacters) {
514 if (sr.start < removedCharacters)
515 sr.start = removedCharacters;
519 styleRangeArray = Arrays.copyOfRange(styleRangeArray, firstStyleRangeToCopy, styleRangeArray.length);
520 for (StyleRange sr : styleRangeArray)
521 sr.start -= removedCharacters;
524 int minimallyRemoveFromBegin = Math.min(currentLength, newLength - lowWatermark);
526 // Find the next line change to prune the text until then
527 StyledTextContent content = output.getContent();
528 int lineCount = content.getLineCount();
529 int lastRemovedLine = content.getLineAtOffset(minimallyRemoveFromBegin);
530 int removeUntilOffset = lastRemovedLine >= (lineCount-1)
532 : content.getOffsetAtLine(lastRemovedLine + 1);
534 insertPos -= removeUntilOffset;
536 outputModiLock = true;
537 output.replaceTextRange(0, removeUntilOffset, "");
538 output.replaceTextRange(insertPos, 0, outputText);
539 outputModiLock = false;
541 // Buffer does not need to be pruned, just append at end
542 outputModiLock = true;
543 output.replaceTextRange(insertPos, 0, outputText);
544 outputModiLock = false;
547 for (StyleRange styleRange : styleRangeArray) {
548 styleRange.start += insertPos;
549 output.setStyleRange(styleRange);
552 output.setCaretOffset(output.getCharCount());
553 output.showSelection();
558 private void execute() {
559 String command = input.getText().trim();
560 if(command.isEmpty())
563 // Add command to command history
564 if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
565 commandHistory.add(command);
566 if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
567 commandHistory = new ArrayList<String>(
568 commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
570 commandHistoryPos = commandHistory.size();
572 // Print it into output area
573 //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
574 input.setText(""); //$NON-NLS-1$
580 public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
582 String errorAnnotationsForCommand;
583 ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
585 private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
586 errorAnnotationsForCommand = forCommand;
587 errorAnnotations = annotations;
590 StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
591 input.setStyleRange(clearRange);
594 for(int i=0;i<annotations.length;++i) {
595 ErrorAnnotation annotation = annotations[i];
596 StyleRange range = new StyleRange(
598 annotation.end-annotation.start,
602 range.underline = true;
603 range.underlineColor = redColor;
604 range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
606 input.setStyleRange(range);
607 } catch(IllegalArgumentException e) {
610 input.setStyleRange(range);
611 getLogger().error("The following error message didn't have a proper location: {}", annotation.description, e); //$NON-NLS-1$
616 private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
617 if(input.isDisposed())
619 input.getDisplay().asyncExec(() -> {
620 if(input.isDisposed())
622 if(!input.getText().equals(forCommand))
624 syncSetErrorAnnotations(forCommand, annotations);
628 private boolean readPreferences() {
630 IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
632 String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
633 Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
635 commandHistory = new ArrayList<String>(recentImportPaths);
636 commandHistoryPos = commandHistory.size();
638 limitConsoleOutput = store.getBoolean(Preferences.CONSOLE_LIMIT_CONSOLE_OUTPUT);
639 lowWatermark = store.getInt(Preferences.CONSOLE_LOW_WATER_MARK);
640 highWatermark = store.getInt(Preferences.CONSOLE_HIGH_WATER_MARK);
642 preferences = InstanceScope.INSTANCE.getNode(PLUGIN_ID);
643 preferences.addPreferenceChangeListener(preferenceListener);
648 private void writePreferences() throws IOException {
650 IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
652 store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
654 if (store.needsSaving())
659 public abstract void execute(String command);
660 public abstract ErrorAnnotation[] validate(String command);
662 public void clear() {
663 outputModiLock = true;
664 output.setText(""); //$NON-NLS-1$
665 outputModiLock = false;
668 public StyledText getOutputWidget() {
672 IPropertyChangeListener fontRegistryListener = new IPropertyChangeListener() {
674 public void propertyChange(PropertyChangeEvent event) {
675 setTextFont( FontDescriptor.createFrom((FontData[]) event.getNewValue()) );
679 private void setTextFont(FontDescriptor font) {
680 FontDescriptor oldFontDesc = textFontDescriptor;
681 textFont = resourceManager.createFont(font);
682 textFontDescriptor = font;
683 applyTextFont(textFont);
685 // Only destroy old font after the new font has been set!
686 if (oldFontDesc != null)
687 resourceManager.destroyFont(oldFontDesc);
690 private void applyTextFont(Font font) {
692 output.setFont(font);
697 adjustInputSize(input.getText());
701 public abstract Logger getLogger();