KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > netbeans > modules > ruby > Formatter


1 /*
2  * The contents of this file are subject to the terms of the Common Development
3  * and Distribution License (the License). You may not use this file except in
4  * compliance with the License.
5  *
6  * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
7  * or http://www.netbeans.org/cddl.txt.
8  *
9  * When distributing Covered Code, include this CDDL Header Notice in each file
10  * and include the License file at http://www.netbeans.org/cddl.txt.
11  * If applicable, add the following below the CDDL Header, with the fields
12  * enclosed by brackets [] replaced by your own identifying information:
13  * "Portions Copyrighted [year] [name of copyright owner]"
14  *
15  * The Original Software is NetBeans. The Initial Developer of the Original
16  * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
17  * Microsystems, Inc. All Rights Reserved.
18  */

19 package org.netbeans.modules.ruby;
20
21 import java.io.PrintWriter JavaDoc;
22 import java.io.StringWriter JavaDoc;
23 import java.util.Iterator JavaDoc;
24
25 import javax.swing.text.BadLocationException JavaDoc;
26 import javax.swing.text.Document JavaDoc;
27
28 import org.jruby.ast.ArgumentNode;
29 import org.jruby.ast.CommentNode;
30 import org.jruby.ast.Node;
31 import org.jruby.ast.visitor.rewriter.DefaultFormatHelper;
32 import org.jruby.ast.visitor.rewriter.FormatHelper;
33 import org.jruby.ast.visitor.rewriter.ReWriteVisitor;
34 import org.jruby.ast.visitor.rewriter.ReWriterFactory;
35 import org.jruby.ast.visitor.rewriter.utils.ReWriterContext;
36 import org.netbeans.api.gsf.FormattingPreferences;
37 import org.netbeans.api.gsf.GsfTokenId;
38 import org.netbeans.api.gsf.ParserResult;
39 import org.netbeans.api.lexer.Token;
40 import org.netbeans.api.lexer.TokenId;
41 import org.netbeans.editor.BaseDocument;
42 import org.netbeans.editor.Utilities;
43 import org.netbeans.modules.ruby.lexer.LexUtilities;
44 import org.netbeans.modules.ruby.lexer.RubyTokenId;
45 import org.openide.util.Exceptions;
46
47
48 /**
49  * Formatting and indentation for Ruby.
50  * WARNING! This is ugly, hacky code. I've recently switched over to the Lexer,
51  * and I want to make this token based; it's currently just document character based.
52  *
53  * @todo Create unit tests
54  * @todo Use in complete file reindentation, then diff with original formatted ruby source
55  * and see where I've gotta improve matters
56  * @todo Use configuration object to pass in Ruby conventions
57  * @todo Handle RDoc conventions; if the previous line has a number or bullet
58  * or label, indent into the matching text.
59  * @todo Use the provided parse tree, if any, to for example check heredoc nodes
60  * and see if they are indentable.
61  *
62  * @author Tor Norbye
63  */

