alvinalexander.com | career | drupal | java | mac | mysql | perl | scala | uml | unix  

What this is

This file is included in the DevDaily.com "Java Source Code Warehouse" project. The intent of this project is to help you "Learn Java by Example" TM.

Other links

The source code

/*
 *                 Sun Public License Notice
 * 
 * The contents of this file are subject to the Sun Public License
 * Version 1.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://www.sun.com/
 * 
 * The Original Code is NetBeans. The Initial Developer of the Original
 * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.editor.ext;

import java.io.CharArrayWriter;
import java.io.Writer;
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.text.Document;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import org.netbeans.editor.BaseKit;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Formatter;
import org.netbeans.editor.Settings;
import org.netbeans.editor.SettingsUtil;
import org.netbeans.editor.SettingsChangeEvent;
import org.netbeans.editor.Utilities;
import org.netbeans.editor.GuardedException;
import org.netbeans.editor.Acceptor;
import org.netbeans.editor.AcceptorFactory;
import org.netbeans.editor.Syntax;

/**
* Unlike the formatter class, the ExtFormatter concentrates
* on providing a support for the real formatting process.
* Each formatter (there's only one per each kit) can contain
* one or more formatting layers. The FormatLayer
* operates over the chain of the tokens provided
* by the FormatWriter. The formatting consist
* of changing the chain of the tokens until it gets
* the desired look.
* Each formatting requires a separate instance
* of FormatWriter but the same set of format-layers
* is used for all the format-writers. Although the base
* implementation is synchronized so that only one
* format-writer at time is processed by each format-writer,
* in general it's not necessary.
* The basic implementation processes all the format-layers
* sequentialy in the order they were added to the formatter
* but this can be redefined.
* The getSettingValue enables to get the up-to-date
* value for the particular setting.
*
* @author Miloslav Metelka
* @version 1.00
*/

public class ExtFormatter extends Formatter implements FormatLayer {

    /** List holding the format layers */
    private List formatLayerList = new ArrayList();

    /** Use this instead of testing by containsKey() */
    private static final Object NULL_VALUE = new Object();

    /** Map that contains the requested [setting-name, setting-value] pairs */
    private HashMap settingsMap = new HashMap();

    /** Contains the names of the keys that were turned
     * into custom settings and are no longer read from
     * the Settings.
     */
    private HashMap customSettingsNamesMap = new HashMap();

    private Acceptor indentHotCharsAcceptor;
    private boolean reindentWithTextBefore;

    public ExtFormatter(Class kitClass) {
        super(kitClass);

        initFormatLayers();
    }

    /** Add the desired format-layers to the formatter */
    protected void initFormatLayers() {
    }

    /** Return the name of this formatter. By default
     * it's the name of the kit-class for which it's created
     * without the package name.
     */
    public String getName() {
        return getKitClass().getName().substring(
                getKitClass().getName().lastIndexOf('.') + 1);
    }

    public void settingsChange(SettingsChangeEvent evt) {
        super.settingsChange(evt);
        String settingName = (evt != null) ? evt.getSettingName() : null;

        Class kitClass = getKitClass();
        Iterator eit = settingsMap.entrySet().iterator();
        while (eit.hasNext()) {
            Map.Entry e = (Map.Entry)eit.next();
            if (settingName == null || e.getKey().equals(e.getKey())) {
                if (!customSettingsNamesMap.containsKey(e.getKey())) { // not custom
                    e.setValue(Settings.getValue(kitClass, (String)e.getKey()));
                }
            }
        }
        
        indentHotCharsAcceptor = SettingsUtil.getAcceptor(kitClass,
            ExtSettingsNames.INDENT_HOT_CHARS_ACCEPTOR,
            AcceptorFactory.FALSE);

        reindentWithTextBefore = SettingsUtil.getBoolean(kitClass, 
            ExtSettingsNames.REINDENT_WITH_TEXT_BEFORE,
            false);
    }

    /** Get the value of the given setting.
    * @param settingName name of the setting to get.
    */
    public Object getSettingValue(String settingName) {
        synchronized (Settings.class) {
            Object value = settingsMap.get(settingName);
            if (value == null && !customSettingsNamesMap.containsKey(settingName)) {
                value = Settings.getValue(getKitClass(), settingName);
                if (value == null) {
                    value = NULL_VALUE;
                }
                settingsMap.put(settingName, value);
            }
            return (value != NULL_VALUE) ? value : null;
        }
    }

    /** This method allows to set a custom value to a setting thus
     * overriding the value retrieved from the Settings.
     * Once done the value is no longer synchronized with the changes
     * in Settings for the particular setting.
     * There's a map holding the names of all the custom
     * settings.
     */
    public void setSettingValue(String settingName, Object settingValue) {
        synchronized (Settings.class) {
            customSettingsNamesMap.put(settingName, settingName);
            settingsMap.put(settingName, settingValue);
        }
    }

