Colorer.java

/*
 * This file is part of the programmer editor demo
 * Copyright (C) 2001-2005 Stephen Ostermiller
 * http://ostermiller.org/contact.pl?regarding=Syntax+Highlighting
 * 
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * See COPYING.TXT for details.
 */
package com.jsql.view.swing.sql.lexer;

import com.jsql.util.LogLevelUtil;
import com.jsql.view.swing.sql.lexer.syntax.Lexer;
import com.jsql.view.swing.sql.lexer.syntax.Token;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import javax.swing.text.AttributeSet;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.*;

/**
 * Run the Syntax Highlighting as a separate thread. Things that need to be
 * colored are messaged to the thread and put in a list.
 */
class Colorer extends Thread {
    
    /**
     * Log4j logger sent to view.
     */
    private static final Logger LOGGER = LogManager.getRootLogger();
    
    /**
     * A simple wrapper representing something that needs to be colored. Placed
     * into an object so that it can be stored in a Vector.
     */
    private static class RecolorEvent {
        
        private int position;
        
        private int adjustment;

        public RecolorEvent(int position, int adjustment) {

            this.position = position;
            this.adjustment = adjustment;
        }

        public int getPosition() {
            return this.position;
        }

        public void setPosition(int position) {
            this.position = position;
        }

        public int getAdjustment() {
            return this.adjustment;
        }

        public void setAdjustment(int adjustment) {
            this.adjustment = adjustment;
        }
    }
    
    /**
     * Stores the document we are coloring. We use a WeakReference
     * so that the document is eligible for garbage collection when
     * it is no longer being used. At that point, this thread will
     * shut down itself.
     */
    private final WeakReference<HighlightedDocument> document;

    /**
     * Keep a list of places in the file that it is safe to restart the
     * highlighting. This happens whenever the lexer reports that it has
     * returned to its initial state. Since this list needs to be sorted, and
     * we need to be able to retrieve ranges from it, it is stored in a
     * balanced tree.
     */
    private final TreeSet<DocPosition> iniPositions = new TreeSet<>(DocPositionComparator.instance);

    /**
     * As we go through and remove invalid positions we will also be finding
     * new valid positions. Since the position list cannot be deleted from
     * and written to at the same time, we will keep a list of the new
     * positions and simply add it to the list of positions once all the old
     * positions have been removed.
     */
    private final HashSet<DocPosition> newPositions = new HashSet<>();

    /**
     * Vector that stores the communication between the two threads.
     */
    private final LinkedList<RecolorEvent> events = new LinkedList<>();

    /**
     * When accessing the linked list, we need to create a critical section.
     * we will synchronize on this object to ensure that we don't get unsafe
     * thread behavior.
     */
    private final Object eventsLock = new Object();

    /**
     * The amount of change that has occurred before the place in the
     * document that we are currently highlighting (lastPosition).
     */
    private volatile int change = 0;

    /**
     * The last position colored
     */
    private volatile int lastPosition = -1;

    /**
     * Creates the coloring thread for the given document.
     * 
     * @param document The document to be colored.
     */
    public Colorer(HighlightedDocument document) {

        super("ThreadColorer");
        this.document = new WeakReference<>(document);
    }

    /**
     * Tell the Syntax Highlighting thread to take another look at this
     * section of the document. It will process this as a FIFO. This method
     * should be done inside a docLock.
     */
    public void color(int position, int adjustment) {
        
        // figure out if this adjustment effects the current run.
        // if it does, then adjust the place in the document
        // that gets highlighted.
        if (position < this.lastPosition) {
            if (this.lastPosition < position - adjustment) {
                this.change -= this.lastPosition - position;
            } else {
                this.change += adjustment;
            }
        }
        
        synchronized (this.eventsLock) {
            
            if (!this.events.isEmpty()) {
                
                // check whether to coalesce with current last element
                RecolorEvent curLast = this.events.getLast();
                
                if (adjustment < 0 && curLast.getAdjustment() < 0) {
                    // both are removals
                    if (position == curLast.getPosition()) {
                        
                        curLast.setAdjustment(curLast.getAdjustment() + adjustment);
                        return;
                    }
                } else if (adjustment >= 0 && curLast.getAdjustment() >= 0) {
                    // both are insertions
                    if (position == curLast.getPosition() + curLast.getAdjustment()) {
                        
                        curLast.setAdjustment(curLast.getAdjustment() + adjustment);
                        return;
                        
                    } else if (curLast.getPosition() == position + adjustment) {
                        
                        curLast.setPosition(position);
                        curLast.setAdjustment(curLast.getAdjustment() + adjustment);
                        
                        return;
                    }
                }
            }
            
            this.events.add(new RecolorEvent(position, adjustment));
            this.eventsLock.notifyAll();
        }
    }

