KickJava   Java API By Example, From Geeks To Geeks.

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


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.IOException JavaDoc;
22 import java.util.ArrayList JavaDoc;
23 import java.util.Collections JavaDoc;
24 import java.util.Iterator JavaDoc;
25 import java.util.List JavaDoc;
26
27 import javax.swing.text.BadLocationException JavaDoc;
28 import javax.swing.text.Caret JavaDoc;
29 import javax.swing.text.Document JavaDoc;
30
31 import org.jruby.ast.NewlineNode;
32 import org.jruby.ast.Node;
33 import org.netbeans.api.gsf.CompilationInfo;
34 import org.netbeans.api.gsf.GsfTokenId;
35 import org.netbeans.api.gsf.OffsetRange;
36 import org.netbeans.api.lexer.Token;
37 import org.netbeans.api.lexer.TokenHierarchy;
38 import org.netbeans.api.lexer.TokenId;
39 import org.netbeans.api.lexer.TokenSequence;
40 import org.netbeans.editor.BaseDocument;
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  * Provide bracket completion for Ruby.
50  * This class provides three broad services:
51  * - Identifying matching pairs (parentheses, begin/end pairs etc.), which
52  * is used both for highlighting in the IDE (when the caret is on for example
53  * an if statement, the corresponding end token is highlighted), and navigation
54  * where you can jump between matching pairs.
55  * - Automatically inserting corresponding pairs when you insert a character.
56  * For example, if you insert a single quote, a corresponding ending quote
57  * is inserted - unless you're typing "over" the existing quote (you should
58  * be able to type foo = "hello" without having to arrow over the second
59  * quote that was inserted after you typed the first one).
60  * - Automatically adjusting indentation in some scenarios, for example
61  * when you type the final "d" in "end" - and readjusting it back to the
62  * original indentation if you continue typing something other than "end",
63  * e.g. "endian".
64  *
65  * The logic around inserting matching ""'s is heavily based on the Java editor
66  * implementation, and probably should be rewritten to be Ruby oriented.
67  * One thing they did is process the characters BEFORE the character has been
68  * inserted into the document. This has some advantages - it's easy to detect
69  * whether you're typing in the middle of a string since the token hierarchy
70  * has not been updated yet. On the other hand, it makes it hard to identify
71  * whether some characters are what we expect - is a "/" truly a regexp starter
72  * or something else? The Ruby lexer has lots of logic and state to determine
73  * this. I think it would be better to switch to after-insert logic for this.
74  *
75  * @todo Match braces within literal strings, as in #{}
76  * @todo Match || in the argument list of blocks? do { |foo| etc. }
77  * @todo I'm currently highlighting the indentation tokens (else, elsif, ensure, etc.)
78  * by finding the corresponding begin. For "illegal" tokens, e.g. def foo; else; end;
79  * this means I'll show "def" as the matching token for else, which is wrong.
80  * I should make the "indentation tokens" list into a map and associate them
81  * with their corresponding tokens, such that an else is only lined up with an if,
82  * etc.
83  *
84  * @author Tor Norbye
85  */

