blob: 72e4aa1e9e0a9fbfec5457f455f4e7176897567c [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2017 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
*******************************************************************************/
package org.eclipse.dltk.console.ui.internal;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;
import org.eclipse.dltk.compiler.util.Util;
import org.eclipse.dltk.console.IScriptConsoleInterpreter;
import org.eclipse.dltk.console.IScriptExecResult;
import org.eclipse.dltk.console.ScriptConsoleHistory;
import org.eclipse.dltk.console.ScriptConsolePrompt;
import org.eclipse.dltk.console.ui.AnsiColorHelper;
import org.eclipse.dltk.console.ui.AnsiColorHelper.IAnsiColorHandler;
import org.eclipse.dltk.console.ui.IScriptConsoleViewer;
import org.eclipse.dltk.console.ui.ScriptConsole;
import org.eclipse.dltk.console.ui.ScriptConsolePartitioner;
import org.eclipse.dltk.core.DLTKCore;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.hyperlink.HyperlinkManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.DropTarget;
import org.eclipse.swt.dnd.DropTargetAdapter;
import org.eclipse.swt.dnd.DropTargetEvent;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.console.TextConsoleViewer;
public class ScriptConsoleViewer extends TextConsoleViewer implements IScriptConsoleViewer {
public static class ConsoleDocumentListener implements IDocumentListener {
private boolean bEnabled = true;
private ICommandHandler handler;
private boolean handleSynchronously;
private ScriptConsolePrompt prompt;
private ScriptConsoleHistory history;
private int inviteStart = 0;
private int inviteEnd = 0;
private IDocument doc;
private AnsiColorHelper ansiHelper = new AnsiColorHelper();
private List<ScriptConsoleViewer> viewerList = new ArrayList<ScriptConsoleViewer>();
private void addViewer(ScriptConsoleViewer viewer) {
viewerList.add(viewer);
}
private void removeViewer(ScriptConsoleViewer viewer) {
viewerList.remove(viewer);
}
protected void connectListener() {
doc.addDocumentListener(this);
}
protected void disconnectListener() {
doc.removeDocumentListener(this);
}
public void clear() {
try {
disconnectListener();
doc.set(""); //$NON-NLS-1$
ScriptConsoleViewer viewer;
for (Iterator<ScriptConsoleViewer> iter = viewerList.iterator(); iter.hasNext();) {
viewer = iter.next();
IDocumentPartitioner partitioner = viewer.getDocument().getDocumentPartitioner();
if (partitioner instanceof ScriptConsolePartitioner) {
ScriptConsolePartitioner scriptConsolePartitioner = (ScriptConsolePartitioner) partitioner;
scriptConsolePartitioner.clearRanges();
}
}
appendInvitation();
for (Iterator<ScriptConsoleViewer> iter = viewerList.iterator(); iter.hasNext();) {
iter.next().setCaretPosition(doc.getLength());
}
} catch (BadLocationException e) {
e.printStackTrace();
} finally {
connectListener();
}
}
public ConsoleDocumentListener(ICommandHandler handler, ScriptConsolePrompt prompt,
ScriptConsoleHistory history) {
this.prompt = prompt;
this.handler = handler;
this.history = history;
this.doc = null;
}
public void setDocument(IDocument doc) {
if (this.doc != null) {
disconnectListener();
}
this.doc = doc;
if (this.doc != null) {
connectListener();
}
}
@Override
public void documentAboutToBeChanged(DocumentEvent event) {
}
protected void handleCommandLine(final String command) throws BadLocationException, IOException {
if (handleSynchronously) {
IScriptExecResult result = handler.handleCommand(command);
if (((ScriptConsole) handler).getState() != IScriptConsoleInterpreter.WAIT_USER_INPUT) {
processResult(result);
}
return;
}
Thread handlerThread = new Thread(Messages.ScriptConsoleViewer_scriptConsoleCommandHandler) {
@Override
public void run() {
try {
final IScriptExecResult result = handler.handleCommand(command);
if (((ScriptConsole) handler).getState() != IScriptConsoleInterpreter.WAIT_USER_INPUT) {
((ScriptConsole) handler).getPage().getSite().getShell().getDisplay()
.asyncExec(() -> processResult(result));
}
} catch (IOException ixcn) {
ixcn.printStackTrace();
}
}
};
handlerThread.setDaemon(true);
handlerThread.setPriority(Thread.MIN_PRIORITY);
handlerThread.start();
}
protected void appendText(int offset, String text) throws BadLocationException {
doc.replace(offset, 0, text);
}
protected void processText(int originalOffset, String content, boolean isInput, boolean isError,
final boolean shouldReveal, final boolean shouldRedraw) throws BadLocationException {
if (originalOffset == -1) {
originalOffset = doc.getLength();
}
ansiHelper.processText(originalOffset, content, isInput, isError, new IAnsiColorHandler() {
@Override
public void handleText(int start, String content, boolean isInput, boolean isError)
throws BadLocationException {
appendText(start, content);
addToPartitioner(start, content, isInput, isError);
}
@Override
public void processingComplete(int start, int length) {
for (Iterator<ScriptConsoleViewer> iter = viewerList.iterator(); iter.hasNext();) {
final ScriptConsoleViewer viewer = iter.next();
if (shouldReveal == true) {
viewer.setCaretPosition(doc.getLength());
viewer.revealEndOfDocument();
}
if (shouldRedraw == true) {
if (viewer.getTextWidget() != null) {
viewer.getTextWidget().redrawRange(start, length, true);
}
}
}
}
});
}
protected void processResult(final IScriptExecResult result) {
disconnectListener();
try {
if (result != null) {
final String output = result.getOutput();
if (output != null && output.length() != 0) {
ansiHelper.reset();
processText(-1, output, false, result.isError(), false, true);
}
}
appendInvitation();
} catch (BadLocationException bxcn) {
bxcn.printStackTrace();
} finally {
connectListener();
}
}
private void addToPartitioner(ScriptConsoleViewer viewer, StyleRange style) {
IDocumentPartitioner partitioner = viewer.getDocument().getDocumentPartitioner();
if (partitioner instanceof ScriptConsolePartitioner) {
ScriptConsolePartitioner scriptConsolePartitioner = (ScriptConsolePartitioner) partitioner;
scriptConsolePartitioner.addRange(style);
}
}
protected void addToPartitioner(int start, String content, boolean isInput, boolean isError) {
// ssanders: Content has to be tokenized in order for style and
// hyperlinks to display correctly
StringTokenizer tokenizer = new StringTokenizer(content, " \t\n\r\f@#=|,()[]{}<>'\"", true); //$NON-NLS-1$
String token;
int tokenStart = start;
ScriptConsoleViewer viewer;
while (tokenizer.hasMoreTokens() == true) {
token = tokenizer.nextToken();
for (Iterator<ScriptConsoleViewer> iter = viewerList.iterator(); iter.hasNext();) {
viewer = iter.next();
if (isInput == true) {
addToPartitioner(viewer, new StyleRange(tokenStart, token.length(), AnsiColorHelper.COLOR_BLACK,
null, SWT.BOLD));
} else {
addToPartitioner(viewer, ansiHelper.resolveStyleRange(tokenStart, token.length(), isError));
}
}
tokenStart += token.length();
}
for (Iterator<ScriptConsoleViewer> iter = viewerList.iterator(); iter.hasNext();) {
viewer = iter.next();
viewer.getTextWidget().redraw();
}
}
protected void processAddition(int offset, String text) {
if (!bEnabled) {
return;
}
try {
final String delim = TextUtilities.getDefaultLineDelimiter(doc);
text = doc.get(offset, doc.getLength() - offset);
doc.replace(offset, text.length(), ""); //$NON-NLS-1$
text = text.replaceAll("\r\n|\n|\r", delim); //$NON-NLS-1$
int start = 0;
int index;
while ((index = text.indexOf(delim, start)) != -1) {
if (index > start) {
processText(getCommandLineOffset(), text.substring(start, index), true, false, false, true);
}
final String commandLine = getCommandLine();
processText(-1, delim, true, false, false, true);
inviteStart = inviteEnd = doc.getLength();
history.add(commandLine);
start = index + delim.length();
handleCommandLine(commandLine);
}
if (start < text.length()) {
processText(-1, text.substring(start, text.length()), true, false, false, true);
}
} catch (BadLocationException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
} catch (IOException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
}
}
@Override
public void documentChanged(final DocumentEvent event) {
ansiHelper.disableWhile(() -> {
disconnectListener();
try {
processAddition(event.getOffset(), event.getText());
} finally {
connectListener();
}
});
}
public void appendInvitation() throws BadLocationException {
inviteStart = doc.getLength();
processText(inviteStart, prompt.toString(), true, false, true, true);
inviteEnd = doc.getLength();
}
public void appendDelimeter() throws BadLocationException {
processText(-1, TextUtilities.getDefaultLineDelimiter(doc), false, false, false, true);
}
protected int getLastLineLength() throws BadLocationException {
int lastLine = doc.getNumberOfLines() - 1;
return doc.getLineLength(lastLine);
}
public int getCommandLineOffset() throws BadLocationException {
return inviteEnd;
}
public int getCommandLineLength() throws BadLocationException {
return doc.getLength() - inviteEnd;
}
public String getCommandLine() throws BadLocationException {
return doc.get(getCommandLineOffset(), getCommandLineLength());
}
public void setCommandLine(final String command) {
ansiHelper.disableWhile(() -> {
try {
doc.replace(getCommandLineOffset(), getCommandLineLength(), command);
} catch (BadLocationException bxcn) {
bxcn.printStackTrace();
}
});
}
/**
* @param command
*/
public void executeCommand(String command) {
disconnectListener();
try {
final int docLen = doc.getLength();
if (docLen > inviteEnd) {
// clear current command if any
doc.replace(inviteEnd, docLen - inviteEnd, Util.EMPTY_STRING);
// TODO should we restore the text after this command
// execution?
}
processText(getCommandLineOffset(), command + TextUtilities.getDefaultLineDelimiter(doc), true, false,
true, true);
inviteStart = inviteEnd = doc.getLength();
handleCommandLine(command);
} catch (BadLocationException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
} catch (IOException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
} finally {
connectListener();
}
}
/**
* @param text
* @param isError
*/
public void write(final String text, final boolean isError) {
final Display display = PlatformUI.getWorkbench().getDisplay();
if (display != null && !display.isDisposed())
display.asyncExec(() -> {
disconnectListener();
try {
processText(inviteStart, text, false, isError, true, true);
inviteStart += text.length();
inviteEnd += text.length();
} catch (BadLocationException bxcn) {
if (DLTKCore.DEBUG) {
bxcn.printStackTrace();
}
} finally {
connectListener();
}
});
}
}
/**
* @since 2.0
*/
public class ScriptConsoleStyledText extends StyledText {
public ScriptConsoleStyledText(Composite parent, int style) {
super(parent, (style | SWT.WRAP));
}
@Override
public void invokeAction(int action) {
if (isEditable() && isCaretOnLastLine()) {
switch (action) {
case ST.LINE_UP:
updateSelectedLine();
if (history.prev()) {
console.getDocumentListener().setCommandLine(history.get());
setCaretOffset(getDocument().getLength());
} else {
beep();
}
return;
case ST.LINE_DOWN:
updateSelectedLine();
if (history.next()) {
console.getDocumentListener().setCommandLine(history.get());
setCaretOffset(getDocument().getLength());
} else {
beep();
}
return;
case ST.DELETE_PREVIOUS:
if (getCaretOffset() <= getCommandLineOffset() && getSelectionCount() == 0) {
return;
}
break;
case ST.DELETE_NEXT:
if (getCaretOffset() < getCommandLineOffset()) {
return;
}
break;
case ST.DELETE_WORD_PREVIOUS:
return;
case ST.SELECT_LINE_START:
if (isCaretOnLastLine()) {
final int prevCaret = getCaretOffset();
final Point prevSelection = getSelection();
final int caret = getCommandLineOffset();
if (prevCaret == prevSelection.x) {
setSelection(prevSelection.y, caret);
} else if (prevCaret == prevSelection.y) {
setSelection(prevSelection.x, caret);
} else {
setCaretOffset(caret);
}
Point selectedRange = getSelectedRange();
selectionChanged(selectedRange.x, selectedRange.y);
return;
}
break;
case ST.LINE_START:
if (isCaretOnLastLine()) {
setCaretOffset(getCommandLineOffset());
return;
}
break;
case ST.COLUMN_PREVIOUS:
case ST.SELECT_COLUMN_PREVIOUS:
if (isCaretOnLastLine() && getCaretOffset() == getCommandLineOffset()) {
return;
}
}
super.invokeAction(action);
if (isCaretOnLastLine() && getCaretOffset() <= getCommandLineOffset()) {
setCaretOffset(getCommandLineOffset());
}
} else {
super.invokeAction(action);
}
}
private void updateSelectedLine() {
try {
history.updateSelectedLine(console.getDocumentListener().getCommandLine());
} catch (BadLocationException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
}
}
private void beep() {
getDisplay().beep();
}
@Override
public void paste() {
if (isCaretOnLastLine()) {
console.getDocumentListener().ansiHelper.disableWhile(() -> {
checkWidget();
Clipboard clipboard = new Clipboard(getDisplay());
TextTransfer plainTextTransfer = TextTransfer.getInstance();
String text = (String) clipboard.getContents(plainTextTransfer, DND.CLIPBOARD);
clipboard.dispose();
paste(text);
});
}
}
/**
* @param text
*/
private void paste(String text) {
if (text != null && text.length() > 0) {
// ssanders: Process the lines one-by-one in
// order to have the proper prompting
console.getDocumentListener().handleSynchronously = true;
try {
if (text.indexOf("\n") == -1) {
Point selectedRange = getSelectedRange();
getTextWidget().insert(text);
setCaretOffset(selectedRange.x + text.length());
} else {
StringTokenizer tokenizer = new StringTokenizer(text, "\n\r"); //$NON-NLS-1$
while (tokenizer.hasMoreTokens() == true) {
final String finText = tokenizer.nextToken();
insertText(finText + "\n"); //$NON-NLS-1$
}
}
} finally {
console.getDocumentListener().handleSynchronously = false;
}
}
}
}
private ScriptConsoleHistory history;
private ScriptConsole console;
public int getCaretPosition() {
return getTextWidget().getCaretOffset();
}
public void enableProcessing() {
console.getDocumentListener().bEnabled = true;
}
public void disableProcessing() {
console.getDocumentListener().bEnabled = false;
}
public void setCaretPosition(final int offset) {
if (getTextWidget() != null) {
getTextWidget().getDisplay().asyncExec(() -> {
if (getTextWidget() != null) {
getTextWidget().setCaretOffset(offset);
}
});
}
}
// public int beginLineOffset() throws BadLocationException {
// IDocument doc = getDocument();
// int offset = getCaretPosition();
// int line = doc.getLineOfOffset(offset);
// return offset - doc.getLineOffset(line);
// }
protected boolean isCaretOnLastLine() {
try {
IDocument doc = getDocument();
int line = doc.getLineOfOffset(getCaretPosition());
return line == doc.getNumberOfLines() - 1;
} catch (BadLocationException e) {
if (DLTKCore.DEBUG) {
e.printStackTrace();
}
return false;
}
}
@Override
protected StyledText createTextWidget(Composite parent, int styles) {
return new ScriptConsoleStyledText(parent, styles);
}
public ScriptConsoleViewer(Composite parent, final ScriptConsole console,
final IScriptConsoleContentHandler contentHandler) {
super(parent, console);
this.console = console;
this.history = console.getHistory();
console.getDocumentListener().addViewer(this);
final StyledText styledText = getTextWidget();
// styledText.setEditable(false);
// Correct keyboard actions
styledText.addFocusListener(new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
setCaretPosition(getDocument().getLength());
styledText.removeFocusListener(this);
}
@Override
public void focusLost(FocusEvent e) {
}
});
DropTarget target = new DropTarget(styledText,
DND.DROP_DEFAULT | DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK);
target.setTransfer(new Transfer[] { TextTransfer.getInstance() });
target.addDropListener(new DropTargetAdapter() {
@Override
public void dragEnter(DropTargetEvent e) {
if (e.detail == DND.DROP_DEFAULT)
e.detail = DND.DROP_COPY;
}
@Override
public void dragOperationChanged(DropTargetEvent e) {
if (e.detail == DND.DROP_DEFAULT)
e.detail = DND.DROP_COPY;
}
@Override
public void drop(DropTargetEvent e) {
((ScriptConsoleStyledText) styledText).paste((String) e.data);
}
});
styledText.setKeyBinding('X' | SWT.MOD1, ST.COPY);
styledText.addVerifyKeyListener(event -> {
try {
if (event.character != '\0') {
if ((event.stateMask & SWT.MOD1) == 0) {
// Printable character
// ssanders: Ensure selection is on last line
ConsoleDocumentListener listener = console.getDocumentListener();
int selStart = getSelectedRange().x;
int selEnd = (getSelectedRange().x + getSelectedRange().y);
int clOffset = listener.getCommandLineOffset();
int clLength = listener.getCommandLineLength();
if (selStart < clOffset) {
int selLength;
if (selEnd < clOffset) {
selStart = (clOffset + clLength);
selLength = 0;
} else {
selStart = clOffset;
selLength = (selEnd - selStart);
}
setSelectedRange(selStart, selLength);
}
if (getCaretPosition() < console.getDocumentListener().getCommandLineOffset()) {
event.doit = false;
return;
}
}
if (event.character == SWT.CR) {
getTextWidget().setCaretOffset(getDocument().getLength());
return;
}
// ssanders: Avoid outputting " " when invoking
// completion on Mac OS X
if (event.keyCode == 32 && (event.stateMask & SWT.CTRL) > 0) {
event.doit = false;
return;
}
// ssanders: Avoid outputting "<Tab>" when invoking
// completion on Mac OS X
if (event.keyCode == SWT.TAB) {
event.doit = false;
return;
}
}
} catch (Exception e) {
e.printStackTrace();
}
});
styledText.addKeyListener(new KeyListener() {
@Override
public void keyPressed(KeyEvent e) {
if (e.keyCode == SWT.TAB || (e.keyCode == ' ' && e.stateMask == SWT.CTRL)) {
contentHandler.contentAssistRequired();
}
}
@Override
public void keyReleased(KeyEvent e) {
}
});
if (console.getDocumentListener().viewerList.size() == 1) {
clear();
}
}
// IConsoleTextViewer
@Override
public String getCommandLine() {
try {
return console.getDocumentListener().getCommandLine();
} catch (BadLocationException e) {
return null;
}
}
@Override
public int getCommandLineOffset() {
try {
return console.getDocumentListener().getCommandLineOffset();
} catch (BadLocationException e) {
return -1;
}
}
public void clear() {
console.getDocumentListener().clear();
}
public void insertText(String text) {
getTextWidget().append(text);
}
@Override
public boolean canDoOperation(int operation) {
boolean canDoOperation = super.canDoOperation(operation);
if (canDoOperation) {
switch (operation) {
case CUT:
case DELETE:
// case PASTE:
case SHIFT_LEFT:
case SHIFT_RIGHT:
case PREFIX:
case STRIP_PREFIX:
canDoOperation = isCaretOnLastLine();
}
}
return canDoOperation;
}
@Override
public void activatePlugins() {
fHyperlinkManager = new HyperlinkManager(HyperlinkManager.LONGEST_REGION_FIRST);
fHyperlinkManager.install(this, fHyperlinkPresenter, fHyperlinkDetectors, fHyperlinkStateMask);
super.activatePlugins();
}
public void dispose() {
console.getDocumentListener().removeViewer(this);
}
}