    /**
     * The colorer runs forever and may sleep for long periods of time. It
     * should be interrupted every time there is something for it to do.
     */
    @Override
    public void run() {
        while (this.document.get() != null) {
            try {
                RecolorEvent re = new RecolorEvent(0, 0);
                synchronized (this.eventsLock) {
                    
                    // get the next event to process - stalling until the
                    // event becomes available
                    while(this.events.isEmpty() && this.document.get() != null) {
                        // stop waiting after a second in case document
                        // has been cleared.
                        this.eventsLock.wait(1000);
                    }
                    
                    if (!this.events.isEmpty()) {
                        re = this.events.removeFirst();
                    }
                }
                this.processEvent(re.getPosition(), re.getAdjustment());
                Thread.sleep(100);
                
            } catch(InterruptedException e) {
                
                LOGGER.log(LogLevelUtil.IGNORE, e, e);
                Thread.currentThread().interrupt();
            }
        }
    }
    
    private void processEvent(int position, int adjustment) {
        
        HighlightedDocument doc = this.document.get();
        if (doc == null) {
            return;
        }
        
        // slurp everything up into local variables in case another
        // thread changes them during coloring process
        AttributeSet globalStyle = doc.getGlobalStyle();
        Lexer syntaxLexer = doc.getSyntaxLexer();
        DocumentReader documentReader = doc.getDocumentReader();
        Object docLock = doc.getDocumentLock();

        if (globalStyle != null) {
            
            int start = Math.min(position, position + adjustment);
            int stop = Math.max(position, position + adjustment);
            
            synchronized (docLock) {
                doc.setCharacterAttributes(start, stop - start, globalStyle, true);
            }
            
            return;
        }
        
        SortedSet<DocPosition> workingSet;
        Iterator<DocPosition> workingIt;
        DocPosition startRequest = new DocPosition(position);
        DocPosition endRequest = new DocPosition(position + Math.abs(adjustment));
        DocPosition dp;
        DocPosition dpStart = null;
        DocPosition dpEnd;

        // find the starting position. We must start at least one
        // token before the current position
        try {
            // all the good positions before
            workingSet = this.iniPositions.headSet(startRequest);
            // the last of the stuff before
            dpStart = workingSet.last();
            
        } catch (NoSuchElementException e) {
            
            // if there were no good positions before the requested
            // start,
            // we can always start at the very beginning.
            dpStart = new DocPosition(0);
            
            LOGGER.log(LogLevelUtil.IGNORE, e);
        }

        // if stuff was removed, take any removed positions off the
        // list.
        if (adjustment < 0) {
            
            workingSet = this.iniPositions.subSet(startRequest, endRequest);
            workingIt = workingSet.iterator();
            
            while (workingIt.hasNext()) {
                
                workingIt.next();
                workingIt.remove();
            }
        }

        // adjust the positions of everything after the
        // insertion/removal.
        workingSet = this.iniPositions.tailSet(startRequest);
        workingIt = workingSet.iterator();
        while (workingIt.hasNext()) {
            workingIt.next().adjustPosition(adjustment);
        }

        // now go through and highlight as much as needed
        workingSet = this.iniPositions.tailSet(dpStart);
        workingIt = workingSet.iterator();
        dp = null;
        
        if (workingIt.hasNext()) {
            dp = workingIt.next();
        }
        
        try {
            Token t;
            boolean done = false;
            dpEnd = dpStart;
            
            synchronized (docLock) {
                
                // we are playing some games with the lexer for
                // efficiency.
                // we could just create a new lexer each time here,
                // but instead,
                // we will just reset it so that it thinks it is
                // starting at the
                // beginning of the document but reporting a funny
                // start position.
                // Reseting the lexer causes the close() method on
                // the reader
                // to be called but because the close() method has
                // no effect on the
                // DocumentReader, we can do this.
                syntaxLexer.reset(documentReader, 0, dpStart
                        .getPosition(), 0);
                // After the lexer has been set up, scroll the
                // reader so that it
                // is in the correct spot as well.
                documentReader.seek(dpStart.getPosition());
                // we will highlight tokens until we reach a good
                // stopping place.
                // the first obvious stopping place is the end of
                // the document.
                // the lexer will return null at the end of the
                // document and wee
                // need to stop there.
                t = syntaxLexer.getNextToken();
            }
            
            this.newPositions.add(dpStart);
            
            while (!done && t != null) {
                // this is the actual command that colors the stuff.
                // Color stuff with the description of the styles
                // stored in tokenStyles.
                if (t.getCharEnd() <= doc.getLength()) {
                    
                    doc.setCharacterAttributes(
                        t.getCharBegin() + this.change,
                        t.getCharEnd() - t.getCharBegin(),
                        TokenStyles.getStyle(t.getDescription()),
                        true
                    );
                    // record the position of the last bit of
                    // text that we colored
                    dpEnd = new DocPosition(t.getCharEnd());
                }
                
                this.lastPosition = t.getCharEnd() + this.change;
                
                // The other more complicated reason for doing no
                // more highlighting
                // is that all the colors are the same from here on
                // out anyway.
                // We can detect this by seeing if the place that
                // the lexer returned
                // to the initial state last time we highlighted is
                // the same as the
                // place that returned to the initial state this
                // time.
                // As long as that place is after the last changed
                // text, everything
                // from there on is fine already.
                if (t.getState() == Token.INITIAL_STATE) {
                    
                    // look at all the positions from last time that
                    // are less than or
                    // equal to the current position
                    while (dp != null && dp.getPosition() <= t.getCharEnd()) {
                        
                        if (dp.getPosition() == t.getCharEnd() && dp.getPosition() >= endRequest.getPosition()) {
                            
                            // we have found a state that is the
                            // same
                            done = true;
                            dp = null;
                            
                        } else if (workingIt.hasNext()) {
                            
                            // didn't find it, try again.
                            dp = workingIt.next();
                            
                        } else {
                            
                            // didn't find it, and there is no more
                            // info from last
                            // time. This means that we will just
                            // continue
                            // until the end of the document.
                            dp = null;
                        }
                    }
                    
                    // so that we can do this check next time,
                    // record all the
                    // initial states from this time.
                    this.newPositions.add(dpEnd);
                }
                
                synchronized (docLock) {
                    t = syntaxLexer.getNextToken();
                }
            }

            // remove all the old initial positions from the place
            // where
            // we started doing the highlighting right up through
            // the last
            // bit of text we touched.
            workingIt = this.iniPositions.subSet(dpStart, dpEnd).iterator();
            while (workingIt.hasNext()) {
                
                workingIt.next();
                workingIt.remove();
            }

            // Remove all the positions that are after the end of
            // the file.:
            workingIt = this.iniPositions.tailSet(new DocPosition(doc.getLength())).iterator();
            while (workingIt.hasNext()) {
                
                workingIt.next();
                workingIt.remove();
            }

            // and put the new initial positions that we have found
            // on the list.
            this.iniPositions.addAll(this.newPositions);
            this.newPositions.clear();
            
        } catch (IOException e) {
            LOGGER.log(LogLevelUtil.IGNORE, e);
        }
        
        synchronized (docLock) {
            
            this.lastPosition = -1;
            this.change = 0;
        }
    }
    
    /**
     * Stop the thread's method run()
     */
    public void stopThread() {
        this.document.clear();
    }
}