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