64 public class Formatter implements org.netbeans.api.gsf.Formatter {
65     private static final int KEEP_INDENT = -1;
66
67     public Formatter() {
68     }
69
70     public int getLineIndent(Document JavaDoc document, int offset, FormattingPreferences preferences) {
71         BaseDocument doc = (BaseDocument)document;
72
73         Token<?extends GsfTokenId> token = LexUtilities.getToken(doc, offset);
74
75         if ((token.id() == RubyTokenId.STRING_LITERAL) ||
76                 (token.id() == RubyTokenId.QUOTED_STRING_LITERAL) ||
77                 (token.id() == RubyTokenId.REGEXP_LITERAL)) {
78             // No indentation for literal strings in Ruby, since they can
79
// contain newlines
80
return 0;
81         }
82
83         // Indentation algorithm:
84
// Look at the previous (non-empty, non-comment) line.
85
// Compute its indentation. This is the default result.
86
// However, we will adjust it up or down if there are structure
87
// tokens on the previous or current lines.
88
//
89
// If the previous line begins with an indentation word,
90
// we increase the indent.
91
// If not, if the previous line has a positive balance
92
// (of begin/end, lbrace/rbrace pairs), then increase
93
// the indent.
94
// Then look at the current line. (When used for smart indent, e.g
95
// indent a new line after pressing return, this part is a noop.)
96
// If the current line begins with "end" or "}, we reduce
97
// the indentation.
98
try {
99             int begin = Utilities.getRowStart(doc, offset);
100
101             // Skip back to the previous (non empty) line
102
begin--;
103
104             while ((begin >= 0) &&
105                     (LexUtilities.isCommentOnlyLine(doc, begin) ||
106                     Utilities.isRowWhite(doc, begin))) {
107                 begin--;
108             }
109
110             if (begin < 0) {
111                 return 0; // Already first line
112
}
113
114             int indent = LexUtilities.getLineIndent(doc, begin);
115             indent = computeIndent(doc, offset, begin, indent, preferences.getIndentation());
116
117             if (indent == KEEP_INDENT) {
118                 indent = LexUtilities.getLineIndent(doc, offset);
119             }
120
121             return indent;
122         } catch (BadLocationException JavaDoc ex) {
123             Exceptions.printStackTrace(ex);
124         }
125
126         return 0;
127     }
128
129     public void reformat(Document JavaDoc document, ParserResult result, FormattingPreferences preferences) {
130         RubyParseResult parseResult = (RubyParseResult)result;
131
132         if ((parseResult == null) || (parseResult.getRealRoot() == null)) {
133             // just reindent instead
134
reindent(document, 0, document.getLength(), result, preferences);
135
136             return;
137         }
138
139         if (!parseResult.isCommentsAdded()) {
140             new StructureAnalyzer().addComments(parseResult);
141         }
142
143         BaseDocument doc = (BaseDocument)document;
144
145         StringWriter JavaDoc stringWriter = new StringWriter JavaDoc(document.getLength() * 2);
146         PrintWriter JavaDoc output = new PrintWriter JavaDoc(stringWriter);
147         String JavaDoc source = parseResult.getSource();
148         FormatHelper formatHelper = new DefaultFormatHelper();
149         ReWriterContext context = new ReWriterContext(output, source, formatHelper);
150         ReWriterFactory factory = new ReWriterFactory(context);
151
152         //ReWriteVisitor visitor = factory.createReWriteVisitor();
153
ReWriteVisitor visitor = new CommentRewriter(context);
154
155         Node root = parseResult.getRealRoot();
156         root.accept(visitor);
157         visitor.flushStream();
158
159         String JavaDoc reformatted = stringWriter.toString();
160
161         try {
162             doc.atomicLock();
163             doc.replace(0, doc.getLength(), reformatted, null);
164         } catch (BadLocationException JavaDoc ble) {
165             Exceptions.printStackTrace(ble);
166         } finally {
167             doc.atomicUnlock();
168         }
169     }
170
171     public void reindent(Document JavaDoc document, int startOffset, int endOffset, ParserResult result,
172         FormattingPreferences preferences) {
173         // PENDING:
174
// The reformatting APIs in NetBeans should be lexer based. They are still
175
// based on the old TokenID apis. Once we get a lexer version, convert this over.
176
// I just need -something- in place until that is provided.
177

178         // For now, there is a simple place-holder implementation, which just
179
// reformats the entire document by iterating over lines and building up a
180
// document string which it finally sticks back into the buffer.
181
BaseDocument doc = (BaseDocument)document;
182
183         try {
184             int offset = Utilities.getRowStart(doc, startOffset);
185             int end = Utilities.getRowEnd(doc, endOffset);
186             int replaceBegin = offset;
187             int replaceEnd = end;
188
189             int prevIndent = 0;
190             int prevOffset = -1;
191
192             if (offset > 0) {
193                 prevOffset = offset - 1;
194                 prevIndent = LexUtilities.getLineIndent(doc, prevOffset);
195             }
196
197             int indentSize = preferences.getIndentation();
198             StringBuilder JavaDoc sb = new StringBuilder JavaDoc(2 * doc.getLength());
199
200             while (offset < end) {
201                 // Indentation algorithm:
202
// Look at the previous (non-empty, non-comment) line.
203
// Compute its indentation. This is the default result.
204
// However, we will adjust it up or down if there are structure
205
// tokens on the previous or current lines.
206
//
207
// If the previous line begins with an indentation word,
208
// we increase the indent.
209
// If not, if the previous line has a positive balance
210
// (of begin/end, lbrace/rbrace pairs), then increase
211
// the indent.
212
// Then look at the current line. (When used for smart indent, e.g
213
// indent a new line after pressing return, this part is a noop.)
214
// If the current line begins with "end" or "}, we reduce
215
// the indentation.
216
int indent;
217
218                 if (prevOffset == -1) {
219                     indent = 0;
220                 } else {
221                     indent = computeIndent(doc, offset, prevOffset, prevIndent, indentSize);
222                 }
223
224                 if (indent == KEEP_INDENT) {
225                     indent = LexUtilities.getLineIndent(doc, offset);
226                 } else {
227                     prevIndent = indent;
228                     prevOffset = offset;
229                 }
230
231                 // TODO - handle tab expansion
232
int lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
233                 int lineEnd = Utilities.getRowLastNonWhite(doc, offset) + 1;
234
235                 if ((lineBegin != -1) && (lineEnd > lineBegin)) {
236                     for (int i = 0; i < indent; i++) {
237                         sb.append(' ');
238                     }
239
240                     String JavaDoc text = doc.getText(lineBegin, lineEnd - lineBegin);
241                     sb.append(text);
242                 }
243
244                 sb.append("\n");
245
246                 offset = Utilities.getRowEnd(doc, offset) + 1;
247             }
248
249             String JavaDoc reindented = sb.toString();
250
251             try {
252                 doc.atomicLock();
253                 doc.replace(replaceBegin, replaceEnd - replaceBegin, reindented, null);
254             } finally {
255                 doc.atomicUnlock();
256             }
257         } catch (BadLocationException JavaDoc ble) {
258             Exceptions.printStackTrace(ble);
259         }
260     }
261
262     private int computeIndent(BaseDocument doc, int offset, int prevLineOffset, int prevIndent,
263         int indentSize) throws BadLocationException JavaDoc {
264         int indent = prevIndent;
265         int lineBegin = Utilities.getRowFirstNonWhite(doc, prevLineOffset);
266         Token<?extends GsfTokenId> token = LexUtilities.getToken(doc, lineBegin);
267
268         if (token == null) {
269             return indent;
270         }
271
272         // Line starts with an indentation word: if, else, ...
273
// (if it starts with a structure word like begin, we check the
274
// balance to make sure we're not dealing with a line like this:
275
// def extract_hex(s); s.hex if s &&! skip; end
276
TokenId id = token.id();
277
278         if ((LexUtilities.getBeginEndLineBalance(doc, prevLineOffset) > 0) || // "class foo", but not "def foo; print; end"
279
(LexUtilities.isIndentToken(id) && !LexUtilities.isBeginToken(id))) { // else, rescue, etc.
280
indent += indentSize;
281         } else {
282             // Check line balance to see if we're introducing indentation
283
// context somehow - such as a line like this:
284
// return $1.scan(FLAG_REGEXP).collect { |flag, atom|
285
//
286
int balance =
287                 LexUtilities.getLineBalance(doc, prevLineOffset, RubyTokenId.LBRACE,
288                     RubyTokenId.RBRACE);
289
290             if (balance > 0) {
291                 indent += indentSize;
292             }
293         }
294
295         // TODO: Look at the current line to decide if we need to adjust it back
296
lineBegin = Utilities.getRowFirstNonWhite(doc, offset);
297
298         if (lineBegin != -1) {
299             token = LexUtilities.getToken(doc, lineBegin);
300
301             if ((token.id() == RubyTokenId.STRING_LITERAL) ||
302                     (token.id() == RubyTokenId.QUOTED_STRING_LITERAL) ||
303                     (token.id() == RubyTokenId.REGEXP_LITERAL)) {
304                 // No indentation for literal strings in Ruby, since they can
305
// contain newlines. Leave it as is.
306
return KEEP_INDENT;
307             }
308
309             if (token != null) {
310                 id = token.id();
311
312                 if ((LexUtilities.isIndentToken(id) && !LexUtilities.isBeginToken(id)) ||
313                         (id == RubyTokenId.END) || (id == RubyTokenId.RBRACE)) {
314                     indent -= indentSize;
315                 }
316             }
317         }
318
319         return indent;
320     }
321
322     public int indentSize() {
323         return 2;
324     }
325
326     private static class CommentRewriter extends ReWriteVisitor {
327         public CommentRewriter(ReWriterContext config) {
328             super(config);
329         }
330
331         public void visitNode(Node iVisited) {
332             if (iVisited == null) {
333                 return;
334             }
335
336             printCommentsBefore(iVisited);
337
338             if (iVisited instanceof ArgumentNode) {
339                 print(((ArgumentNode)iVisited).getName());
340             } else {
341                 iVisited.accept(this);
342             }
343
344             printCommentsAfter(iVisited);
345             config.setLastPosition(iVisited.getPosition());
346         }
347
348         private static int getEndLine(Node n) {
349             return n.getPosition().getEndLine();
350         }
351
352         private static int getStartLine(Node n) {
353             return n.getPosition().getStartLine();
354         }
355
356         // protected void printNewlineAndIndentation() {
357
// if (config.hasHereDocument()) config.fetchHereDocument().print();
358
//
359
// print('\n');
360
// config.getIndentor().printIndentation(config.getOutput());
361
// }
362
private void printCommentsBefore(Node iVisited) {
363             for (Iterator JavaDoc it = iVisited.getComments().iterator(); it.hasNext();) {
364                 CommentNode n = (CommentNode)it.next();
365
366                 if (getStartLine(n) < getStartLine(iVisited)) {
367                     String JavaDoc comment = n.getContent();
368                     visitNode(n);
369                     print(comment);
370                     printNewlineAndIndentation();
371                 }
372             }
373         }
374
375         protected boolean printCommentsAfter(Node iVisited) {
376             boolean hasComment = false;
377
378             for (Iterator JavaDoc it = iVisited.getComments().iterator(); it.hasNext();) {
379                 CommentNode n = (CommentNode)it.next();
380
381                 if (getStartLine(n) >= getEndLine(iVisited)) {
382                     print(' ');
383                     visitNode(n);
384
385                     String JavaDoc comment = n.getContent();
386                     print(comment);
387                     hasComment = true;
388                 }
389             }
390
391             return hasComment;
392         }
393     }
394 }
395
Popular Tags