1 package org.simantics.scl.ui.console;
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.Deque;
7 import org.eclipse.core.runtime.IProgressMonitor;
8 import org.eclipse.core.runtime.IStatus;
9 import org.eclipse.core.runtime.Status;
10 import org.eclipse.core.runtime.jobs.Job;
11 import org.eclipse.core.runtime.preferences.InstanceScope;
12 import org.eclipse.jface.preference.IPersistentPreferenceStore;
13 import org.eclipse.jface.preference.IPreferenceStore;
14 import org.eclipse.jface.resource.FontDescriptor;
15 import org.eclipse.jface.resource.JFaceResources;
16 import org.eclipse.jface.resource.LocalResourceManager;
17 import org.eclipse.jface.window.DefaultToolTip;
18 import org.eclipse.jface.window.ToolTip;
19 import org.eclipse.swt.SWT;
20 import org.eclipse.swt.custom.StyleRange;
21 import org.eclipse.swt.custom.StyledText;
22 import org.eclipse.swt.events.VerifyEvent;
23 import org.eclipse.swt.events.VerifyListener;
24 import org.eclipse.swt.graphics.Color;
25 import org.eclipse.swt.graphics.Font;
26 import org.eclipse.swt.graphics.GC;
27 import org.eclipse.swt.graphics.Point;
28 import org.eclipse.swt.graphics.RGB;
29 import org.eclipse.swt.graphics.Rectangle;
30 import org.eclipse.swt.layout.FormAttachment;
31 import org.eclipse.swt.layout.FormData;
32 import org.eclipse.swt.layout.FormLayout;
33 import org.eclipse.swt.widgets.Composite;
34 import org.eclipse.swt.widgets.Control;
35 import org.eclipse.swt.widgets.Display;
36 import org.eclipse.swt.widgets.Event;
37 import org.eclipse.swt.widgets.Listener;
38 import org.eclipse.swt.widgets.Sash;
39 import org.eclipse.ui.preferences.ScopedPreferenceStore;
40 import org.simantics.scl.runtime.tuple.Tuple2;
43 * A console with input and output area that can be embedded
44 * into any editor or view.
45 * @author Hannu Niemistö
47 public abstract class AbstractCommandConsole extends Composite {
50 * Use this option mask to hide and disable the console input field.
52 public static final int HIDE_INPUT = 1 << 0;
54 public static final String PLUGIN_ID = "org.simantics.scl.ui";
56 public static final int COMMAND_HISTORY_SIZE = 50;
58 public static final int SASH_HEIGHT = 3;
60 LocalResourceManager resourceManager;
62 protected final int options;
66 protected StyledText input;
68 int userInputHeight=0;
71 protected Color greenColor;
72 protected Color redColor;
73 protected Font textFont;
75 ArrayList<String> commandHistory = new ArrayList<String>();
76 int commandHistoryPos = 0;
78 boolean outputModiLock = false;
85 public AbstractCommandConsole(Composite parent, int style, int options) {
87 this.options = options;
92 public boolean setFocus() {
93 return input != null ? input.setFocus() : output.setFocus();
96 protected boolean canExecuteCommand() {
100 private boolean hasOption(int mask) {
101 return (options & mask) != 0;
104 private void createControl() {
105 resourceManager = new LocalResourceManager(JFaceResources.getResources(), this);
106 greenColor = resourceManager.createColor(new RGB(0, 128, 0));
107 redColor = resourceManager.createColor(new RGB(172, 0, 0));
108 textFont = resourceManager.createFont( FontDescriptor.createFrom("Courier New", 12, SWT.NONE) );
110 setLayout(new FormLayout());
113 sash = new Sash(this, /*SWT.BORDER |*/ SWT.HORIZONTAL);
114 sash.addListener(SWT.Selection, new Listener () {
115 public void handleEvent(Event e) {
116 Rectangle bounds = AbstractCommandConsole.this.getBounds();
117 int max = bounds.y + bounds.height;
119 userInputHeight = max-e.y;
121 int actualInputHeight = Math.max(userInputHeight, minInputHeight);
122 sash.setBounds(e.x, max-actualInputHeight, e.width, e.height);
123 setInputHeight(actualInputHeight);
128 output = new StyledText(this, SWT.MULTI /*| SWT.READ_ONLY*/ | SWT.V_SCROLL | SWT.H_SCROLL);
129 output.setFont(textFont);
130 output.setLayoutData( formData(0, sash, 0, 100) );
131 output.addVerifyListener(new VerifyListener() {
133 public void verifyText(VerifyEvent e) {
137 input.append(e.text);
139 input.setCaretOffset(input.getText().length());
145 if (hasOption(HIDE_INPUT)) {
146 sash.setLayoutData( formData(new Tuple2(100, 0), null, 0, 100, 0) );
154 addListener(SWT.Dispose, event -> {
157 } catch (IOException e) {
163 protected void createInputArea() {
165 StyledText deco = new StyledText(this, SWT.MULTI | SWT.READ_ONLY);
166 deco.setFont(textFont);
167 deco.setEnabled(false);
168 GC gc = new GC(deco);
169 int inputLeftPos = gc.getFontMetrics().getAverageCharWidth()*2;
172 deco.setLayoutData( formData(sash, 100, 0, new Tuple2(0, inputLeftPos)) );
175 input = new StyledText(this, SWT.MULTI);
176 input.setFont(textFont);
177 input.setLayoutData( formData(sash, 100, new Tuple2(0, inputLeftPos), 100) );
179 input.addVerifyKeyListener(event -> {
180 switch(event.keyCode) {
183 if((event.stateMask & SWT.CTRL) == 0) {
184 if(canExecuteCommand())
191 if((event.stateMask & SWT.CTRL) != 0) {
192 int targetHistoryPos = commandHistoryPos;
193 if(event.keyCode == SWT.ARROW_UP) {
194 if(commandHistoryPos <= 0)
199 if(commandHistoryPos >= commandHistory.size()-1)
203 setInputText(commandHistory.get(targetHistoryPos));
204 commandHistoryPos = targetHistoryPos;
210 // commandHistoryPos = commandHistory.size();
214 input.addVerifyListener(e -> {
215 if(e.text.contains("\n")) {
216 int lineId = input.getLineAtOffset(e.start);
217 int lineOffset = input.getOffsetAtLine(lineId);
220 lineOffset+indentAmount < input.getCharCount() &&
221 input.getTextRange(lineOffset+indentAmount, 1).equals(" ");
223 StringBuilder indent = new StringBuilder();
225 for(int i=0;i<indentAmount;++i)
227 e.text = e.text.replace("\n", indent);
230 input.addModifyListener(e -> {
231 adjustInputSize(input.getText());
232 commandHistoryPos = commandHistory.size();
235 Listener hoverListener = new Listener() {
237 DefaultToolTip toolTip = new DefaultToolTip(input, ToolTip.RECREATE, true);
240 boolean toolTipVisible = false;
243 public void handleEvent(Event e) {
245 case SWT.MouseHover: {
246 int offset = getOffsetInInput(e.x, e.y);
250 min = Integer.MIN_VALUE;
251 max = Integer.MAX_VALUE;
252 StringBuilder description = new StringBuilder();
253 boolean first = true;
254 for(ErrorAnnotation annotation : errorAnnotations) {
255 if(annotation.start <= offset && annotation.end > offset) {
256 min = Math.max(min, annotation.start);
257 max = Math.max(min, annotation.end);
261 description.append('\n');
262 description.append(annotation.description);
266 if(min != Integer.MIN_VALUE) {
267 Rectangle bounds = input.getTextBounds(min, max-1);
268 toolTip.setText(description.toString());
269 toolTip.show(new Point(bounds.x, bounds.y+bounds.height));
270 toolTipVisible = true;
276 int offset = getOffsetInInput(e.x, e.y);
277 if(offset < min || offset >= max) {
279 toolTipVisible = false;
287 toolTipVisible = false;
293 input.addListener(SWT.MouseHover, hoverListener);
294 input.addListener(SWT.MouseMove, hoverListener);
295 input.addListener(SWT.MouseExit, hoverListener);
298 private FormData formData(Object top, Object bottom, Object left, Object right) {
299 return formData(top, bottom, left, right, null);
302 private FormData formData(Object top, Object bottom, Object left, Object right, Integer height) {
303 FormData d = new FormData();
304 d.top = formAttachment(top);
305 d.bottom = formAttachment(bottom);
306 d.left = formAttachment(left);
307 d.right = formAttachment(right);
308 d.height = height != null ? (Integer) height : SWT.DEFAULT;
312 private FormAttachment formAttachment(Object o) {
315 if (o instanceof Control)
316 return new FormAttachment((Control) o);
317 if (o instanceof Integer)
318 return new FormAttachment((Integer) o);
319 if (o instanceof Tuple2) {
320 Tuple2 t = (Tuple2) o;
321 return new FormAttachment((Integer) t.c0, (Integer) t.c1);
323 throw new IllegalArgumentException("argument not supported: " + o);
326 private int getOffsetInInput(int x, int y) {
329 offset = input.getOffsetAtLocation(new Point(x, y));
330 } catch(IllegalArgumentException e) {
333 if(offset == input.getText().length())
335 else if(offset > 0) {
336 Rectangle rect = input.getTextBounds(offset, offset);
337 if(!rect.contains(x, y))
343 public void setInputText(String text) {
347 input.setCaretOffset(text.length());
348 adjustInputSize(text);
351 String validatedText;
353 Job validationJob = new Job("SCL input validation") {
356 protected IStatus run(IProgressMonitor monitor) {
357 String text = validatedText;
358 asyncSetErrorAnnotations(text, validate(text));
359 return Status.OK_STATUS;
364 Job preValidationJob = new Job("SCL input validation") {
366 protected IStatus run(IProgressMonitor monitor) {
367 if(!input.isDisposed()) {
368 input.getDisplay().asyncExec(() -> {
369 if(!input.isDisposed()) {
370 validatedText = input.getText();
371 validationJob.setPriority(Job.BUILD);
372 validationJob.schedule();
377 return Status.OK_STATUS;
381 private void asyncValidate() {
382 if(!input.getText().equals(errorAnnotationsForCommand)) {
383 preValidationJob.cancel();
384 preValidationJob.setPriority(Job.BUILD);
385 preValidationJob.schedule(500);
389 private static int rowCount(String text) {
391 for(int i=0;i<text.length();++i)
392 if(text.charAt(i)=='\n')
397 private void adjustInputSize(String text) {
398 int lineHeight = input.getLineHeight();
399 int height = rowCount(text)*lineHeight+SASH_HEIGHT;
400 if(height != minInputHeight) {
401 minInputHeight = height;
402 setInputHeight(Math.max(minInputHeight, userInputHeight));
406 private void setInputHeight(int inputHeight) {
407 sash.setLayoutData( formData(new Tuple2(100, -inputHeight), null, 0, 100, SASH_HEIGHT) );
408 AbstractCommandConsole.this.layout(true);
411 private StringBuilder outputBuffer = new StringBuilder();
412 private ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>();
413 private volatile boolean outputScheduled = false;
415 public void appendOutput(final String text, final Color foreground, final Color background) {
416 synchronized (outputBuffer) {
417 styleRanges.add(new StyleRange(outputBuffer.length(), text.length(), foreground, background));
418 outputBuffer.append(text);
420 if(!outputScheduled) {
421 outputScheduled = true;
422 final Display display = Display.getDefault();
423 if(display.isDisposed()) return;
424 display.asyncExec(() -> {
425 if(output.isDisposed()) return;
427 StyleRange[] styleRangeArray;
428 synchronized(outputBuffer) {
429 outputScheduled = false;
431 outputText = outputBuffer.toString();
432 outputBuffer = new StringBuilder();
434 styleRangeArray = styleRanges.toArray(new StyleRange[styleRanges.size()]);
437 int pos = output.getCharCount();
439 outputModiLock = true;
440 output.replaceTextRange(pos, 0, outputText);
441 outputModiLock = false;
443 for(StyleRange styleRange : styleRangeArray) {
444 styleRange.start += pos;
445 output.setStyleRange(styleRange);
448 output.setCaretOffset(output.getCharCount());
449 output.showSelection();
454 private void execute() {
455 String command = input.getText().trim();
456 if(command.isEmpty())
459 // Add command to command history
460 if(commandHistory.isEmpty() || !commandHistory.get(commandHistory.size()-1).equals(command)) {
461 commandHistory.add(command);
462 if(commandHistory.size() > COMMAND_HISTORY_SIZE*2)
463 commandHistory = new ArrayList<String>(
464 commandHistory.subList(COMMAND_HISTORY_SIZE, COMMAND_HISTORY_SIZE*2));
466 commandHistoryPos = commandHistory.size();
468 // Print it into output area
469 //appendOutput("> " + command.replace("\n", "\n ") + "\n", greenColor, null);
476 public static final ErrorAnnotation[] EMPTY_ANNOTATION_ARRAY = new ErrorAnnotation[0];
478 String errorAnnotationsForCommand;
479 ErrorAnnotation[] errorAnnotations = EMPTY_ANNOTATION_ARRAY;
481 private void syncSetErrorAnnotations(String forCommand, ErrorAnnotation[] annotations) {
482 errorAnnotationsForCommand = forCommand;
483 errorAnnotations = annotations;
486 StyleRange clearRange = new StyleRange(0, forCommand.length(), null, null);
487 input.setStyleRange(clearRange);
490 for(int i=0;i<annotations.length;++i) {
491 ErrorAnnotation annotation = annotations[i];
492 StyleRange range = new StyleRange(
494 annotation.end-annotation.start,
498 range.underline = true;
499 range.underlineColor = redColor;
500 range.underlineStyle = SWT.UNDERLINE_SQUIGGLE;
502 input.setStyleRange(range);
503 } catch(IllegalArgumentException e) {
506 input.setStyleRange(range);
507 System.err.println("The following error message didn't have a proper location:");
508 System.err.println(annotation.description);
513 private void asyncSetErrorAnnotations(final String forCommand, final ErrorAnnotation[] annotations) {
514 if(input.isDisposed())
516 input.getDisplay().asyncExec(new Runnable() {
519 if(input.isDisposed())
521 if(!input.getText().equals(forCommand))
523 syncSetErrorAnnotations(forCommand, annotations);
528 private boolean readPreferences() {
530 IPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
532 String commandHistoryPref = store.getString(Preferences.COMMAND_HISTORY);
533 Deque<String> recentImportPaths = Preferences.decodePaths(commandHistoryPref);
535 commandHistory = new ArrayList<String>(recentImportPaths);
536 commandHistoryPos = commandHistory.size();
541 private void writePreferences() throws IOException {
543 IPersistentPreferenceStore store = new ScopedPreferenceStore(InstanceScope.INSTANCE, PLUGIN_ID);
545 store.putValue(Preferences.COMMAND_HISTORY, Preferences.encodePaths(commandHistory));
547 if (store.needsSaving())
552 public abstract void execute(String command);
553 public abstract ErrorAnnotation[] validate(String command);
555 public void clear() {
556 outputModiLock = true;
558 outputModiLock = false;
561 public StyledText getOutputWidget() {