
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-15 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 processing.mode.java.JavaInputHandler;
import processing.mode.java.JavaMode;
import processing.mode.java.JavaEditor;
import processing.mode.java.tweak.ColorControlBox;
import processing.mode.java.tweak.Handle;
import java.awt.*;
import java.awt.event.*;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.swing.DefaultListModel;
import javax.swing.SwingWorker;
import processing.app.Messages;
import processing.app.Mode;
import processing.app.Platform;
import processing.app.syntax.JEditTextArea;
import processing.app.syntax.TextAreaDefaults;
import processing.app.ui.Editor;
// TODO The way listeners are added/removed here is fragile and
// likely to cause bugs that are very difficult to find.
// We shouldn't be re-inventing the wheel with how listeners are handled.
// TODO We're overriding more things in JEditTextArea than we should, which
// makes it trickier for other Modes (Python, etc) to subclass because
// they'll need to re-implement what's in here, but first wade through it.
// To fix, we need to clean this up and put the appropriate cross-Mode
// changes into JEditTextArea (or a subclass in processing.app)
public class JavaTextArea extends JEditTextArea {
protected final JavaEditor editor;
protected Image gutterGradient;
/// the text marker for highlighting breakpoints in the gutter
static public final String BREAK_MARKER = "<>";
/// the text marker for highlighting the current line in the gutter
static public final String STEP_MARKER = "->";
/// maps line index to gutter text
protected final Map gutterText = new HashMap<>();
private CompletionPanel suggestion;
public JavaTextArea(TextAreaDefaults defaults, JavaEditor editor) {
super(defaults, new JavaInputHandler(editor));
this.editor = editor;
// change cursor to pointer in the gutter area
painter.addMouseMotionListener(gutterCursorMouseAdapter);
//addCompletionPopupListner();
add(CENTER, painter);
// load settings from theme.txt
Mode mode = editor.getMode();
gutterGradient = mode.makeGradient("editor", Editor.LEFT_GUTTER, 500);
// TweakMode code
prevCompListeners = painter.getComponentListeners();
prevMouseListeners = painter.getMouseListeners();
prevMMotionListeners = painter.getMouseMotionListeners();
prevKeyListeners = editor.getKeyListeners();
suggestionGenerator = new CompletionGenerator();
tweakMode = false;
}
@Override
protected JavaTextAreaPainter createPainter(final TextAreaDefaults defaults) {
return new JavaTextAreaPainter(this, defaults);
}
protected JavaTextAreaPainter getCustomPainter() {
return (JavaTextAreaPainter) painter;
}
public void setMode(JavaMode mode) {
getCustomPainter().setMode(mode);
}
/**
* Handles KeyEvents for TextArea (code completion begins from here).
*/
@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 (!editor.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(editor.getCurrentTab());
int lineStartOffset = editor.getTextArea().getLineStartOffset(caretLineIndex);
editor.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;
}
public Image getGutterGradient() {
return gutterGradient;
}
/**
* Set the gutter text of a specific line.
*
* @param lineIdx
* the line index (0-based)
* @param text
* the text
*/
public void setGutterText(int lineIdx, String text) {
gutterText.put(lineIdx, text);
painter.invalidateLine(lineIdx);
}
/**
* Clear the gutter text of a specific line.
*
* @param lineIdx
* the line index (0-based)
*/
public void clearGutterText(int lineIdx) {
gutterText.remove(lineIdx);
painter.invalidateLine(lineIdx);
}
/**
* Clear all gutter text.
*/
public void clearGutterText() {
for (int lineIdx : gutterText.keySet()) {
painter.invalidateLine(lineIdx);
}
gutterText.clear();
}
/**
* Retrieve the gutter text of a specific line.
*
* @param lineIdx
* the line index (0-based)
* @return the gutter text
*/
public String getGutterText(int lineIdx) {
return gutterText.get(lineIdx);
}
/**
* Convert a character offset to a horizontal pixel position inside the text
* area. Overridden to take gutter width into account.
*
* @param line
* the 0-based line number
* @param offset
* the character offset (0 is the first character on a line)
* @return the horizontal position
*/
@Override
public int _offsetToX(int line, int offset) {
return super._offsetToX(line, offset) + Editor.LEFT_GUTTER;
}
/**
* Convert a horizontal pixel position to a character offset. Overridden to
* take gutter width into account.
*
* @param line
* the 0-based line number
* @param x
* the horizontal pixel position
* @return he character offset (0 is the first character on a line)
*/
@Override
public int xToOffset(int line, int x) {
return super.xToOffset(line, x - Editor.LEFT_GUTTER);
}
/**
* Sets default cursor (instead of text cursor) in the gutter area.
*/
protected final MouseMotionAdapter gutterCursorMouseAdapter = new MouseMotionAdapter() {
private int lastX; // previous horizontal positon of the mouse cursor
@Override
public void mouseMoved(MouseEvent me) {
if (me.getX() < Editor.LEFT_GUTTER) {
if (lastX >= Editor.LEFT_GUTTER) {
painter.setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
}
} else {
if (lastX < Editor.LEFT_GUTTER) {
painter.setCursor(new Cursor(Cursor.TEXT_CURSOR));
}
}
lastX = me.getX();
}
};
// appears unused, removed when looking to change completion trigger [fry 140801]
/*
public void showSuggestionLater(final DefaultListModel defListModel, final String word) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
showSuggestion(defListModel,word);
}
});
}
*/
/**
* Calculates location of caret and displays the suggestion popup at the location.
*
* @param listModel
* @param subWord
*/
protected void showSuggestion(DefaultListModel listModel, String subWord) {
hideSuggestion();
if (listModel.size() == 0) {
Messages.log("TextArea: No suggestions to show.");
} else {
int position = getCaretPosition();
Point location = new Point();
try {
location.x = offsetToX(getCaretLine(), position
- getLineStartOffset(getCaretLine()));
location.y = lineToY(getCaretLine())
+ getPainter().getFontMetrics().getHeight() + getPainter().getFontMetrics().getDescent();
//log("TA position: " + location);
} catch (Exception e2) {
e2.printStackTrace();
return;
}
suggestion = new CompletionPanel(this, position, subWord,
listModel, location, editor);
requestFocusInWindow();
}
}
/** 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 final ComponentListener[] prevCompListeners;
protected final MouseListener[] prevMouseListeners;
protected final MouseMotionListener[] prevMMotionListeners;
protected final KeyListener[] prevKeyListeners;
protected boolean tweakMode;
/* remove all standard interaction listeners */
public void removeAllListeners() {
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) {
removeAllListeners();
getCustomPainter().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) {
removeAllListeners();
addPrevListeners();
getCustomPainter().stopTweakMode();
editable = true;
caretBlinks = true;
setCaretVisible(true);
tweakMode = false;
}
}
private void addPrevListeners() {
// add the original text-edit listeners
for (ComponentListener cl : prevCompListeners) {
painter.addComponentListener(cl);
}
for (MouseListener ml : prevMouseListeners) {
painter.addMouseListener(ml);
}
for (MouseMotionListener mml : prevMMotionListeners) {
painter.addMouseMotionListener(mml);
}
for (KeyListener kl : prevKeyListeners) {
editor.addKeyListener(kl);
}
}
public void updateInterface(List> handles,
List> colorBoxes) {
getCustomPainter().updateInterface(handles, colorBoxes);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy