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