PmpEditor.java Source code

Java tutorial

Introduction

Here is the source code for PmpEditor.java

Source

//Code revised from
/*
The Definitive Guide to SWT and JFace
by Robert Harris and Rob Warner 
Apress 2004
*/
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Stack;
import java.util.StringTokenizer;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.ExtendedModifyEvent;
import org.eclipse.swt.custom.ExtendedModifyListener;
import org.eclipse.swt.custom.LineStyleEvent;
import org.eclipse.swt.custom.LineStyleListener;
import org.eclipse.swt.custom.ST;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.custom.StyledTextPrintOptions;
import org.eclipse.swt.events.ArmEvent;
import org.eclipse.swt.events.ArmListener;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.layout.FormAttachment;
import org.eclipse.swt.layout.FormData;
import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.printing.Printer;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.MenuItem;
import org.eclipse.swt.widgets.MessageBox;
import org.eclipse.swt.widgets.Shell;

public class PmpEditor {
    // The number of operations that can be undone
    private static final int UNDO_LIMIT = 500;

    // Contains a reference to this application
    private static PmpEditor app;

    // Contains a reference to the main window
    private Shell shell;

    // Displays the file
    private StyledText st;

    // The full path of the current file
    private String filename;

    // The font for the StyledText
    private Font font;

    // The label to display statistics
    private Label status;

    // The print options and printer
    private StyledTextPrintOptions options;

    private Printer printer;

    // The stack used to store the undo information
    private Stack changes;

    // Flag to set before performaing an undo, so the undo
    // operation doesn't get stored with the rest of the undo
    // information
    private boolean ignoreUndo = false;

    // Syntax data for the current extension
    private SyntaxData sd;

    // Line style listener
    private PmpeLineStyleListener lineStyleListener;

    /**
     * Gets the reference to this application
     * 
     * @return HexEditor
     */
    public static PmpEditor getApp() {
        return app;
    }

    /**
     * Constructs a PmpEditor
     */
    public PmpEditor() {
        app = this;
        changes = new Stack();

        // Set up the printing options
        options = new StyledTextPrintOptions();
        options.footer = StyledTextPrintOptions.SEPARATOR + StyledTextPrintOptions.PAGE_TAG
                + StyledTextPrintOptions.SEPARATOR + "Confidential";
    }

    /**
     * Runs the application
     */
    public void run() {
        Display display = new Display();
        shell = new Shell(display);
        // Choose a monospaced font
        font = new Font(display, "Terminal", 12, SWT.NONE);

        createContents(shell);
        shell.open();
        while (!shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
        font.dispose();
        display.dispose();
        if (printer != null)
            printer.dispose();
    }

    /**
     * Creates the main window's contents
     * 
     * @param shell
     *          the main window
     */
    private void createContents(Shell shell) {
        // Set the layout and the menu bar
        shell.setLayout(new FormLayout());
        shell.setMenuBar(new PmpEditorMenu(shell).getMenu());

        // Create the status bar
        status = new Label(shell, SWT.BORDER);
        FormData data = new FormData();
        data.left = new FormAttachment(0, 0);
        data.right = new FormAttachment(100, 0);
        data.bottom = new FormAttachment(100, 0);
        data.height = status.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
        status.setLayoutData(data);

        // Create the styled text
        st = new StyledText(shell, SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
        data = new FormData();
        data.left = new FormAttachment(0);
        data.right = new FormAttachment(100);
        data.top = new FormAttachment(0);
        data.bottom = new FormAttachment(status);
        st.setLayoutData(data);

        // Set the font
        st.setFont(font);

        // Add Brief delete next word
        // Use SWT.MOD1 instead of SWT.CTRL for portability
        st.setKeyBinding('k' | SWT.MOD1, ST.DELETE_NEXT);

        // Add vi end of line (kind of)
        // Use SWT.MOD1 instead of SWT.CTRL for portability
        // Use SWT.MOD2 instead of SWT.SHIFT for portability
        // Shift+4 is $
        st.setKeyBinding('4' | SWT.MOD1 | SWT.MOD2, ST.LINE_END);

        // Handle key presses
        st.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent event) {
                // Update the status bar
                updateStatus();
            }
        });

