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: * If you wish your version of this file to be governed by only the CDDL
025: * or only the GPL Version 2, indicate your decision by adding
026: * "[Contributor] elects to include this software in this distribution
027: * under the [CDDL or GPL Version 2] license." If you do not indicate a
028: * single choice of license, a recipient has the option to distribute
029: * your version of this file under either the CDDL, the GPL Version 2 or
030: * to extend the choice of license to its licensees as provided above.
031: * However, if you add GPL Version 2 code and therefore, elected the GPL
032: * Version 2 license, then the option applies only if the new code is
033: * made subject to such option by the copyright holder.
034: *
035: * Contributor(s):
036: *
037: * Portions Copyrighted 2008 Sun Microsystems, Inc.
038: */
039: package org.netbeans.modules.css.gsf;
040:
041: import java.util.List;
042: import java.util.Set;
043: import javax.swing.text.BadLocationException;
044: import javax.swing.text.Document;
045: import org.netbeans.api.lexer.LanguagePath;
046: import org.netbeans.modules.gsf.api.CompilationInfo;
047: import org.netbeans.modules.gsf.api.Formatter;
048: import org.netbeans.api.lexer.Token;
049: import org.netbeans.api.lexer.TokenHierarchy;
050: import org.netbeans.api.lexer.TokenSequence;
051: import org.netbeans.editor.BaseDocument;
052: import org.netbeans.editor.Utilities;
053: import org.netbeans.modules.css.lexer.api.CSSTokenId;
054: import org.netbeans.modules.editor.indent.api.IndentUtils;
055: import org.openide.util.Exceptions;
056:
057: /**
058: *
059: * @author marek
060: *
061: * @todo fix the section reformatting so it can reformat multiple embedded pieces of css
062: */
063: public class CSSFormatter implements Formatter {
064:
065: public boolean needsParserResult() {
066: return false;
067: }
068:
069: public void reformat(Document doc, int startOffset, int endOffset,
070: CompilationInfo info) {
071: reindent(doc, startOffset, endOffset);
072: }
073:
074: public void reindent(Document document, int startOffset,
075: int endOffset) {
076: BaseDocument bdoc = (BaseDocument) document;
077:
078: //XXX still using the old formatting API, the new one is a subject of change in next version of netbeans.
079: org.netbeans.editor.Formatter editorFormatter = bdoc
080: .getFormatter();
081: // try {
082: // editorFormatter.indentLock();
083: try {
084: int indentLevel = IndentUtils.indentLevelSize(document);
085: int lastLine = Utilities.getLineOffset(bdoc, bdoc
086: .getLength());
087: int indents[] = new int[lastLine + 1];
088: int indentShift[] = new int[lastLine + 1];
089: boolean formattableLines[] = new boolean[lastLine + 1];
090: bdoc.atomicLock();
091: int lineBegin = Utilities.getRowStart(bdoc, startOffset);
092: //int firstLineOffset = Utilities.getLineOffset(bdoc, startOffset);
093: //int lastLineOffset = Utilities.getLineOffset(bdoc, endOffset);
094:
095: //XXX wrong impl - it needs to operate on multiple token sequences???
096: //does the infrastructure call me separately for each ts or just once?
097: TokenHierarchy th = TokenHierarchy.get(bdoc);
098:
099: for (LanguagePath languagePath : (Set<LanguagePath>) th
100: .languagePaths()) {
101: if (languagePath.innerLanguage() == CSSTokenId
102: .language()) {
103: for (TokenSequence ts : (List<TokenSequence>) th
104: .tokenSequenceList(languagePath, 0, bdoc
105: .getLength())) {
106: TextBounds tsBounds = findTokenSequenceBounds(
107: bdoc, ts);
108:
109: if (tsBounds.getAbsoluteEnd() < startOffset
110: || tsBounds.getAbsoluteStart() > endOffset
111: || tsBounds.getStartPos() == -1) { // empty CSS section
112: continue;
113: }
114:
115: if (startOffset == endOffset) {
116: //enter pressed - indent just the line under cursor
117: if (lineBegin == 1) {
118: //pressed on first line - do not indent
119: return;
120: }
121:
122: //go back and find first line with ident(ifier) or '{' char - and indent accordingly
123: // ident = 0
124: // { = +1
125: ts.move(lineBegin);
126: while (ts.movePrevious()) {
127: Token t = ts.token();
128: if (t.id() == CSSTokenId.IDENT
129: || t.id() == CSSTokenId.RBRACE) {
130: //we are on a line with css rule item e.g. color:red;
131: //where the property name is of IDENT token type
132: //..or.. we are just after end of a rule ("}" token)
133: //=> use the same indent level
134: int indent = Utilities
135: .getRowIndent(bdoc, t
136: .offset(th));
137: editorFormatter.changeRowIndent(
138: bdoc, lineBegin, indent);
139: return;
140: } else if (t.id() == CSSTokenId.LBRACE) {
141: // just rule beginning before current position - increase indent
142: int indent = Utilities
143: .getRowIndent(bdoc, t
144: .offset(th));
145:
146: //XXX or should I use the this.indentSize() instead????
147: editorFormatter.changeRowIndent(
148: bdoc, lineBegin, indent
149: + indentLevel);
150: return;
151: }
152: }
153: } else {
154: //user peformed reformat on a selection or the whole text
155:
156: int firstLineOffset = tsBounds
157: .getStartLine();
158: int lastLineOffset = tsBounds.getEndLine();
159:
160: int firstLineOriginalIndent = Utilities
161: .getRowIndent(bdoc, tsBounds
162: .getStartPos());
163:
164: for (int line = firstLineOffset; line <= lastLineOffset; line++) {
165: indentShift[line] = firstLineOriginalIndent;
166: formattableLines[line] = true;
167: }
168:
169: //contains <line_start_offset; indent_level> pairs
170: int indent = 0;
171: for (int line = firstLineOffset; line <= lastLineOffset; line++) {
172: int indents_index = line;
173: if (indents_index == 0) {
174: //do we start inside a rule?
175:
176: Token[] ruleBoundaries = findRule(
177: ts, lineBegin);
178: if (ruleBoundaries != null) {
179: //yes, we start in a rule
180: indent = Utilities
181: .getRowIndent(
182: bdoc,
183: ruleBoundaries[0]
184: .offset(th));
185: //and add the content indent
186: indent = indent + indentLevel;
187: }
188: }
189:
190: //identify the line content
191: int lStart = Utilities
192: .getRowStartFromLineOffset(
193: bdoc, line);
194: int lEnd = Utilities.getRowEnd(bdoc,
195: lStart);
196: ts.move(lStart);
197: int ruleStart = -1;
198: int ruleEnd = -1;
199: int ident = -1;
200: while (ts.moveNext()
201: && ts.offset() < lEnd) {
202: Token t = ts.token();
203: if (t.id() == CSSTokenId.LBRACE) {
204: ruleStart = ts.offset();
205: } else if (t.id() == CSSTokenId.RBRACE) {
206: ruleEnd = ts.offset();
207: } else if (t.id() == CSSTokenId.IDENT) {
208: ident = ts.offset();
209: }
210: }
211:
212: if (ruleStart != -1 && ruleEnd == -1) {
213: //rule started
214: indents[indents_index] = indent;
215: //increase indent of the content
216: indent = indent + indentLevel;
217: } else if (ruleStart == -1
218: && ruleEnd != -1) {
219: //rule ended
220:
221: //test if there is an identifier before the rule closing symbol
222: //in such case do not lower the indent of the line
223: if (ident != -1 && ident < ruleEnd) {
224: //sg. like "color: red; }" on the line
225: indents[indents_index] = indent;
226: indent = indent - indentLevel;
227: } else {
228: indent = indent - indentLevel;
229: indents[indents_index] = indent;
230: }
231: } else {
232: //just text inside or outside of a rule
233: indents[indents_index] = indent;
234: }
235:
236: }
237: }
238: }
239: }
240:
241: }
242:
243: int firstLineWithinFormattingRange = Utilities
244: .getLineOffset(bdoc, startOffset);
245: int lastLineWithinFormattingRange = Utilities
246: .getLineOffset(bdoc, endOffset);
247:
248: //apply the formatting
249: for (int line = firstLineWithinFormattingRange; line <= lastLineWithinFormattingRange; line++) {
250: if (formattableLines[line]) {
251: int lStart = Utilities.getRowStartFromLineOffset(
252: bdoc, line);
253: editorFormatter.changeRowIndent(bdoc, lStart,
254: indents[line] + indentShift[line]);
255: }
256: }
257:
258: } catch (BadLocationException ex) {
259: Exceptions.printStackTrace(ex);
260: } finally {
261: bdoc.atomicUnlock();
262: }
263: // } finally {
264: // editorFormatter.indentUnlock();
265: // }
266:
267: }
268:
269: /**
270: * @param ts - tokenSequence
271: * @param offset - where to start the search
272: * @return an array of two Tokens which represents the curly brackets of
273: * css rule.
274: */
275: private Token[] findRule(TokenSequence ts, int offset) {
276: ts.move(offset);
277: while (ts.movePrevious()) {
278: Token t = ts.token();
279: if (t.id() == CSSTokenId.RBRACE) {
280: //we found } => we are out of a rule
281: return null;
282: } else if (t.id() == CSSTokenId.LBRACE) {
283: //we are in a rule -> find the end of the rule
284: while (ts.moveNext()) {
285: Token t2 = ts.token();
286: if (t2.id() == CSSTokenId.RBRACE) {
287: //end of the rule
288: return new Token[] { t, t2 };
289: }
290: }
291:
292: break;
293: }
294:
295: }
296: //din't find anything reasonable
297: return null;
298: }
299:
300: public int indentSize() {
301: return 4;
302: }
303:
304: public int hangingIndentSize() {
305: return 4;
306: }
307:
308: private TextBounds findTokenSequenceBounds(BaseDocument doc,
309: TokenSequence tokenSequence) throws BadLocationException {
310: tokenSequence.moveStart();
311: tokenSequence.moveNext();
312: int absoluteStart = tokenSequence.offset();
313: tokenSequence.moveEnd();
314: tokenSequence.movePrevious();
315: int absoluteEnd = tokenSequence.offset()
316: + tokenSequence.token().length();
317:
318: // trim whitespaces from both ends
319: while (isWSToken(tokenSequence.token())) {
320: if (!tokenSequence.movePrevious()) {
321: return new TextBounds(absoluteStart, absoluteEnd); // a block of empty text
322: }
323: }
324:
325: int whiteSpaceSuffixLen = 0;
326:
327: while (Character.isWhitespace(tokenSequence.token().text()
328: .charAt(
329: tokenSequence.token().length() - 1
330: - whiteSpaceSuffixLen))) {
331: whiteSpaceSuffixLen++;
332: }
333:
334: int languageBlockEnd = tokenSequence.offset()
335: + tokenSequence.token().length() - whiteSpaceSuffixLen;
336:
337: tokenSequence.moveStart();
338:
339: do {
340: tokenSequence.moveNext();
341: } while (isWSToken(tokenSequence.token()));
342:
343: int whiteSpacePrefixLen = 0;
344:
345: while (Character.isWhitespace(tokenSequence.token().text()
346: .charAt(whiteSpacePrefixLen))) {
347: whiteSpacePrefixLen++;
348: }
349:
350: int languageBlockStart = tokenSequence.offset()
351: + whiteSpacePrefixLen;
352: int firstLineOfTheLanguageBlock = Utilities.getLineOffset(doc,
353: languageBlockStart);
354: int lastLineOfTheLanguageBlock = Utilities.getLineOffset(doc,
355: languageBlockEnd);
356: return new TextBounds(absoluteStart, absoluteEnd,
357: languageBlockStart, languageBlockEnd,
358: firstLineOfTheLanguageBlock, lastLineOfTheLanguageBlock);
359: }
360:
361: protected boolean isWSToken(Token token) {
362: return isOnlyWhiteSpaces(token.text());
363: }
364:
365: protected boolean isOnlyWhiteSpaces(CharSequence txt) {
366: for (int i = 0; i < txt.length(); i++) {
367: if (!Character.isWhitespace(txt.charAt(i))) {
368: return false;
369: }
370: }
371:
372: return true;
373: }
374:
375: public static class TextBounds {
376:
377: private int absoluteStart; // start offset regardless of white spaces
378: private int absoluteEnd; // end --
379: private int startPos = -1;
380: private int endPos = -1;
381: private int startLine = -1;
382: private int endLine = -1;
383:
384: public TextBounds(int absoluteStart, int absoluteEnd) {
385: this .absoluteStart = absoluteStart;
386: this .absoluteEnd = absoluteEnd;
387: }
388:
389: public TextBounds(int absoluteStart, int absoluteEnd,
390: int startPos, int endPos, int startLine, int endLine) {
391: this .absoluteStart = absoluteStart;
392: this .absoluteEnd = absoluteEnd;
393: this .startPos = startPos;
394: this .endPos = endPos;
395: this .startLine = startLine;
396: this .endLine = endLine;
397: }
398:
399: public int getEndPos() {
400: return endPos;
401: }
402:
403: public int getStartPos() {
404: return startPos;
405: }
406:
407: public int getEndLine() {
408: return endLine;
409: }
410:
411: public int getStartLine() {
412: return startLine;
413: }
414:
415: public int getAbsoluteEnd() {
416: return absoluteEnd;
417: }
418:
419: public int getAbsoluteStart() {
420: return absoluteStart;
421: }
422:
423: @Override
424: public String toString() {
425: return "pos " + startPos + "-" + endPos + ", lines "
426: + startLine + "-" + endLine; //NOI18N
427: }
428: }
429: }
|