processing.mode.java.pdex.JavaTextArea Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-mode Show documentation
Show all versions of java-mode Show documentation
Processing is a programming language, development environment, and online community.
This Java Mode package contains the Java mode for Processing IDE.
/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2012-16 The Processing Foundation
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 2
as published by the Free Software Foundation.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package processing.mode.java.pdex;
import java.awt.EventQueue;
import java.awt.Point;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import javax.swing.DefaultListModel;
import javax.swing.SwingWorker;
import processing.app.Messages;
import processing.app.Platform;
import processing.app.syntax.PdeTextArea;
import processing.app.syntax.TextAreaDefaults;
import processing.mode.java.JavaEditor;
import processing.mode.java.JavaInputHandler;
import processing.mode.java.JavaMode;
import processing.mode.java.tweak.ColorControlBox;
import processing.mode.java.tweak.Handle;
/**
* TextArea implementation for Java Mode. Primary differences from PdeTextArea
* are completions, suggestions, and tweak handling.
*/
public class JavaTextArea extends PdeTextArea {
private CompletionPanel suggestion;
public JavaTextArea(TextAreaDefaults defaults, JavaEditor editor) {
super(defaults, new JavaInputHandler(editor), editor);
suggestionGenerator = new CompletionGenerator();
tweakMode = false;
}
public JavaEditor getJavaEditor() {
return (JavaEditor) editor;
}
@Override
protected JavaTextAreaPainter createPainter(final TextAreaDefaults defaults) {
return new JavaTextAreaPainter(this, defaults);
}
// used by Tweak Mode
protected JavaTextAreaPainter getJavaPainter() {
return (JavaTextAreaPainter) painter;
}
/**
* Handles KeyEvents for TextArea (code completion begins from here).
* TODO Needs explanation of why this implemented with an override
* of processKeyEvent() instead of using listeners.
*/
@Override
public void processKeyEvent(KeyEvent evt) {
if (evt.getKeyCode() == KeyEvent.VK_ESCAPE) {
if (suggestion != null){
if (suggestion.isVisible()){
Messages.log("ESC key");
hideSuggestion();
evt.consume();
return;
}
}
} else if (evt.getKeyCode() == KeyEvent.VK_ENTER &&
evt.getID() == KeyEvent.KEY_PRESSED) {
if (suggestion != null &&
suggestion.isVisible() &&
suggestion.insertSelection(CompletionPanel.KEYBOARD_COMPLETION)) {
evt.consume();
// Still try to show suggestions after inserting if it's
// the case of overloaded methods. See #2755
if (suggestion.isVisible()) {
prepareSuggestions(evt);
}
return;
}
}
if (evt.getID() == KeyEvent.KEY_PRESSED) {
switch (evt.getKeyCode()) {
case KeyEvent.VK_DOWN:
if (suggestion != null)
if (suggestion.isVisible()) {
//log("KeyDown");
suggestion.moveDown();
return;
}
break;
case KeyEvent.VK_UP:
if (suggestion != null)
if (suggestion.isVisible()) {
//log("KeyUp");
suggestion.moveUp();
return;
}
break;
case KeyEvent.VK_BACK_SPACE:
Messages.log("BK Key");
break;
case KeyEvent.VK_SPACE:
if (suggestion != null) {
if (suggestion.isVisible()) {
Messages.log("Space bar, hide completion list");
suggestion.setInvisible();
}
}
break;
}
}
super.processKeyEvent(evt);
// code completion disabled if Java tabs present
if (!getJavaEditor().hasJavaTabs()) {
if (evt.getID() == KeyEvent.KEY_TYPED) {
processCompletionKeys(evt);
} else if (!Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processCompletionKeys(evt);
} else if (Platform.isMacOS() && evt.getID() == KeyEvent.KEY_RELEASED) {
processControlSpace(evt);
}
}
}
// Special case for OS X, where Ctrl-Space is not detected as KEY_TYPED
// https://github.com/processing/processing/issues/2699
private void processControlSpace(final KeyEvent event) {
if (event.getKeyCode() == KeyEvent.VK_SPACE && event.isControlDown()) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
fetchPhrase();
}
}
}
private void processCompletionKeys(final KeyEvent event) {
char keyChar = event.getKeyChar();
int keyCode = event.getKeyCode();
if (keyChar == KeyEvent.VK_ENTER ||
keyChar == KeyEvent.VK_ESCAPE ||
keyChar == KeyEvent.VK_TAB ||
(event.getID() == KeyEvent.KEY_RELEASED &&
keyCode != KeyEvent.VK_LEFT && keyCode != KeyEvent.VK_RIGHT)) {
// ignore
} else if (keyChar == ')') {
// https://github.com/processing/processing/issues/2741
hideSuggestion();
} else if (keyChar == '.') {
if (JavaMode.codeCompletionsEnabled) {
Messages.log("[KeyEvent]" + KeyEvent.getKeyText(event.getKeyCode()) + " |Prediction started");
fetchPhrase();
}
} else if (keyChar == ' ') { // Trigger on Ctrl-Space
if (!Platform.isMacOS() && JavaMode.codeCompletionsEnabled &&
(event.isControlDown() || event.isMetaDown())) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled) {
// Removed for https://github.com/processing/processing/issues/3847
//try {
// getDocument().remove(getCaretPosition() - 1, 1); // Remove the typed space
Messages.log("[KeyEvent]" + event.getKeyChar() + " |Prediction started");
fetchPhrase();
//} catch (BadLocationException e) {
// e.printStackTrace();
//}
}
} else {
hideSuggestion(); // hide on spacebar
}
} else {
if (JavaMode.codeCompletionsEnabled) {
prepareSuggestions(event);
}
}
}
/** Kickstart auto-complete suggestions */
private void prepareSuggestions(final KeyEvent evt) {
// Provide completions only if it's enabled
if (JavaMode.codeCompletionsEnabled &&
(JavaMode.ccTriggerEnabled ||
(suggestion != null && suggestion.isVisible()))) {
Messages.log("[KeyEvent]" + evt.getKeyChar() + " |Prediction started");
fetchPhrase();
}
}
CompletionGenerator suggestionGenerator;
SwingWorker suggestionWorker = null;
volatile boolean suggestionRunning = false;
volatile boolean suggestionRequested = false;
/**
* Retrieves the current word typed just before the caret.
* Then triggers code completion for that word.
* @param evt - the KeyEvent which triggered this method
*/
protected void fetchPhrase() {
if (suggestionRunning) {
suggestionRequested = true;
return;
}
suggestionRunning = true;
suggestionRequested = false;
final String text;
final int caretLineIndex;
final int caretLinePosition;
{
// Get caret position
int caretPosition = getCaretPosition();
if (caretPosition < 0) {
suggestionRunning = false;
return;
}
// Get line index
caretLineIndex = getCaretLine();
if (caretLineIndex < 0) {
suggestionRunning = false;
return;
}
// Get text of the line
String lineText = getLineText(caretLineIndex);
if (lineText == null) {
suggestionRunning = false;
return;
}
// Get caret position on the line
caretLinePosition = getCaretPosition() - getLineStartOffset(caretLineIndex);
if (caretLinePosition <= 0) {
suggestionRunning = false;
return;
}
// Get part of the line to the left of the caret
if (caretLinePosition > lineText.length()) {
suggestionRunning = false;
return;
}
text = lineText.substring(0, caretLinePosition);
}
// Adjust line number for tabbed sketches
//int codeIndex = editor.getSketch().getCodeIndex(getJavaEditor().getCurrentTab());
int codeIndex = editor.getSketch().getCurrentCodeIndex();
int lineStartOffset = editor.getTextArea().getLineStartOffset(caretLineIndex);
getJavaEditor().getPreprocessingService().whenDone(ps -> {
int lineNumber = ps.tabOffsetToJavaLine(codeIndex, lineStartOffset);
String phrase = null;
DefaultListModel defListModel = null;
try {
Messages.log("phrase parse start");
phrase = parsePhrase(text);
Messages.log("phrase: " + phrase);
if (phrase != null) {
List candidates;
candidates = suggestionGenerator.preparePredictions(ps, phrase, lineNumber);
if (!suggestionRequested) {
// // don't show completions when the outline is visible
// boolean showSuggestions =
// astGenerator.sketchOutline == null || !astGenerator.sketchOutline.isVisible();
// if (showSuggestions && phrase != null &&
if (candidates != null && !candidates.isEmpty()) {
Collections.sort(candidates);
defListModel = CompletionGenerator.filterPredictions(candidates);
Messages.log("Got: " + candidates.size() + " candidates, " + defListModel.size() + " filtered");
}
}
}
final String finalPhrase = phrase;
final DefaultListModel finalDefListModel = defListModel;
EventQueue.invokeLater(() -> {
suggestionRunning = false;
if (suggestionRequested) {
Messages.log("completion invalidated");
fetchPhrase();
return;
}
Messages.log("completion finishing");
if (finalDefListModel != null) {
showSuggestion(finalDefListModel, finalPhrase);
} else {
hideSuggestion();
}
});
} catch (Exception e) {
Messages.loge("error while preparing suggestions", e);
}
});
}
protected static String parsePhrase(final String lineText) {
boolean overloading = false;
{ // Check if we can provide suggestions for this phrase ending
String trimmedLineText = lineText.trim();
if (trimmedLineText.length() == 0) return null;
char lastChar = trimmedLineText.charAt(trimmedLineText.length() - 1);
if (lastChar == '.') {
trimmedLineText = trimmedLineText.substring(0, trimmedLineText.length() - 1).trim();
if (trimmedLineText.length() == 0) return null;
lastChar = trimmedLineText.charAt(trimmedLineText.length() - 1);
switch (lastChar) {
case ')':
case ']':
case '"':
break; // We can suggest for these
default:
if (!Character.isJavaIdentifierPart(lastChar)) {
return null; // Not something we can suggest
}
break;
}
} else if (lastChar == '(') {
overloading = true; // We can suggest overloaded methods
} else if (!Character.isJavaIdentifierPart(lastChar)) {
return null; // Not something we can suggest
}
}
final int currentCharIndex = lineText.length() - 1;
{ // Check if the caret is in the comment
int commentStart = lineText.indexOf("//", 0);
if (commentStart >= 0 && currentCharIndex > commentStart) {
return null;
}
}
// Index the line
BitSet isInLiteral = new BitSet(lineText.length());
BitSet isInBrackets = new BitSet(lineText.length());
{ // Mark parts in literals
boolean inString = false;
boolean inChar = false;
boolean inEscaped = false;
for (int i = 0; i < lineText.length(); i++) {
if (!inEscaped) {
switch (lineText.charAt(i)) {
case '\"':
if (!inChar) inString = !inString;
break;
case '\'':
if (!inString) inChar = !inChar;
break;
case '\\':
if (inString || inChar) {
inEscaped = true;
}
break;
}
} else {
inEscaped = false;
}
isInLiteral.set(i, inString || inChar);
}
}
if (isInLiteral.get(currentCharIndex)) return null;
{ // Mark parts in top level brackets
int depth = overloading ? 1 : 0;
int bracketStart = overloading ? lineText.length() : 0;
int squareDepth = 0;
int squareBracketStart = 0;
bracketLoop: for (int i = lineText.length() - 1; i >= 0; i--) {
if (!isInLiteral.get(i)) {
switch (lineText.charAt(i)) {
case ')':
if (depth == 0) bracketStart = i;
depth++;
break;
case '(':
depth--;
if (depth == 0) {
isInBrackets.set(i, bracketStart);
} else if (depth < 0) {
break bracketLoop;
}
break;
case ']':
if (squareDepth == 0) squareBracketStart = i;
squareDepth++;
break;
case '[':
squareDepth--;
if (squareDepth == 0) {
isInBrackets.set(i, squareBracketStart);
} else if (squareDepth < 0) {
break bracketLoop;
}
break;
}
}
}
if (depth > 0) isInBrackets.set(0, bracketStart);
if (squareDepth > 0) isInBrackets.set(0, squareBracketStart);
}
// Walk the line from the end while it makes sense
int position = currentCharIndex;
parseLoop: while (position >= 0) {
int currChar = lineText.charAt(position);
switch (currChar) {
case '.': // Grab it
position--;
break;
case '[':
break parseLoop; // End of scope
case ']': // Grab the whole region in square brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '(':
if (isInBrackets.get(position)) {
position--; // This checks for first bracket while overloading
break;
}
break parseLoop; // End of scope
case ')': // Grab the whole region in brackets
position = isInBrackets.previousClearBit(position-1);
break;
case '"': // Grab the whole literal and quit
position = isInLiteral.previousClearBit(position - 1);
break parseLoop;
default:
if (Character.isJavaIdentifierPart(currChar)) {
position--; // Grab the identifier
} else if (Character.isWhitespace(currChar)) {
position--; // Grab whitespace too
} else {
break parseLoop; // Got a char ending the phrase
}
break;
}
}
position++;
// Extract phrase
String phrase = lineText.substring(position, lineText.length()).trim();
Messages.log(phrase);
if (phrase.length() == 0 || Character.isDigit(phrase.charAt(0))) {
return null; // Can't suggest for numbers or empty phrases
}
return phrase;
}
/**
* Calculates location of caret and displays the suggestion pop-up.
*/
protected void showSuggestion(DefaultListModel listModel, String subWord) {
// TODO can this be ListModel instead? why is size() in DefaultListModel
// different from getSize() in ListModel (or are they, really?)
hideSuggestion();
if (listModel.size() != 0) {
int position = getCaretPosition();
try {
Point location =
new Point(offsetToX(getCaretLine(),
position - getLineStartOffset(getCaretLine())),
lineToY(getCaretLine()) + getPainter().getLineHeight());
suggestion = new CompletionPanel(this, position, subWord,
listModel, location, getJavaEditor());
requestFocusInWindow();
} catch (Exception e) {
e.printStackTrace();
}
} else {
Messages.log("TextArea: No suggestions to show.");
}
}
/** Hides suggestion popup */
public void hideSuggestion() {
if (suggestion != null) {
suggestion.setInvisible();
//log("Suggestion hidden.");
suggestion = null; // TODO: check if we dispose the window properly
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// TWEAK MODE
// save input listeners to stop/start text edit
protected ComponentListener[] baseCompListeners;
protected MouseListener[] baseMouseListeners;
protected MouseMotionListener[] baseMotionListeners;
protected KeyListener[] baseKeyListeners;
protected boolean tweakMode;
/* remove all standard interaction listeners */
public void tweakRemoveListeners() {
if (baseCompListeners == null) {
// First time in tweak mode, grab the default listeners. Moved from the
// constructor since not all listeners may have been added at that point.
baseCompListeners = painter.getComponentListeners();
baseMouseListeners = painter.getMouseListeners();
baseMotionListeners = painter.getMouseMotionListeners();
baseKeyListeners = editor.getKeyListeners();
}
ComponentListener[] componentListeners = painter.getComponentListeners();
MouseListener[] mouseListeners = painter.getMouseListeners();
MouseMotionListener[] mouseMotionListeners = painter.getMouseMotionListeners();
KeyListener[] keyListeners = editor.getKeyListeners();
for (ComponentListener cl : componentListeners) {
painter.removeComponentListener(cl);
}
for (MouseListener ml : mouseListeners) {
painter.removeMouseListener(ml);
}
for (MouseMotionListener mml : mouseMotionListeners) {
painter.removeMouseMotionListener(mml);
}
for (KeyListener kl : keyListeners) {
editor.removeKeyListener(kl);
}
}
public void startTweakMode() {
// ignore if we are already in interactiveMode
if (!tweakMode) {
tweakRemoveListeners();
getJavaPainter().startTweakMode();
this.editable = false;
this.caretBlinks = false;
this.setCaretVisible(false);
tweakMode = true;
}
}
public void stopTweakMode() {
// ignore if we are not in interactive mode
if (tweakMode) {
tweakRemoveListeners();
tweakRestoreBaseListeners();
getJavaPainter().stopTweakMode();
editable = true;
caretBlinks = true;
setCaretVisible(true);
tweakMode = false;
}
}
private void tweakRestoreBaseListeners() {
// add the original text-edit listeners
for (ComponentListener cl : baseCompListeners) {
painter.addComponentListener(cl);
}
for (MouseListener ml : baseMouseListeners) {
painter.addMouseListener(ml);
}
for (MouseMotionListener mml : baseMotionListeners) {
painter.addMouseMotionListener(mml);
}
for (KeyListener kl : baseKeyListeners) {
editor.addKeyListener(kl);
}
}
public void updateInterface(List> handles,
List> colorBoxes) {
getJavaPainter().updateTweakInterface(handles, colorBoxes);
}
}