86 public class BracketCompleter implements org.netbeans.api.gsf.BracketCompletion {
87     /** Tokens which indicate that we're within a literal string */
88     private static TokenId[] STRING_TOKENS = // XXX What about RubyTokenId.STRING_BEGIN or QUOTED_STRING_BEGIN?
89
{
90             RubyTokenId.STRING_LITERAL, RubyTokenId.QUOTED_STRING_LITERAL, RubyTokenId.CHAR_LITERAL,
91             RubyTokenId.STRING_END, RubyTokenId.QUOTED_STRING_END
92         };
93
94     /** Tokens which indicate that we're within a regexp string */
95     // XXX What about RubyTokenId.REGEXP_BEGIN?
96
private static TokenId[] REGEXP_TOKENS = { RubyTokenId.REGEXP_LITERAL, RubyTokenId.REGEXP_END };
97
98     /** When != -1, this indicates that we previously adjusted the indentation of the
99      * line to the given offset, and if it turns out that the user changes that token,
100      * we revert to the original indentation
101      */

102     private int previousAdjustmentOffset = -1;
103
104     /**
105      * The indentation to revert to when previousAdjustmentOffset is set and the token
106      * changed
107      */

108     private int previousAdjustmentIndent;
109
110     public BracketCompleter() {
111     }
112
113     /** Newline inserted: consider adding missing "end", "}" and "=end" markers, etc. */
114     public int beforeBreak(Document JavaDoc document, int offset, Caret JavaDoc caret)
115         throws BadLocationException JavaDoc {
116         BaseDocument doc = (BaseDocument)document;
117         TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
118
119         if (ts == null) {
120             return -1;
121         }
122
123         ts.move(offset);
124
125         if (!ts.moveNext() && !ts.movePrevious()) {
126             return -1;
127         }
128
129         Token<?extends GsfTokenId> token = ts.token();
130         TokenId id = token.id();
131
132         // Is it an umatched =begin token?
133
if ((id == RubyTokenId.ERROR) && (ts.offset() == (offset - 6)) &&
134                 token.text().toString().startsWith("=begin")) {
135             doc.insertString(offset, "\n=end", null);
136             caret.setDot(offset);
137
138             return -1;
139         } else if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN)) {
140             String JavaDoc text = token.text().toString();
141
142             if (text.startsWith("<<")) {
143                 // A here doc. Make sure it is properly terminated, and if not,
144
// insert a terminator
145
StringBuilder JavaDoc markerBuilder = new StringBuilder JavaDoc();
146
147                 for (int i = 2, n = text.length(); i < n; i++) {
148                     char c = text.charAt(i);
149
150                     if ((c == '\n') || (c == '\r')) {
151                         break;
152                     }
153
154                     markerBuilder.append(c);
155                 }
156
157                 String JavaDoc marker = markerBuilder.toString();
158
159                 // Next token should be string contents or a string end marker
160
boolean addEndMarker = true;
161
162                 if (ts.moveNext()) {
163                     token = ts.token();
164                     id = token.id();
165                     text = token.text().toString();
166
167                     if (text.endsWith(marker)) {
168                         addEndMarker = false;
169                     }
170                 }
171
172                 if (addEndMarker) {
173                     // Text doesn't end with marker -- insert it
174
String JavaDoc end = "\n" + marker;
175
176                     if (offset == doc.getLength()) {
177                         // At the end of the buffer we need a newline after the end
178
// marker. On other lines, we don't.
179
end = end + "\n";
180                     }
181
182                     doc.insertString(offset, end, null);
183                     caret.setDot(offset);
184
185                     return -1;
186                 }
187             }
188         }
189
190         // Insert an end statement? Insert a } marker?
191
boolean[] insertEndResult = new boolean[1];
192         boolean[] insertRBraceResult = new boolean[1];
193         int[] indentResult = new int[1];
194         boolean insert =
195             isEndMissing(doc, offset, false, insertEndResult, insertRBraceResult, null, indentResult);
196
197         if (insert) {
198             boolean insertEnd = insertEndResult[0];
199             boolean insertRBrace = insertRBraceResult[0];
200             int indent = indentResult[0];
201
202             // We've either encountered a further indented line, or a line that doesn't
203
// look like the end we're after, so insert a matching end.
204
StringBuilder JavaDoc sb = new StringBuilder JavaDoc();
205             sb.append("\n"); // XXX On Windows, do \r\n?
206
LexUtilities.indent(sb, indent);
207
208             if (insertEnd) {
209                 sb.append("end"); // NOI18N
210
} else {
211                 assert insertRBrace;
212                 sb.append("}"); // NOI18N
213
}
214
215             int insertOffset = offset; // offset < length ? offset+1 : offset;
216
doc.insertString(insertOffset, sb.toString(), null);
217             caret.setDot(insertOffset);
218         }
219
220         // Special case: since I do hash completion, if you try to type
221
// y = Thread.start {
222
// code here
223
// }
224
// you end up with
225
// y = Thread.start {|}
226
// If you hit newline at this point, you end up with
227
// y = Thread.start {
228
// |}
229
// which is not as helpful as it would be if we were not doing hash-matching
230
// (in that case we'd notice the brace imbalance, and insert the closing
231
// brace on the line below the insert position, and indent properly.
232
// Catch this scenario and handle it properly.
233
if ((id == RubyTokenId.RBRACE) && (Utilities.getRowLastNonWhite(doc, offset) == offset)) {
234             int indent = LexUtilities.getLineIndent(doc, offset);
235             StringBuilder JavaDoc sb = new StringBuilder JavaDoc();
236             sb.append("\n"); // XXX On Windows, do \r\n?
237
LexUtilities.indent(sb, indent);
238
239             int insertOffset = offset; // offset < length ? offset+1 : offset;
240
doc.insertString(insertOffset, sb.toString(), null);
241             caret.setDot(insertOffset);
242         }
243
244         return -1;
245     }
246
247     /**
248      * Determine if an "end" or "}" is missing following the caret offset.
249      * The logic used is to check the text on the current line for block initiators
250      * (e.g. "def", "for", "{" etc.) and then see if a corresponding close is
251      * found after the same indentation level.
252      *
253      * @param doc The document to be checked
254      * @param offset The offset of the current line
255      * @param skipJunk If false, only consider the current line (of the offset)
256      * as the possible "block opener"; if true, look backwards across empty
257      * lines and comment lines as well.
258      * @param insertEndResult Null, or a boolean 1-element array whose first
259      * element will be set to true iff this method determines that "end" should
260      * be inserted
261      * @param insertRBraceResult Null, or a boolean 1-element array whose first
262      * element will be set to true iff this method determines that "}" should
263      * be inserted
264      * @param startOffsetResult Null, or an integer 1-element array whose first
265      * element will be set to the starting offset of the opening block.
266      * @param indentResult Null, or an integer 1-element array whose first
267      * element will be set to the indentation level "end" or "}" should be
268      * indented to when inserted.
269      * @return true if something is missing; insertEndResult, insertRBraceResult
270      * and identResult will provide the more specific return values in their
271      * first elements.
272      */