        // Handle text modifications
        st.addModifyListener(new ModifyListener() {
            public void modifyText(ModifyEvent event) {
                // Update the status bar
                updateStatus();

                // Update the comments
                if (lineStyleListener != null) {
                    lineStyleListener.refreshMultilineComments(st.getText());
                    st.redraw();
                }
            }
        });

        // Store undo information
        st.addExtendedModifyListener(new ExtendedModifyListener() {
            public void modifyText(ExtendedModifyEvent event) {
                if (!ignoreUndo) {
                    // Push this change onto the changes stack
                    changes.push(new TextChange(event.start, event.length, event.replacedText));
                    if (changes.size() > UNDO_LIMIT)
                        changes.remove(0);
                }
            }
        });

        // Update the title bar and the status bar
        updateTitle();
        updateStatus();
    }

    /**
     * Opens a file
     */
    public void openFile() {
        FileDialog dlg = new FileDialog(shell);
        String temp = dlg.open();
        if (temp != null) {
            try {
                // Get the file's contents
                String text = PmpeIoManager.getFile(temp);
                // File loaded, so save the file name
                filename = temp;

                // Update the syntax properties to use
                updateSyntaxData();

                // Put the new file's data in the StyledText
                st.setText(text);

                // Update the title bar
                updateTitle();

                // Delete any undo information
                changes.clear();
            } catch (IOException e) {
                showError(e.getMessage());
            }
        }
    }

    /**
     * Saves a file
     */
    public void saveFile() {
        if (filename == null) {
            saveFileAs();
        } else {
            try {
                // Save the file and update the title bar based on the new file name
                PmpeIoManager.saveFile(filename, st.getText().getBytes());
                updateTitle();
            } catch (IOException e) {
                showError(e.getMessage());
            }
        }
    }

    /**
     * Saves a file under a different name
     */
    public void saveFileAs() {
        FileDialog dlg = new FileDialog(shell);
        if (filename != null) {
            dlg.setFileName(filename);
        }
        String temp = dlg.open();
        if (temp != null) {
            filename = temp;

            // The extension may have changed; update the syntax data accordingly
            updateSyntaxData();
            saveFile();
        }
    }

    /**
     * Prints the document to the default printer
     */
    public void print() {
        if (printer == null)
            printer = new Printer();
        options.header = StyledTextPrintOptions.SEPARATOR + filename + StyledTextPrintOptions.SEPARATOR;
        st.print(printer, options).run();
    }

    /**
     * Cuts the current selection to the clipboard
     */
    public void cut() {
        st.cut();
    }

    /**
     * Copies the current selection to the clipboard
     */
    public void copy() {
        st.copy();
    }

    /**
     * Pastes the clipboard's contents
     */
    public void paste() {
        st.paste();
    }

    /**
     * Selects all the text
     */
    public void selectAll() {
        st.selectAll();
    }

    /**
     * Undoes the last change
     */
    public void undo() {
        // Make sure undo stack isn't empty
        if (!changes.empty()) {
            // Get the last change
            TextChange change = (TextChange) changes.pop();

            // Set the flag. Otherwise, the replaceTextRange call will get placed
            // on the undo stack
            ignoreUndo = true;
            // Replace the changed text
            st.replaceTextRange(change.getStart(), change.getLength(), change.getReplacedText());

            // Move the caret
            st.setCaretOffset(change.getStart());

            // Scroll the screen
            st.setTopIndex(st.getLineAtOffset(change.getStart()));
            ignoreUndo = false;
        }
    }

    /**
     * Toggles word wrap
     */
    public void toggleWordWrap() {
        st.setWordWrap(!st.getWordWrap());
    }

    /**
     * Gets the current word wrap settings
     * 
     * @return boolean
     */
    public boolean getWordWrap() {
        return st.getWordWrap();
    }

    /**
     * Shows an about box
     */
    public void about() {
        MessageBox mb = new MessageBox(shell, SWT.ICON_INFORMATION | SWT.OK);
        mb.setMessage("Poor Man's Programming Editor");
        mb.open();
    }

    /**
     * Updates the title bar
     */
    private void updateTitle() {
        String fn = filename == null ? "Untitled" : filename;
        shell.setText(fn + " -- PmPe");
    }

    /**
     * Updates the status bar
     */
    private void updateStatus() {
        // Show the offset into the file, the total number of characters in the
        // file,
        // the current line number (1-based) and the total number of lines
        StringBuffer buf = new StringBuffer();
        buf.append("Offset: ");
        buf.append(st.getCaretOffset());
        buf.append("\tChars: ");
        buf.append(st.getCharCount());
        buf.append("\tLine: ");
        buf.append(st.getLineAtOffset(st.getCaretOffset()) + 1);
        buf.append(" of ");
        buf.append(st.getLineCount());
        status.setText(buf.toString());
    }

    /**
     * Updates the syntax data based on the filename's extension
     */
    private void updateSyntaxData() {
        // Determine the extension of the current file
        String extension = "";
        if (filename != null) {
            int pos = filename.lastIndexOf(".");
            if (pos > -1 && pos < filename.length() - 2) {
                extension = filename.substring(pos + 1);
            }
        }

        // Get the syntax data for the extension
        sd = SyntaxManager.getSyntaxData(extension);

        // Reset the line style listener
        if (lineStyleListener != null) {
            st.removeLineStyleListener(lineStyleListener);
        }
        lineStyleListener = new PmpeLineStyleListener(sd);
        st.addLineStyleListener(lineStyleListener);

        // Redraw the contents to reflect the new syntax data
        st.redraw();
    }

    /**
     * Shows an error message
     * 
     * @param error
     *          the text to show
     */
    private void showError(String error) {
        MessageBox mb = new MessageBox(shell, SWT.ICON_ERROR | SWT.OK);
        mb.setMessage(error);
        mb.open();
    }

    /**
     * The application entry point
     * 
     * @param args
     *          the command line arguments
     */
    public static void main(String[] args) {
        new PmpEditor().run();
    }
}

