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;
043:
044: import java.util.Map;
045: import java.util.HashMap;
046: import java.util.Iterator;
047: import java.io.IOException;
048: import java.io.Writer;
049: import java.io.CharArrayWriter;
050: import javax.swing.text.Document;
051: import javax.swing.text.JTextComponent;
052: import javax.swing.text.BadLocationException;
053: import org.netbeans.lib.editor.util.CharSequenceUtilities;
054: import org.netbeans.lib.editor.util.swing.DocumentUtilities;
055:
056: /**
057: * Various services related to indentation and text formatting
058: * are located here. Each kit can have different formatter
059: * so the first action should be getting the right formatter
060: * for the given kit by calling Formatter.getFormatter(kitClass).
061: *
062: * @author Miloslav Metelka
063: * @version 1.00
064: */
065:
066: public class Formatter implements SettingsChangeListener {
067:
068: private static Map kitClass2Formatter = new HashMap();
069:
070: /** Get the formatter for the given kit-class */
071: public static synchronized Formatter getFormatter(Class kitClass) {
072: Formatter f = (Formatter) kitClass2Formatter.get(kitClass);
073: if (f == null) {
074: f = BaseKit.getKit(kitClass).createFormatter();
075: kitClass2Formatter.put(kitClass, f);
076: }
077: return f;
078: }
079:
080: /** Set the formatter for the given kit-class.
081: * @param kitClass class of the kit for which the formatter
082: * is being assigned.
083: * @param formatter new formatter for the given kit
084: */
085: public static synchronized void setFormatter(Class kitClass,
086: Formatter formatter) {
087: kitClass2Formatter.put(kitClass, formatter);
088: }
089:
090: /** Maximum tab size for which the indent strings will be cached. */
091: private static final int ISC_MAX_TAB_SIZE = 16;
092:
093: /** Cache the indentation strings up to this size */
094: private static final int ISC_MAX_INDENT_SIZE = 32;
095:
096: /** Cache holding the indentation strings for various tab-sizes. */
097: private static final String[][] indentStringCache = new String[ISC_MAX_TAB_SIZE][];
098:
099: private final Class kitClass;
100:
101: /** Whether values were already inited from the cache */
102: private boolean inited;
103:
104: private int tabSize;
105:
106: private boolean customTabSize;
107:
108: private Integer shiftWidth;
109:
110: private boolean customShiftWidth;
111:
112: private boolean expandTabs;
113:
114: private boolean customExpandTabs;
115:
116: private int spacesPerTab;
117:
118: private boolean customSpacesPerTab;
119:
120: /** Construct new formatter.
121: * @param kitClass the class of the kit for which this formatter is being
122: * constructed.
123: */
124: public Formatter(Class kitClass) {
125: this .kitClass = kitClass;
126: Settings.addSettingsChangeListener(this );
127: }
128:
129: /** Get the kit-class for which this formatter is constructed. */
130: public Class getKitClass() {
131: return kitClass;
132: }
133:
134: public void settingsChange(SettingsChangeEvent evt) {
135: String settingName = (evt != null) ? evt.getSettingName()
136: : null;
137: if (!inited || settingName == null
138: || SettingsNames.TAB_SIZE.equals(settingName)) {
139: if (!customTabSize) {
140: tabSize = SettingsUtil.getInteger(kitClass,
141: SettingsNames.TAB_SIZE,
142: SettingsDefaults.defaultTabSize);
143: }
144: }
145:
146: // Shift-width often depends on the rest of parameters
147: if (!customShiftWidth) {
148: Object shw = Settings.getValue(kitClass,
149: SettingsNames.INDENT_SHIFT_WIDTH);
150: if (shw instanceof Integer) {
151: shiftWidth = (Integer) shw;
152: }
153: }
154:
155: if (!inited || settingName == null
156: || SettingsNames.EXPAND_TABS.equals(settingName)) {
157: if (!customExpandTabs) {
158: expandTabs = SettingsUtil.getBoolean(kitClass,
159: SettingsNames.EXPAND_TABS,
160: SettingsDefaults.defaultExpandTabs);
161: }
162: }
163: if (!inited || settingName == null
164: || SettingsNames.SPACES_PER_TAB.equals(settingName)) {
165: if (!customSpacesPerTab) {
166: spacesPerTab = SettingsUtil.getInteger(kitClass,
167: SettingsNames.SPACES_PER_TAB,
168: SettingsDefaults.defaultSpacesPerTab);
169: }
170: }
171:
172: inited = true;
173: }
174:
175: /** Get the number of spaces the TAB character ('\t') visually represents
176: * for non-BaseDocument documents. It shouldn't be used for BaseDocument
177: * based documents. The reason for that is that the returned value
178: * reflects the value of the setting for the kit class over which
179: * this formatter was constructed. However it's possible that the kit class of
180: * the document being formatted is different than the kit of the formatter.
181: * For example java document could be formatted by html formatter.
182: * Therefore <code>BaseDocument.getTabSize()</code> must be used
183: * for BaseDocuments to reflect the document's own tabsize.
184: * @see BaseDocument.getTabSize()
185: */
186: public int getTabSize() {
187: if (!customTabSize && !inited) {
188: settingsChange(null);
189: }
190:
191: return tabSize;
192: }
193:
194: /** Set the number of spaces the TAB character ('\t') visually represents
195: * for non-BaseDocument documents. It doesn't affect BaseDocument
196: * based documents.
197: *
198: * @see getTabSize()
199: * @see BaseDocument.setTabSize()
200: */
201: public void setTabSize(int tabSize) {
202: customTabSize = true;
203: this .tabSize = tabSize;
204: }
205:
206: /** Get the width of one indentation level for non-BaseDocument documents.
207: * The algorithm first checks whether there's a value for the INDENT_SHIFT_WIDTH
208: * setting. If so it uses it, otherwise it uses <code>getSpacesPerTab()</code>
209: *
210: * @see setShiftWidth()
211: * @see getSpacesPerTab()
212: */
213: public int getShiftWidth() {
214: if (!customShiftWidth && !inited) {
215: settingsChange(null);
216: }
217:
218: return (shiftWidth != null) ? shiftWidth.intValue()
219: : getSpacesPerTab();
220: }
221:
222: /** Set the width of one indentation level for non-BaseDocument documents.
223: * It doesn't affect BaseDocument based documents.
224: *
225: * @see getShiftWidth()
226: */
227: public void setShiftWidth(int shiftWidth) {
228: customShiftWidth = true;
229: if (this .shiftWidth == null
230: || this .shiftWidth.intValue() != shiftWidth) {
231: this .shiftWidth = new Integer(shiftWidth);
232: }
233: }
234:
235: /** Should the typed tabs be expanded to the spaces? */
236: public boolean expandTabs() {
237: if (!customExpandTabs && !inited) {
238: settingsChange(null);
239: }
240:
241: return expandTabs;
242: }
243:
244: public void setExpandTabs(boolean expandTabs) {
245: customExpandTabs = true;
246: this .expandTabs = expandTabs;
247: }
248:
249: /** Get the number of spaces that should be inserted into the document
250: * instead of one typed tab.
251: */
252: public int getSpacesPerTab() {
253: if (!customSpacesPerTab && !inited) {
254: settingsChange(null);
255: }
256:
257: return spacesPerTab;
258: }
259:
260: public void setSpacesPerTab(int spacesPerTab) {
261: customSpacesPerTab = true;
262: this .spacesPerTab = spacesPerTab;
263: }
264:
265: static String getIndentString(int indent, boolean expandTabs,
266: int tabSize) {
267: if (indent <= 0) {
268: return "";
269: }
270:
271: if (expandTabs) { // store in 0th slot
272: tabSize = 0;
273: }
274:
275: synchronized (Settings.class) {
276: boolean large = (tabSize >= indentStringCache.length)
277: || (indent > ISC_MAX_INDENT_SIZE); // indexed by (indent - 1)
278: String indentString = null;
279: String[] tabCache = null;
280: if (!large) {
281: tabCache = indentStringCache[tabSize]; // cache for this tab
282: if (tabCache == null) {
283: tabCache = new String[ISC_MAX_INDENT_SIZE];
284: indentStringCache[tabSize] = tabCache;
285: }
286: indentString = tabCache[indent - 1];
287: }
288:
289: if (indentString == null) {
290: indentString = Analyzer.getIndentString(indent,
291: expandTabs, tabSize);
292:
293: if (!large) {
294: tabCache[indent - 1] = indentString;
295: }
296: }
297:
298: return indentString;
299: }
300: }
301:
302: public String getIndentString(BaseDocument doc, int indent) {
303: return getIndentString(indent, expandTabs(), doc.getTabSize());
304: }
305:
306: /** Get the string that is appropriate for the requested indentation.
307: * The returned string respects the <tt>expandTabs()</tt> and
308: * the <tt>getTabSize()</tt> and will contain either spaces only
309: * or fully or partially tabs as necessary.
310: */
311: public String getIndentString(int indent) {
312: return getIndentString(indent, expandTabs(), getTabSize());
313: }
314:
315: /** Modify the line to move the text starting at dotPos one tab
316: * column to the right. Whitespace preceeding dotPos may be
317: * replaced by a TAB character if tabs expanding is on.
318: * @param doc document to operate on
319: * @param dotPos insertion point
320: */
321: public void insertTabString(BaseDocument doc, int dotPos)
322: throws BadLocationException {
323: doc.atomicLock();
324: try {
325: // Determine first white char before dotPos
326: int rsPos = Utilities.getRowStart(doc, dotPos);
327: int startPos = Utilities.getFirstNonWhiteBwd(doc, dotPos,
328: rsPos);
329: startPos = (startPos >= 0) ? (startPos + 1) : rsPos;
330:
331: int startCol = Utilities.getVisualColumn(doc, startPos);
332: int endCol = Utilities.getNextTabColumn(doc, dotPos);
333: String tabStr = Analyzer.getWhitespaceString(startCol,
334: endCol, expandTabs(), doc.getTabSize());
335:
336: // Search for the first non-common char
337: char[] removeChars = doc.getChars(startPos, dotPos
338: - startPos);
339: int ind = 0;
340: while (ind < removeChars.length
341: && removeChars[ind] == tabStr.charAt(ind)) {
342: ind++;
343: }
344:
345: startPos += ind;
346: doc.remove(startPos, dotPos - startPos);
347: doc.insertString(startPos, tabStr.substring(ind), null);
348:
349: } finally {
350: doc.atomicUnlock();
351: }
352: }
353:
354: /** Change the indent of the given row. Document is atomically locked
355: * during this operation.
356: */
357: public void changeRowIndent(BaseDocument doc, int pos, int newIndent)
358: throws BadLocationException {
359: doc.atomicLock();
360: try {
361: if (newIndent < 0) {
362: newIndent = 0;
363: }
364: int firstNW = Utilities.getRowFirstNonWhite(doc, pos);
365: if (firstNW == -1) { // valid first non-blank
366: firstNW = Utilities.getRowEnd(doc, pos);
367: }
368: int replacePos = Utilities.getRowStart(doc, pos);
369: int removeLen = firstNW - replacePos;
370: CharSequence removeText = DocumentUtilities.getText(doc,
371: replacePos, removeLen);
372: String newIndentText = getIndentString(doc, newIndent);
373: if (CharSequenceUtilities.startsWith(newIndentText,
374: removeText)) {
375: // Skip removeLen chars at start
376: newIndentText = newIndentText.substring(removeLen);
377: replacePos += removeLen;
378: removeLen = 0;
379: } else if (CharSequenceUtilities.endsWith(newIndentText,
380: removeText)) {
381: // Skip removeLen chars at the end
382: newIndentText = newIndentText.substring(0,
383: newIndentText.length() - removeLen);
384: removeLen = 0;
385: }
386:
387: if (removeLen != 0) {
388: doc.remove(replacePos, removeLen);
389: }
390:
391: doc.insertString(replacePos, newIndentText, null);
392: } finally {
393: doc.atomicUnlock();
394: }
395: }
396:
397: /** Increase/decrease indentation of the block of the code. Document
398: * is atomically locked during the operation.
399: * @param doc document to operate on
400: * @param startPos starting line position
401: * @param endPos ending line position
402: * @param shiftCnt positive/negative count of shiftwidths by which indentation
403: * should be shifted right/left
404: */
405: public void changeBlockIndent(BaseDocument doc, int startPos,
406: int endPos, int shiftCnt) throws BadLocationException {
407:
408: GuardedDocument gdoc = (doc instanceof GuardedDocument) ? (GuardedDocument) doc
409: : null;
410: if (gdoc != null) {
411: for (int i = startPos; i < endPos; i++) {
412: if (gdoc.isPosGuarded(i)) {
413: java.awt.Toolkit.getDefaultToolkit().beep();
414: return;
415: }
416: }
417: }
418:
419: doc.atomicLock();
420: try {
421:
422: int indentDelta = shiftCnt * doc.getShiftWidth();
423: if (endPos > 0
424: && Utilities.getRowStart(doc, endPos) == endPos) {
425: endPos--;
426: }
427:
428: int pos = Utilities.getRowStart(doc, startPos);
429: for (int lineCnt = Utilities.getRowCount(doc, startPos,
430: endPos); lineCnt > 0; lineCnt--) {
431: int indent = Utilities.getRowIndent(doc, pos);
432: if (Utilities.isRowWhite(doc, pos)) {
433: indent = -indentDelta; // zero indentation for white line
434: }
435: changeRowIndent(doc, pos, Math.max(
436: indent + indentDelta, 0));
437: pos = Utilities.getRowStart(doc, pos, +1);
438: }
439:
440: } finally {
441: doc.atomicUnlock();
442: }
443: }
444:
445: /** Shift line either left or right */
446: public void shiftLine(BaseDocument doc, int dotPos, boolean right)
447: throws BadLocationException {
448: int ind = doc.getShiftWidth();
449: if (!right) {
450: ind = -ind;
451: }
452:
453: if (Utilities.isRowWhite(doc, dotPos)) {
454: ind += Utilities.getVisualColumn(doc, dotPos);
455: } else {
456: ind += Utilities.getRowIndent(doc, dotPos);
457: }
458: ind = Math.max(ind, 0);
459: changeRowIndent(doc, dotPos, ind);
460: }
461:
462: /** Reformat a block of code.
463: * @param doc document to work with
464: * @param startOffset offset at which the formatting starts
465: * @param endOffset offset at which the formatting ends
466: * @return length of the reformatted code
467: */
468: public int reformat(BaseDocument doc, int startOffset, int endOffset)
469: throws BadLocationException {
470: try {
471: CharArrayWriter cw = new CharArrayWriter();
472: Writer w = createWriter(doc, startOffset, cw);
473: w.write(doc.getChars(startOffset, endOffset - startOffset));
474: w.close();
475: String out = new String(cw.toCharArray());
476: doc.remove(startOffset, endOffset - startOffset);
477: doc.insertString(startOffset, out, null);
478: return out.length();
479: } catch (IOException e) {
480: Utilities.annotateLoggable(e);
481: return 0;
482: }
483: }
484:
485: /** Indents the current line. Should not affect any other
486: * lines.
487: * @param doc the document to work on
488: * @param offset the offset of a character on the line
489: * @return new offset of the original character
490: */
491: public int indentLine(Document doc, int offset) {
492: return offset;
493: }
494:
495: /** Inserts new line at given position and indents the new line with
496: * spaces.
497: *
498: * @param doc the document to work on
499: * @param offset the offset of a character on the line
500: * @return new offset to place cursor to
501: */
502: public int indentNewLine(Document doc, int offset) {
503: try {
504: doc.insertString(offset, "\n", null); // NOI18N
505: offset++;
506:
507: } catch (GuardedException e) {
508: java.awt.Toolkit.getDefaultToolkit().beep();
509:
510: } catch (BadLocationException e) {
511: Utilities.annotateLoggable(e);
512: }
513:
514: return offset;
515: }
516:
517: /** Creates a writer that formats text that is inserted into it.
518: * The writer should not modify the document but use the
519: * provided writer to write to. Usually the underlaying writer
520: * will modify the document itself and optionally it can remember
521: * the current position in document. That is why the newly created
522: * writer should do no buffering.
523: * <P>
524: * The provided document and pos are only informational,
525: * should not be modified but only used to find correct indentation
526: * strategy.
527: *
528: * @param doc document
529: * @param offset position to begin inserts at
530: * @param writer writer to write to
531: * @return new writer that will format written text and pass it
532: * into the writer
533: */
534: public Writer createWriter(Document doc, int offset, Writer writer) {
535: return writer;
536: }
537:
538: /**
539: * Formatter clients should call this method
540: * before acquiring of the document's write lock
541: * and using of the {@link #indentLine(Document,int)}
542: * and {@link #indentNewLine(Document,int)} methods.
543: * <br/>
544: * Subclasses may override this method
545: * and perform necessary pre-locking (e.g. of java infrastructure).
546: * <br/>
547: * The following pattern should be used:
548: * <pre>
549: * formatter.indentLock();
550: * try {
551: * doc.atomicLock();
552: * try {
553: * formatter.indentLine(...);
554: * } finally {
555: * doc.atomicUnlock();
556: * }
557: * } finally {
558: * formatter.indentUnlock();
559: * }
560: * </pre>
561: */
562: public void indentLock() {
563: // No extra locking by default
564: }
565:
566: /**
567: * Formatter clients should call this method
568: * after releasing of the document's write lock
569: * as a counterpart of {@link #indentLock()}.
570: * <br/>
571: * Subclasses may override this method
572: * and perform necessary post-unlocking (e.g. of java infrastructure).
573: * <br/>
574: * The following pattern should be used:
575: * <pre>
576: * formatter.indentLock();
577: * try {
578: * doc.atomicLock();
579: * try {
580: * formatter.indentLine(...);
581: * } finally {
582: * doc.atomicUnlock();
583: * }
584: * } finally {
585: * formatter.indentUnlock();
586: * }
587: * </pre>
588: */
589: public void indentUnlock() {
590: // No extra locking by default
591: }
592:
593: /**
594: * Formatter clients should call this method
595: * before acquiring of the document's write lock
596: * before using of {@link #reformat(BaseDocument,int,int)} method.
597: * <br/>
598: * Subclasses may override this method
599: * and perform necessary pre-locking (e.g. of java infrastructure).
600: * <br/>
601: * The following pattern should be used:
602: * <pre>
603: * formatter.reformatLock();
604: * try {
605: * doc.atomicLock();
606: * try {
607: * formatter.reformat(...);
608: * } finally {
609: * doc.atomicUnlock();
610: * }
611: * } finally {
612: * formatter.reformatUnlock();
613: * }
614: * </pre>
615: */
616: public void reformatLock() {
617: // No extra locking by default
618: }
619:
620: /**
621: * Formatter clients should call this method
622: * after releasing of the document's write lock
623: * as a counterpart of {@link #reformatLock()}.
624: * <br/>
625: * Subclasses may override this method
626: * and perform necessary post-unlocking (e.g. of java infrastructure).
627: * <br/>
628: * The following pattern should be used:
629: * <pre>
630: * formatter.reformatLock();
631: * try {
632: * doc.atomicLock();
633: * try {
634: * formatter.reformat(...);
635: * } finally {
636: * doc.atomicUnlock();
637: * }
638: * } finally {
639: * formatter.reformatUnlock();
640: * }
641: * </pre>
642: */
643: public void reformatUnlock() {
644: // No extra locking by default
645: }
646:
647: }
|