/*
* 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 org.netbeans.editor.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.Writer;
import java.util.*;
import org.netbeans.editor.Formatter;
/**
* 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 <tt>FormatLayer</tt>
* operates over the chain of the tokens provided
* by the <tt>FormatWriter</tt>. The formatting consist
* of changing the chain of the tokens until it gets
* the desired look.
* Each formatting requires a separate instance
* of <tt>FormatWriter</tt> 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 <tt>getSettingValue</tt> 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 <tt>Settings</tt>.
* Once done the value is no longer synchronized with the changes
* in <tt>Settings</tt> 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) {
if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
e.printStackTrace();
}
} catch (IOException e) {
if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
e.printStackTrace();
}
}
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) {
if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
e.printStackTrace();
}
} catch (IOException e) {
if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
e.printStackTrace();
}
} 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;
}
}
}