273     static boolean isEndMissing(BaseDocument doc, int offset, boolean skipJunk,
274         boolean[] insertEndResult, boolean[] insertRBraceResult, int[] startOffsetResult,
275         int[] indentResult) throws BadLocationException JavaDoc {
276         int length = doc.getLength();
277
278         // Insert an end statement? Insert a } marker?
279
// Do so if the current line contains an unmatched begin marker,
280
// AND a "corresponding" marker does not exist.
281
// This will be determined as follows: Look forward, and check
282
// that we don't have "indented" code already (tokens at an
283
// indentation level higher than the current line was), OR that
284
// there is no actual end or } coming up.
285
if (startOffsetResult != null) {
286             startOffsetResult[0] = Utilities.getRowFirstNonWhite(doc, offset);
287         }
288
289         int beginEndBalance = LexUtilities.getBeginEndLineBalance(doc, offset);
290         int braceBalance =
291             LexUtilities.getLineBalance(doc, offset, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
292
293         if ((beginEndBalance == 1) || (braceBalance == 1)) {
294             // There is one more opening token on the line than a corresponding
295
// closing token. (If there's is more than one we don't try to help.)
296
int indent = LexUtilities.getLineIndent(doc, offset);
297
298             // Look for the next nonempty line, and if its indent is > indent,
299
// or if its line balance is -1 (e.g. it's an end) we're done
300
boolean insertEnd = beginEndBalance > 0;
301             boolean insertRBrace = braceBalance > 0;
302             int next = Utilities.getRowEnd(doc, offset) + 1;
303
304             for (; next < length; next = Utilities.getRowEnd(doc, next) + 1) {
305                 if (Utilities.isRowEmpty(doc, next) || Utilities.isRowWhite(doc, next) ||
306                         LexUtilities.isCommentOnlyLine(doc, next)) {
307                     continue;
308                 }
309
310                 int nextIndent = LexUtilities.getLineIndent(doc, next);
311
312                 if (nextIndent > indent) {
313                     insertEnd = false;
314                     insertRBrace = false;
315                 } else if (nextIndent == indent) {
316                     if (insertEnd) {
317                         if (LexUtilities.getBeginEndLineBalance(doc, next) < 0) {
318                             insertEnd = false;
319                         } else {
320                             // See if I have a structure word like "else", "ensure", etc.
321
// (These are indent words that are not also begin words)
322
// and if so refrain from inserting the end
323
int lineBegin = Utilities.getRowFirstNonWhite(doc, next);
324
325                             Token<?extends GsfTokenId> token =
326                                 LexUtilities.getToken(doc, lineBegin);
327
328                             if ((token != null) && LexUtilities.isIndentToken(token.id()) &&
329                                     !LexUtilities.isBeginToken(token.id())) {
330                                 insertEnd = false;
331                             }
332                         }
333                     } else if (insertRBrace &&
334                             (LexUtilities.getLineBalance(doc, next, RubyTokenId.LBRACE,
335                                 RubyTokenId.RBRACE) < 0)) {
336                         insertRBrace = false;
337                     }
338                 }
339
340                 break;
341             }
342
343             if (insertEndResult != null) {
344                 insertEndResult[0] = insertEnd;
345             }
346
347             if (insertRBraceResult != null) {
348                 insertRBraceResult[0] = insertRBrace;
349             }
350
351             if (indentResult != null) {
352                 indentResult[0] = indent;
353             }
354
355             return insertEnd || insertRBrace;
356         }
357
358         return false;
359     }
360
361     public boolean beforeCharInserted(Document JavaDoc document, int caretOffset, Caret JavaDoc caret, char ch)
362         throws BadLocationException JavaDoc {
363         BaseDocument doc = (BaseDocument)document;
364
365         //dumpTokens(doc, caretOffset);
366

367         // Gotta look for the string begin pair in tokens since ANY character can
368
// be used in Ruby string like the %x!! form.
369
if (caretOffset == 0) {
370             return false;
371         }
372
373         TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
374
375         if (ts == null) {
376             return false;
377         }
378
379         ts.move(caretOffset);
380
381         if (!ts.moveNext() && !ts.movePrevious()) {
382             return false;
383         }
384
385         Token<?extends GsfTokenId> token = ts.token();
386         TokenId id = token.id();
387         TokenId[] stringTokens = null;
388         TokenId beginTokenId = null;
389
390         if (ch == '\"') {
391             stringTokens = STRING_TOKENS;
392             beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
393         } else if (ch == '\'') {
394             stringTokens = STRING_TOKENS;
395             beginTokenId = RubyTokenId.STRING_BEGIN;
396         } else if (ch == '/') {
397             // TODO: Check to see that it's really going to be a regexp when it's inserted...
398
stringTokens = REGEXP_TOKENS;
399             beginTokenId = RubyTokenId.REGEXP_BEGIN;
400         } else if (id == RubyTokenId.ERROR) {
401             String JavaDoc text = token.text().toString();
402
403             if (text.equals("%")) {
404                 // Depending on the character we're going to continue
405
if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed
406
stringTokens = STRING_TOKENS;
407                     beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
408                 }
409             } else if ((text.length() == 2) && (text.charAt(0) == '%') &&
410                     Character.isLetter(text.charAt(1))) {
411                 char c = text.charAt(1);
412
413                 switch (c) {
414                 case 'q':
415                     stringTokens = STRING_TOKENS;
416                     beginTokenId = RubyTokenId.STRING_BEGIN;
417
418                     break;
419
420                 case 'Q':
421                     stringTokens = STRING_TOKENS;
422                     beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
423
424                     break;
425
426                 case 'r':
427                     stringTokens = REGEXP_TOKENS;
428                     beginTokenId = RubyTokenId.REGEXP_BEGIN;
429
430                     break;
431
432                 default:
433                     // ?
434
stringTokens = STRING_TOKENS;
435                     beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
436                 }
437             } else {
438                 ts.movePrevious();
439
440                 TokenId prevId = ts.token().id();
441
442                 if ((prevId == RubyTokenId.STRING_BEGIN) ||
443                         (prevId == RubyTokenId.QUOTED_STRING_BEGIN)) {
444                     stringTokens = STRING_TOKENS;
445                     beginTokenId = prevId;
446                 } else if (prevId == RubyTokenId.REGEXP_BEGIN) {
447                     stringTokens = REGEXP_TOKENS;
448                     beginTokenId = RubyTokenId.REGEXP_BEGIN;
449                 }
450             }
451         } else if (((((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN)) &&
452                 (caretOffset == (ts.offset() + 1))))) {
453             if (!Character.isLetter(ch)) { // %q, %x, etc. Only %[], %!!, %<space> etc. is allowed
454
stringTokens = STRING_TOKENS;
455                 beginTokenId = id;
456             }
457         } else if (((id == RubyTokenId.STRING_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
458                 (id == RubyTokenId.STRING_END)) {
459             stringTokens = STRING_TOKENS;
460             beginTokenId = RubyTokenId.STRING_BEGIN;
461         } else if (((id == RubyTokenId.QUOTED_STRING_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
462                 (id == RubyTokenId.QUOTED_STRING_END)) {
463             stringTokens = STRING_TOKENS;
464             beginTokenId = RubyTokenId.QUOTED_STRING_BEGIN;
465         } else if (((id == RubyTokenId.REGEXP_BEGIN) && (caretOffset == (ts.offset() + 2))) ||
466                 (id == RubyTokenId.REGEXP_END)) {
467             stringTokens = REGEXP_TOKENS;
468             beginTokenId = RubyTokenId.REGEXP_BEGIN;
469         }
470
471         if (stringTokens != null) {
472             boolean inserted =
473                 completeQuote(doc, caretOffset, caret, ch, stringTokens, beginTokenId);
474
475             if (inserted) {
476                 caret.setDot(caretOffset + 1);
477
478                 return true;
479             } else {
480                 return false;
481             }
482         }
483
484         return false;
485     }
486
487     // For debugging purposes
488
// Probably obsolete - see the tokenspy utility in gsf debugging tools for better help
489
//private void dumpTokens(BaseDocument doc, int dot) {
490
// TokenSequence< ?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
491
//
492
// System.out.println("Dumping tokens for dot=" + dot);
493
// int prevOffset = -1;
494
// if (ts != null) {
495
// ts.moveFirst();
496
// int index = 0;
497
// do {
498
// Token<? extends GsfTokenId> token = ts.token();
499
// int offset = ts.offset();
500
// String id = token.id().toString();
501
// String text = token.text().toString().replaceAll("\n", "\\\\n");
502
// if (prevOffset < dot && dot <= offset) {
503
// System.out.print(" ===> ");
504
// }
505
// System.out.println("Token " + index + ": offset=" + offset + ": id=" + id + ": text=" + text);
506
// index++;
507
// prevOffset = offset;
508
// } while (ts.moveNext());
509
// }
510
//}
511

512     /**
513      * A hook method called after a character was inserted into the
514      * document. The function checks for special characters for
515      * completion ()[]'"{} and other conditions and optionally performs
516      * changes to the doc and or caret (complets braces, moves caret,
517      * etc.)
518      * @param doc the document where the change occurred
519      * @param dotPos position of the character insertion
520      * @param caret caret
521      * @param ch the character that was inserted
522      * @throws BadLocationException if dotPos is not correct
523      */

524     public boolean afterCharInserted(Document JavaDoc document, int dotPos, Caret JavaDoc caret, char ch)
525         throws BadLocationException JavaDoc {
526         BaseDocument doc = (BaseDocument)document;
527
528         // See if our automatic adjustment of indentation when typing (for example) "end" was
529
// premature - if you were typing a longer word beginning with one of my adjustment
530
// prefixes, such as "endian", then put the indentation back.
531
if (previousAdjustmentOffset != -1) {
532             if (dotPos == previousAdjustmentOffset) {
533                 // Revert indentation iff the character at the insert position does
534
// not start a new token (e.g. the previous token that we reindented
535
// was not complete)
536
TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
537
538                 if (ts != null) {
539                     ts.move(dotPos);
540                 }
541
542                 if (ts.moveNext() && (ts.offset() < dotPos)) {
543                     LexUtilities.setLineIndentation(doc, dotPos, previousAdjustmentIndent);
544                 }
545             }
546
547             previousAdjustmentOffset = -1;
548         }
549
550         //dumpTokens(doc, dotPos);
551
switch (ch) {
552         case '}':
553         case '{':
554         case ')':
555         case ']':
556         case '(':
557         case '[': {
558             Token<?extends GsfTokenId> token = LexUtilities.getToken(doc, dotPos);
559             TokenId id = token.id();
560
561             if (id == RubyTokenId.IDENTIFIER) {
562                 int length = token.length();
563
564                 if ((length == 2) && "[]".equals(token.text().toString())) { // Special case
565
skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACKET);
566
567                     return true;
568                 }
569             }
570
571             if (((id == RubyTokenId.IDENTIFIER) && (token.length() == 1)) ||
572                     (id == RubyTokenId.LBRACKET) || (id == RubyTokenId.RBRACKET) ||
573                     (id == RubyTokenId.LBRACE) || (id == RubyTokenId.RBRACE) ||
574                     (id == RubyTokenId.LPAREN) || (id == RubyTokenId.RPAREN)) {
575                 if (ch == ']') {
576                     skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACKET);
577                 } else if (ch == ')') {
578                     skipClosingBracket(doc, caret, ch, RubyTokenId.RPAREN);
579                 } else if (ch == '}') {
580                     skipClosingBracket(doc, caret, ch, RubyTokenId.RBRACE);
581                 } else if ((ch == '[') || (ch == '(') || (ch == '{')) {
582                     completeOpeningBracket(doc, dotPos, caret, ch);
583                 }
584             }
585
586             // Reindent blocks (won't do anything if } is not at the beginning of a line
587
if (ch == '}') {
588                 reindent(doc, dotPos, RubyTokenId.RBRACE, caret);
589             }
590         }
591
592         break;
593
594         case 'd':
595             // See if it's the end of an "end" - if so, reindent
596
reindent(doc, dotPos, RubyTokenId.END, caret);
597
598             break;
599
600         case 'e':
601             // See if it's the end of an "else" or an "ensure" - if so, reindent
602
reindent(doc, dotPos, RubyTokenId.ELSE, caret);
603             reindent(doc, dotPos, RubyTokenId.ENSURE, caret);
604             reindent(doc, dotPos, RubyTokenId.RESCUE, caret);
605
606             break;
607
608         case 'f':
609             // See if it's the end of an "else" - if so, reindent
610
reindent(doc, dotPos, RubyTokenId.ELSIF, caret);
611
612             break;
613
614         case 'n':
615             // See if it's the end of an "when" - if so, reindent
616
reindent(doc, dotPos, RubyTokenId.WHEN, caret);
617         }
618
619         return true;
620     }
621
622     private void reindent(BaseDocument doc, int offset, TokenId id, Caret JavaDoc caret)
623         throws BadLocationException JavaDoc {
624         TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
625
626         if (ts != null) {
627             ts.move(offset);
628
629             if (!ts.moveNext() && !ts.movePrevious()) {
630                 return;
631             }
632
633             Token<?extends GsfTokenId> token = ts.token();
634
635             if ((token.id() == id)) {
636                 // Ensure that this token is at the beginning of the line
637
if (ts.offset() > Utilities.getRowFirstNonWhite(doc, offset)) {
638                     return;
639                 }
640
641                 OffsetRange begin;
642
643                 if (id == RubyTokenId.RBRACE) {
644                     begin = LexUtilities.findBwd(ts, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
645                 } else {
646                     begin = LexUtilities.findBegin(ts);
647                 }
648
649                 if (begin != OffsetRange.NONE) {
650                     int beginOffset = begin.getStart();
651                     int indent = LexUtilities.getLineIndent(doc, beginOffset);
652                     previousAdjustmentIndent = LexUtilities.getLineIndent(doc, offset);
653                     LexUtilities.setLineIndentation(doc, offset, indent);
654                     previousAdjustmentOffset = caret.getDot();
655                 }
656             }
657         }
658     }
659
660     public OffsetRange findMatching(Document JavaDoc document, int offset /*, boolean simpleSearch*/) {
661         BaseDocument doc = (BaseDocument)document;
662
663         TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
664
665         if (ts != null) {
666             ts.move(offset);
667
668             if (!ts.moveNext()) {
669                 return OffsetRange.NONE;
670             }
671
672             Token<?extends GsfTokenId> token = ts.token();
673
674             if (token == null) {
675                 return OffsetRange.NONE;
676             }
677
678             TokenId id = token.id();
679
680             if (id == RubyTokenId.WHITESPACE) {
681                 // ts.move(offset) gives the token to the left of the caret.
682
// If you have the caret right at the beginning of a token, try
683
// the token to the right too - this means that if you have
684
// " |def" it will show the matching "end" for the "def".
685
ts.move(offset + 1);
686
687                 if (ts.moveNext() && (ts.offset() <= (offset + 1))) {
688                     token = ts.token();
689                     id = token.id();
690                 }
691             }
692
693             if (id == RubyTokenId.QUOTED_STRING_BEGIN) {
694                 return LexUtilities.findFwd(ts, RubyTokenId.QUOTED_STRING_BEGIN,
695                     RubyTokenId.QUOTED_STRING_END);
696             } else if (id == RubyTokenId.QUOTED_STRING_END) {
697                 return LexUtilities.findBwd(ts, RubyTokenId.QUOTED_STRING_BEGIN,
698                     RubyTokenId.QUOTED_STRING_END);
699             } else if (id == RubyTokenId.STRING_BEGIN) {
700                 return LexUtilities.findFwd(ts, RubyTokenId.STRING_BEGIN, RubyTokenId.STRING_END);
701             } else if (id == RubyTokenId.STRING_END) {
702                 return LexUtilities.findBwd(ts, RubyTokenId.STRING_BEGIN, RubyTokenId.STRING_END);
703             } else if (id == RubyTokenId.REGEXP_BEGIN) {
704                 return LexUtilities.findFwd(ts, RubyTokenId.REGEXP_BEGIN, RubyTokenId.REGEXP_END);
705             } else if (id == RubyTokenId.REGEXP_END) {
706                 return LexUtilities.findBwd(ts, RubyTokenId.REGEXP_BEGIN, RubyTokenId.REGEXP_END);
707             } else if (id == RubyTokenId.LPAREN) {
708                 return LexUtilities.findFwd(ts, RubyTokenId.LPAREN, RubyTokenId.RPAREN);
709             } else if (id == RubyTokenId.RPAREN) {
710                 return LexUtilities.findBwd(ts, RubyTokenId.LPAREN, RubyTokenId.RPAREN);
711             } else if (id == RubyTokenId.LBRACE) {
712                 return LexUtilities.findFwd(ts, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
713             } else if (id == RubyTokenId.RBRACE) {
714                 return LexUtilities.findBwd(ts, RubyTokenId.LBRACE, RubyTokenId.RBRACE);
715             } else if (id == RubyTokenId.LBRACKET) {
716                 return LexUtilities.findFwd(ts, RubyTokenId.LBRACKET, RubyTokenId.RBRACKET);
717             } else if (id == RubyTokenId.RBRACKET) {
718                 return LexUtilities.findBwd(ts, RubyTokenId.LBRACKET, RubyTokenId.RBRACKET);
719             } else if (id.primaryCategory().equals("keyword")) {
720                 if (LexUtilities.isBeginToken(id)) {
721                     return LexUtilities.findEnd(ts);
722                 } else if ((id == RubyTokenId.END) || LexUtilities.isIndentToken(id)) { // Find matching block
723

724                     return LexUtilities.findBegin(ts);
725                 }
726             }
727         }
728
729         return OffsetRange.NONE;
730     }
731
732     /**
733     * Hook called after a character *ch* was backspace-deleted from
734     * *doc*. The function possibly removes bracket or quote pair if
735     * appropriate.
736     * @param doc the document
737     * @param dotPos position of the change
738     * @param caret caret
739     * @param ch the character that was deleted
740     */

741     public boolean charBackspaced(Document JavaDoc document, int dotPos, Caret JavaDoc caret, char ch)
742         throws BadLocationException JavaDoc {
743         BaseDocument doc = (BaseDocument)document;
744
745         if ((ch == '(') || (ch == '[')) {
746             char tokenAtDot = LexUtilities.getTokenChar(doc, dotPos);
747
748             if (((tokenAtDot == ']') &&
749                     (LexUtilities.getTokenBalance(doc, RubyTokenId.LBRACKET, RubyTokenId.RBRACKET) != 0)) ||
750                     ((tokenAtDot == ')') &&
751                     (LexUtilities.getTokenBalance(doc, RubyTokenId.LPAREN, RubyTokenId.RPAREN) != 0))) {
752                 doc.remove(dotPos, 1);
753             }
754         } else if (ch == '\"') {
755             char[] match = doc.getChars(dotPos, 1);
756
757             if ((match != null) && (match[0] == '\"')) {
758                 doc.remove(dotPos, 1);
759             }
760         } else if (ch == '\'') {
761             char[] match = doc.getChars(dotPos, 1);
762
763             if ((match != null) && (match[0] == '\'')) {
764                 doc.remove(dotPos, 1);
765             }
766         }
767
768         return true;
769     }
770
771     /**
772      * A hook to be called after closing bracket ) or ] was inserted into
773      * the document. The method checks if the bracket should stay there
774      * or be removed and some exisitng bracket just skipped.
775      *
776      * @param doc the document
777      * @param dotPos position of the inserted bracket
778      * @param caret caret
779      * @param bracket the bracket character ']' or ')'
780      */

781     private void skipClosingBracket(BaseDocument doc, Caret JavaDoc caret, char bracket, TokenId bracketId)
782         throws BadLocationException JavaDoc {
783         int caretOffset = caret.getDot();
784
785         if (isSkipClosingBracket(doc, caretOffset, bracketId)) {
786             doc.remove(caretOffset - 1, 1);
787             caret.setDot(caretOffset); // skip closing bracket
788
}
789     }
790
791     /**
792      * Check whether the typed bracket should stay in the document
793      * or be removed.
794      * <br>
795      * This method is called by <code>skipClosingBracket()</code>.
796      *
797      * @param doc document into which typing was done.
798      * @param caretOffset
799      */

800     private boolean isSkipClosingBracket(BaseDocument doc, int caretOffset, TokenId bracketId)
801         throws BadLocationException JavaDoc {
802         // First check whether the caret is not after the last char in the document
803
// because no bracket would follow then so it could not be skipped.
804
if (caretOffset == doc.getLength()) {
805             return false; // no skip in this case
806
}
807
808         boolean skipClosingBracket = false; // by default do not remove
809

810         TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
811
812         if (ts == null) {
813             return false;
814         }
815
816         // XXX BEGIN TOR MODIFICATIONS
817
//ts.move(caretOffset+1);
818
ts.move(caretOffset);
819
820         if (!ts.moveNext()) {
821             return false;
822         }
823
824         Token<?extends GsfTokenId> token = ts.token();
825
826         // Check whether character follows the bracket is the same bracket
827
if ((token != null) && (token.id() == bracketId)) {
828             int bracketIntId = bracketId.ordinal();
829             int leftBracketIntId =
830                 (bracketIntId == RubyTokenId.RPAREN.ordinal()) ? RubyTokenId.LPAREN.ordinal()
831                                                                : RubyTokenId.LBRACKET.ordinal();
832
833             // Skip all the brackets of the same type that follow the last one
834
ts.moveNext();
835
836             Token<?extends GsfTokenId> nextToken = ts.token();
837
838             while ((nextToken != null) && (nextToken.id() == bracketId)) {
839                 token = nextToken;
840
841                 if (!ts.moveNext()) {
842                     break;
843                 }
844
845                 nextToken = ts.token();
846             }
847
848             // token var points to the last bracket in a group of two or more right brackets
849
// Attempt to find the left matching bracket for it
850
// Search would stop on an extra opening left brace if found
851
int braceBalance = 0; // balance of '{' and '}'
852
int bracketBalance = -1; // balance of the brackets or parenthesis
853
Token<?extends GsfTokenId> lastRBracket = token;
854             ts.movePrevious();
855             token = ts.token();
856
857             boolean finished = false;
858
859             while (!finished && (token != null)) {
860                 int tokenIntId = token.id().ordinal();
861
862                 if ((token.id() == RubyTokenId.LPAREN) || (token.id() == RubyTokenId.LBRACKET)) {
863                     if (tokenIntId == bracketIntId) {
864                         bracketBalance++;
865
866                         if (bracketBalance == 0) {
867                             if (braceBalance != 0) {
868                                 // Here the bracket is matched but it is located
869
// inside an unclosed brace block
870
// e.g. ... ->( } a()|)
871
// which is in fact illegal but it's a question
872
// of what's best to do in this case.
873
// We chose to leave the typed bracket
874
// by setting bracketBalance to 1.
875
// It can be revised in the future.
876
bracketBalance = 1;
877                             }
878
879                             finished = true;
880                         }
881                     }
882                 } else if ((token.id() == RubyTokenId.RPAREN) ||
883                         (token.id() == RubyTokenId.RBRACKET)) {
884                     if (tokenIntId == bracketIntId) {
885                         bracketBalance--;
886                     }
887                 } else if (token.id() == RubyTokenId.LBRACE) {
888                     braceBalance++;
889
890                     if (braceBalance > 0) { // stop on extra left brace
891
finished = true;
892                     }
893                 } else if (token.id() == RubyTokenId.RBRACE) {
894                     braceBalance--;
895                 }
896
897                 if (!ts.movePrevious()) {
898                     break;
899                 }
900
901                 token = ts.token();
902             }
903
904             if (bracketBalance != 0) { // not found matching bracket
905
// Remove the typed bracket as it's unmatched
906
skipClosingBracket = true;
907             } else { // the bracket is matched
908
// Now check whether the bracket would be matched
909
// when the closing bracket would be removed
910
// i.e. starting from the original lastRBracket token
911
// and search for the same bracket to the right in the text
912
// The search would stop on an extra right brace if found
913
braceBalance = 0;
914                 bracketBalance = 1; // simulate one extra left bracket
915

916                 //token = lastRBracket.getNext();
917
TokenHierarchy<BaseDocument> th = TokenHierarchy.get(doc);
918
919                 int ofs = lastRBracket.offset(th);
920
921                 ts.move(ofs);
922                 ts.moveNext();
923                 token = ts.token();
924                 finished = false;
925
926                 while (!finished && (token != null)) {
927                     //int tokenIntId = token.getTokenID().getNumericID();
928
if ((token.id() == RubyTokenId.LPAREN) || (token.id() == RubyTokenId.LBRACKET)) {
929                         if (token.id().ordinal() == leftBracketIntId) {
930                             bracketBalance++;
931                         }
932                     } else if ((token.id() == RubyTokenId.RPAREN) ||
933                             (token.id() == RubyTokenId.RBRACKET)) {
934                         if (token.id().ordinal() == bracketIntId) {
935                             bracketBalance--;
936
937                             if (bracketBalance == 0) {
938                                 if (braceBalance != 0) {
939                                     // Here the bracket is matched but it is located
940
// inside an unclosed brace block
941
// which is in fact illegal but it's a question
942
// of what's best to do in this case.
943
// We chose to leave the typed bracket
944
// by setting bracketBalance to -1.
945
// It can be revised in the future.
946
bracketBalance = -1;
947                                 }
948
949                                 finished = true;
950                             }
951                         }
952                     } else if (token.id() == RubyTokenId.LBRACE) {
953                         braceBalance++;
954                     } else if (token.id() == RubyTokenId.RBRACE) {
955                         braceBalance--;
956
957                         if (braceBalance < 0) { // stop on extra right brace
958
finished = true;
959                         }
960                     }
961
962                     //token = token.getPrevious(); // done regardless of finished flag state
963
if (!ts.movePrevious()) {
964                         break;
965                     }
966
967                     token = ts.token();
968                 }
969
970                 // If bracketBalance == 0 the bracket would be matched
971
// by the bracket that follows the last right bracket.
972
skipClosingBracket = (bracketBalance == 0);
973             }
974         }
975
976         return skipClosingBracket;
977     }
978
979     /**
980      * Check for various conditions and possibly add a pairing bracket
981      * to the already inserted.
982      * @param doc the document
983      * @param dotPos position of the opening bracket (already in the doc)
984      * @param caret caret
985      * @param bracket the bracket that was inserted
986      */

987     private void completeOpeningBracket(BaseDocument doc, int dotPos, Caret JavaDoc caret, char bracket)
988         throws BadLocationException JavaDoc {
989         if (isCompletablePosition(doc, dotPos + 1)) {
990             String JavaDoc matchinBracket = "" + matching(bracket);
991             doc.insertString(dotPos + 1, matchinBracket, null);
992             caret.setDot(dotPos + 1);
993         }
994     }
995
996     // XXX TODO Use embedded string sequence here and see if it
997
// really is escaped. I know where those are!
998
// TODO Adjust for Ruby
999
private boolean isEscapeSequence(BaseDocument doc, int dotPos)
1000        throws BadLocationException JavaDoc {
1001        if (dotPos <= 0) {
1002            return false;
1003        }
1004
1005        char previousChar = doc.getChars(dotPos - 1, 1)[0];
1006
1007        return previousChar == '\\';
1008    }
1009
1010    /**
1011     * Check for conditions and possibly complete an already inserted
1012     * quote .
1013     * @param doc the document
1014     * @param dotPos position of the opening bracket (already in the doc)
1015     * @param caret caret
1016     * @param bracket the character that was inserted
1017     */

1018    private boolean completeQuote(BaseDocument doc, int dotPos, Caret JavaDoc caret, char bracket,
1019        TokenId[] stringTokens, TokenId beginToken) throws BadLocationException JavaDoc {
1020        if (isEscapeSequence(doc, dotPos)) { // \" or \' typed
1021

1022            return false;
1023        }
1024
1025        // Examine token at the caret offset
1026
if (doc.getLength() < dotPos) {
1027            return false;
1028        }
1029
1030        TokenSequence<?extends GsfTokenId> ts = LexUtilities.getTokenSequence(doc);
1031
1032        if (ts == null) {
1033            return false;
1034        }
1035
1036        ts.move(dotPos);
1037
1038        if (!ts.moveNext() && !ts.movePrevious()) {
1039            return false;
1040        }
1041
1042        Token<?extends GsfTokenId> token = ts.token();
1043        Token<?extends GsfTokenId> previousToken = null;
1044
1045        if (ts.movePrevious()) {
1046            previousToken = ts.token();
1047        }
1048
1049        int lastNonWhite = Utilities.getRowLastNonWhite(doc, dotPos);
1050
1051        // eol - true if the caret is at the end of line (ignoring whitespaces)
1052
boolean eol = lastNonWhite < dotPos;
1053
1054        if ((token.id() == RubyTokenId.BLOCK_COMMENT) || (token.id() == RubyTokenId.LINE_COMMENT)) {
1055            return false;
1056        } else if ((token.id() == RubyTokenId.WHITESPACE) && eol && ((dotPos - 1) > 0)) {
1057            // check if the caret is at the very end of the line comment
1058
token = LexUtilities.getToken(doc, dotPos - 1);
1059
1060            if (token.id() == RubyTokenId.LINE_COMMENT) {
1061                return false;
1062            }
1063        }
1064
1065        boolean completablePosition = isQuoteCompletablePosition(doc, dotPos);
1066
1067        boolean insideString = false;
1068        TokenId id = token.id();
1069
1070        for (TokenId currId : stringTokens) {
1071            if (id == currId) {
1072                insideString = true;
1073            }
1074        }
1075
1076        if ((id == RubyTokenId.ERROR) && (previousToken != null) &&
1077                (previousToken.id() == beginToken)) {
1078            insideString = true;
1079        }
1080
1081        if (!insideString) {
1082            // check if the caret is at the very end of the line and there
1083
// is an unterminated string literal
1084
if ((token.id() == RubyTokenId.WHITESPACE) && eol) {
1085                if ((dotPos - 1) > 0) {
1086                    token = LexUtilities.getToken(doc, dotPos - 1);
1087                    // XXX TODO use language embedding to handle this
1088
insideString = (token.id() == RubyTokenId.STRING_LITERAL) ||
1089                        (token.id() == RubyTokenId.CHAR_LITERAL);
1090                }
1091            }
1092        }
1093
1094        if (insideString) {
1095            if (eol) {
1096                return false; // do not complete
1097
} else {
1098                //#69524
1099
char chr = doc.getChars(dotPos, 1)[0];
1100
1101                if (chr == bracket) {
1102                    doc.insertString(dotPos, "" + bracket, null); //NOI18N
1103
doc.remove(dotPos, 1);
1104
1105                    return true;
1106                }
1107            }
1108        }
1109
1110        if ((completablePosition && !insideString) || eol) {
1111            doc.insertString(dotPos, "" + bracket + matching(bracket), null); //NOI18N
1112

1113            return true;
1114        }
1115
1116        return false;
1117    }
1118
1119    /**
1120     * Checks whether dotPos is a position at which bracket and quote
1121     * completion is performed. Brackets and quotes are not completed
1122     * everywhere but just at suitable places .
1123     * @param doc the document
1124     * @param dotPos position to be tested
1125     */

1126    private boolean isCompletablePosition(BaseDocument doc, int dotPos)
1127        throws BadLocationException JavaDoc {
1128        if (dotPos == doc.getLength()) { // there's no other character to test
1129

1130            return true;
1131        } else {
1132            // test that we are in front of ) , " or '
1133
char chr = doc.getChars(dotPos, 1)[0];
1134
1135            return ((chr == ')') || (chr == ',') || (chr == '\"') || (chr == '\'') || (chr == ' ') ||
1136            (chr == ']') || (chr == '}') || (chr == '\n') || (chr == '\t') || (chr == ';'));
1137        }
1138    }
1139
1140    private boolean isQuoteCompletablePosition(BaseDocument doc, int dotPos)
1141        throws BadLocationException JavaDoc {
1142        if (dotPos == doc.getLength()) { // there's no other character to test
1143

1144            return true;
1145        } else {
1146            // test that we are in front of ) , " or ' ... etc.
1147
int eol = Utilities.getRowEnd(doc, dotPos);
1148
1149            if ((dotPos == eol) || (eol == -1)) {
1150                return false;
1151            }
1152
1153            int firstNonWhiteFwd = Utilities.getFirstNonWhiteFwd(doc, dotPos, eol);
1154
1155            if (firstNonWhiteFwd == -1) {
1156                return false;
1157            }
1158
1159            char chr = doc.getChars(firstNonWhiteFwd, 1)[0];
1160
1161            return ((chr == ')') || (chr == ',') || (chr == '+') || (chr == '}') || (chr == ';') ||
1162            (chr == ']'));
1163        }
1164    }
1165
1166    /**
1167     * Returns for an opening bracket or quote the appropriate closing
1168     * character.
1169     */

1170    private char matching(char bracket) {
1171        switch (bracket) {
1172        case '(':
1173            return ')';
1174
1175        case '/':
1176            return '/';
1177
1178        case '[':
1179            return ']';
1180
1181        case '\"':
1182            return '\"'; // NOI18N
1183

1184        case '\'':
1185            return '\'';
1186
1187        case '{':
1188            return '}';
1189
1190        case '}':
1191            return '{';
1192
1193        default:
1194            return bracket;
1195        }
1196    }
1197
1198    public List JavaDoc<OffsetRange> findLogicalRanges(CompilationInfo info, int caretOffset) {
1199        Node root = AstUtilities.getRoot(info);
1200
1201        if (root == null) {
1202            return Collections.emptyList();
1203        }
1204
1205        AstPath path = new AstPath(root, caretOffset);
1206        List JavaDoc<OffsetRange> ranges = new ArrayList JavaDoc<OffsetRange>();
1207
1208        // Check if the caret is within a comment, and if so insert a new
1209
// leaf "node" which contains the comment line and then comment block
1210
try {
1211            BaseDocument doc = (BaseDocument)info.getDocument();
1212            Token<?extends GsfTokenId> token = LexUtilities.getToken(doc, caretOffset);
1213
1214            if ((token != null) && (token.id() == RubyTokenId.LINE_COMMENT)) {
1215                // First add a range for the current line
1216
int begin = Utilities.getRowStart(doc, caretOffset);
1217                int end = Utilities.getRowEnd(doc, caretOffset);
1218
1219                if (LexUtilities.isCommentOnlyLine(doc, caretOffset)) {
1220                    ranges.add(new OffsetRange(begin, end));
1221
1222                    int lineBegin = begin;
1223                    int lineEnd = end;
1224
1225                    while (true) {
1226                        int newBegin = Utilities.getRowStart(doc, begin - 1);
1227
1228                        if ((newBegin <= 0) || !LexUtilities.isCommentOnlyLine(doc, newBegin)) {
1229                            break;
1230                        }
1231
1232                        begin = newBegin;
1233                    }
1234
1235                    int length = doc.getLength();
1236
1237                    while (true) {
1238                        int newEnd = Utilities.getRowEnd(doc, end + 1);
1239
1240                        if ((newEnd >= length) || !LexUtilities.isCommentOnlyLine(doc, newEnd)) {
1241                            break;
1242                        }
1243
1244                        end = newEnd;
1245                    }
1246
1247                    if ((lineBegin > begin) || (lineEnd < end)) {
1248                        ranges.add(new OffsetRange(begin, end));
1249                    }
1250                } else {
1251                    // It's just a line comment next to some code; select the comment
1252
TokenHierarchy<Document JavaDoc> th = TokenHierarchy.get((Document JavaDoc)doc);
1253                    int offset = token.offset(th);
1254                    ranges.add(new OffsetRange(offset, offset + token.length()));
1255                }
1256            }
1257        } catch (BadLocationException JavaDoc ble) {
1258            Exceptions.printStackTrace(ble);
1259        } catch (IOException JavaDoc ioe) {
1260            Exceptions.printStackTrace(ioe);
1261        }
1262
1263        Iterator JavaDoc<Node> it = path.leafToRoot();
1264
1265        while (it.hasNext()) {
1266            Node node = it.next();
1267
1268            // Filter out some uninteresting nodes
1269
if (node instanceof NewlineNode) {
1270                continue;
1271            }
1272
1273            OffsetRange range = AstUtilities.getRange(node);
1274
1275            // The contains check should be unnecessary, but I end up getting
1276
// some weird positions for some JRuby AST nodes
1277
if (range.containsInclusive(caretOffset)) {
1278                ranges.add(range);
1279            }
1280        }
1281
1282        return ranges;
1283    }
1284}
1285
Popular Tags