    /** Add the new format layer to the layer hierarchy.
    */
    public synchronized void addFormatLayer(FormatLayer layer) {
        formatLayerList.add(layer);
    }

    /** Replace the format-layer with the layerName
    * with the the given layer. If there's no such layer with the same
    * name, the layer is not replaced and false is returned.
    */
    public synchronized boolean replaceFormatLayer(String layerName, FormatLayer layer) {
        int cnt = formatLayerList.size();
        for (int i = 0; i < cnt; i++) {
            if (layerName.equals(((FormatLayer)formatLayerList.get(i)).getName())) {
                formatLayerList.set(i, layer);
                return true;
            }
        }
        return false;
    }

    /** Remove the first layer which has the same name as the given one.
    */
    public synchronized void removeFormatLayer(String layerName) {
        Iterator it = formatLayerIterator();
        while (it.hasNext()) {
            if (layerName.equals(((FormatLayer)it.next()).getName())) {
                it.remove();
                return;
            }
        }
    }

    /** Get the iterator over the format layers.
    */
    public Iterator formatLayerIterator() {
        return formatLayerList.iterator();
    }

    /** Whether do no formatting at all. If this method returns true,
     * the FormatWriter will simply write its input into the underlying
     * writer.
     */
    public boolean isSimple() {
        return false;
    }

    /** Called by format-writer to do the format */
    public synchronized void format(FormatWriter fw) {
        boolean done = false;
        int safetyCounter = 0;
        do {
            // Mark the chain as unmodified at the begining
            fw.setChainModified(false);
            fw.setRestartFormat(false);

            Iterator it = formatLayerIterator();
            while (it.hasNext()) {
                ((FormatLayer)it.next()).format(fw);
                if (fw.isRestartFormat()) {
                    break;
                }
            }

            if (!it.hasNext() && !fw.isRestartFormat()) {
                done = true;
            }

            if (safetyCounter > 1000) { // prevent infinite loop
                new Exception("Indentation infinite loop detected").printStackTrace(); // NOI18N
                break;
            }
        } while (!done);
    }

    /** Reformat a block of code.
    * @param doc document to work with
    * @param startOffset position at which the formatting starts
    * @param endOffset position at which the formatting ends
    * @param indentOnly whether just the indentation should be changed
    *  or regular formatting should be performed.
    * @return formatting writer. The text was already reformatted
    *  but the writer can contain useful information.
    */
    public Writer reformat(BaseDocument doc, int startOffset, int endOffset,
    boolean indentOnly) throws BadLocationException, IOException {
        CharArrayWriter cw = new CharArrayWriter();
        Writer w = createWriter(doc, startOffset, cw);
        FormatWriter fw = (w instanceof FormatWriter) ? (FormatWriter)w : null;
        
        boolean fix5620 = true; // whether apply fix for #5620 or not

        if (fw != null) {
            fw.setIndentOnly(indentOnly);
            if (fix5620) {
                fw.setReformatting(true); // #5620
            }
        }

        w.write(doc.getChars(startOffset, endOffset - startOffset));
        w.close();

        if (!fix5620 || fw == null) { // #5620 - for (fw != null) the doc was already modified
            String out = new String(cw.toCharArray());
            doc.remove(startOffset, endOffset - startOffset);
            doc.insertString(startOffset, out, null);
        }
        
        return w;
    }

    /** Fix of #5620 - same method exists in Formatter (predecessor */
    public int reformat(BaseDocument doc, int startOffset, int endOffset)
    throws BadLocationException {
        try {
            javax.swing.text.Position pos = doc.createPosition(endOffset);
            reformat(doc, startOffset, endOffset, false);
            return pos.getOffset() - startOffset;
        } catch (IOException e) {
            e.printStackTrace();
            return 0;
        }
    }

    /** Get the block to be reformatted after keystroke was pressed.
     * @param target component to which the text was typed. Caaret position
     *  can be checked etc.
     * @param typedText text (usually just one character) that the user has typed.
     * @return block of the code to be reformatted or null if nothing should
     *  reformatted. It can return block containing just one character. The caller
     *  usually expands even one character to the whole line because less than
     *  the whole line usually doesn't provide enough possibilities for formatting.
     * @see ExtKit.ExtDefaultKeyTypedAction.checkIndentHotChars()
     */
    public int[] getReformatBlock(JTextComponent target, String typedText) {
        if (indentHotCharsAcceptor == null) { // init if necessary
            settingsChange(null);
        }

        if (indentHotCharsAcceptor.accept(typedText.charAt(0))) {
            /* This is bugfix 10771. See the issue for problem description.
             * The behaviour before fix was that whenever the lbrace is
             * entered, the line is indented. This make no sense if a text
             * exist on the line before the lbrace. In this case we
             * simply will not indent the line. This is handled by the hasTextBefore 
             * check
             */
            if(!reindentWithTextBefore) {
                if(hasTextBefore(target, typedText)) {
                    return null;
                }
            }
            int dotPos = target.getCaret().getDot();
            return new int[] { Math.max(dotPos - 1, 0), dotPos };
            
        } else {
            return null;
        }
    }