class PmpEditorMenu {
    // The underlying menu this class wraps
    Menu menu = null;

    /**
     * Constructs a PmpEditorMenu
     * 
     * @param shell
     *          the parent shell
     */
    public PmpEditorMenu(final Shell shell) {
        // Create the menu
        menu = new Menu(shell, SWT.BAR);

        // Create the File top-level menu
        MenuItem item = new MenuItem(menu, SWT.CASCADE);
        item.setText("File");
        Menu dropMenu = new Menu(shell, SWT.DROP_DOWN);
        item.setMenu(dropMenu);

        // Create File->Open
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Open...\tCtrl+O");
        item.setAccelerator(SWT.CTRL + 'O');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().openFile();
            }
        });

        // Create File->Save
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Save\tCtrl+S");
        item.setAccelerator(SWT.CTRL + 'S');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().saveFile();
            }
        });

        // Create File->Save As
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Save As...");
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().saveFileAs();
            }
        });

        new MenuItem(dropMenu, SWT.SEPARATOR);

        // Create File->Print
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Print\tCtrl+P");
        item.setAccelerator(SWT.CTRL + 'P');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().print();
            }
        });

        new MenuItem(dropMenu, SWT.SEPARATOR);

        // Create File->Exit
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Exit\tAlt+F4");
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                shell.close();
            }
        });

        // Create Edit
        item = new MenuItem(menu, SWT.CASCADE);
        item.setText("Edit");
        dropMenu = new Menu(shell, SWT.DROP_DOWN);
        item.setMenu(dropMenu);

        // Create Edit->Cut
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Cut\tCtrl+X");
        item.setAccelerator(SWT.CTRL + 'X');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().cut();
            }
        });

        // Create Edit->Copy
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Copy\tCtrl+C");
        item.setAccelerator(SWT.CTRL + 'C');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().copy();
            }
        });

        // Create Edit->Paste
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Paste\tCtrl+V");
        item.setAccelerator(SWT.CTRL + 'V');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().paste();
            }
        });

        new MenuItem(dropMenu, SWT.SEPARATOR);

        // Create Select All
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Select All\tCtrl+A");
        item.setAccelerator(SWT.CTRL + 'A');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().selectAll();
            }
        });

        new MenuItem(dropMenu, SWT.SEPARATOR);

        // Create Undo
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("Undo\tCtrl+Z");
        item.setAccelerator(SWT.CTRL + 'Z');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().undo();
            }
        });

        new MenuItem(dropMenu, SWT.SEPARATOR);
        // Create Word Wrap
        final MenuItem wwItem = new MenuItem(dropMenu, SWT.CHECK);
        wwItem.setText("Word Wrap\tCtrl+W");
        wwItem.setAccelerator(SWT.CTRL + 'W');
        wwItem.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().toggleWordWrap();
            }
        });
        wwItem.addArmListener(new ArmListener() {
            public void widgetArmed(ArmEvent event) {
                wwItem.setSelection(PmpEditor.getApp().getWordWrap());
            }
        });

        // Create Help
        item = new MenuItem(menu, SWT.CASCADE);
        item.setText("Help");
        dropMenu = new Menu(shell, SWT.DROP_DOWN);
        item.setMenu(dropMenu);

        // Create Help->About
        item = new MenuItem(dropMenu, SWT.NULL);
        item.setText("About\tCtrl+A");
        item.setAccelerator(SWT.CTRL + 'A');
        item.addSelectionListener(new SelectionAdapter() {
            public void widgetSelected(SelectionEvent event) {
                PmpEditor.getApp().about();
            }
        });
    }

    /**
     * Gets the underlying menu
     * 
     * @return Menu
     */
    public Menu getMenu() {
        return menu;
    }
}

