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.modules.editor.structure.formatting;
043:
044: import java.io.IOException;
045: import java.io.Writer;
046: import java.util.ArrayList;
047: import java.util.Arrays;
048: import java.util.LinkedList;
049: import java.util.List;
050: import javax.swing.text.BadLocationException;
051: import javax.swing.text.JTextComponent;
052: import javax.swing.text.Position;
053: import org.netbeans.editor.BaseDocument;
054: import org.netbeans.editor.TokenItem;
055: import org.netbeans.editor.Utilities;
056: import org.netbeans.editor.ext.ExtFormatter;
057: import org.netbeans.editor.ext.ExtSyntaxSupport;
058: import org.openide.ErrorManager;
059:
060: /**
061: *
062: * @author Tomasz.Slota@Sun.COM
063: */
064: @Deprecated
065: // use TagBasedLexerFormatter instead
066: public abstract class TagBasedFormatter extends ExtFormatter {
067:
068: /** Creates a new instance of TagBases */
069: public TagBasedFormatter(Class kitClass) {
070: super (kitClass);
071: ErrorManager.getDefault().log(
072: ErrorManager.WARNING,
073: "Class " + getClass().getName()
074: + " is deprecated, use *IndentTask");
075: }
076:
077: protected abstract ExtSyntaxSupport getSyntaxSupport(
078: BaseDocument doc);
079:
080: protected abstract boolean isClosingTag(TokenItem token);
081:
082: protected abstract boolean isUnformattableToken(TokenItem token);
083:
084: protected abstract boolean isUnformattableTag(String tag);
085:
086: protected abstract boolean isOpeningTag(TokenItem token);
087:
088: protected abstract String extractTagName(TokenItem tknTag);
089:
090: protected abstract boolean areTagNamesEqual(String tagName1,
091: String tagName2);
092:
093: protected abstract boolean isClosingTagRequired(BaseDocument doc,
094: String tagName);
095:
096: protected abstract int getOpeningSymbolOffset(TokenItem tknTag);
097:
098: protected abstract TokenItem getTagTokenEndingAtPosition(
099: BaseDocument doc, int position) throws BadLocationException;
100:
101: protected abstract int getTagEndOffset(TokenItem token);
102:
103: protected Writer extFormatterReformat(final BaseDocument doc,
104: final int startOffset, final int endOffset,
105: final boolean indentOnly) throws BadLocationException,
106: IOException {
107: return super .reformat(doc, startOffset, endOffset, indentOnly);
108: }
109:
110: protected boolean isWSTag(TokenItem tag) {
111: char chars[] = tag.getImage().toCharArray();
112:
113: for (char c : chars) {
114: if (!Character.isWhitespace(c)) {
115: return false;
116: }
117: }
118:
119: return true;
120: }
121:
122: protected int getIndentForTagParameter(BaseDocument doc,
123: TokenItem tag) throws BadLocationException {
124: int tagStartLine = Utilities
125: .getLineOffset(doc, tag.getOffset());
126: TokenItem currentToken = tag.getNext();
127:
128: /*
129: * Find the offset of the first attribute if it is specified on the same line as the opening of the tag
130: * e.g. <tag |attr=
131: *
132: */
133: while (currentToken != null
134: && isWSTag(currentToken)
135: && tagStartLine == Utilities.getLineOffset(doc,
136: currentToken.getOffset())) {
137: currentToken = currentToken.getNext();
138: }
139:
140: if (tag != null
141: && !isWSTag(currentToken)
142: && tagStartLine == Utilities.getLineOffset(doc,
143: currentToken.getOffset())) {
144: return currentToken.getOffset()
145: - Utilities.getRowIndent(doc, currentToken
146: .getOffset())
147: - Utilities.getRowStart(doc, currentToken
148: .getOffset());
149: }
150:
151: return getShiftWidth(); // default;
152: }
153:
154: @Override
155: public Writer reformat(BaseDocument doc, int startOffset,
156: int endOffset, boolean indentOnly)
157: throws BadLocationException {
158:
159: if (!hasValidSyntaxSupport(doc)) {
160: return null;
161: }
162:
163: LinkedList<TagIndentationData> unprocessedOpeningTags = new LinkedList<TagIndentationData>();
164: List<TagIndentationData> matchedOpeningTags = new ArrayList<TagIndentationData>();
165: doc.atomicLock();
166:
167: try {
168: int lastLine = Utilities
169: .getLineOffset(doc, doc.getLength());
170: int firstRefBlockLine = Utilities.getLineOffset(doc,
171: startOffset);
172: int lastRefBlockLine = Utilities.getLineOffset(doc,
173: endOffset);
174: int firstUnformattableLine = -1;
175:
176: boolean unformattableLines[] = new boolean[lastLine + 1];
177: int indentsWithinTags[] = new int[lastLine + 1];
178:
179: ExtSyntaxSupport sup = getSyntaxSupport(doc);
180: TokenItem token = sup.getTokenChain(0, doc.getLength() - 1);
181:
182: if (token != null) {
183: // calc line indents - pass 1
184: do {
185: boolean isOpenTag = isOpeningTag(token);
186: boolean isCloseTag = isClosingTag(token);
187:
188: if (isOpenTag || isCloseTag) {
189:
190: String tagName = extractTagName(token);
191: int tagEndOffset = getTagEndOffset(token);
192:
193: if (tagEndOffset == -1) {
194: break; // incomplete closing tag
195: }
196:
197: int lastTagLine = Utilities.getLineOffset(doc,
198: tagEndOffset);
199:
200: if (isOpenTag) {
201:
202: TagIndentationData tagData = new TagIndentationData(
203: tagName, lastTagLine);
204: unprocessedOpeningTags.add(tagData);
205:
206: // format lines within tag
207: int firstTagLine = Utilities.getLineOffset(
208: doc, token.getOffset());
209:
210: if (firstTagLine < lastTagLine) { // performance!
211: int indentWithinTag = getIndentForTagParameter(
212: doc, token);
213:
214: for (int i = firstTagLine + 1; i <= lastTagLine; i++) {
215: indentsWithinTags[i] = indentWithinTag;
216: }
217:
218: // if there is only the closing symbol on the last line of tag do not indent it
219: TokenItem currentToken = token
220: .getNext();
221: while (Utilities.getLineOffset(doc,
222: currentToken.getOffset()) < lastTagLine
223: || isWSTag(currentToken)) {
224:
225: currentToken = currentToken
226: .getNext();
227: }
228:
229: if (currentToken.getOffset() == tagEndOffset) {
230: indentsWithinTags[lastTagLine] = 0;
231: }
232: }
233: } else {
234: // isCloseTag - find matching opening tag record
235: LinkedList<TagIndentationData> tagsToBeRemoved = new LinkedList<TagIndentationData>();
236:
237: while (!unprocessedOpeningTags.isEmpty()) {
238: TagIndentationData processedTD = unprocessedOpeningTags
239: .removeLast();
240:
241: if (areTagNamesEqual(tagName,
242: processedTD.getTagName())) {
243: processedTD
244: .setClosedOnLine(lastTagLine);
245: matchedOpeningTags.add(processedTD);
246:
247: // mark all the stuff between unformattable tag as unformattable
248: if (isUnformattableTag(tagName)) {
249: for (int i = lastTagLine - 1; i > processedTD
250: .getLine(); i--) {
251: unformattableLines[i] = true;
252: }
253: }
254:
255: // forgetting preceding tags permanently
256: tagsToBeRemoved.clear();
257: break;
258: } else {
259: tagsToBeRemoved.add(processedTD);
260: }
261: }
262:
263: // if matching opening tag was not found on the stack put all the tags back
264: unprocessedOpeningTags
265: .addAll(tagsToBeRemoved);
266: }
267: }
268:
269: boolean wasPreviousTokenUnformattable = isUnformattableToken(token);
270:
271: if (wasPreviousTokenUnformattable
272: && firstUnformattableLine == -1) {
273: firstUnformattableLine = Utilities
274: .getLineOffset(doc, token.getOffset());
275: }
276:
277: token = token.getNext();
278:
279: // detect an end of unformattable block; mark it
280: if (firstUnformattableLine > -1
281: && (!wasPreviousTokenUnformattable || token == null)) {
282:
283: int lastUnformattableLine = token == null ? lastLine
284: : Utilities.getLineOffset(doc, token
285: .getOffset() - 1);
286:
287: for (int i = firstUnformattableLine + 1; i < lastUnformattableLine; i++) {
288: unformattableLines[i] = true;
289: }
290:
291: firstUnformattableLine = -1;
292: }
293: } while (token != null);
294: }
295:
296: // calc line indents - pass 2
297: // TODO: optimize it
298: int indentLevels[] = new int[lastLine + 1];
299: Arrays.fill(indentLevels, 0);
300:
301: for (TagIndentationData td : matchedOpeningTags) {
302: // increase indent from one line after the opening tag
303: // up to one line before the closing tag
304:
305: for (int i = td.getLine() + 1; i <= td
306: .getClosedOnLine() - 1; i++) {
307: indentLevels[i]++;
308: }
309: }
310:
311: // when reformatting only a part of file
312: // we need to take into account the local bias
313: InitialIndentData initialIndentData = new InitialIndentData(
314: doc, indentLevels, indentsWithinTags,
315: firstRefBlockLine, lastRefBlockLine);
316:
317: // apply line indents
318: for (int line = firstRefBlockLine; line <= lastRefBlockLine; line++) {
319: int lineStart = Utilities.getRowStartFromLineOffset(
320: doc, line);
321:
322: if (!unformattableLines[line]
323: && initialIndentData.isEligibleToIndent(line)) {
324: changeRowIndent(doc, lineStart, initialIndentData
325: .getIndent(line));
326: }
327: }
328: } finally {
329: doc.atomicUnlock();
330: }
331:
332: return null;
333: }
334:
335: protected void enterPressed(JTextComponent txtComponent, int dotPos)
336: throws BadLocationException {
337: BaseDocument doc = Utilities.getDocument(txtComponent);
338: int lineNumber = Utilities.getLineOffset(doc, dotPos);
339: int initialIndent = getInitialIndentFromPreviousLine(doc,
340: lineNumber);
341: int endOfPreviousLine = Utilities.getFirstNonWhiteBwd(doc,
342: dotPos);
343: endOfPreviousLine = endOfPreviousLine == -1 ? 0
344: : endOfPreviousLine;
345:
346: // workaround for \n passed from code completion to reformatter
347: if (lineNumber == Utilities.getLineOffset(doc,
348: endOfPreviousLine)) {
349: return;
350: }
351:
352: TokenItem tknOpeningTag = getTagTokenEndingAtPosition(doc,
353: endOfPreviousLine);
354:
355: if (isOpeningTag(tknOpeningTag)) {
356: TokenItem tknClosingTag = getNextClosingTag(doc, dotPos + 1);
357:
358: if (tknClosingTag != null) {
359: TokenItem tknMatchingOpeningTag = getMatchingOpeningTag(tknClosingTag);
360:
361: if (tknMatchingOpeningTag != null
362: && tknMatchingOpeningTag.getOffset() == tknOpeningTag
363: .getOffset()) {
364:
365: int openingTagLine = Utilities.getLineOffset(doc,
366: tknOpeningTag.getOffset());
367: int closingTagLine = Utilities.getLineOffset(doc,
368: tknClosingTag.getOffset());
369:
370: if (closingTagLine == Utilities.getLineOffset(doc,
371: dotPos)) {
372:
373: if (openingTagLine == closingTagLine - 1) {
374: /* "smart enter"
375: * <t>|optional text</t>
376: */
377: Position closingTagPos = doc
378: .createPosition(getOpeningSymbolOffset(tknClosingTag));
379: changeRowIndent(doc, dotPos, initialIndent
380: + doc.getShiftWidth());
381: doc.insertString(closingTagPos.getOffset(),
382: "\n", null); //NOI18N
383: int newCaretPos = closingTagPos.getOffset() - 1;
384: changeRowIndent(doc, closingTagPos
385: .getOffset() + 1, initialIndent);
386: newCaretPos = Utilities.getRowEnd(doc,
387: newCaretPos);
388: txtComponent.setCaretPosition(newCaretPos);
389: } else {
390: /* <t>
391: *
392: * |</t>
393: */
394: changeRowIndent(doc, dotPos, initialIndent);
395: }
396: }
397: }
398:
399: int indent = initialIndent;
400:
401: if (isClosingTagRequired(doc,
402: extractTagName(tknOpeningTag))) {
403: indent += doc.getShiftWidth();
404: }
405:
406: changeRowIndent(doc, dotPos, indent);
407: }
408: } else {
409: int indent = initialIndent;
410:
411: if (isJustBeforeClosingTag(doc, dotPos)) {
412: indent -= doc.getShiftWidth();
413: indent = indent < 0 ? 0 : indent;
414: }
415:
416: // preceeding token is not opening tag, keep same indentation
417: changeRowIndent(doc, dotPos, indent);
418: }
419: }
420:
421: @Override
422: public int[] getReformatBlock(JTextComponent target,
423: String typedText) {
424: BaseDocument doc = Utilities.getDocument(target);
425:
426: if (!hasValidSyntaxSupport(doc)) {
427: return null;
428: }
429:
430: char lastChar = typedText.charAt(typedText.length() - 1);
431:
432: try {
433: int dotPos = target.getCaret().getDot();
434:
435: if (lastChar == '>') {
436: TokenItem tknPrecedingToken = getTagTokenEndingAtPosition(
437: doc, dotPos - 1);
438:
439: if (isClosingTag(tknPrecedingToken)) {
440: // the user has just entered a closing tag
441: // - reformat it unless matching opening tag is on the same line
442:
443: TokenItem tknOpeningTag = getMatchingOpeningTag(tknPrecedingToken);
444:
445: if (tknOpeningTag != null) {
446: int openingTagLine = Utilities.getLineOffset(
447: doc, tknOpeningTag.getOffset());
448: int closingTagSymbolLine = Utilities
449: .getLineOffset(doc, dotPos);
450:
451: if (openingTagLine != closingTagSymbolLine) {
452: return new int[] {
453: tknPrecedingToken.getOffset(),
454: dotPos };
455: }
456: }
457: }
458: }
459:
460: else if (lastChar == '\n') {
461: // just pressed enter
462: enterPressed(target, dotPos);
463: }
464:
465: } catch (Exception e) {
466: ErrorManager.getDefault().notify(ErrorManager.WARNING, e);
467: }
468:
469: return null;
470: }
471:
472: protected TokenItem getMatchingOpeningTag(TokenItem tknClosingTag) {
473: String searchedTagName = extractTagName(tknClosingTag);
474: TokenItem token = tknClosingTag.getPrevious();
475: int balance = 0;
476:
477: while (token != null) {
478: if (areTagNamesEqual(searchedTagName, extractTagName(token))) {
479: if (isOpeningTag(token)) {
480: if (balance == 0) {
481: return token;
482: }
483:
484: balance--;
485: } else if (isClosingTag(token)) {
486: balance++;
487: }
488: }
489:
490: token = token.getPrevious();
491: }
492:
493: return null;
494: }
495:
496: protected int getInitialIndentFromPreviousLine(
497: final BaseDocument doc, final int line)
498: throws BadLocationException {
499:
500: // get initial indent from the previous line
501: int initialIndent = 0;
502:
503: if (line > 0) {
504: int lineStart = Utilities.getRowStartFromLineOffset(doc,
505: line);
506: int previousNonWhiteLineEnd = Utilities
507: .getFirstNonWhiteBwd(doc, lineStart);
508:
509: if (previousNonWhiteLineEnd > 0) {
510: initialIndent = Utilities.getRowIndent(doc,
511: previousNonWhiteLineEnd);
512: }
513: }
514:
515: return initialIndent;
516: }
517:
518: private int getInitialIndentFromNextLine(final BaseDocument doc,
519: final int line) throws BadLocationException {
520:
521: // get initial indent from the next line
522: int initialIndent = 0;
523:
524: int lineStart = Utilities.getRowStartFromLineOffset(doc, line);
525: int lineEnd = Utilities.getRowEnd(doc, lineStart);
526: int nextNonWhiteLineStart = Utilities.getFirstNonWhiteFwd(doc,
527: lineEnd);
528:
529: if (nextNonWhiteLineStart > 0) {
530: initialIndent = Utilities.getRowIndent(doc,
531: nextNonWhiteLineStart, true);
532: }
533:
534: return initialIndent;
535: }
536:
537: private boolean hasValidSyntaxSupport(BaseDocument doc) {
538: ExtSyntaxSupport sup = getSyntaxSupport(doc);
539:
540: if (sup == null) {
541: ErrorManager
542: .getDefault()
543: .log(
544: ErrorManager.WARNING,
545: "TagBasedFormatter: failed to retrieve SyntaxSupport for document;"
546: + //NOI18N
547: " probably attempt to use incompatible indentation engine"); //NOI18N
548:
549: return false;
550: }
551:
552: return true;
553: }
554:
555: protected static int getNumberOfLines(BaseDocument doc)
556: throws BadLocationException {
557: return Utilities.getLineOffset(doc, doc.getLength() - 1) + 1;
558: }
559:
560: protected TokenItem getNextClosingTag(BaseDocument doc, int offset)
561: throws BadLocationException {
562: ExtSyntaxSupport sup = getSyntaxSupport(doc);
563: TokenItem token = sup.getTokenChain(offset, offset + 1);
564:
565: while (token != null) {
566: if (isClosingTag(token)) {
567: return token;
568: }
569:
570: token = token.getNext();
571: }
572:
573: return null;
574: }
575:
576: protected boolean isJustBeforeClosingTag(BaseDocument doc, int pos)
577: throws BadLocationException {
578: ExtSyntaxSupport sup = getSyntaxSupport(doc);
579: TokenItem tknTag = sup.getTokenChain(pos, pos + 1);
580:
581: if (isClosingTag(tknTag)) {
582: return true;
583: }
584:
585: return false;
586: }
587:
588: protected class InitialIndentData {
589: private final int indentLevelBias;
590: private final int indentBias;
591: private final int indentLevels[];
592: private final int indentsWithinTags[];
593: private BaseDocument doc;
594:
595: public InitialIndentData(BaseDocument doc, int indentLevels[],
596: int indentsWithinTags[], int firstRefBlockLine,
597: int lastRefBlockLine) throws BadLocationException {
598:
599: int initialIndent = getInitialIndentFromPreviousLine(doc,
600: firstRefBlockLine);
601: int indentLevelBiasFromTheTop = initialIndent
602: / doc.getShiftWidth()
603: - (firstRefBlockLine > 0 ? indentLevels[firstRefBlockLine - 1]
604: : 0);
605:
606: int initialIndentFromTheBottom = getInitialIndentFromNextLine(
607: doc, lastRefBlockLine);
608: int indentLevelBiasFromTheBottom = initialIndentFromTheBottom
609: / doc.getShiftWidth()
610: - (lastRefBlockLine < getNumberOfLines(doc) - 1 ? indentLevels[lastRefBlockLine + 1]
611: : 0);
612:
613: if (indentLevelBiasFromTheBottom > indentLevelBiasFromTheTop) {
614: indentLevelBias = indentLevelBiasFromTheBottom;
615: initialIndent = initialIndentFromTheBottom;
616: } else {
617: indentLevelBias = indentLevelBiasFromTheTop;
618: }
619:
620: indentBias = initialIndent % doc.getShiftWidth();
621: this .indentLevels = indentLevels;
622: this .indentsWithinTags = indentsWithinTags;
623: this .doc = doc;
624: }
625:
626: public boolean isEligibleToIndent(int line) {
627: return getActualIndentLevel(line) >= 0;
628: }
629:
630: public int getIndent(int line) {
631: return indentBias + indentsWithinTags[line]
632: + getActualIndentLevel(line) * doc.getShiftWidth();
633: }
634:
635: private int getActualIndentLevel(int line) {
636: return indentLevels[line] + indentLevelBias;
637: }
638: }
639:
640: protected static class TagIndentationData {
641: private final String tagName;
642: private final int line;
643: private int closedOnLine;
644:
645: public TagIndentationData(String tagName, int line) {
646: this .tagName = tagName;
647: this .line = line;
648: }
649:
650: public String getTagName() {
651: return tagName;
652: }
653:
654: public int getLine() {
655: return line;
656: }
657:
658: public int getClosedOnLine() {
659: return closedOnLine;
660: }
661:
662: public void setClosedOnLine(int closedOnLine) {
663: this.closedOnLine = closedOnLine;
664: }
665: }
666: }
|