    protected boolean hasTextBefore(JTextComponent target, String typedText) {
        BaseDocument doc = Utilities.getDocument(target);
        int dotPos = target.getCaret().getDot();
        try {
            int fnw = Utilities.getRowFirstNonWhite(doc, dotPos);
            return dotPos != fnw+typedText.length();
        } catch (BadLocationException e) {
            return false;
        }
    }

    /** Create the indentation writer.
    */
    public Writer createWriter(Document doc, int offset, Writer writer) {
        return new FormatWriter(this, doc, offset, writer, false);
    }

    /** Indents the current line. Should not affect any other
    * lines.
    * @param doc the document to work on
    * @param offset the offset of a character on the line
    * @return new offset of the original character
    */
    public int indentLine(Document doc, int offset) {
        if (doc instanceof BaseDocument) {
            try {
                BaseDocument bdoc = (BaseDocument)doc;
                int lineStart = Utilities.getRowStart(bdoc, offset);
                int nextLineStart = Utilities.getRowStart(bdoc, offset, 1);
                if (nextLineStart < 0) { // end of doc
                    nextLineStart = bdoc.getLength();
                }
                reformat(bdoc, lineStart, nextLineStart, false);
                return Utilities.getRowEnd(bdoc, lineStart);
            } catch (GuardedException e) {
                java.awt.Toolkit.getDefaultToolkit().beep();

            } catch (BadLocationException e) {
                Utilities.annotateLoggable(e);
            } catch (IOException e) {
                Utilities.annotateLoggable(e);
            }

            return offset;

        }

        return super.indentLine(doc, offset);
    }
    
    /** Returns offset of EOL for the white line */
    protected int getEOLOffset(BaseDocument bdoc, int offset) throws BadLocationException{
        return Utilities.getRowEnd(bdoc, offset);
    }

    /** Inserts new line at given position and indents the new line with
    * spaces.
    *
    * @param doc the document to work on
    * @param offset the offset of a character on the line
    * @return new offset to place cursor to
    */
    public int indentNewLine(Document doc, int offset) {
        if (doc instanceof BaseDocument) {
            BaseDocument bdoc = (BaseDocument)doc;
            boolean newLineInserted = false;

            bdoc.atomicLock();
            try {
                bdoc.insertString(offset, "\n", null); // NOI18N
                offset++;
                newLineInserted = true;

                int eolOffset = Utilities.getRowEnd(bdoc, offset);

                // Try to change the indent of the new line
                // It may fail when inserting '\n' before the guarded block
                Writer w = reformat(bdoc, offset, eolOffset, true);

                // Find the caret position
                eolOffset = Utilities.getRowFirstNonWhite(bdoc, offset);
                if (eolOffset < 0) { // white line
                    eolOffset = getEOLOffset(bdoc, offset);
                }

                offset = eolOffset;
                
                // Resulting offset (caret position) can be shifted
                if (w instanceof FormatWriter) {
                    offset += ((FormatWriter)w).getIndentShift();
                }

            } catch (GuardedException e) {
                // Possibly couldn't insert additional indentation
                // at the begining of the guarded block
                // but the initial '\n' could be fine
                if (!newLineInserted) {
                    java.awt.Toolkit.getDefaultToolkit().beep();
                }

            } catch (BadLocationException e) {
                Utilities.annotateLoggable(e);
            } catch (IOException e) {
                Utilities.annotateLoggable(e);
            } finally {
                bdoc.atomicUnlock();
            }

        } else { // not BaseDocument
            try {
                doc.insertString (offset, "\n", null); // NOI18N
                offset++;
            } catch (BadLocationException ex) {
            }
        }

        return offset;
    }

    /** Whether the formatter accepts the given syntax
     * that will be used for parsing the text passed to
     * the FormatWriter.
     * @param syntax syntax to be tested.
     * @return true whether this formatter is able to process
     *  the tokens created by the syntax or false otherwise.
     */
    protected boolean acceptSyntax(Syntax syntax) {
        return true;
    }

    /** Simple formatter */
    public static class Simple extends ExtFormatter {

        public Simple(Class kitClass) {
            super(kitClass);
        }

        public boolean isSimple() {
            return true;
        }
        
        /** Returns offset of EOL for the white line */
        protected int getEOLOffset(BaseDocument bdoc, int offset) throws BadLocationException{
            return offset;
        }
        
    }

}
... this post is sponsored by my books ...

#1 New Release!

FP Best Seller

 

new blog posts

 

Copyright 1998-2021 Alvin Alexander, alvinalexander.com
All Rights Reserved.

A percentage of advertising revenue from
pages under the /java/jwarehouse URI on this website is
paid back to open source projects.