class PmpeIoManager {
    /**
     * Gets a file (loads it) from the filesystem
     * 
     * @param filename
     *          the full path of the file
     * @return String
     * @throws IOException
     *           if file cannot be loaded
     */
    public static String getFile(String filename) throws IOException {
        InputStream in = new BufferedInputStream(new FileInputStream(filename));
        StringBuffer buf = new StringBuffer();
        int c;
        while ((c = in.read()) != -1) {
            buf.append((char) c);
        }
        return buf.toString();
    }

    /**
     * Saves a file
     * 
     * @param filename
     *          the full path of the file to save
     * @param data
     *          the data to save
     * @throws IOException
     *           if file cannot be saved
     */
    public static void saveFile(String filename, byte[] data) throws IOException {
        File outputFile = new File(filename);
        FileOutputStream out = new FileOutputStream(outputFile);
        out.write(data);
        out.close();
    }
}

class TextChange {
    // The starting offset of the change
    private int start;

    // The length of the change
    private int length;

    // The replaced text
    String replacedText;

    /**
     * Constructs a TextChange
     * 
     * @param start
     *          the starting offset of the change
     * @param length
     *          the length of the change
     * @param replacedText
     *          the text that was replaced
     */
    public TextChange(int start, int length, String replacedText) {
        this.start = start;
        this.length = length;
        this.replacedText = replacedText;
    }

    /**
     * Returns the start
     * 
     * @return int
     */
    public int getStart() {
        return start;
    }

    /**
     * Returns the length
     * 
     * @return int
     */
    public int getLength() {
        return length;
    }

    /**
     * Returns the replacedText
     * 
     * @return String
     */
    public String getReplacedText() {
        return replacedText;
    }
}

/**
 * This class contains information for syntax coloring and styling for an
 * extension
 */
class SyntaxData {
    private String extension;

    private Collection keywords;

    private String punctuation;

    private String comment;

    private String multiLineCommentStart;

    private String multiLineCommentEnd;

    /**
     * Constructs a SyntaxData
     * 
     * @param extension
     *          the extension
     */
    public SyntaxData(String extension) {
        this.extension = extension;
    }

    /**
     * Gets the extension
     * 
     * @return String
     */
    public String getExtension() {
        return extension;
    }

    /**
     * Gets the comment
     * 
     * @return String
     */
    public String getComment() {
        return comment;
    }

    /**
     * Sets the comment
     * 
     * @param comment
     *          The comment to set.
     */
    public void setComment(String comment) {
        this.comment = comment;
    }

    /**
     * Gets the keywords
     * 
     * @return Collection
     */
    public Collection getKeywords() {
        return keywords;
    }

    /**
     * Sets the keywords
     * 
     * @param keywords
     *          The keywords to set.
     */
    public void setKeywords(Collection keywords) {
        this.keywords = keywords;
    }

    /**
     * Gets the multiline comment end
     * 
     * @return String
     */
    public String getMultiLineCommentEnd() {
        return multiLineCommentEnd;
    }

    /**
     * Sets the multiline comment end
     * 
     * @param multiLineCommentEnd
     *          The multiLineCommentEnd to set.
     */
    public void setMultiLineCommentEnd(String multiLineCommentEnd) {
        this.multiLineCommentEnd = multiLineCommentEnd;
    }

