/* * Copyright (C) 2012-14 Manindra Moharana * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * 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., 59 Temple * Place - Suite 330, Boston, MA 02111-1307, USA. */ package processing.mode.experimental; import static processing.mode.experimental.ExperimentalMode.log; import static processing.mode.experimental.ExperimentalMode.logE; import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.TreeMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.table.DefaultTableModel; import javax.swing.text.BadLocationException; import javax.swing.text.Element; import javax.swing.text.PlainDocument; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jdt.core.compiler.IProblem; import org.eclipse.jdt.core.dom.AST; import org.eclipse.jdt.core.dom.ASTNode; import org.eclipse.jdt.core.dom.ASTParser; import org.eclipse.jdt.core.dom.CompilationUnit; import org.eclipse.jdt.internal.compiler.impl.CompilerOptions; import processing.app.Base; import processing.app.Editor; import processing.app.EditorStatus; import processing.app.Library; import processing.app.SketchCode; import processing.app.syntax.SyntaxDocument; import processing.core.PApplet; import processing.mode.java.preproc.PdePreprocessor; /** * The main error checking service * * @author Manindra Moharana <me@mkmoharana.com> * */ public class ErrorCheckerService implements Runnable{ protected DebugEditor editor; /** * Error check happens every sleepTime milliseconds */ public static final int sleepTime = 1000; /** * The amazing eclipse ast parser */ protected ASTParser parser; /** * Used to indirectly stop the Error Checker Thread */ protected AtomicBoolean stopThread; /** * If true, Error Checking is paused. Calls to checkCode() become useless. */ protected AtomicBoolean pauseThread; //protected ErrorWindow errorWindow; /** * IProblem[] returned by parser stored in here */ protected IProblem[] problems; /** * Class name of current sketch */ protected String className; /** * Source code of current sketch */ protected String sourceCode; /** * URLs of extra imports jar files stored here. */ protected URL[] classpath; /** * Stores all Problems in the sketch */ public ArrayList problemsList; /** * How many lines are present till the initial class declaration? In static * mode, this would include imports, class declaration and setup * declaration. In nomral mode, this would include imports, class * declaration only. It's fate is decided inside preprocessCode() */ public int mainClassOffset; /** * Fixed p5 offsets for all sketches */ public int defaultImportsOffset; /** * Is the sketch running in static mode or active mode? */ public boolean staticMode = false; /** * Compilation Unit for current sketch */ protected CompilationUnit cu; /** * The Compilation Unit generated during compile check */ protected CompilationUnit compileCheckCU; /** * This Compilation Unit points to the last error free CU */ protected CompilationUnit lastCorrectCU; /** * If true, compilation checker will be reloaded with updated classpath * items. */ protected boolean loadCompClass = true; /** * Compiler Checker class. Note that methods for compilation checking are * called from the compilationChecker object, not from this */ protected Class checkerClass; /** * Compilation Checker object. */ protected CompilationChecker compilationChecker; /** * List of jar files to be present in compilation checker's classpath */ protected ArrayList classpathJars; /** * Timestamp - for measuring total overhead */ protected long lastTimeStamp = System.currentTimeMillis(); /** * Used for displaying the rotating slash on the Problem Window title bar */ protected String[] slashAnimation = { "|", "/", "--", "\\", "|", "/", "--", "\\" }; protected int slashAnimationIndex = 0; /** * Used to detect if the current tab index has changed and thus repaint the * textarea. */ public int currentTab = 0, lastTab = 0; /** * Stores the current import statements in the program. Used to compare for * changed import statements and update classpath if needed. */ protected ArrayList programImports; /** * List of imports when sketch was last checked. Used for checking for * changed imports */ protected ArrayList previousImports = new ArrayList(); /** * Teh Preprocessor */ protected XQPreprocessor xqpreproc; /** * Regexp for import statements. (Used from Processing source) */ final public String importRegexp = "(?:^|;)\\s*(import\\s+)((?:static\\s+)?\\S+)(\\s*;)"; /** * Regexp for function declarations. (Used from Processing source) */ final Pattern FUNCTION_DECL = Pattern .compile("(^|;)\\s*((public|private|protected|final|static)\\s+)*" + "(void|int|float|double|String|char|byte|boolean)" + "(\\s*\\[\\s*\\])?\\s+[a-zA-Z0-9]+\\s*\\(", Pattern.MULTILINE); protected ErrorMessageSimplifier errorMsgSimplifier; public ErrorCheckerService(DebugEditor debugEditor) { ensureMinP5Version(); this.editor = debugEditor; stopThread = new AtomicBoolean(false); pauseThread = new AtomicBoolean(false); problemsList = new ArrayList(); classpathJars = new ArrayList(); initParser(); //initializeErrorWindow(); xqpreproc = new XQPreprocessor(); PdePreprocessor pdePrepoc = new PdePreprocessor(null); defaultImportsOffset = pdePrepoc.getCoreImports().length + pdePrepoc.getDefaultImports().length + 1; astGenerator = new ASTGenerator(this); syntaxErrors = new AtomicBoolean(true); containsErrors = new AtomicBoolean(true); errorMsgSimplifier = new ErrorMessageSimplifier(); tempErrorLog = new TreeMap(); sketchChangedListener = new SketchChangedListener(); // for (final SketchCode sc : editor.getSketch().getCode()) { // sc.getDocument().addDocumentListener(sketchChangedListener); // } } /** * Initializes ASTParser */ protected void initParser() { try { parser = ASTParser.newParser(AST.JLS4); } catch (Exception e) { System.err.println("Experimental Mode initialization failed. " + "Are you running the right version of Processing? "); pauseThread(); } catch (Error e) { System.err.println("Experimental Mode initialization failed. "); e.printStackTrace(); pauseThread(); } } /** * Initialiazes the Error Window */ /*public void initializeErrorWindow() { if (editor == null) { return; } if (errorWindow != null) { return; } final ErrorCheckerService thisService = this; final DebugEditor thisEditor = editor; EventQueue.invokeLater(new Runnable() { public void run() { try { errorWindow = new ErrorWindow(thisEditor, thisService); // errorWindow.setVisible(true); editor.toFront(); errorWindow.errorTable.setFocusable(false); editor.setSelection(0, 0); } catch (Exception e) { e.printStackTrace(); } } }); }*/ /** * Ensure user is running the minimum P5 version */ public void ensureMinP5Version(){ //TODO: Now defunct? // Processing 2.1.2 - Revision 0225 if(Base.getRevision() < 225){ // System.err.println("ERROR: PDE X requires Processing 2.1.2 or higher."); Base.showWarning("Error", "ERROR: PDE X requires Processing 2.1.2 or higher.", null); } } /** * Error checking doesn't happen before this interval has ellapsed since the * last runManualErrorCheck() call. */ private final static long errorCheckInterval = 500; /** * Bypass sleep time */ private volatile boolean noSleep = false; /** * The way the error checking happens is: DocumentListeners are added * to each SketchCode object. Whenever the document is edited, runManualErrorCheck() * is called. Internally, an atomic integer counter is incremented. * The ECS thread checks the value of this counter evey sleepTime seconds. * If the counter is non zero, error checking is done(in the ECS thread) * and the counter is reset. */ public void run() { stopThread.set(false); checkCode(); lastErrorCheckCall = System.currentTimeMillis(); if(!hasSyntaxErrors()) editor.showProblemListView(XQConsoleToggle.CONSOLE); // Make sure astGen has at least one CU to start with // This is when the loaded sketch already has syntax errors. // Completion wouldn't be complete, but it'd be still something // better than nothing astGenerator.buildAST(cu); handleErrorCheckingToggle(); while (!stopThread.get()) { try { // Take a nap. if(!noSleep) { Thread.sleep(sleepTime); } else { noSleep = false; log("Didn't sleep!"); } } catch (Exception e) { log("Oops! [ErrorCheckerThreaded]: " + e); // e.printStackTrace(); } updatePaintedThingys(); updateEditorStatus(); updateSketchCodeListeners(); if (pauseThread.get()) continue; if(textModified.get() == 0) continue; // Check if a certain interval has passed after the call. Only then // begin error check. Helps prevent unnecessary flickering. See #2677 if (System.currentTimeMillis() - lastErrorCheckCall > errorCheckInterval) { log("Interval passed, starting error check"); checkCode(); checkForMissingImports(); } } astGenerator.disposeAllWindows(); compilationChecker = null; checkerClass = null; classLoader = null; System.gc(); logE("Thread stopped: " + editor.getSketch().getName()); System.gc(); } protected void updateSketchCodeListeners() { for (final SketchCode sc : editor.getSketch().getCode()) { boolean flag = false; if (sc.getDocument() == null || ((SyntaxDocument) sc.getDocument()).getDocumentListeners() == null) continue; for (DocumentListener dl : ((SyntaxDocument)sc.getDocument()).getDocumentListeners()) { if(dl.equals(sketchChangedListener)){ flag = true; break; } } if(!flag){ // log("Adding doc listener to " + sc.getPrettyName()); sc.getDocument().addDocumentListener(sketchChangedListener); } } } protected void checkForMissingImports() { if(!ExperimentalMode.importSuggestEnabled) return; for (Problem p : problemsList) { if(p.getIProblem().getID() == IProblem.UndefinedType) { String args[] = p.getIProblem().getArguments(); if (args.length > 0) { String missingClass = args[0]; log("Will suggest for type:" + missingClass); //astGenerator.suggestImports(missingClass); } } } } protected ASTGenerator astGenerator; public ASTGenerator getASTGenerator() { return astGenerator; } /** * This thing acts as an event queue counter of sort. * Since error checking happens on demand, anytime this counter * goes above 0, error check is triggered, and counter reset. * It's thread safe to avoid any mess. */ protected AtomicInteger textModified = new AtomicInteger(); /** * Time stamp of last runManualErrorCheck() call. */ private volatile long lastErrorCheckCall = 0; /** * Triggers error check */ public void runManualErrorCheck() { // log("Error Check."); textModified.incrementAndGet(); lastErrorCheckCall = System.currentTimeMillis(); } public void quickErrorCheck() { //TODO: Experimental, lookout for threading related issues noSleep = true; log("quickErrorCheck()"); } protected SketchChangedListener sketchChangedListener; protected class SketchChangedListener implements DocumentListener{ private SketchChangedListener(){ } @Override public void insertUpdate(DocumentEvent e) { if (ExperimentalMode.errorCheckEnabled){ runManualErrorCheck(); //log("doc insert update, man error check.."); } } @Override public void removeUpdate(DocumentEvent e) { if (ExperimentalMode.errorCheckEnabled){ runManualErrorCheck(); //log("doc remove update, man error check.."); } } @Override public void changedUpdate(DocumentEvent e) { if (ExperimentalMode.errorCheckEnabled){ runManualErrorCheck(); //log("doc changed update, man error check.."); } } } /** * state = 1 > syntax check done
* state = 2 > compilation check done */ public int compilationUnitState = 0; protected boolean checkCode() { // log("checkCode() " + textModified.get()); lastTimeStamp = System.currentTimeMillis(); try { sourceCode = preprocessCode(editor.getSketch().getMainProgram()); compilationUnitState = 0; syntaxCheck(); // log(editor.getSketch().getName() + "1 MCO " + mainClassOffset); // No syntax errors, proceed for compilation check, Stage 2. //if(hasSyntaxErrors()) astGenerator.buildAST(null); if (!hasSyntaxErrors()) { } if (problems.length == 0 && editor.compilationCheckEnabled) { //mainClassOffset++; // just a hack. sourceCode = xqpreproc.doYourThing(sourceCode, programImports); prepareCompilerClasspath(); // mainClassOffset = xqpreproc.mainClassOffset; // tiny, but // // significant // if (staticMode) { // mainClassOffset++; // Extra line for setup() decl. // } // log(sourceCode); // log("--------------------------"); compileCheck(); // log(editor.getSketch().getName() + "2 MCO " + mainClassOffset); } astGenerator.buildAST(cu); if(!ExperimentalMode.errorCheckEnabled){ problemsList.clear(); log("Error Check disabled, so not updating UI."); } calcPDEOffsetsForProbList(); updateErrorTable(); editor.updateErrorBar(problemsList); updateEditorStatus(); editor.getTextArea().repaint(); updatePaintedThingys(); editor.updateErrorToggle(); int x = textModified.get(); //log("TM " + x); if (x >= 2) { textModified.set(2); x = 2; } if (x > 0) textModified.set(x - 1); else textModified.set(0); return true; } catch (Exception e) { log("Oops! [ErrorCheckerService.checkCode]: " + e); e.printStackTrace(); } return false; } protected AtomicBoolean syntaxErrors, containsErrors; public boolean hasSyntaxErrors(){ return syntaxErrors.get(); } public boolean hasErrors(){ return containsErrors.get(); } protected TreeMap tempErrorLog; protected void syntaxCheck() { syntaxErrors.set(true); containsErrors.set(true); parser.setSource(sourceCode.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); Map options = JavaCore.getOptions(); JavaCore.setComplianceOptions(JavaCore.VERSION_1_6, options); options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_6); options.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, JavaCore.ENABLED); parser.setCompilerOptions(options); if (cu == null) cu = (CompilationUnit) parser.createAST(null); else { synchronized (cu) { cu = (CompilationUnit) parser.createAST(null); } } compilationUnitState = 1; synchronized (problemsList) { // Store errors returned by the ast parser problems = cu.getProblems(); // log("Problem Count: " + problems.length); // Populate the probList problemsList = new ArrayList(); for (int i = 0; i < problems.length; i++) { int a[] = calculateTabIndexAndLineNumber(problems[i].getSourceLineNumber()); Problem p = new Problem(problems[i], a[0], a[1]); problemsList.add(p); // log(problems[i].getMessage()); // for (String j : problems[i].getArguments()) { // log("arg " + j); // } // log(p.toString()); } if (problems.length == 0) { syntaxErrors.set(false); containsErrors.set(false); parser.setSource(sourceCode.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); parser.setCompilerOptions(options); lastCorrectCU = (CompilationUnit) parser.createAST(null); } else { syntaxErrors.set(true); containsErrors.set(true); } } } protected URLClassLoader classLoader; protected void compileCheck() { // CU needs to be updated coz before compileCheck xqpreprocessor is run on // the source code which makes some further changes //TODO Check if this breaks things parser.setSource(sourceCode.toCharArray()); parser.setKind(ASTParser.K_COMPILATION_UNIT); Map options = JavaCore.getOptions(); JavaCore.setComplianceOptions(JavaCore.VERSION_1_6, options); options.put(JavaCore.COMPILER_SOURCE, JavaCore.VERSION_1_6); options.put(JavaCore.COMPILER_DOC_COMMENT_SUPPORT, JavaCore.ENABLED); parser.setCompilerOptions(options); if (compileCheckCU == null) compileCheckCU = (CompilationUnit) parser.createAST(null); else { synchronized (compileCheckCU) { compileCheckCU = (CompilationUnit) parser.createAST(null); } } if(!hasSyntaxErrors()) lastCorrectCU = compileCheckCU; cu = compileCheckCU; compilationUnitState = 2; // Currently (Sept, 2012) I'm using Java's reflection api to load the // CompilationChecker class(from CompilationChecker.jar) that houses the // Eclispe JDT compiler, and call its getErrorsAsObj method to obtain // errors. This way, I'm able to add the paths of contributed libraries // to the classpath of CompilationChecker, dynamically. The eclipse compiler // needs all referenced libraries in the classpath. Totally a hack. If you find // a better method, do let me know. try { // NOTE TO SELF: If classpath contains null Strings // URLClassLoader shoots NPE bullets. // If imports have changed, reload classes with new classpath. if (loadCompClass) { classpath = new URL[classpathJars.size()]; int ii = 0; for (; ii < classpathJars.size(); ii++) { classpath[ii] = classpathJars.get(ii); } compilationChecker = null; classLoader = null; System.gc(); // log("CP Len -- " + classpath.length); classLoader = new URLClassLoader(classpath); compilationChecker = new CompilationChecker(); loadCompClass = false; } if (compilerSettings == null) { prepareCompilerSetting(); } synchronized (problemsList) { problems = compilationChecker.getErrors(className, sourceCode, compilerSettings, classLoader); if (problems == null) { return; } for (int i = 0; i < problems.length; i++) { IProblem problem = problems[i]; // added a -1 to line number because in compile check code // an extra package statement is added, so all line numbers // are increased by 1 int a[] = calculateTabIndexAndLineNumber(problem.getSourceLineNumber() - 1); Problem p = new Problem(problem, a[0], a[1]); if (problem.isError()) { p.setType(Problem.ERROR); containsErrors.set(true); // set flag } if (problem.isWarning()) { p.setType(Problem.WARNING); } // If warnings are disabled, skip 'em if (p.isWarning() && !ExperimentalMode.warningsEnabled) { continue; } problemsList.add(p); } } } catch (Exception e) { System.err.println("compileCheck() problem." + e); e.printStackTrace(); pauseThread(); } catch (NoClassDefFoundError e) { System.err .println(e + " compileCheck() problem. Somebody tried to mess with Experimental Mode files."); pauseThread(); } catch(OutOfMemoryError e) { System.err.println("Processing has used up its maximum alloted memory. Please close some Processing " + " windows and then reopen this sketch."); pauseThread(); } // log("Compilecheck, Done."); } /** * Calculates PDE Offsets from Java Offsets for Problems */ private void calcPDEOffsetsForProbList() { try { PlainDocument javaSource = new PlainDocument(); // Code in pde tabs stored as PlainDocument PlainDocument pdeTabs[] = new PlainDocument[editor.getSketch() .getCodeCount()]; // log("calcPDEOffsetsForProbList() mco: " + mainClassOffset + " CU state: " // + compilationUnitState); javaSource.insertString(0, sourceCode, null); for (int i = 0; i < pdeTabs.length; i++) { SketchCode sc = editor.getSketch().getCode(i); pdeTabs[i] = new PlainDocument(); if (editor.getSketch().getCurrentCode().equals(sc)) { pdeTabs[i].insertString(0, sc.getDocument().getText(0, sc.getDocument() .getLength()), null); } else { pdeTabs[i].insertString(0, sc.getProgram(), null); } } int pkgNameOffset = ("package " + className + ";\n").length(); // package name is added only during compile check if(compilationUnitState != 2) pkgNameOffset = 0; for (Problem p : problemsList) { int prbStart = p.getIProblem().getSourceStart() - pkgNameOffset, prbEnd = p .getIProblem().getSourceEnd() - pkgNameOffset; // log(p.toString()); // log("IProblem Start " + prbStart + ", End " + prbEnd); int javaLineNumber = p.getSourceLineNumber() - ((compilationUnitState != 2) ? 1 : 2); Element lineElement = javaSource.getDefaultRootElement() .getElement(javaLineNumber); if (lineElement == null) { log("calcPDEOffsetsForProbList(): Couldn't fetch javalinenum " + javaLineNumber + "\nProblem: " + p); p.setPDEOffsets(-1,-1); continue; } String javaLine = javaSource .getText(lineElement.getStartOffset(), lineElement.getEndOffset() - lineElement.getStartOffset()); Element pdeLineElement = pdeTabs[p.getTabIndex()] .getDefaultRootElement().getElement(p.getLineNumber()); if (pdeLineElement == null) { log("calcPDEOffsetsForProbList(): Couldn't fetch pdelinenum " + javaLineNumber + "\nProblem: " + p); p.setPDEOffsets(-1,-1); continue; } String pdeLine = pdeTabs[p.getTabIndex()] .getText(pdeLineElement.getStartOffset(), pdeLineElement.getEndOffset() - pdeLineElement.getStartOffset()); //log("calcPDEOffsetsForProbList(): P " + pdeLine); //log("calcPDEOffsetsForProbList(): J " + javaLine); OffsetMatcher ofm = new OffsetMatcher(pdeLine, javaLine); //log(""); int pdeOffset = ofm.getPdeOffForJavaOff(prbStart - lineElement.getStartOffset(), (prbEnd - prbStart + 1)); // astGenerator.highlightPDECode(p.getTabIndex(), p.getLineNumber(), // pdeOffset, (prbEnd - prbStart + 1)); p.setPDEOffsets(pdeOffset, pdeOffset + prbEnd - prbStart); } } catch (BadLocationException ble) { ble.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } } public CompilationUnit getLastCorrectCU(){ return lastCorrectCU; } public CompilationUnit getLatestCU(){ return compileCheckCU; } private int loadClassCounter = 0; public URLClassLoader getSketchClassLoader() { loadClassCounter++; if(loadClassCounter > 100){ loadClassCounter = 0; classLoader = null; System.gc(); classLoader = new URLClassLoader(classpath); } return classLoader; } /** * Processes import statements to obtain classpaths of contributed * libraries. This would be needed for compilation check. Also, adds * stuff(jar files, class files, candy) from the code folder. And it looks * messed up. * */ protected void prepareCompilerClasspath() { if (!loadCompClass) { return; } synchronized (classpathJars) { // log("1.."); classpathJars = new ArrayList(); String entry = ""; boolean codeFolderChecked = false; for (ImportStatement impstat : programImports) { String item = impstat.getImportName(); int dot = item.lastIndexOf('.'); entry = (dot == -1) ? item : item.substring(0, dot); entry = entry.substring(6).trim(); // log("Entry--" + entry); if (ignorableImport(entry)) { // log("Ignoring: " + entry); continue; } Library library = null; // Try to get the library classpath and add it to the list try { library = editor.getMode().getLibrary(entry); // log("lib->" + library.getClassPath() + "<-"); String libraryPath[] = PApplet.split(library.getClassPath() .substring(1).trim(), File.pathSeparatorChar); for (int i = 0; i < libraryPath.length; i++) { // log(entry + " ::" // + new File(libraryPath[i]).toURI().toURL()); classpathJars.add(new File(libraryPath[i]).toURI().toURL()); } // log("-- "); // classpath[count] = (new File(library.getClassPath() // .substring(1))).toURI().toURL(); // log(" found "); // log(library.getClassPath().substring(1)); } catch (Exception e) { if (library == null && !codeFolderChecked) { // log(1); // Look around in the code folder for jar files if (editor.getSketch().hasCodeFolder()) { File codeFolder = editor.getSketch().getCodeFolder(); // get a list of .jar files in the "code" folder // (class files in subfolders should also be picked up) String codeFolderClassPath = Base .contentsToClassPath(codeFolder); codeFolderChecked = true; if (codeFolderClassPath.equalsIgnoreCase("")) { System.err.println("Experimental Mode: Yikes! Can't find \"" + entry + "\" library! Line: " + impstat.getLineNumber() + " in tab: " + editor.getSketch().getCode(impstat.getTab()) .getPrettyName()); System.out .println("Please make sure that the library is present in /libraries folder or in the code folder of your sketch"); } else { String codeFolderPath[] = PApplet.split( codeFolderClassPath.substring(1).trim(), File.pathSeparatorChar); try { for (int i = 0; i < codeFolderPath.length; i++) { classpathJars.add(new File(codeFolderPath[i]) .toURI().toURL()); } } catch (Exception e2) { System.out .println("Yikes! codefolder, prepareImports(): " + e2); } } } else { System.err.println("Experimental Mode: Yikes! Can't find \"" + entry + "\" library! Line: " + impstat.getLineNumber() + " in tab: " + editor.getSketch().getCode(impstat.getTab()) .getPrettyName()); System.out .println("Please make sure that the library is present in /libraries folder or in the code folder of your sketch"); } } else { System.err .println("Yikes! There was some problem in prepareImports(): " + e); System.err.println("I was processing: " + entry); // e.printStackTrace(); } } } } astGenerator.loadJars(); // update jar file for completion lookup } /** * Ignore processing packages, java.*.*. etc. * * @param packageName * @return boolean */ protected boolean ignorableImport(String packageName) { // packageName.startsWith("processing.") // || if (packageName.startsWith("java.") || packageName.startsWith("javax.")) { return true; } return false; } /** * Various option for JDT Compiler */ @SuppressWarnings("rawtypes") protected Map compilerSettings; /** * Sets compiler options for JDT Compiler */ @SuppressWarnings({ "unchecked", "rawtypes" }) protected void prepareCompilerSetting() { compilerSettings = new HashMap(); compilerSettings.put(CompilerOptions.OPTION_LineNumberAttribute, CompilerOptions.GENERATE); compilerSettings.put(CompilerOptions.OPTION_SourceFileAttribute, CompilerOptions.GENERATE); compilerSettings.put(CompilerOptions.OPTION_Source, CompilerOptions.VERSION_1_6); compilerSettings.put(CompilerOptions.OPTION_ReportUnusedImport, CompilerOptions.IGNORE); compilerSettings.put(CompilerOptions.OPTION_ReportMissingSerialVersion, CompilerOptions.IGNORE); compilerSettings.put(CompilerOptions.OPTION_ReportRawTypeReference, CompilerOptions.IGNORE); compilerSettings.put( CompilerOptions.OPTION_ReportUncheckedTypeOperation, CompilerOptions.IGNORE); } /** * Updates the error table in the Error Window. */ public void updateErrorTable() { try { String[][] errorData = new String[problemsList.size()][3]; for (int i = 0; i < problemsList.size(); i++) { errorData[i][0] = problemsList.get(i).getMessage(); ////TODO: this is temporary //+ " : " + errorMsgSimplifier.getIDName(problemsList.get(i).getIProblem().getID()); errorData[i][1] = editor.getSketch() .getCode(problemsList.get(i).getTabIndex()).getPrettyName(); errorData[i][2] = (problemsList.get(i).getLineNumber() + 1) + ""; // Added +1 because lineNumbers internally are 0-indexed //TODO: This is temporary if (tempErrorLog.size() < 200) tempErrorLog.put(problemsList.get(i).getMessage(), problemsList .get(i).getIProblem()); if(!ExperimentalMode.importSuggestEnabled) continue; Problem p = problemsList.get(i); if(p.getIProblem().getID() == IProblem.UndefinedType) { String args[] = p.getIProblem().getArguments(); if (args.length > 0) { String missingClass = args[0]; // log("Will suggest for type:" + missingClass); //astGenerator.suggestImports(missingClass); String[] si = astGenerator.getSuggestImports(missingClass); if(si != null && si.length > 0){ p.setImportSuggestions(si); errorData[i][0] = "" + problemsList.get(i).getMessage() + " (Import Suggestions available)"; } } } } DefaultTableModel tm = new DefaultTableModel(errorData, XQErrorTable.columnNames); // Update error table in the editor editor.updateTable(tm); /* if (errorWindow != null) { if (errorWindow.isVisible()) { errorWindow.updateTable(tm); } // A rotating slash animation on the title bar to show // that error checker thread is running slashAnimationIndex++; if (slashAnimationIndex == slashAnimation.length) { slashAnimationIndex = 0; } if (editor != null) { String info = slashAnimation[slashAnimationIndex] + " T:" + (System.currentTimeMillis() - lastTimeStamp) + "ms"; errorWindow.setTitle("Problems - " + editor.getSketch().getName() + " " + info); } }*/ } catch (Exception e) { log("Exception at updateErrorTable() " + e); e.printStackTrace(); pauseThread(); } } /** * Repaints the textarea if required */ public void updatePaintedThingys() { currentTab = editor.getSketch().getCodeIndex( editor.getSketch().getCurrentCode()); //log("Tab changed " + currentTab + " LT " + lastTab); if (currentTab != lastTab) { textModified.set(5); lastTab = currentTab; editor.getTextArea().repaint(); editor.statusEmpty(); return; } } protected int lastCaretLine = -1; /** * Updates editor status bar, depending on whether the caret is on an error * line or not */ public void updateEditorStatus() { if(editor.getStatusMode() == EditorStatus.EDIT) return; // editor.statusNotice("Position: " + // editor.getTextArea().getCaretLine()); if(ExperimentalMode.errorCheckEnabled) synchronized (editor.errorBar.errorPoints) { for (ErrorMarker emarker : editor.errorBar.errorPoints) { if (emarker.getProblem().getLineNumber() == editor.getTextArea() .getCaretLine()) { if (emarker.getType() == ErrorMarker.Warning) { editor.statusMessage(emarker.getProblem().getMessage(), DebugEditor.STATUS_INFO); //+ " : " + errorMsgSimplifier.getIDName(emarker.problem.getIProblem().getID())); //TODO: this is temporary } else { editor.statusMessage(emarker.getProblem().getMessage(), DebugEditor.STATUS_COMPILER_ERR); //+ " : " + errorMsgSimplifier.getIDName(emarker.problem.getIProblem().getID())); } return; } } } // This line isn't an error line anymore, so probably just clear it if (editor.statusMessageType == DebugEditor.STATUS_COMPILER_ERR) { editor.statusEmpty(); return; } // if (editor.ta.getCaretLine() != lastCaretLine) { // editor.statusEmpty(); // lastCaretLine = editor.ta.getCaretLine(); // } } /** * Maps offset from java code to pde code. Returns a bunch of offsets as array * * @param line * - line number in java code * @param offset * - offset from the start of the 'line' * @return int[0] - tab number, int[1] - line number in the int[0] tab, int[2] * - line start offset, int[3] - offset from line start. int[2] and * int[3] are on TODO */ public int[] JavaToPdeOffsets(int line, int offset){ int codeIndex = 0; int x = line - mainClassOffset; if (x < 0) { // log("Negative line number " // + problem.getSourceLineNumber() + " , offset " // + mainClassOffset); x = line - 2; // Another -1 for 0 index if (x < programImports.size() && x >= 0) { ImportStatement is = programImports.get(x); // log(is.importName + ", " + is.tab + ", " // + is.lineNumber); return new int[] { is.getTab(), is.getLineNumber() }; } else { // Some seriously ugly stray error, just can't find the source // line! Simply return first line for first tab. return new int[] { 0, 1 }; } } try { for (SketchCode sc : editor.getSketch().getCode()) { if (sc.isExtension("pde")) { int len = 0; if (editor.getSketch().getCurrentCode().equals(sc)) { len = Base.countLines(sc.getDocument().getText(0, sc.getDocument().getLength())) + 1; } else { len = Base.countLines(sc.getProgram()) + 1; } // log("x,len, CI: " + x + "," + len + "," // + codeIndex); if (x >= len) { // We're in the last tab and the line count is greater // than the no. // of lines in the tab, if (codeIndex >= editor.getSketch().getCodeCount() - 1) { // log("Exceeds lc " + x + "," + len // + problem.toString()); // x = len x = editor.getSketch().getCode(codeIndex) .getLineCount(); // TODO: Obtain line having last non-white space // character in the code. break; } else { x -= len; codeIndex++; } } else { if (codeIndex >= editor.getSketch().getCodeCount()) { codeIndex = editor.getSketch().getCodeCount() - 1; } break; } } } } catch (Exception e) { System.err .println("Things got messed up in ErrorCheckerService.JavaToPdeOffset()"); } return new int[] { codeIndex, x }; } public String getPDECodeAtLine(int tab, int linenumber){ if(linenumber < 0) return null; editor.getSketch().setCurrentCode(tab); return editor.ta.getLineText(linenumber); } /** * Calculates the tab number and line number of the error in that particular * tab. Provides mapping between pure java and pde code. * * @param problem * - IProblem * @return int[0] - tab number, int[1] - line number */ public int[] calculateTabIndexAndLineNumber(int javalineNumber) { // String[] lines = {};// = PApplet.split(sourceString, '\n'); int codeIndex = 0; int x = javalineNumber - mainClassOffset; if (x < 0) { // log("Negative line number " // + problem.getSourceLineNumber() + " , offset " // + mainClassOffset); x = javalineNumber - 2; // Another -1 for 0 index if (x < programImports.size() && x >= 0) { ImportStatement is = programImports.get(x); // log(is.importName + ", " + is.tab + ", " // + is.lineNumber); return new int[] { is.getTab(), is.getLineNumber() }; } else { // Some seriously ugly stray error, just can't find the source // line! Simply return first line for first tab. return new int[] { 0, 1 }; } } try { for (SketchCode sc : editor.getSketch().getCode()) { if (sc.isExtension("pde")) { int len = 0; if (editor.getSketch().getCurrentCode().equals(sc)) { len = Base.countLines(sc.getDocument().getText(0, sc.getDocument().getLength())) + 1; } else { len = Base.countLines(sc.getProgram()) + 1; } // log("x,len, CI: " + x + "," + len + "," // + codeIndex); if (x >= len) { // We're in the last tab and the line count is greater // than the no. // of lines in the tab, if (codeIndex >= editor.getSketch().getCodeCount() - 1) { // log("Exceeds lc " + x + "," + len // + problem.toString()); // x = len x = editor.getSketch().getCode(codeIndex) .getLineCount(); // TODO: Obtain line having last non-white space // character in the code. break; } else { x -= len; codeIndex++; } } else { if (codeIndex >= editor.getSketch().getCodeCount()) { codeIndex = editor.getSketch().getCodeCount() - 1; } break; } } } } catch (Exception e) { System.err .println("Things got messed up in ErrorCheckerService.calculateTabIndexAndLineNumber()"); } return new int[] { codeIndex, x }; } /** * Returns line number of corresponding java source * @param tab * @param pdeLineNum * @return */ public int getJavaLineNumFromPDElineNum(int tab, int pdeLineNum){ int jLineNum = programImports.size() + 1; for (int i = 0; i < tab; i++) { SketchCode sc = editor.getSketch().getCode(i); int len = Base.countLines(sc.getProgram()) + 1; jLineNum += len; } return jLineNum; } /** * Fetches code from the editor tabs and pre-processes it into parsable pure * java source. And there's a difference between parsable and compilable. * XQPrerocessor.java makes this code compilable.
* Handles:
  • Removal of import statements
  • Conversion of int(), * char(), etc to PApplet.parseInt(), etc.
  • Replacing '#' with 0xff for * color representation
  • Converts all 'color' datatypes to int * (experimental)
  • Appends class declaration statement after determining * the mode the sketch is in - ACTIVE or STATIC * * @return String - Pure java representation of PDE code. Note that this * code is not yet compile ready. */ protected String preprocessCode(String pdeCode) { programImports = new ArrayList(); StringBuilder rawCode = new StringBuilder(); try { for (SketchCode sc : editor.getSketch().getCode()) { if (sc.isExtension("pde")) { try { if (editor.getSketch().getCurrentCode().equals(sc)) { rawCode.append(scrapImportStatements(sc.getDocument() .getText(0, sc.getDocument() .getLength()), editor.getSketch() .getCodeIndex(sc))); } else { rawCode.append(scrapImportStatements(sc.getProgram(), editor .getSketch().getCodeIndex(sc))); } rawCode.append('\n'); } catch (Exception e) { System.err.println("Exception in preprocessCode() - bigCode " + e.toString()); } rawCode.append('\n'); } } } catch (Exception e) { log("Exception in preprocessCode()"); } String sourceAlt = rawCode.toString(); // Replace comments with whitespaces // sourceAlt = scrubComments(sourceAlt); // Find all int(*), replace with PApplet.parseInt(*) // \bint\s*\(\s*\b , i.e all exclusive "int(" String dataTypeFunc[] = { "int", "char", "float", "boolean", "byte" }; for (String dataType : dataTypeFunc) { String dataTypeRegexp = "\\b" + dataType + "\\s*\\("; Pattern pattern = Pattern.compile(dataTypeRegexp); Matcher matcher = pattern.matcher(sourceAlt); // while (matcher.find()) { // System.out.print("Start index: " + matcher.start()); // log(" End index: " + matcher.end() + " "); // log("-->" + matcher.group() + "<--"); // } sourceAlt = matcher.replaceAll("PApplet.parse" + Character.toUpperCase(dataType.charAt(0)) + dataType.substring(1) + "("); } // Find all #[web color] and replace with 0xff[webcolor] // Should be 6 digits only. final String webColorRegexp = "#{1}[A-F|a-f|0-9]{6}\\W"; Pattern webPattern = Pattern.compile(webColorRegexp); Matcher webMatcher = webPattern.matcher(sourceAlt); while (webMatcher.find()) { // log("Found at: " + webMatcher.start()); String found = sourceAlt.substring(webMatcher.start(), webMatcher.end()); // log("-> " + found); sourceAlt = webMatcher.replaceFirst("0xff" + found.substring(1)); webMatcher = webPattern.matcher(sourceAlt); } // Replace all color data types with int // Regex, Y U SO powerful? final String colorTypeRegex = "color(?![a-zA-Z0-9_])(?=\\[*)(?!(\\s*\\())"; Pattern colorPattern = Pattern.compile(colorTypeRegex); Matcher colorMatcher = colorPattern.matcher(sourceAlt); sourceAlt = colorMatcher.replaceAll("int"); checkForChangedImports(); className = (editor == null) ? "DefaultClass" : editor.getSketch() .getName(); // Check whether the code is being written in STATIC mode(no function // declarations) - append class declaration and void setup() declaration Matcher matcher = FUNCTION_DECL.matcher(sourceAlt); if (!matcher.find()) { sourceAlt = xqpreproc.prepareImports(programImports) + "public class " + className + " extends PApplet {\n" + "public void setup() {\n" + sourceAlt + "\nnoLoop();\n}\n" + "\n}\n"; staticMode = true; } else { sourceAlt = xqpreproc.prepareImports(programImports) + "public class " + className + " extends PApplet {\n" + sourceAlt + "\n}"; staticMode = false; } int position = sourceAlt.indexOf("{") + 1; mainClassOffset = 1; for (int i = 0; i <= position; i++) { if (sourceAlt.charAt(i) == '\n') { mainClassOffset++; } } if(staticMode) { mainClassOffset++; } //mainClassOffset += 2; // Handle unicode characters sourceAlt = substituteUnicode(sourceAlt); // log("-->\n" + sourceAlt + "\n<--"); // log("PDE code processed - " // + editor.getSketch().getName()); sourceCode = sourceAlt; return sourceAlt; } /** * Now defunct. * The super method that highlights any ASTNode in the pde editor =D * @param node * @return true - if highlighting happened correctly. */ private boolean highlightNode(ASTNodeWrapper awrap){ log("Highlighting: " + awrap); try { int pdeoffsets[] = awrap.getPDECodeOffsets(this); int javaoffsets[] = awrap.getJavaCodeOffsets(this); log("offsets: " +pdeoffsets[0] + "," + pdeoffsets[1]+ "," +javaoffsets[1]+ "," + javaoffsets[2]); scrollToErrorLine(editor, pdeoffsets[0], pdeoffsets[1],javaoffsets[1], javaoffsets[2]); return true; } catch (Exception e) { logE("Scrolling failed for " + awrap); // e.printStackTrace(); } return false; } public boolean highlightNode(ASTNode node){ ASTNodeWrapper awrap = new ASTNodeWrapper(node); return highlightNode(awrap); } /** * Scrolls to the error source in code. And selects the line text. Used by * XQErrorTable and ErrorBar * * @param errorIndex * - index of error */ public void scrollToErrorLine(int errorIndex) { if (editor == null) { return; } if (errorIndex < problemsList.size() && errorIndex >= 0) { Problem p = problemsList.get(errorIndex); scrollToErrorLine(p); } } public void scrollToErrorLine(Problem p) { if (editor == null) { return; } if (p == null) return; try { if(p.getPDELineStartOffset() == -1 || p.getPDELineStopOffset() == -1){ // bad offsets, don't highlight, just scroll. editor.toFront(); editor.getSketch().setCurrentCode(p.getTabIndex()); } else { astGenerator.highlightPDECode(p.getTabIndex(), p.getLineNumber(), p.getPDELineStartOffset(), (p.getPDELineStopOffset() - p.getPDELineStartOffset() + 1)); } // scroll, but within boundaries // It's also a bit silly that if parameters to scrollTo() are out of range, // a BadLocation Exception is thrown internally and caught in JTextArea AND // even the stack trace gets printed! W/o letting me catch it later! SMH if (p.getLineNumber() < Base.countLines(editor.textArea().getDocument() .getText(0, editor.textArea().getDocument().getLength())) && p.getLineNumber() >= 0) { editor.getTextArea().scrollTo(p.getLineNumber(), 0); } editor.repaint(); } catch (Exception e) { logE(e + " : Error while selecting text in scrollToErrorLine(), for problem: " + p); } // log("---"); } /** * Static method for scroll to a particular line in the PDE. Also highlights * the length of the text. Requires the editor instance as arguement. * * @param edt * @param tabIndex * @param lineNoInTab * - line number in the corresponding tab * @param lineStartOffset * - selection start offset(from line start non-whitespace offset) * @param length * - length of selection * @return - true, if scroll was successful */ public static boolean scrollToErrorLine(Editor edt, int tabIndex, int lineNoInTab, int lineStartOffset, int length) { if (edt == null) { return false; } try { edt.toFront(); edt.getSketch().setCurrentCode(tabIndex); int lsno = edt.getTextArea() .getLineStartNonWhiteSpaceOffset(lineNoInTab - 1) + lineStartOffset; edt.setSelection(lsno, lsno + length); edt.getTextArea().scrollTo(lineNoInTab - 1, 0); edt.repaint(); log(lineStartOffset + " LSO,len " + length); } catch (Exception e) { System.err.println(e + " : Error while selecting text in static scrollToErrorLine()"); e.printStackTrace(); return false; } return true; } /** * Checks if import statements in the sketch have changed. If they have, * compiler classpath needs to be updated. */ protected void checkForChangedImports() { // log("Imports: " + programImports.size() + // " Prev Imp: " // + previousImports.size()); if (programImports.size() != previousImports.size()) { // log(1); loadCompClass = true; previousImports = programImports; } else { for (int i = 0; i < programImports.size(); i++) { if (!programImports.get(i).getImportName().equals(previousImports .get(i).getImportName())) { // log(2); loadCompClass = true; previousImports = programImports; break; } } } // log("load..? " + loadCompClass); } protected int pdeImportsCount; public int getPdeImportsCount() { return pdeImportsCount; } /** * Removes import statements from tabSource, replaces each with white spaces * and adds the import to the list of program imports * * @param tabProgram * - Code in a tab * @param tabNumber * - index of the tab * @return String - Tab code with imports replaced with white spaces */ protected String scrapImportStatements(String tabProgram, int tabNumber) { //TODO: Commented out imports are still detected as main imports. pdeImportsCount = 0; String tabSource = new String(tabProgram); do { // log("-->\n" + sourceAlt + "\n<--"); String[] pieces = PApplet.match(tabSource, importRegexp); // Stop the loop if we've removed all the import lines if (pieces == null) { break; } String piece = pieces[1] + pieces[2] + pieces[3]; int len = piece.length(); // how much to trim out // programImports.add(piece); // the package name // find index of this import in the program int idx = tabSource.indexOf(piece); // System.out.print("Import -> " + piece); // log(" - " // + Base.countLines(tabSource.substring(0, idx)) + " tab " // + tabNumber); programImports.add(new ImportStatement(piece, tabNumber, Base .countLines(tabSource.substring(0, idx)))); // Remove the import from the main program // Substitute with white spaces String whiteSpace = ""; for (int j = 0; j < piece.length(); j++) { whiteSpace += " "; } tabSource = tabSource.substring(0, idx) + whiteSpace + tabSource.substring(idx + len); pdeImportsCount++; } while (true); // log(tabSource); return tabSource; } /** * Replaces non-ascii characters with their unicode escape sequences and * stuff. Used as it is from * processing.src.processing.mode.java.preproc.PdePreprocessor * * @param program * - Input String containing non ascii characters * @return String - Converted String */ public static String substituteUnicode(String program) { // check for non-ascii chars (these will be/must be in unicode format) char p[] = program.toCharArray(); int unicodeCount = 0; for (int i = 0; i < p.length; i++) { if (p[i] > 127) { unicodeCount++; } } if (unicodeCount == 0) { return program; } // if non-ascii chars are in there, convert to unicode escapes // add unicodeCount * 5.. replacing each unicode char // with six digit uXXXX sequence (xxxx is in hex) // (except for nbsp chars which will be a replaced with a space) int index = 0; char p2[] = new char[p.length + unicodeCount * 5]; for (int i = 0; i < p.length; i++) { if (p[i] < 128) { p2[index++] = p[i]; } else if (p[i] == 160) { // unicode for non-breaking space p2[index++] = ' '; } else { int c = p[i]; p2[index++] = '\\'; p2[index++] = 'u'; char str[] = Integer.toHexString(c).toCharArray(); // add leading zeros, so that the length is 4 // for (int i = 0; i < 4 - str.length; i++) p2[index++] = '0'; for (int m = 0; m < 4 - str.length; m++) p2[index++] = '0'; System.arraycopy(str, 0, p2, index, str.length); index += str.length; } } return new String(p2, 0, index); } public void handleErrorCheckingToggle(){ if (!ExperimentalMode.errorCheckEnabled) { // unticked Menu Item // pauseThread(); log(editor.getSketch().getName() + " - Error Checker paused."); editor.errorBar.errorPoints.clear(); problemsList.clear(); updateErrorTable(); updateEditorStatus(); editor.getTextArea().repaint(); editor.errorBar.repaint(); } else { //resumeThread(); log(editor.getSketch().getName() + " - Error Checker resumed."); runManualErrorCheck(); } } /** * Stops the Error Checker Service thread */ public void stopThread() { logE("Stopping thread: " + editor.getSketch().getName()); stopThread.set(true); } /** * Pauses the Error Checker Service thread */ public void pauseThread() { pauseThread.set(true); } /** * Resumes the Error Checker Service thread */ public void resumeThread() { pauseThread.set(false); } public DebugEditor getEditor() { return editor; } // public static void log(String message){ // if(ExperimentalMode.DEBUG) // log(message); // } // // public static void log2(String message){ // if(ExperimentalMode.DEBUG) // System.out.print(message); // } public ArrayList getProgramImports() { return programImports; } }