package ij.plugin.frame;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.awt.datatransfer.*;                                                                                                                                                                                                                                                                                                                                                                                 
import ij.*;
import ij.gui.*;
import ij.util.Tools;
import ij.text.*;
import ij.macro.*;
import ij.plugin.MacroInstaller;
import ij.plugin.Commands;
import ij.plugin.Macro_Runner;
import ij.plugin.JavaScriptEvaluator;
import ij.io.SaveDialog;

/** This is a simple TextArea based editor for editing and compiling plugins. */
public class Editor extends PlugInFrame implements ActionListener, ItemListener,
    TextListener, KeyListener, MouseListener, ClipboardOwner, MacroConstants, Runnable, Debugger {
    
    /** ImportPackage statements added in front of scripts. Contains no 
    newlines so that lines numbers in error messages are not changed. */
    public static String JavaScriptIncludes =
        "importPackage(Packages.ij);"+
        "importPackage(Packages.ij.gui);"+
        "importPackage(Packages.ij.process);"+
        "importPackage(Packages.ij.measure);"+
        "importPackage(Packages.ij.util);"+
        "importPackage(Packages.ij.macro);"+
        "importPackage(Packages.ij.plugin);"+
        "importPackage(Packages.ij.io);"+
        "importPackage(Packages.ij.plugin.filter);"+
        "importPackage(Packages.ij.plugin.frame);"+
        "importPackage(Packages.ij.plugin.tool);"+
        "importPackage(java.lang);"+
        "importPackage(java.awt);"+
        "importPackage(java.awt.image);"+
        "importPackage(java.awt.geom);"+
        "importPackage(java.util);"+
        "importPackage(java.io);"+
        "function print(s) {IJ.log(s);};";
        
    private static String JS_EXAMPLES =
        "img = IJ.openImage(\"http://imagej.net/images/blobs.gif\")\n"
        +"img = IJ.createImage(\"Untitled\", \"16-bit ramp\", 500, 500, 1)\n"       
        +"img.show()\n"
        +"ip = img.getProcessor()\n"
        +"ip.getStats()\n"
        +"IJ.setAutoThreshold(img, \"IsoData\")\n"
        +"IJ.run(img, \"Analyze Particles...\", \"show=Overlay display clear\")\n"
        +"ip.invert()\n"
        +"ip.blurGaussian(5)\n"  
        +"ip.get(10,10)\n"
        +"ip.set(10,10,222)\n"
        +"(To run, move cursor to end of a line and press 'enter'.\n"
        +"Visible images are automatically updated.)\n";

    public static final int MAX_SIZE=28000, XINC=10, YINC=18;
    public static final int MONOSPACED=1, MENU_BAR=2, RUN_BAR=4, INSTALL_BUTTON=8;
    public static final int MACROS_MENU_ITEMS = 15;
    public static final String INTERACTIVE_NAME = "Interactive Interpreter";
    static final String FONT_SIZE = "editor.font.size";
    static final String FONT_MONO= "editor.font.mono";
    static final String CASE_SENSITIVE= "editor.case-sensitive";
    static final String DEFAULT_DIR= "editor.dir";
    static final String INSERT_SPACES= "editor.spaces";
    static final String TAB_INC= "editor.tab-inc";
    private final static int MACRO=0, JAVASCRIPT=1, BEANSHELL=2, PYTHON=3;
    private final static String[] languages = {"Macro", "JavaScript", "BeanShell", "Python"};
    private final static String[] extensions = {".ijm", ".js", ".bsh", ".py"};  
    public static Editor currentMacroEditor;
    private TextArea ta;
    private String path;
    protected boolean changes;
    private static String searchString = "";
    private static boolean caseSensitive = Prefs.get(CASE_SENSITIVE, true);
    private static int lineNumber = 1;
    private static int xoffset, yoffset;
    private static int nWindows;
    private Menu fileMenu, editMenu;
    private Properties p = new Properties();
    private int[] macroStarts;
    private String[] macroNames;
    private MenuBar mb;
    private Menu macrosMenu;
    private int nMacros;
    private Program pgm;
    private int eventCount;
    private String shortcutsInUse;
    private int inUseCount;
    private MacroInstaller installer;
    private static String defaultDir = Prefs.get(DEFAULT_DIR, null);;
    private boolean dontShowWindow;
    private int[] sizes = {9, 10, 11, 12, 13, 14, 16, 18, 20, 24, 36, 48, 60, 72};
    private int fontSizeIndex = (int)Prefs.get(FONT_SIZE, 6); // defaults to 16-point
    private CheckboxMenuItem monospaced;
    private static boolean wholeWords;
    private boolean isMacroWindow;
    private int debugStart, debugEnd;
    private static TextWindow debugWindow;
    private boolean step;
    private int previousLine;
    private static Editor instance;
    private int runToLine;
    private String downloadUrl;
    private boolean downloading;
    private FunctionFinder functionFinder;
    private ArrayList undoBuffer = new ArrayList();
    private boolean performingUndo;
    private boolean checkForCurlyQuotes;
    private static int tabInc = (int)Prefs.get(TAB_INC, 3);
    private static boolean insertSpaces = Prefs.get(INSERT_SPACES, false);
    private CheckboxMenuItem insertSpacesItem;
    private boolean interactiveMode;
    private Interpreter interpreter;
    private JavaScriptEvaluator evaluator;
    private int messageCount;
    private String rejectMacrosMsg;
    private Button runButton, installButton;
    private Choice language;
    
    public Editor() {
        this(24, 80, 0, MENU_BAR);
    }

    public Editor(String name) {
        this(24, 80, 0, getOptions(name));
    }

    public Editor(int rows, int columns, int fontSize, int options) {
        super("Editor");
        WindowManager.addWindow(this);
        addMenuBar(options);    
        if ((options&RUN_BAR)!=0) {
            Panel panel = new Panel(new FlowLayout(FlowLayout.LEFT, 0, 0));
            runButton = new Button("Run");
            runButton.addActionListener(this);
            panel.setFont(new Font("SansSerif", Font.PLAIN, sizes[fontSizeIndex]));
            panel.add(runButton);           
            if ((options&INSTALL_BUTTON)!=0) {
                installButton = new Button("Install");
                installButton.addActionListener(this);
                panel.add(installButton);
            }           
            language = new Choice();
            for (int i=0; i<languages.length; i++)
                language.addItem(languages[i]);
            language.addItemListener(this);
            panel.add(language);
            add("North", panel);
        }
        ta = new TextArea(rows, columns);
        ta.addTextListener(this);
        ta.addKeyListener(this);
        if (IJ.isLinux()) ta.setBackground(Color.white);
        addKeyListener(IJ.getInstance());  // ImageJ handles keyboard shortcuts
        ta.addMouseListener(this);  // ImageJ handles keyboard shortcuts
        add(ta);
        pack();
        setFont();
        positionWindow();
        if (!IJ.isJava18() && !IJ.isLinux())
            insertSpaces = false;
    }
    
    private static int getOptions(String name) {
        int options = MENU_BAR;
        if (name==null)
            return options;
        if (name.endsWith(".ijm") || name.endsWith(".js") || name.endsWith(".bsh") || name.endsWith(".py"))
            options |= RUN_BAR;
        if (name.endsWith(".ijm"))
            options |= INSTALL_BUTTON;
        return options;
    }
    
    void addMenuBar(int options) {
        mb = new MenuBar();
        if (Menus.getFontSize()!=0) ;
            mb.setFont(Menus.getFont());
        Menu m = new Menu("File");
        m.add(new MenuItem("New...", new MenuShortcut(KeyEvent.VK_N, true)));
        m.add(new MenuItem("Open...", new MenuShortcut(KeyEvent.VK_O)));
        m.add(new MenuItem("Save", new MenuShortcut(KeyEvent.VK_S)));
        m.add(new MenuItem("Save As..."));
        m.add(new MenuItem("Revert"));
        m.add(new MenuItem("Print..."));
        m.addActionListener(this);
        fileMenu = m;
        mb.add(m);
        
        m = new Menu("Edit");
        MenuItem item = null;
        if (IJ.isWindows())
            item = new MenuItem("Undo  Ctrl+Z");
        else
            item = new MenuItem("Undo",new MenuShortcut(KeyEvent.VK_Z));        
        m.add(item);
        m.addSeparator();       
        if (IJ.isWindows())
            item = new MenuItem("Cut  Ctrl+X");
        else
            item = new MenuItem("Cut",new MenuShortcut(KeyEvent.VK_X));
        m.add(item);
        if (IJ.isWindows())
            item = new MenuItem("Copy  Ctrl+C");
        else
            item = new MenuItem("Copy", new MenuShortcut(KeyEvent.VK_C));
        m.add(item);
        if (IJ.isWindows())
            item = new MenuItem("Paste  Ctrl+V");
        else
            item = new MenuItem("Paste",new MenuShortcut(KeyEvent.VK_V));
        m.add(item);
        m.addSeparator();
        m.add(new MenuItem("Find...", new MenuShortcut(KeyEvent.VK_F)));
        m.add(new MenuItem("Find Next", new MenuShortcut(KeyEvent.VK_G)));
        m.add(new MenuItem("Go to Line...", new MenuShortcut(KeyEvent.VK_L)));
        m.addSeparator();
        m.add(new MenuItem("Select All", new MenuShortcut(KeyEvent.VK_A)));
        m.add(new MenuItem("Balance", new MenuShortcut(KeyEvent.VK_B,false)));
        m.add(new MenuItem("Detab..."));
        insertSpacesItem = new CheckboxMenuItem("Tab Key Inserts Spaces");
        insertSpacesItem.addItemListener(this);
        insertSpacesItem.setState(insertSpaces);
        m.add(insertSpacesItem);
        m.add(new MenuItem("Zap Gremlins"));
        m.add(new MenuItem("Copy to Image Info"));
        m.addActionListener(this);
        mb.add(m);
        editMenu = m;
        if ((options&MENU_BAR)!=0)
            setMenuBar(mb);
        
        m = new Menu("Font");
        m.add(new MenuItem("Make Text Smaller"));
        m.add(new MenuItem("Make Text Larger"));
        m.addSeparator();
        monospaced = new CheckboxMenuItem("Monospaced Font", Prefs.get(FONT_MONO, false));
        if ((options&MONOSPACED)!=0) monospaced.setState(true);
        monospaced.addItemListener(this);
        m.add(monospaced);
        m.add(new MenuItem("Save Settings"));
        m.addActionListener(this);
        mb.add(m);
        
        m = Menus.getExamplesMenu(this);
        mb.add(m);
    }           
            
    public void positionWindow() {
        Dimension screen = IJ.getScreenSize();
        Dimension window = getSize();
        if (window.width==0)
            return;
        int left = screen.width/2-window.width/2;
        int top = screen.height/(IJ.isWindows()?6:5);
        if (IJ.isMacOSX())
            top = (screen.height-window.height)/4;
        if (top<0) top = 0;
        if (nWindows<=0 || xoffset>8*XINC)
            {xoffset=0; yoffset=0;}
        setLocation(left+xoffset, top+yoffset);
        xoffset+=XINC; yoffset+=YINC;
        nWindows++;
    }

    void setWindowTitle(String title) {
        Menus.updateWindowMenuItem(getTitle(), title);
        setTitle(title);
    }
    
    public void create(String name, String text) {
        ta.append(text);
        if (IJ.isMacOSX()) IJ.wait(25); // needed to get setCaretPosition() on OS X
        ta.setCaretPosition(0);
        setWindowTitle(name);
        boolean macroExtension = name.endsWith(".txt") || name.endsWith(".ijm");
        if (macroExtension || name.endsWith(".js") || name.endsWith(".bsh") || name.endsWith(".py") || name.indexOf(".")==-1) {
            macrosMenu = new Menu("Macros");            
            macrosMenu.add(new MenuItem("Run Macro", new MenuShortcut(KeyEvent.VK_R)));
            macrosMenu.add(new MenuItem("Evaluate Line", new MenuShortcut(KeyEvent.VK_Y)));
            macrosMenu.add(new MenuItem("Abort Macro"));
            macrosMenu.add(new MenuItem("Install Macros", new MenuShortcut(KeyEvent.VK_I)));
            macrosMenu.add(new MenuItem("Macro Functions...", new MenuShortcut(KeyEvent.VK_M, true)));
            macrosMenu.add(new MenuItem("Function Finder...", new MenuShortcut(KeyEvent.VK_F, true)));
            macrosMenu.add(new MenuItem("Enter Interactive Mode"));
            macrosMenu.add(new MenuItem("Assign to Repeat Cmd",new MenuShortcut(KeyEvent.VK_A, true)));
            macrosMenu.addSeparator();
            macrosMenu.add(new MenuItem("Evaluate Macro"));
            macrosMenu.add(new MenuItem("Evaluate JavaScript", new MenuShortcut(KeyEvent.VK_J, false)));
            macrosMenu.add(new MenuItem("Evaluate BeanShell", new MenuShortcut(KeyEvent.VK_B, true)));
            macrosMenu.add(new MenuItem("Evaluate Python", new MenuShortcut(KeyEvent.VK_P, false)));
            macrosMenu.add(new MenuItem("Show Log Window", new MenuShortcut(KeyEvent.VK_L, true)));
            macrosMenu.addSeparator();
            // MACROS_MENU_ITEMS must be updated if items are added to this menu
            macrosMenu.addActionListener(this);
            mb.add(macrosMenu);
            if (!(name.endsWith(".js")||name.endsWith(".bsh")||name.endsWith(".py"))) {
                Menu debugMenu = new Menu("Debug");         
                debugMenu.add(new MenuItem("Debug Macro", new MenuShortcut(KeyEvent.VK_D)));
                debugMenu.add(new MenuItem("Step", new MenuShortcut(KeyEvent.VK_E)));
                debugMenu.add(new MenuItem("Trace", new MenuShortcut(KeyEvent.VK_T)));
                debugMenu.add(new MenuItem("Fast Trace", new MenuShortcut(KeyEvent.VK_T,true)));
                debugMenu.add(new MenuItem("Run"));
                debugMenu.add(new MenuItem("Run to Insertion Point", new MenuShortcut(KeyEvent.VK_E, true)));
                debugMenu.add(new MenuItem("Abort"));
                debugMenu.addActionListener(this);
                mb.add(debugMenu);
            }
        } else {
            fileMenu.addSeparator();
            fileMenu.add(new MenuItem("Compile and Run", new MenuShortcut(KeyEvent.VK_R)));
        }
        if (language!=null) {
            for (int i=0; i<languages.length; i++) {
                if (name.endsWith(extensions[i])) {
                    language.select(languages[i]);
                    break;
                }
            }       
        }
        if (text.startsWith("//@AutoInstall") && (name.endsWith(".ijm")||name.endsWith(".txt"))) {
            boolean installInPluginsMenu = !name.contains("Tool.");
            installMacros(text, installInPluginsMenu);
            if ( text.startsWith("//@AutoInstallAndHide"))
                dontShowWindow = true;      
        }
        if (IJ.getInstance()!=null && !dontShowWindow)
            show();
        if (dontShowWindow) {
            dispose();
            dontShowWindow = false;
        }
        if (name.equals(INTERACTIVE_NAME)) {
            enterInteractiveMode();
            String txt = ta.getText();
            ta.setCaretPosition(txt.length());
        }
        WindowManager.setWindow(this);
        checkForCurlyQuotes = true;
        changes = false;
    }

    public void createMacro(String name, String text) {
        create(name, text);
    }
    public void setRejectMacrosMsg(String msg ) {
        rejectMacrosMsg = msg;
    }
    public String getRejectMacrosMsg() {
        return rejectMacrosMsg;
    }
    
    void installMacros(String text, boolean installInPluginsMenu) {
        if (rejectMacrosMsg != null){
            if (rejectMacrosMsg.length()> 0)
                    IJ.showMessage("", rejectMacrosMsg);
            return;
        }
        String functions = Interpreter.getAdditionalFunctions();
        if (functions!=null && text!=null) {
            if (!(text.endsWith("\n") || functions.startsWith("\n")))
                text = text + "\n" + functions;
            else
                text = text + functions;
        }
        installer = new MacroInstaller();
        installer.setFileName(getTitle());
        int nShortcuts = installer.install(text, macrosMenu);
        if (installInPluginsMenu || nShortcuts>0)
            installer.install(null);
        dontShowWindow = installer.isAutoRunAndHide();
        currentMacroEditor = this;
    }
        
    /** Opens a file and replaces the text (if any) by the contents of the file. */
    public void open(String dir, String name) {
        path = dir+name;
        File file = new File(path);
        if (!file.exists()) {
            IJ.error("File not found: "+path);
            return;
        }
        try {
            StringBuffer sb = new StringBuffer(5000);
            BufferedReader r = new BufferedReader(new FileReader(file));
            while (true) {
                String s=r.readLine();
                if (s==null)
                    break;
                else
                    sb.append(s+"\n");
            }
            r.close();
            if (ta!=null && ta.getText().length()>0) {
                ta.setText("");  //delete previous contents (if any)
                eventCount = 0;
            }
            create(name, new String(sb));
            changes = false;
        }
        catch (Exception e) {
            IJ.handleException(e);
            return;
        }
    }

    public String getText() {
        if (ta==null)
            return "";
        else
            return ta.getText();
    }

    public TextArea getTextArea() {
        return ta;
    }

    public void display(String title, String text) {
        ta.selectAll();
        ta.replaceRange(text, ta.getSelectionStart(), ta.getSelectionEnd());
        ta.setCaretPosition(0);
        setWindowTitle(title);
        changes = false;
        if (IJ.getInstance()!=null)
            show();
        WindowManager.setWindow(this);
    }

    void save() {
        if (path==null) {
            saveAs(); 
            return;
        }
        File f = new File(path);
        if (f.exists() && !f.canWrite()) {
            IJ.showMessage("Editor", "Unable to save because file is write-protected. \n \n" + path);
            return;
        }
        String text = ta.getText();
        char[] chars = new char[text.length()];
        text.getChars(0, text.length(), chars, 0);
        try {
            BufferedReader br = new BufferedReader(new CharArrayReader(chars));
            BufferedWriter bw = new BufferedWriter(new FileWriter(path));
            while (true) {
                String s = br.readLine();
                if (s==null) break;
                bw.write(s, 0, s.length());
                bw.newLine();
            }
            bw.close();
            IJ.showStatus(text.length()+" chars saved to " + path);
            changes = false;
        } catch
            (IOException e) {}
    }

    void compileAndRun() {
        if (path==null)
            saveAs();
        if (path!=null) {
            save();
            String text = ta.getText();
            if (text.contains("implements PlugInFilter") && text.contains("IJ.run("))
                IJ.log("<<Plugins that call IJ.run() should probably implement PlugIn, not PlugInFilter.>>");
            IJ.runPlugIn("ij.plugin.Compiler", path);
        }
    }
    
    final void runMacro(boolean debug) {
        if (path!=null)
            Macro_Runner.setFilePath(path);
        if (getTitle().endsWith(".js"))
            {evaluateJavaScript(); return;}
        else if (getTitle().endsWith(".bsh"))
            {evaluateScript(".bsh"); return;}
        else if (getTitle().endsWith(".py"))
            {evaluateScript(".py"); return;}
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        String text;
        if (start==end)
            text = ta.getText();
        else
            text = ta.getSelectedText();
        Interpreter.abort();  // abort any currently running macro
        if (checkForCurlyQuotes && text.contains("\u201D")) {
            // replace curly quotes with standard quotes
            text = text.replaceAll("\u201C", "\""); 
            text = text.replaceAll("\u201D", "\"");
            if (start==end)
                ta.setText(text);
            else {
                String text2 = ta.getText();
                text2 = text2.replaceAll("\u201C", "\""); 
                text2 = text2.replaceAll("\u201D", "\"");
                ta.setText(text2);
            }
            changes = true;
            checkForCurlyQuotes = false;
        }
        currentMacroEditor = this;
        new MacroRunner(text, debug?this:null);
    }
    
    void evaluateMacro() {
        String title = getTitle();
        if (title.endsWith(".js")||title.endsWith(".bsh")||title.endsWith(".py"))
            setWindowTitle(title.substring(0,title.length()-3)+".ijm");
        runMacro(false);
    }

    void evaluateJavaScript() {
        if (!getTitle().endsWith(".js"))
            setWindowTitle(SaveDialog.setExtension(getTitle(), ".js"));
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        String text;
        if (start==end)
            text = ta.getText();
        else
            text = ta.getSelectedText();
        if (text.equals(""))
            return;
        boolean strictMode = false;
        if (IJ.isJava18()) {
            // text.matches("^( |\t)*(\"use strict\"|'use strict')");
            String text40 = text.substring(0,Math.min(40,text.length()));
            strictMode =  text40.contains("'use strict'") || text40.contains("\"use strict\"");
        }
        text = getJSPrefix("") + text;
        if (IJ.isJava18()) {
            text = "load(\"nashorn:mozilla_compat.js\");" + text;
            if (strictMode)
                text = "'use strict';" + text;
        }
        if (!(IJ.isMacOSX()&&!IJ.is64Bit())) {
            // Use JavaScript engine built into Java 6 and later.
            IJ.runPlugIn("ij.plugin.JavaScriptEvaluator", text);
        } else {
            Object js = IJ.runPlugIn("JavaScript", text);
            if (js==null)
                download("/download/tools/JavaScript.jar");
        }
    }

    public void evaluateScript(String ext) {
        if (downloading) {
            IJ.beep();
            IJ.showStatus("Download in progress");
            return;
        }
        if (ext.endsWith(".js")) {
            evaluateJavaScript();
            return;
        }
        if (!getTitle().endsWith(ext))
            setWindowTitle(SaveDialog.setExtension(getTitle(), ext));
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        String text;
        if (start==end)
            text = ta.getText();
        else
            text = ta.getSelectedText();
        if (text.equals("")) return;
        String plugin, url;
        if (ext.equals(".bsh")) {
            plugin = "bsh";
            url = "/plugins/bsh/BeanShell.jar";
        } else {
            // download Jython from ../../../../../plugins/jython/
            plugin = "Jython";
            url = "/plugins/jython/Jython.jar";
        }
        Object obj = IJ.runPlugIn(plugin, text);
        if (obj==null)
            download(url);
    }
    
    private void download(String url) {
        this.downloadUrl = url;
        Thread thread = new Thread(this, "Downloader");
        thread.setPriority(Math.max(thread.getPriority()-2, Thread.MIN_PRIORITY));
        thread.start();
    }

    void evaluateLine() {
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        if (end>start)
            {runMacro(false); return;}
        String text = ta.getText();
        while (start>0) {
            start--;
            if (text.charAt(start)=='\n')
                {start++; break;}
        }
        while (end<text.length()-1) {
            end++;
            if (text.charAt(end)=='\n')
                break;
        }
        ta.setSelectionStart(start);
        ta.setSelectionEnd(end);
        runMacro(false);
    }

    void print () {
        PrintJob pjob = Toolkit.getDefaultToolkit().getPrintJob(this, "Cool Stuff", p);
        if (pjob != null) {
            Graphics pg = pjob.getGraphics( );
            if (pg != null) {
                String s = ta.getText();
                printString(pjob, pg, s);
                pg.dispose( );  
            }
            pjob.end( );
        }
    }

    void printString (PrintJob pjob, Graphics pg, String s) {
        int pageNum = 1;
        int linesForThisPage = 0;
        int linesForThisJob = 0;
        int topMargin = 30;
        int leftMargin = 30;
        int bottomMargin = 30;
        
        if (!(pg instanceof PrintGraphics))
            throw new IllegalArgumentException ("Graphics contextt not PrintGraphics");
        if (IJ.isMacintosh()) {
            topMargin = 0;
            leftMargin = 0;
            bottomMargin = 0;
        }
        StringReader sr = new StringReader (s);
        LineNumberReader lnr = new LineNumberReader (sr);
        String nextLine;
        int pageHeight = pjob.getPageDimension().height - bottomMargin;
        Font helv = new Font(getFontName(), Font.PLAIN, 10);
        pg.setFont (helv);
        FontMetrics fm = pg.getFontMetrics(helv);
        int fontHeight = fm.getHeight();
        int fontDescent = fm.getDescent();
        int curHeight = topMargin;
        try {
            do {
                nextLine = lnr.readLine();
               if (nextLine != null) {         
                    nextLine = detabLine(nextLine);
                    if ((curHeight + fontHeight) > pageHeight) {
                        // New Page
                        pageNum++;
                        linesForThisPage = 0;
                        pg.dispose();
                        pg = pjob.getGraphics();
                        if (pg != null)
                            pg.setFont (helv);
                        curHeight = topMargin;
                    }
                    curHeight += fontHeight;
                    if (pg != null) {
                        pg.drawString (nextLine, leftMargin, curHeight - fontDescent);
                        linesForThisPage++;
                        linesForThisJob++;
                    } 
                }
            } while (nextLine != null);
        } catch (EOFException eof) {
       // Fine, ignore
        } catch (Throwable t) { // Anything else
            IJ.handleException(t);
        }
    }
    
    String detabLine(String s) {
        if (s.indexOf('\t')<0)
            return s;
        int tabSize = 4;
        StringBuffer sb = new StringBuffer((int)(s.length()*1.25));
        char c;
        for (int i=0; i<s.length(); i++) {
            c = s.charAt(i);
            if (c=='\t') {
                  for (int j=0; j<tabSize; j++)
                      sb.append(' '); 
        } else
            sb.append(c);
         }
        return sb.toString();
    }      

    void undo() {
        if (IJ.isWindows()) {
            IJ.showMessage("Editor", "Press Ctrl-Z to undo");
            return;
        }
        if (IJ.debugMode) IJ.log("Undo1: "+undoBuffer.size());
        int position = ta.getCaretPosition();
        if (undoBuffer.size()>1) {
            undoBuffer.remove(undoBuffer.size()-1);
            String text = (String)undoBuffer.get(undoBuffer.size()-1);
            performingUndo = true;
            ta.setText(text);
            if (position<=text.length())
                ta.setCaretPosition(position-offset(position));
            if (IJ.debugMode) IJ.log("Undo2: "+undoBuffer.size()+" "+text);
        }
    }
    
    boolean copy() { 
        String s; 
        s = ta.getSelectedText();
        Clipboard clip = getToolkit().getSystemClipboard();
        if (clip!=null) {
            StringSelection cont = new StringSelection(s);
            clip.setContents(cont,this);
            return true;
        } else
            return false;
    }
      
    void cut() {
        if (copy()) {
            int start = ta.getSelectionStart();
            int end = ta.getSelectionEnd();
            ta.replaceRange("", start-offset(start), end-offset(end-2>=start?end-2:start));
            if (IJ.isMacOSX())
                ta.setCaretPosition(start);
        }   
    }
    
    private void assignToRepeatCommand() {
        String title = getTitle();
        if (!(title.endsWith(".ijm")||title.endsWith(".txt")||!title.contains("."))) {
            IJ.error("Assign to Repeat Command", "One or more lines of macro code required.");
            return;
        }
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        String text;
        if (start==end)
            text = ta.getText();
        else
            text = ta.getSelectedText();
        Executer.setAsRepeatCommand(text);
    }

    void paste() {
        String s;
        s = ta.getSelectedText();
        Clipboard clipboard = getToolkit( ). getSystemClipboard(); 
        Transferable clipData = clipboard.getContents(s);
        try {
            s = (String)(clipData.getTransferData(DataFlavor.stringFlavor));
        } catch  (Exception e)  {
            s  = e.toString( );
        }
        int start = ta.getSelectionStart( );
        int end = ta.getSelectionEnd( );
        ta.replaceRange(s, start-offset(start), end-offset(end-2>=start?end-2:start));
        if (IJ.isMacOSX())
            ta.setCaretPosition(start+s.length());
        checkForCurlyQuotes = true;
    }
    
    // workaround for TextArea.getCaretPosition() bug on Windows
    private int offset(int pos) {
        if (!IJ.isWindows())
            return 0;
        String text = ta.getText();
        int rcount = 0;
        for (int i=0; i<=pos; i++) {
            if (text.charAt(i)=='\r')
                rcount++;
        }
        if (IJ.debugMode) IJ.log("offset: "+pos+" "+rcount);
        return pos-rcount>=0?rcount:0;
    }

    void copyToInfo() { 
        ImagePlus imp = WindowManager.getCurrentImage();
        if (imp==null) {
            IJ.noImage();
            return;
        }
        int start = ta.getSelectionStart();
        int end = ta.getSelectionEnd();
        String text;
        if (start==end)
            text = ta.getText();
        else
            text = ta.getSelectedText();
        imp.setProperty("Info", text);
    }
    
    public void actionPerformed(ActionEvent e) {
        String what = e.getActionCommand();
        int flags = e.getModifiers();
        boolean altKeyDown = (flags & Event.ALT_MASK)!=0;
        if (e.getSource()==runButton) {
            runMacro(false);
            return;
        } else if (e.getSource()==installButton) {
            String text = ta.getText();
            if (text.contains("macro \"") || text.contains("macro\""))
                installMacros(text, true);
            else
                IJ.error("Editor", "File must contain at least one macro or macro tool.");
            return;
        } 
        if ("Save".equals(what))
            save();
        else if ("Compile and Run".equals(what))
                compileAndRun();
        else if ("Run Macro".equals(what)) {
            if (altKeyDown) {
                enableDebugging();
                runMacro(true);
            } else
                runMacro(false);
        } else if ("Debug Macro".equals(what)) {
                enableDebugging();
                runMacro(true);
        } else if ("Step".equals(what))
            setDebugMode(STEP);
        else if ("Trace".equals(what))
            setDebugMode(TRACE);
        else if ("Fast Trace".equals(what))
            setDebugMode(FAST_TRACE);
        else if ("Run".equals(what))
            setDebugMode(RUN_TO_COMPLETION);
        else if ("Run to Insertion Point".equals(what))
            runToInsertionPoint();
        else if ("Abort".equals(what) || "Abort Macro".equals(what)) {
            Interpreter.abort();
            IJ.beep();      
        } else if ("Evaluate Line".equals(what))
            evaluateLine();
        else if ("Install Macros".equals(what))
            installMacros(ta.getText(), true);
        else if ("Macro Functions...".equals(what))
            showMacroFunctions();
        else if ("Function Finder...".equals(what))
            functionFinder = new FunctionFinder(this);
        else if ("Evaluate Macro".equals(what))
            evaluateMacro();
        else if ("Evaluate JavaScript".equals(what))
            evaluateJavaScript();
        else if ("Evaluate BeanShell".equals(what))
            evaluateScript(".bsh");
        else if ("Evaluate Python".equals(what))
            evaluateScript(".py");
        else if ("Show Log Window".equals(what))
            showLogWindow();
        else if ("Revert".equals(what))
            revert();
        else if ("Print...".equals(what))
            print();
        else if (what.startsWith("Undo"))
           undo();
        else if (what.startsWith("Paste"))
            paste();
        else if (what.equals("Copy to Image Info"))
            copyToInfo();
        else if (what.startsWith("Copy"))
            copy();
        else if (what.startsWith("Cut"))
           cut();
        else if ("Save As...".equals(what))
            saveAs();
        else if ("Select All".equals(what))
            selectAll();
        else if ("Find...".equals(what))
            find(null);
        else if ("Find Next".equals(what))
            find(searchString);
        else if ("Go to Line...".equals(what))
            gotoLine();
        else if ("Balance".equals(what))
            balance();
        else if ("Detab...".equals(what))
            detab();
        else if ("Zap Gremlins".equals(what))
            zapGremlins();
        else if ("Make Text Larger".equals(what))
            changeFontSize(true);
        else if ("Make Text Smaller".equals(what))
            changeFontSize(false);
        else if ("Save Settings".equals(what))
            saveSettings();
        else if ("New...".equals(what))
            IJ.run("Text Window");
        else if ("Open...".equals(what))
            IJ.open();
        else if (what.equals("Enter Interactive Mode"))
            enterInteractiveMode();
        else if (what.equals("Assign to Repeat Cmd"))
            assignToRepeatCommand();
        else if (what.endsWith(".ijm") || what.endsWith(".java") || what.endsWith(".js") || what.endsWith(".bsh") || what.endsWith(".py"))
            openExample(what);
        else {
            if (altKeyDown) {
                enableDebugging();
                installer.runMacro(what, this);
            } else
                installer.runMacro(what, null);
        }
    }
    
    /** Opens an example from the Help/Examples menu
        and runs if "Autorun Exampes" is checked. */
    public static boolean openExample(String name) {
        boolean isJava = name.endsWith(".java");
        boolean isJavaScript = name.endsWith(".js");
        boolean isBeanShell = name.endsWith(".bsh");
        boolean isPython = name.endsWith(".py");
        boolean isMacro = name.endsWith(".ijm");
        if (!(isMacro||isJava||isJavaScript||isBeanShell||isPython))
            return false;
        boolean run = !isJava && !name.contains("_Tool") && Prefs.autoRunExamples;
        int rows = 24;
        int columns = 70;
        int options = MENU_BAR + RUN_BAR;
        if (isMacro)
            options += INSTALL_BUTTON;
        String text = null;
        Editor ed = new Editor(rows, columns, 0, options);
        String dir = "Macro/";
        if (isJava)
            dir = "Java/";
        else if (isJavaScript)
            dir = "JavaScript/";
        else if (isBeanShell)
            dir = "BeanShell/";
        else if (isPython)
            dir = "Python/";
        String url = "http://wsr.imagej.net/download/Examples/"+dir+name;
        text = IJ.openUrlAsString(url);
        if (text.startsWith("<Error: ")) {
            IJ.error("Open Example", text);
            return true;
        }
        ed.create(name, text);
        if (run)
            ed.runMacro(false);
        return true;
    }
    
    protected void showMacroFunctions() {
        String url= "/developer/macro/functions.html";
        String selText = ta.getSelectedText().replace("\n", " ");
        String[] selectedWords = Tools.split(selText, "/,(,[\"\'&+");
        if (selectedWords.length==1 && selectedWords[0].length()>0) 
            url += "#" +selectedWords[0];//append selection as hash tag
        IJ.runPlugIn("ij.plugin.BrowserLauncher", IJ.URL+url);
    }

    final void runToInsertionPoint() {
        Interpreter interp = Interpreter.getInstance();
        if (interp==null)
            IJ.beep();
        else {
            runToLine = getCurrentLine();
            setDebugMode(RUN_TO_CARET);
        }
    }
        
    final int getCurrentLine() {
        int pos = ta.getCaretPosition();
        int currentLine = 0;
        String text = ta.getText();
        if (IJ.isWindows())
            text = text.replaceAll("\r\n", "\n");
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        int count=0;
        int start=0, end=0;
        int len = chars.length;
        for (int i=0; i<len; i++) {
            if (chars[i]=='\n') {
                count++;
                start = end;
                end = i;
                if (pos>=start && pos<end) {
                    currentLine = count;
                    break;
                }
            }
        }
        if (currentLine==0 && pos>end)
            currentLine = count;
        return currentLine;
    }

    final void enableDebugging() {
            step = true;
            int start = ta.getSelectionStart();
            int end = ta.getSelectionEnd();
            if (start==debugStart && end==debugEnd)
                ta.select(start, start);
    }
    
    final void setDebugMode(int mode) {
        step = true;
        Interpreter interp = Interpreter.getInstance();
        if (interp!=null) {
            if (interp.getDebugger()==null)
                fixLineEndings();
            interp.setDebugger(this);
            interp.setDebugMode(mode);
        }
    }

    public void textValueChanged(TextEvent e) {
        String text = ta.getText();
        //if (undo2==null || text.length()!=undo2.length()+1 || text.charAt(text.length()-1)=='\n')
        int length = 0;
        if (!performingUndo) {
            for (int i=0; i<undoBuffer.size(); i++)
                length += ((String)undoBuffer.get(i)).length();
            if (length<2000000)
                undoBuffer.add(text);
            else {
                for (int i=1; i<undoBuffer.size(); i++)
                    undoBuffer.set(i-1, undoBuffer.get(i));
                undoBuffer.set(undoBuffer.size()-1, text);
            }
        }
        performingUndo = false;
        if (isMacroWindow) return;
        // first few textValueChanged events may be bogus
        eventCount++;
        if (eventCount>2 || !IJ.isMacOSX() && eventCount>1)
            changes = true;
        if (IJ.isMacOSX()) // screen update bug work around
            ta.setCaretPosition(ta.getCaretPosition());
    }
    
    public void keyPressed(KeyEvent e) { }  
    public void mousePressed (MouseEvent e) {}
    public void mouseExited (MouseEvent e) {}
    public void mouseEntered (MouseEvent e) {}
    
    public void mouseReleased (MouseEvent e) {
        showLinePos();
    }
    public void mouseClicked (MouseEvent e) {
        showLinePos();
    }

    private void showLinePos() { // show line numbers in status bar (Norbert Vischer)
        char[] chars = ta.getText().toCharArray();
        if(chars.length > 1e6)
                return;
        int selStart = ta.getSelectionStart();
        int selEnd = ta.getSelectionEnd();
        int line=0;
        int startLine = 1;
        int endLine = 1;
        for (int i=1; i<=chars.length; i++) {
            if (chars[i-1]=='\n') line++;
            if (i==selStart)
                startLine=line + 1; 
            if (i<=selEnd)
                endLine=line + 1; 
            if (i>=selEnd)
                break;
        }
        String msg = "Line " + startLine;
        if (startLine != endLine) {
            msg += "-" + endLine;
        }
        IJ.showStatus(msg);
    }
                
    public void keyReleased(KeyEvent e) {
        int pos = ta.getCaretPosition();
        showLinePos();
        if (insertSpaces && pos>0 && e.getKeyCode()==KeyEvent.VK_TAB) {
            String spaces = " ";
            for (int i=1; i<tabInc; i++)
                spaces += " ";
            ta.replaceRange(spaces, pos-1, pos);
        }
        if (interactiveMode && e.getKeyChar()=='\n')
            runMacro(e);
    }
    
    private void runMacro(KeyEvent e) {
        boolean isScript = getTitle().endsWith(".js");
        String text = ta.getText();
        int pos2 = ta.getCaretPosition()-2;
        if (pos2<0) pos2=0;
        int pos1 = 0;
        for (int i=pos2; i>=0; i--) {
            if (i==0 || text.charAt(i)=='\n') {
                pos1 = i;
                break;
            }
        }
        if (isScript) {
            if (evaluator==null) {
                interpreter = null;
                evaluator = new JavaScriptEvaluator();
            }
        } else {
            if (interpreter==null) {
                evaluator = null;
                interpreter = new Interpreter();
            }
        }
        String code = text.substring(pos1,pos2+1);
        if (code.length()==0 || code.equals("\n"))
            return;     
        else if (code.length()<=6 && code.contains("help")) {
            ta.appendText("  Type a statement (e.g., \"run('Invert')\") to run it.\n");         
            ta.appendText("  Enter an expression (e.g., \"x/2\" or \"log(2)\") to evaluate it.\n");         
            ta.appendText("  Move cursor to end of line and press 'enter' to repeat.\n");           
            ta.appendText("  \"quit\" - exit interactive mode\n");          
            ta.appendText("  "+(IJ.isMacOSX()?"cmd":"ctrl")+"+M - enter interactive mode\n");
            if (isScript) { 
                ta.appendText("  \"macro\" - switch language to macro\n");
                ta.appendText("  \"examples\" - show JavaScript examples\n");   
            } else {
                ta.appendText("  "+(IJ.isMacOSX()?"cmd":"ctrl")+"+shift+F - open Function Finder\n");   
                ta.appendText("  \"js\" - switch language to JavaScript\n");    
            }
        } else if (isScript && code.length()==9 && code.contains("examples")) {
            ta.appendText(JS_EXAMPLES);                 
        } else if (code.length()<=3 && code.contains("js")) {
            interactiveMode = false;
            interpreter = null;
            evaluator = null;
            changeExtension(".js");
            enterInteractiveMode();
        } else if (code.length()<=6 && code.contains("macro")) {
            interactiveMode = false;
            interpreter = null;
            evaluator = null;
            changeExtension(".txt");
            enterInteractiveMode();
        } else if (code.length()<=6 && code.contains("quit")) {
            interactiveMode = false;
            interpreter = null;
            evaluator = null;
            ta.appendText("[Exiting interactive mode.]\n");
        } else if (isScript) {
            boolean updateImage = code.contains("ip.");
            code = "load(\"nashorn:mozilla_compat.js\");"+JavaScriptIncludes+code;
            String rtn = evaluator.eval(code);
            if (rtn!=null && rtn.length()>0) {
                int index = rtn.indexOf("at line number ");
                if (index>-1)
                    rtn = rtn.substring(0,index);
                insertText(rtn);    
            }
            if (updateImage && (rtn==null||rtn.length()==0)) {
                ImagePlus imp = WindowManager.getCurrentImage();
                if (imp!=null)
                    imp.updateAndDraw();
            }
        } else if (!code.startsWith("[Macro ")) {
            String rtn = interpreter.eval(code);
            if (rtn!=null)
                insertText(rtn);
        }
    }
    
    private void changeExtension(String ext) {
        String title = getTitle();
        int index = title.indexOf(".");
        if (index>-1)
            title = title.substring(0,index);
        setTitle(title+ext);
    }
    
    private void enterInteractiveMode() {
        if (interactiveMode)
            return;
        String title = getTitle();
        if (ta!=null && ta.getText().length()>400 && !(title.startsWith("Untitled")||title.startsWith(INTERACTIVE_NAME))) {
            GenericDialog gd = new GenericDialog("Enter Interactive Mode");
            gd.addMessage("Enter mode that supports interactive\nediting and running of macros and scripts?");
            gd.setOKLabel("Enter");
            gd.showDialog();
            if (gd.wasCanceled())
                return;
        }
        String language = title.endsWith(".js")?"JavaScript ":"Macro ";
        messageCount++;
        String help = messageCount<=2?" Type \"help\" for info.":"";
        ta.appendText("["+language+"interactive mode."+help+"]\n");
        interactiveMode = true;
    }
    
    public void insertText(String text) {
        if (ta==null) return;           
        int start = ta.getSelectionStart( );
        ta.replaceRange("  "+text+"\n", start, start);
    }
        
    public void keyTyped(KeyEvent e) {
    }

    public void itemStateChanged(ItemEvent e) {
        String cmd = e.getItem().toString();
        if (e.getSource()==language) {
            setExtension(cmd);
            return;
        }
        CheckboxMenuItem item = (CheckboxMenuItem)e.getSource();
        if ("Tab Key Inserts Spaces".equals(cmd)) {
            insertSpaces = e.getStateChange()==1;
            Prefs.set(INSERT_SPACES, insertSpaces);
        } else
            setFont();
    }
    
    private void setExtension(String language) {
        String title = getTitle();
        int dot = title.lastIndexOf(".");
        if (dot>=0)
            title = title.substring(0, dot);
        for (int i=0; i<languages.length; i++) {
            if (language.equals(languages[i])) {
                title += extensions[i];
                break;
            }
        }
        setWindowTitle(title);      
    }

    /** Override windowActivated in PlugInFrame to
        prevent Mac menu bar from being installed. */
    public void windowActivated(WindowEvent e) {
            if (IJ.debugMode) IJ.log("Editor.windowActivated");
            WindowManager.setWindow(this);
            instance = this;
    }

    /** Overrides close() in PlugInFrame. */
    public void close() {
        boolean okayToClose = true;
        ImageJ ij = IJ.getInstance();
        if (!getTitle().equals("Errors") && changes && !IJ.isMacro() && ij!=null && !ij.quittingViaMacro()) {
            String msg = "Save changes to \"" + getTitle() + "\"?";
            YesNoCancelDialog d = new YesNoCancelDialog(this, "Editor", msg);
            if (d.cancelPressed())
                okayToClose = false;
            else if (d.yesPressed())
                save();
        }
        if (okayToClose) {
            //setVisible(false);
            dispose();
            WindowManager.removeWindow(this);
            nWindows--;
            instance = null;
            changes = false;
            if (functionFinder!=null)
                functionFinder.close();
        }
    }

    public void saveAs() {
        String name1 = getTitle();
        if (name1.indexOf(".")==-1) name1 += ".txt";
        if (defaultDir!=null && name1.endsWith(".java") && !defaultDir.startsWith(Menus.getPlugInsPath())) {
            defaultDir = null;
        }
        if (defaultDir==null) {
            if (name1.endsWith(".txt")||name1.endsWith(".ijm"))
                defaultDir = Menus.getMacrosPath();
            else
                defaultDir = Menus.getPlugInsPath();
        }
        SaveDialog sd = new SaveDialog("Save As...", defaultDir, name1, null);
        String name2 = sd.getFileName();
        String dir = sd.getDirectory();
        if (name2!=null) {
            if (name2.endsWith(".java"))
                updateClassName(name1, name2);
            path = dir+name2;
            save();
            changes = false;
            setWindowTitle(name2);
            setDefaultDirectory(dir);
            if (defaultDir!=null)
                Prefs.set(DEFAULT_DIR, defaultDir);
            if (Recorder.record)
                Recorder.record("saveAs", "Text", path);
        }
    }
    
    protected void revert() {
        if (!changes)
            return;
        String title = getTitle();
        if (path==null || !(new File(path).exists()) || !path.endsWith(title)) {
            IJ.showStatus("Cannot revert, no file "+getTitle());
            return;
        }
        if (!IJ.showMessageWithCancel("Revert?", "Revert to saved version of\n\""+getTitle()+"\"?"))
            return;
        String directory = path.substring(0, path.length()-title.length());
        open(directory, title);
        undoBuffer = new ArrayList();
    }

    /** Changes a plugins class name to reflect a new file name. */
    public void updateClassName(String oldName, String newName) {
        if (newName.indexOf("_")<0)
            IJ.showMessage("Plugin Editor", "Plugins without an underscore in their name will not\n"
                +"be automatically installed when ImageJ is restarted.");
        if (oldName.equals(newName) || !oldName.endsWith(".java") || !newName.endsWith(".java"))
            return;
        oldName = oldName.substring(0,oldName.length()-5);
        newName = newName.substring(0,newName.length()-5);
        String text1 = ta.getText();
        int index = text1.indexOf("public class "+oldName);
        if (index<0)
            return;
        String text2 = text1.substring(0,index+13)+newName+text1.substring(index+13+oldName.length(),text1.length());
        ta.setText(text2);
    }
    
    void find(String s) {
        if (s==null) {
            GenericDialog gd = new GenericDialog("Find", this);
            gd.addStringField("Find: ", searchString, 20);
            String[] labels = {"Case Sensitive", "Whole Words"};
            boolean[] states = {caseSensitive, wholeWords};
            gd.addCheckboxGroup(1, 2, labels, states);
            gd.showDialog();
            if (gd.wasCanceled())
                return;
            s = gd.getNextString();
            caseSensitive = gd.getNextBoolean();
            wholeWords = gd.getNextBoolean();
            Prefs.set(CASE_SENSITIVE, caseSensitive);
        }
        if (s.equals(""))
            return;
        String text = ta.getText();
        String s2 = s;
        if (!caseSensitive) {
            text = text.toLowerCase(Locale.US);
            s = s.toLowerCase(Locale.US);
        }
        int index = -1;
        if (wholeWords) {
            int position = ta.getCaretPosition()+1;
            while (true) {
                index = text.indexOf(s, position);
                if (index==-1) break;
                if (isWholeWordMatch(text, s, index)) break;
                position = index + 1;
                if (position>=text.length()-1)
                    {index=-1; break;}
            }
        } else
            index = text.indexOf(s, ta.getCaretPosition()+1);
        searchString = s2;
        if (index<0)
            {IJ.beep(); return;}
        ta.setSelectionStart(index);
        ta.setSelectionEnd(index+s.length());
    }
    
    boolean isWholeWordMatch(String text, String word, int index) {
        char c = index==0?' ':text.charAt(index-1);
        if (Character.isLetterOrDigit(c) || c=='_') return false;
        c = index+word.length()>=text.length()?' ':text.charAt(index+word.length());
        if (Character.isLetterOrDigit(c) || c=='_') return false;
        return true;
    }
    
    void gotoLine() {
        GenericDialog gd = new GenericDialog("Go to Line", this);
        gd.addNumericField("Go to line number: ", lineNumber, 0);
        gd.showDialog();
        if (gd.wasCanceled())
            return;
        int n = (int)gd.getNextNumber();
        if (n<1) return;
        String text = ta.getText();
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        int count=1, loc=0;
        for (int i=0; i<chars.length; i++) {
            if (chars[i]=='\n') count++;
            if (count==n)
                {loc=i+1; break;}
        }
        ta.setCaretPosition(loc);
        lineNumber = n;
    }
    
    //extracts  characters  "({[]})" as string and removes inner pairs
    private void balance() { //modified: N.Vischer
        String text = ta.getText();
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        maskComments(chars);
        maskQuotes(chars);
        int position = ta.getCaretPosition();
        if (position == 0) {
            IJ.error("Balance", "This command locates the pair of brackets, curly braces or\nparentheses that surround the insertion point.");
            return;
        }
        int start = -1;
        int stop = -1;
        String leftBows = "";
        for (int i = position -1 ; i >= 0; i--) {
            char ch = chars[i];
            if ("({[]})".indexOf(ch) >= 0) {
                leftBows = ch + leftBows;
                leftBows = leftBows.replace("[]", "");//skip nested pairs
                leftBows = leftBows.replace("()", "");
                leftBows = leftBows.replace("{}", "");
                if (leftBows.equals ("[") || leftBows.equals ("{") || leftBows.equals ("(")) {
                    start = i;
                    break;
                }
            }
        }
        String rightBows = "";
        for (int i = position ; i < chars.length; i++) {
            char ch = chars[i];
            if ("({[]})".indexOf(ch) >= 0) {
                rightBows += ch;
                rightBows = rightBows.replace("[]", "");//skip nested pairs
                rightBows = rightBows.replace("()", "");
                rightBows = rightBows.replace("{}", "");
                String pair = leftBows + rightBows;
                if (pair.equals("[]") ||  pair.equals("{}") || pair.equals("()")) {
                    stop = i;
                    break;
                }
            }
        }
        if (start == -1 || stop == -1) {
            IJ.beep();
            return;
        }
        ta.setSelectionStart(start);
        ta.setSelectionEnd(stop + 1);
        IJ.showStatus(chars.length + " " + position + " " + start + " " + stop);
    }

    // replaces contents of comments with blanks
    private void maskComments(char[] chars) {
        int n = chars.length;
        boolean inSlashSlashComment = false;
        boolean inSlashStarComment = false;
        for (int i=0; i<n-1; i++) {
            if (chars[i]=='/' && chars[i+1]=='/')
                inSlashSlashComment = true;
            if (chars[i]=='\n' )
                inSlashSlashComment = false;
            if (!inSlashSlashComment){
                if (chars[i]=='/' && chars[i+1]=='*')
                    inSlashStarComment = true;
                if (chars[i]=='*' && chars[i+1]=='/')
                    inSlashStarComment = false;
            }
            if (inSlashSlashComment||inSlashStarComment)
                chars[i] = ' ';
        }
    }

    // replaces contents of single and double quotes with blanks - N. Vischer
    private void maskQuotes(char[] chars) {
        int n = chars.length;
        char quote = '\'';//single quote
        for (int loop = 1; loop <= 2; loop++) {
            if (loop == 2)
                quote = '"';//double quote
            boolean inQuotes = false;
            int startMask = 0;
            int stopMask = 0;
            for (int i = 0; i < n - 1; i++) {
                boolean escaped = i > 0 && chars[i - 1] == '\\';
                if (chars[i] == '\n')
                    inQuotes = false;
                if (chars[i] == quote && !escaped) {
                    if (!inQuotes) {
                        startMask = i;
                        inQuotes = true;
                    } else {
                        stopMask = i;
                        for (int jj = startMask; jj <= stopMask; jj++) {
                            chars[jj] = ' ';
                        }
                        inQuotes = false;
                    }
                }
            }
        }
    }
    
   //replaces contents of comments with blanks
    private void rmaskComments(char[] chars) {
        int n = chars.length;
        boolean inSlashSlashComment = false;
        boolean inSlashStarComment = false;
        for (int i=0; i<n-1; i++) {
            if (chars[i]=='/' && chars[i+1]=='/')
                inSlashSlashComment = true;
            if (chars[i]=='\n' )
                inSlashSlashComment = false;
            if (chars[i]=='/' && chars[i+1]=='*')
                inSlashStarComment = true;
            if (chars[i]=='*' && chars[i+1]=='/')
                inSlashStarComment = false;
            if (inSlashSlashComment||inSlashStarComment)
                chars[i] = ' ';
        }
    }

    void zapGremlins() {
        String text = ta.getText();
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        int count=0;
        boolean inQuotes = false;
        char quoteChar = 0;
        for (int i=0; i<chars.length; i++) {
            char c = chars[i];
            if (!inQuotes && (c=='"' || c=='\'')) {
                inQuotes = true;
                quoteChar = c;
            } else  {
                if (inQuotes && (c==quoteChar || c=='\n'))
                inQuotes = false;
            }
            if (!inQuotes && c!='\n' && c!='\t' && (c<32||c>127)) {
                count++;
                chars[i] = ' ';
            }
        }
        if (count>0) {
            text = new String(chars);
            ta.setText(text);
        }
        if (count>0)
            IJ.showMessage("Zap Gremlins", count+" invalid characters converted to spaces");
        else
            IJ.showMessage("Zap Gremlins", "No invalid characters found");
    }
    
    
    private void detab() {
        GenericDialog gd = new GenericDialog("Detab", this);
        gd.addNumericField("Spaces per tab: ", tabInc, 0);
        gd.addCheckbox("Tab key inserts spaces: ", insertSpaces);
        gd.showDialog();
        if (gd.wasCanceled())
            return;
        int tabInc2 = tabInc;
        tabInc = (int)gd.getNextNumber();
        if (tabInc<1) tabInc=1;
        if (tabInc>8) tabInc=8;
        if (tabInc!=tabInc2)
            Prefs.set(TAB_INC, tabInc);
        boolean insertSpaces2 = insertSpaces;
        insertSpaces = gd.getNextBoolean();
        if (insertSpaces!=insertSpaces2) {
            Prefs.set(INSERT_SPACES, insertSpaces);
            insertSpacesItem.setState(insertSpaces);
        }
        int nb = 0;
        int pos = 1;
        String text = ta.getText();
        if (text.indexOf('\t')<0)
            return;
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        StringBuffer sb = new StringBuffer((int)(chars.length*1.25));
        for (int i=0; i<chars.length; i++) {
            char c = chars[i];
            if (c=='\t') {
                nb = tabInc - ((pos-1)%tabInc);
                while(nb>0) {
                    sb.append(' ');
                    ++pos;
                    --nb;
                }
            } else if (c=='\n') {
                sb.append(c);
                pos = 1;
            } else {
                sb.append(c);
                ++pos;
            }
        }
        ta.setText(sb.toString());
    }

    void selectAll() {
        ta.selectAll();
        showLinePos();
    }
    
    void changeFontSize(boolean larger) {
        int in = fontSizeIndex;
        if (larger) {
            fontSizeIndex++;
            if (fontSizeIndex==sizes.length)
                fontSizeIndex = sizes.length-1;
        } else {
            fontSizeIndex--;
            if (fontSizeIndex<0)
                fontSizeIndex = 0;
        }
        IJ.showStatus(sizes[fontSizeIndex]+" point");
        setFont();
    }
    
    void saveSettings() {
        Prefs.set(FONT_SIZE, fontSizeIndex);
        Prefs.set(FONT_MONO, monospaced.getState());
        IJ.showStatus("Font settings saved (size="+sizes[fontSizeIndex]+", monospaced="+monospaced.getState()+")");
    }
    
    void setFont() {
        ta.setFont(new Font(getFontName(), Font.PLAIN, sizes[fontSizeIndex]));
    }
    
    String getFontName() {
        return monospaced.getState()?"Monospaced":"SansSerif";
    }
    
    public void setFont(Font font) {
        ta.setFont(font);
    }
    
    public int getFontSize() {
        return sizes[fontSizeIndex];
    }

    public void append(String s) {
        ta.append(s);
    }

    public void setIsMacroWindow(boolean mw) {
        isMacroWindow = mw;
    }

    public static void setDefaultDirectory(String dir) {
        dir = IJ.addSeparator(dir);
        defaultDir = dir;
    }
    
    public void lostOwnership (Clipboard clip, Transferable cont) {}
    
    public int debug(Interpreter interp, int mode) {
        if (IJ.debugMode)
            IJ.log("debug: "+interp.getLineNumber()+"  "+mode+"  "+interp);
        if (mode==RUN_TO_COMPLETION)
            return 0;
        int n = interp.getLineNumber();
        if (mode==RUN_TO_CARET) {
            if (n==runToLine) {
                mode = STEP;
                interp.setDebugMode(mode);
            } else
                return 0;
        }
        if (!isVisible()) { // abort macro if user closes window
            interp.abortMacro();
            return 0;
        }
        if (n==previousLine) {
            previousLine=0;
            return 0;
        }
        Window win = WindowManager.getActiveWindow();
        if (win!=this)
            IJ.wait(50);
        toFront();
        previousLine = n;
        String text = ta.getText();
        if (IJ.isWindows())
            text = text.replaceAll("\r\n", "\n");
        char[] chars = new char[text.length()];
        chars = text.toCharArray();
        int count=1;
        debugStart=0;
        int len = chars.length;
        debugEnd = len;
        for (int i=0; i<len; i++) {
            if (chars[i]=='\n') count++;
            if (count==n && debugStart==0)
                debugStart=i+1;
            else if (count==n+1) {
                debugEnd=i;
                break;
            }
        }
        //IJ.log("debug: "+debugStart+"  "+debugEnd+"  "+len+"  "+count);
        if (debugStart==1) debugStart = 0;
        if ((debugStart==0||debugStart==len) && debugEnd==len)
            return 0; // skip code added with Interpreter.setAdditionalFunctions()
        ta.select(debugStart, debugEnd);
        if (debugWindow!=null && !debugWindow.isShowing()) {
            interp.setDebugger(null);
            debugWindow = null;
        } else
            debugWindow = interp.updateDebugWindow(interp.getVariables(), debugWindow);
        if (debugWindow!=null) {
            interp.updateArrayInspector();
            toFront();
        }
        if (mode==STEP) {
            step = false;
            while (!step && !interp.done() && isVisible())
                IJ.wait(5);
        } else {
            if (mode==FAST_TRACE)
                IJ.wait(5);
            else
                IJ.wait(150);
        }
        return 0;
    }
        
    public static Editor getInstance() {
        return instance;
    }
    
    public static String getJSPrefix(String arg) {
        if (arg==null)
            arg = "";
        return JavaScriptIncludes+"function getArgument() {return \""+arg+"\";};";
    }
    
    /** Changes Windows (CRLF) line separators to line feeds (LF). */
    public void fixLineEndings() {
        if (!IJ.isWindows())
            return;
        String text = ta.getText();
        text = text.replaceAll("\r\n", "\n");
        ta.setText(text);
    }
    
    public void showLogWindow() {
        Frame log = WindowManager.getFrame("Log");
        if (log!=null)
            log.toFront();
        else
            IJ.log("");
    }

    public boolean fileChanged() {
        return changes;
    }
    
    /** Downloads BeanShell or Jython interpreter using a separate thread. */
    public void run() {
        if (downloading || downloadUrl==null)
            return;
        downloading = true;
        boolean ok = Macro_Runner.downloadJar(downloadUrl);
        downloading = false;
    }

}