    /**
     * Gets the multiline comment start
     * 
     * @return String
     */
    public String getMultiLineCommentStart() {
        return multiLineCommentStart;
    }

    /**
     * Sets the multiline comment start
     * 
     * @param multiLineCommentStart
     *          The multiLineCommentStart to set.
     */
    public void setMultiLineCommentStart(String multiLineCommentStart) {
        this.multiLineCommentStart = multiLineCommentStart;
    }

    /**
     * Gets the punctuation
     * 
     * @return String
     */
    public String getPunctuation() {
        return punctuation;
    }

    /**
     * Sets the punctuation
     * 
     * @param punctuation
     *          The punctuation to set.
     */
    public void setPunctuation(String punctuation) {
        this.punctuation = punctuation;
    }
}

/**
 * This class manages the syntax coloring and styling data
 */
class SyntaxManager {
    // Lazy cache of SyntaxData objects
    private static Map data = new Hashtable();

    /**
     * Gets the syntax data for an extension
     */
    public static synchronized SyntaxData getSyntaxData(String extension) {
        // Check in cache
        SyntaxData sd = (SyntaxData) data.get(extension);
        if (sd == null) {
            // Not in cache; load it and put in cache
            sd = loadSyntaxData(extension);
            if (sd != null)
                data.put(sd.getExtension(), sd);
        }
        return sd;
    }

    /**
     * Loads the syntax data for an extension
     * 
     * @param extension
     *          the extension to load
     * @return SyntaxData
     */
    private static SyntaxData loadSyntaxData(String extension) {
        SyntaxData sd = null;
        try {
            ResourceBundle rb = ResourceBundle.getBundle("examples.ch11." + extension);
            sd = new SyntaxData(extension);
            sd.setComment(rb.getString("comment"));
            sd.setMultiLineCommentStart(rb.getString("multilinecommentstart"));
            sd.setMultiLineCommentEnd(rb.getString("multilinecommentend"));

            // Load the keywords
            Collection keywords = new ArrayList();
            for (StringTokenizer st = new StringTokenizer(rb.getString("keywords"), " "); st.hasMoreTokens();) {
                keywords.add(st.nextToken());
            }
            sd.setKeywords(keywords);

            // Load the punctuation
            sd.setPunctuation(rb.getString("punctuation"));
        } catch (MissingResourceException e) {
            // Ignore
        }
        return sd;
    }
}

/**
 * This class performs the syntax highlighting and styling for Pmpe
 */
