001: /*
002: * Sun Public License Notice
003: *
004: * The contents of this file are subject to the Sun Public License
005: * Version 1.0 (the "License"). You may not use this file except in
006: * compliance with the License. A copy of the License is available at
007: * http://www.sun.com/
008: *
009: * The Original Code is NetBeans. The Initial Developer of the Original
010: * Code is Sun Microsystems, Inc. Portions Copyright 1997-2000 Sun
011: * Microsystems, Inc. All Rights Reserved.
012: */
013:
014: package org.netbeans.editor.ext;
015:
016: import java.io.CharArrayWriter;
017: import java.io.IOException;
018: import java.io.Writer;
019: import java.util.ArrayList;
020: import java.util.HashMap;
021: import java.util.Iterator;
022: import java.util.List;
023: import java.util.Map;
024:
025: import javax.swing.text.BadLocationException;
026: import javax.swing.text.Document;
027: import javax.swing.text.JTextComponent;
028:
029: import org.netbeans.editor.Acceptor;
030: import org.netbeans.editor.AcceptorFactory;
031: import org.netbeans.editor.BaseDocument;
032: import org.netbeans.editor.Formatter;
033: import org.netbeans.editor.GuardedException;
034: import org.netbeans.editor.Settings;
035: import org.netbeans.editor.SettingsChangeEvent;
036: import org.netbeans.editor.SettingsUtil;
037: import org.netbeans.editor.Syntax;
038: import org.netbeans.editor.Utilities;
039:
040: /**
041: * Unlike the formatter class, the ExtFormatter concentrates on providing a
042: * support for the real formatting process. Each formatter (there's only one per
043: * each kit) can contain one or more formatting layers. The <tt>FormatLayer</tt>
044: * operates over the chain of the tokens provided by the <tt>FormatWriter</tt>.
045: * The formatting consist of changing the chain of the tokens until it gets the
046: * desired look. Each formatting requires a separate instance of
047: * <tt>FormatWriter</tt> but the same set of format-layers is used for all the
048: * format-writers. Although the base implementation is synchronized so that only
049: * one format-writer at time is processed by each format-writer, in general it's
050: * not necessary. The basic implementation processes all the format-layers
051: * sequentialy in the order they were added to the formatter but this can be
052: * redefined. The <tt>getSettingValue</tt> enables to get the up-to-date value
053: * for the particular setting.
054: *
055: * @author Miloslav Metelka
056: * @version 1.00
057: */
058:
059: public class ExtFormatter extends Formatter implements FormatLayer {
060:
061: /** List holding the format layers */
062: private List formatLayerList = new ArrayList();
063:
064: /** Use this instead of testing by containsKey() */
065: private static final Object NULL_VALUE = new Object();
066:
067: /** Map that contains the requested [setting-name, setting-value] pairs */
068: private HashMap settingsMap = new HashMap();
069:
070: /**
071: * Contains the names of the keys that were turned into custom settings and
072: * are no longer read from the Settings.
073: */
074: private HashMap customSettingsNamesMap = new HashMap();
075:
076: private Acceptor indentHotCharsAcceptor;
077: private boolean reindentWithTextBefore;
078:
079: public ExtFormatter(Class kitClass) {
080: super (kitClass);
081:
082: initFormatLayers();
083: }
084:
085: /** Add the desired format-layers to the formatter */
086: protected void initFormatLayers() {
087: }
088:
089: /**
090: * Return the name of this formatter. By default it's the name of the
091: * kit-class for which it's created without the package name.
092: */
093: public String getName() {
094: return getKitClass().getName().substring(
095: getKitClass().getName().lastIndexOf('.') + 1);
096: }
097:
098: public void settingsChange(SettingsChangeEvent evt) {
099: super .settingsChange(evt);
100: String settingName = (evt != null) ? evt.getSettingName()
101: : null;
102:
103: Class kitClass = getKitClass();
104: Iterator eit = settingsMap.entrySet().iterator();
105: while (eit.hasNext()) {
106: Map.Entry e = (Map.Entry) eit.next();
107: if (settingName == null || e.getKey().equals(e.getKey())) {
108: if (!customSettingsNamesMap.containsKey(e.getKey())) { // not
109: // custom
110: e.setValue(Settings.getValue(kitClass, (String) e
111: .getKey()));
112: }
113: }
114: }
115:
116: indentHotCharsAcceptor = SettingsUtil.getAcceptor(kitClass,
117: ExtSettingsNames.INDENT_HOT_CHARS_ACCEPTOR,
118: AcceptorFactory.FALSE);
119:
120: reindentWithTextBefore = SettingsUtil.getBoolean(kitClass,
121: ExtSettingsNames.REINDENT_WITH_TEXT_BEFORE, false);
122: }
123:
124: /**
125: * Get the value of the given setting.
126: *
127: * @param settingName
128: * name of the setting to get.
129: */
130: public Object getSettingValue(String settingName) {
131: synchronized (Settings.class) {
132: Object value = settingsMap.get(settingName);
133: if (value == null
134: && !customSettingsNamesMap.containsKey(settingName)) {
135: value = Settings.getValue(getKitClass(), settingName);
136: if (value == null) {
137: value = NULL_VALUE;
138: }
139: settingsMap.put(settingName, value);
140: }
141: return (value != NULL_VALUE) ? value : null;
142: }
143: }
144:
145: /**
146: * This method allows to set a custom value to a setting thus overriding the
147: * value retrieved from the <tt>Settings</tt>. Once done the value is no
148: * longer synchronized with the changes in <tt>Settings</tt> for the
149: * particular setting. There's a map holding the names of all the custom
150: * settings.
151: */
152: public void setSettingValue(String settingName, Object settingValue) {
153: synchronized (Settings.class) {
154: customSettingsNamesMap.put(settingName, settingName);
155: settingsMap.put(settingName, settingValue);
156: }
157: }
158:
159: /**
160: * Add the new format layer to the layer hierarchy.
161: */
162: public synchronized void addFormatLayer(FormatLayer layer) {
163: formatLayerList.add(layer);
164: }
165:
166: /**
167: * Replace the format-layer with the layerName with the the given layer. If
168: * there's no such layer with the same name, the layer is not replaced and
169: * false is returned.
170: */
171: public synchronized boolean replaceFormatLayer(String layerName,
172: FormatLayer layer) {
173: int cnt = formatLayerList.size();
174: for (int i = 0; i < cnt; i++) {
175: if (layerName.equals(((FormatLayer) formatLayerList.get(i))
176: .getName())) {
177: formatLayerList.set(i, layer);
178: return true;
179: }
180: }
181: return false;
182: }
183:
184: /**
185: * Remove the first layer which has the same name as the given one.
186: */
187: public synchronized void removeFormatLayer(String layerName) {
188: Iterator it = formatLayerIterator();
189: while (it.hasNext()) {
190: if (layerName.equals(((FormatLayer) it.next()).getName())) {
191: it.remove();
192: return;
193: }
194: }
195: }
196:
197: /**
198: * Get the iterator over the format layers.
199: */
200: public Iterator formatLayerIterator() {
201: return formatLayerList.iterator();
202: }
203:
204: /**
205: * Whether do no formatting at all. If this method returns true, the
206: * FormatWriter will simply write its input into the underlying writer.
207: */
208: public boolean isSimple() {
209: return false;
210: }
211:
212: /** Called by format-writer to do the format */
213: public synchronized void format(FormatWriter fw) {
214: boolean done = false;
215: int safetyCounter = 0;
216: do {
217: // Mark the chain as unmodified at the begining
218: fw.setChainModified(false);
219: fw.setRestartFormat(false);
220:
221: Iterator it = formatLayerIterator();
222: while (it.hasNext()) {
223: ((FormatLayer) it.next()).format(fw);
224: if (fw.isRestartFormat()) {
225: break;
226: }
227: }
228:
229: if (!it.hasNext() && !fw.isRestartFormat()) {
230: done = true;
231: }
232:
233: if (safetyCounter > 1000) { // prevent infinite loop
234: new Exception("Indentation infinite loop detected")
235: .printStackTrace(); // NOI18N
236: break;
237: }
238: } while (!done);
239: }
240:
241: /**
242: * Reformat a block of code.
243: *
244: * @param doc
245: * document to work with
246: * @param startOffset
247: * position at which the formatting starts
248: * @param endOffset
249: * position at which the formatting ends
250: * @param indentOnly
251: * whether just the indentation should be changed or regular
252: * formatting should be performed.
253: * @return formatting writer. The text was already reformatted but the
254: * writer can contain useful information.
255: */
256: public Writer reformat(BaseDocument doc, int startOffset,
257: int endOffset, boolean indentOnly)
258: throws BadLocationException, IOException {
259: CharArrayWriter cw = new CharArrayWriter();
260: Writer w = createWriter(doc, startOffset, cw);
261: FormatWriter fw = (w instanceof FormatWriter) ? (FormatWriter) w
262: : null;
263:
264: boolean fix5620 = true; // whether apply fix for #5620 or not
265:
266: if (fw != null) {
267: fw.setIndentOnly(indentOnly);
268: if (fix5620) {
269: fw.setReformatting(true); // #5620
270: }
271: }
272:
273: w.write(doc.getChars(startOffset, endOffset - startOffset));
274: w.close();
275:
276: if (!fix5620 || fw == null) { // #5620 - for (fw != null) the doc was
277: // already modified
278: String out = new String(cw.toCharArray());
279: doc.remove(startOffset, endOffset - startOffset);
280: doc.insertString(startOffset, out, null);
281: }
282:
283: return w;
284: }
285:
286: /** Fix of #5620 - same method exists in Formatter (predecessor */
287: public int reformat(BaseDocument doc, int startOffset, int endOffset)
288: throws BadLocationException {
289: try {
290: javax.swing.text.Position pos = doc
291: .createPosition(endOffset);
292: reformat(doc, startOffset, endOffset, false);
293: return pos.getOffset() - startOffset;
294: } catch (IOException e) {
295: e.printStackTrace();
296: return 0;
297: }
298: }
299:
300: /**
301: * Get the block to be reformatted after keystroke was pressed.
302: *
303: * @param target
304: * component to which the text was typed. Caaret position can be
305: * checked etc.
306: * @param typedText
307: * text (usually just one character) that the user has typed.
308: * @return block of the code to be reformatted or null if nothing should
309: * reformatted. It can return block containing just one character.
310: * The caller usually expands even one character to the whole line
311: * because less than the whole line usually doesn't provide enough
312: * possibilities for formatting.
313: * @see ExtKit.ExtDefaultKeyTypedAction.checkIndentHotChars()
314: */
315: public int[] getReformatBlock(JTextComponent target,
316: String typedText) {
317: if (indentHotCharsAcceptor == null) { // init if necessary
318: settingsChange(null);
319: }
320:
321: if (indentHotCharsAcceptor.accept(typedText.charAt(0))) {
322: /*
323: * This is bugfix 10771. See the issue for problem description. The
324: * behaviour before fix was that whenever the lbrace is entered, the
325: * line is indented. This make no sense if a text exist on the line
326: * before the lbrace. In this case we simply will not indent the
327: * line. This is handled by the hasTextBefore check
328: */
329: if (!reindentWithTextBefore) {
330: if (hasTextBefore(target, typedText)) {
331: return null;
332: }
333: }
334: int dotPos = target.getCaret().getDot();
335: return new int[] { Math.max(dotPos - 1, 0), dotPos };
336:
337: } else {
338: return null;
339: }
340: }
341:
342: protected boolean hasTextBefore(JTextComponent target,
343: String typedText) {
344: BaseDocument doc = Utilities.getDocument(target);
345: int dotPos = target.getCaret().getDot();
346: try {
347: int fnw = Utilities.getRowFirstNonWhite(doc, dotPos);
348: return dotPos != fnw + typedText.length();
349: } catch (BadLocationException e) {
350: return false;
351: }
352: }
353:
354: /**
355: * Create the indentation writer.
356: */
357: public Writer createWriter(Document doc, int offset, Writer writer) {
358: return new FormatWriter(this , doc, offset, writer, false);
359: }
360:
361: /**
362: * Indents the current line. Should not affect any other lines.
363: *
364: * @param doc
365: * the document to work on
366: * @param offset
367: * the offset of a character on the line
368: * @return new offset of the original character
369: */
370: public int indentLine(Document doc, int offset) {
371: if (doc instanceof BaseDocument) {
372: try {
373: BaseDocument bdoc = (BaseDocument) doc;
374: int lineStart = Utilities.getRowStart(bdoc, offset);
375: int nextLineStart = Utilities.getRowStart(bdoc, offset,
376: 1);
377: if (nextLineStart < 0) { // end of doc
378: nextLineStart = bdoc.getLength();
379: }
380: reformat(bdoc, lineStart, nextLineStart, false);
381: return Utilities.getRowEnd(bdoc, lineStart);
382: } catch (GuardedException e) {
383: java.awt.Toolkit.getDefaultToolkit().beep();
384:
385: } catch (BadLocationException e) {
386: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
387: e.printStackTrace();
388: }
389:
390: } catch (IOException e) {
391: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
392: e.printStackTrace();
393: }
394:
395: }
396:
397: return offset;
398:
399: }
400:
401: return super .indentLine(doc, offset);
402: }
403:
404: /**
405: * Inserts new line at given position and indents the new line with spaces.
406: *
407: * @param doc
408: * the document to work on
409: * @param offset
410: * the offset of a character on the line
411: * @return new offset to place cursor to
412: */
413: public int indentNewLine(Document doc, int offset) {
414: if (doc instanceof BaseDocument) {
415: BaseDocument bdoc = (BaseDocument) doc;
416: boolean newLineInserted = false;
417:
418: bdoc.atomicLock();
419: try {
420: bdoc.insertString(offset, "\n", null); // NOI18N
421: offset++;
422: newLineInserted = true;
423:
424: int eolOffset = Utilities.getRowEnd(bdoc, offset);
425:
426: // Try to change the indent of the new line
427: // It may fail when inserting '\n' before the guarded block
428: Writer w = reformat(bdoc, offset, eolOffset, true);
429:
430: // Find the caret position
431: eolOffset = Utilities.getRowFirstNonWhite(bdoc, offset);
432: if (eolOffset < 0) { // white line
433: eolOffset = Utilities.getRowEnd(bdoc, offset);
434: }
435:
436: offset = eolOffset;
437:
438: // Resulting offset (caret position) can be shifted
439: if (w instanceof FormatWriter) {
440: offset += ((FormatWriter) w).getIndentShift();
441: }
442:
443: } catch (GuardedException e) {
444: // Possibly couldn't insert additional indentation
445: // at the begining of the guarded block
446: // but the initial '\n' could be fine
447: if (!newLineInserted) {
448: java.awt.Toolkit.getDefaultToolkit().beep();
449: }
450:
451: } catch (BadLocationException e) {
452: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
453: e.printStackTrace();
454: }
455:
456: } catch (IOException e) {
457: if (Boolean.getBoolean("netbeans.debug.exceptions")) { // NOI18N
458: e.printStackTrace();
459: }
460:
461: } finally {
462: bdoc.atomicUnlock();
463: }
464:
465: } else { // not BaseDocument
466: try {
467: doc.insertString(offset, "\n", null); // NOI18N
468: offset++;
469: } catch (BadLocationException ex) {
470: }
471: }
472:
473: return offset;
474: }
475:
476: /**
477: * Whether the formatter accepts the given syntax that will be used for
478: * parsing the text passed to the FormatWriter.
479: *
480: * @param syntax
481: * syntax to be tested.
482: * @return true whether this formatter is able to process the tokens created
483: * by the syntax or false otherwise.
484: */
485: protected boolean acceptSyntax(Syntax syntax) {
486: return true;
487: }
488:
489: /** Simple formatter */
490: public static class Simple extends ExtFormatter {
491:
492: public Simple(Class kitClass) {
493: super (kitClass);
494: }
495:
496: public boolean isSimple() {
497: return true;
498: }
499:
500: }
501:
502: }
|