class PmpeLineStyleListener implements LineStyleListener {
    // Colors
    private static final Color COMMENT_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_GREEN);

    private static final Color COMMENT_BACKGROUND = Display.getCurrent().getSystemColor(SWT.COLOR_GRAY);

    private static final Color PUNCTUATION_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_CYAN);

    private static final Color KEYWORD_COLOR = Display.getCurrent().getSystemColor(SWT.COLOR_DARK_MAGENTA);

    // Holds the syntax data
    private SyntaxData syntaxData;

    // Holds the offsets for all multiline comments
    List commentOffsets;

    /**
     * PmpeLineStyleListener constructor
     * 
     * @param syntaxData
     *          the syntax data to use
     */
    public PmpeLineStyleListener(SyntaxData syntaxData) {
        this.syntaxData = syntaxData;
        commentOffsets = new LinkedList();
    }

    /**
     * Refreshes the offsets for all multiline comments in the parent StyledText.
     * The parent StyledText should call this whenever its text is modified. Note
     * that this code doesn't ignore comment markers inside strings.
     * 
     * @param text
     *          the text from the StyledText
     */
    public void refreshMultilineComments(String text) {
        // Clear any stored offsets
        commentOffsets.clear();

        if (syntaxData != null) {
            // Go through all the instances of COMMENT_START
            for (int pos = text.indexOf(syntaxData.getMultiLineCommentStart()); pos > -1; pos = text
                    .indexOf(syntaxData.getMultiLineCommentStart(), pos)) {
                // offsets[0] holds the COMMENT_START offset
                // and COMMENT_END holds the ending offset
                int[] offsets = new int[2];
                offsets[0] = pos;

                // Find the corresponding end comment.
                pos = text.indexOf(syntaxData.getMultiLineCommentEnd(), pos);

                // If no corresponding end comment, use the end of the text
                offsets[1] = pos == -1 ? text.length() - 1 : pos + syntaxData.getMultiLineCommentEnd().length() - 1;
                pos = offsets[1];
                // Add the offsets to the collection
                commentOffsets.add(offsets);
            }
        }
    }

    /**
     * Checks to see if the specified section of text begins inside a multiline
     * comment. Returns the index of the closing comment, or the end of the line
     * if the whole line is inside the comment. Returns -1 if the line doesn't
     * begin inside a comment.
     * 
     * @param start
     *          the starting offset of the text
     * @param length
     *          the length of the text
     * @return int
     */
    private int getBeginsInsideComment(int start, int length) {
        // Assume section doesn't being inside a comment
        int index = -1;

        // Go through the multiline comment ranges
        for (int i = 0, n = commentOffsets.size(); i < n; i++) {
            int[] offsets = (int[]) commentOffsets.get(i);

            // If starting offset is past range, quit
            if (offsets[0] > start + length)
                break;
            // Check to see if section begins inside a comment
            if (offsets[0] <= start && offsets[1] >= start) {
                // It does; determine if the closing comment marker is inside
                // this section
                index = offsets[1] > start + length ? start + length
                        : offsets[1] + syntaxData.getMultiLineCommentEnd().length() - 1;
            }
        }
        return index;
    }

    /**
     * Called by StyledText to get styles for a line
     */
    public void lineGetStyle(LineStyleEvent event) {
        // Only do styles if syntax data has been loaded
        if (syntaxData != null) {
            // Create collection to hold the StyleRanges
            List styles = new ArrayList();

            int start = 0;
            int length = event.lineText.length();

            // Check if line begins inside a multiline comment
            int mlIndex = getBeginsInsideComment(event.lineOffset, event.lineText.length());
            if (mlIndex > -1) {
                // Line begins inside multiline comment; create the range
                styles.add(new StyleRange(event.lineOffset, mlIndex - event.lineOffset, COMMENT_COLOR,
                        COMMENT_BACKGROUND));
                start = mlIndex;
            }
            // Do punctuation, single-line comments, and keywords
            while (start < length) {
                // Check for multiline comments that begin inside this line
                if (event.lineText.indexOf(syntaxData.getMultiLineCommentStart(), start) == start) {
                    // Determine where comment ends
                    int endComment = event.lineText.indexOf(syntaxData.getMultiLineCommentEnd(), start);

                    // If comment doesn't end on this line, extend range to end of line
                    if (endComment == -1)
                        endComment = length;
                    else
                        endComment += syntaxData.getMultiLineCommentEnd().length();
                    styles.add(new StyleRange(event.lineOffset + start, endComment - start, COMMENT_COLOR,
                            COMMENT_BACKGROUND));

                    // Move marker
                    start = endComment;
                }
                // Check for single line comments
                else if (event.lineText.indexOf(syntaxData.getComment(), start) == start) {
                    // Comment rest of line
                    styles.add(new StyleRange(event.lineOffset + start, length - start, COMMENT_COLOR,
                            COMMENT_BACKGROUND));

                    // Move marker
                    start = length;
                }
                // Check for punctuation
                else if (syntaxData.getPunctuation().indexOf(event.lineText.charAt(start)) > -1) {
                    // Add range for punctuation
                    styles.add(new StyleRange(event.lineOffset + start, 1, PUNCTUATION_COLOR, null));
                    ++start;
                } else if (Character.isLetter(event.lineText.charAt(start))) {
                    // Get the next word
                    StringBuffer buf = new StringBuffer();
                    int i = start;
                    // Call any consecutive letters a word
                    for (; i < length && Character.isLetter(event.lineText.charAt(i)); i++) {
                        buf.append(event.lineText.charAt(i));
                    }
                    // See if the word is a keyword
                    if (syntaxData.getKeywords().contains(buf.toString())) {
                        // It's a keyword; create the StyleRange
                        styles.add(
                                new StyleRange(event.lineOffset + start, i - start, KEYWORD_COLOR, null, SWT.BOLD));
                    }
                    // Move the marker to the last char (the one that wasn't a letter)
                    // so it can be retested in the next iteration through the loop
                    start = i;
                } else
                    // It's nothing we're interested in; advance the marker
                    ++start;
            }

            // Copy the StyleRanges back into the event
            event.styles = (StyleRange[]) styles.toArray(new StyleRange[0]);
        }
    }
}