KickJava   Java API By Example, From Geeks To Geeks.

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


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.BufferedInputStream JavaDoc;
22 import java.io.IOException JavaDoc;
23 import java.io.IOException JavaDoc;
24 import java.io.InputStream JavaDoc;
25 import java.util.ArrayList JavaDoc;
26 import java.util.Collection JavaDoc;
27 import java.util.Collections JavaDoc;
28 import java.util.HashMap JavaDoc;
29 import java.util.HashSet JavaDoc;
30 import java.util.Iterator JavaDoc;
31 import java.util.List JavaDoc;
32 import java.util.Map JavaDoc;
33 import java.util.Set JavaDoc;
34 import javax.swing.ImageIcon JavaDoc;
35 import javax.swing.text.BadLocationException JavaDoc;
36 import javax.swing.text.Document JavaDoc;
37 import org.jruby.ast.ArgsNode;
38 import org.jruby.ast.ArgumentNode;
39 import org.jruby.ast.ClassNode;
40 import org.jruby.ast.ClassVarDeclNode;
41 import org.jruby.ast.ClassVarNode;
42 import org.jruby.ast.ConstDeclNode;
43 import org.jruby.ast.DAsgnNode;
44 import org.jruby.ast.DVarNode;
45 import org.jruby.ast.DefnNode;
46 import org.jruby.ast.DefsNode;
47 import org.jruby.ast.GlobalAsgnNode;
48 import org.jruby.ast.IScopingNode;
49 import org.jruby.ast.InstAsgnNode;
50 import org.jruby.ast.InstVarNode;
51 import org.jruby.ast.ListNode;
52 import org.jruby.ast.LocalAsgnNode;
53 import org.jruby.ast.LocalVarNode;
54 import org.jruby.ast.ModuleNode;
55 import org.jruby.ast.Node;
56 import org.jruby.ast.types.INameNode;
57 import org.jruby.lexer.yacc.ISourcePosition;
58 import org.netbeans.api.gsf.CompilationInfo;
59 import org.netbeans.api.gsf.Completable;
60 import org.netbeans.api.gsf.CompletionProposal;
61 import org.netbeans.api.gsf.Element;
62 import org.netbeans.api.gsf.ElementKind;
63 import org.netbeans.api.gsf.GsfTokenId;
64 import org.netbeans.api.gsf.HtmlFormatter;
65 import org.netbeans.modules.ruby.elements.AstModuleElement;
66 import static org.netbeans.api.gsf.Index.*;
67 import org.netbeans.api.gsf.Modifier;
68 import org.netbeans.api.gsf.ParserResult;
69 import org.netbeans.api.lexer.Token;
70 import org.netbeans.api.lexer.TokenHierarchy;
71 import org.netbeans.api.lexer.TokenId;
72 import org.netbeans.api.lexer.TokenSequence;
73 import org.netbeans.editor.BaseDocument;
74 import org.netbeans.editor.Utilities;
75 import org.netbeans.editor.Utilities;
76 import org.netbeans.modules.ruby.elements.AstClassElement;
77 import org.netbeans.modules.ruby.elements.AstElement;
78 import org.netbeans.modules.ruby.elements.AstFieldElement;
79 import org.netbeans.modules.ruby.elements.AstVariableElement;
80 import org.netbeans.modules.ruby.elements.ClassElement;
81 import org.netbeans.modules.ruby.elements.IndexedClass;
82 import org.netbeans.modules.ruby.elements.IndexedElement;
83 import org.netbeans.modules.ruby.elements.IndexedMethod;
84 import org.netbeans.modules.ruby.elements.KeywordElement;
85 import org.netbeans.modules.ruby.elements.MethodElement;
86 import org.netbeans.modules.ruby.lexer.LexUtilities;
87 import org.netbeans.modules.ruby.lexer.LexUtilities.Call;
88 import org.netbeans.modules.ruby.lexer.RubyStringTokenId;
89 import org.netbeans.modules.ruby.lexer.RubyTokenId;
90 import org.openide.ErrorManager;
91 import org.openide.filesystems.FileStateInvalidException;
92 import org.openide.util.Exceptions;
93
94
95 /**
96  * Code completion handler for Ruby.
97  * Bug: I add lists of fields etc. But if these -overlap- the current line,
98  * I throw them away. The problem is that there may be other references
99  * to the field that I should -not- throw away, elsewhere!
100  * @todo Ensure that I prefer assignment over reference such that javadoc is
101  * more likely to be there!
102  *
103  * @todo Handle this case: {@code class HTTPBadResponse < StandardError; end}
104  * @todo Use lexical tokens to avoid attempting code completion within comments,
105  * literal strings and regexps
106  * @todo Percent-completion doesn't work if you at this to the end of the
107  * document: x = % and try to complete.
108  * @todo Handle more completion scenarios: Classes (no keywords) after "class Foo <",
109  * classes after "::", parameter completion (!!!), .new() completion (initialize), etc.
110  * @todo Make sure completion works during a "::"
111  *
112  * @author Tor Norbye
113  */

114 public class CodeCompleter implements Completable {
115     private static final String JavaDoc EMPTY = ""; //NOI18N
116
private static final String JavaDoc[] RUBY_BUILTINS =
117         new String JavaDoc[] {
118             // Keywords
119
"alias", "and", "BEGIN", "begin", "break", "case", "class", "def", "defined?", "do",
120             "else", "elsif", "END", "end", "ensure", "false", "for", "if", "in", "module", "next",
121             "nil", "not", "or", "redo", "rescue", "retry", "return", "self", "super", "then", "true",
122             "undef", "unless", "until", "when", "while", "yield",
123             
124             // Predefined variables
125
"__FILE__", "__LINE__", "STDIN", "STDOUT", "STDERR", "ENV", "ARGF", "ARGV", "DATA",
126             "RUBY_VERSION", "RUBY_RELEASE_DATE", "RUBY_PLATFORM", "$DEBUG", "$FILENAME",
127             "$LOAD_PATH", "$stderr", "$stdin", "$stdout", "$VERBOSE",
128         };
129     private static final String JavaDoc[] RUBY_REGEXP_WORDS =
130         new String JavaDoc[] {
131             // Dbl-space lines to keep formatter from collapsing pairs into a block
132
"^", "Start of line",
133             
134             "$", "End of line",
135             
136             "\\A", "Beginning of string",
137             
138             "\\z", "End of string",
139             
140             "\\Z", "End of string (except \\n)",
141             
142             "\\w", "Letter or digit; same as [0-9A-Za-z]",
143             
144             "\\W", "Neither letter or digit",
145             
146             "\\s", "Space character; same as [ \\t\\n\\r\\f]",
147             
148             "\\S", "Non-space character",
149             
150             "\\d", "Digit character; same as [0-9]",
151             
152             "\\D", "Non-digit character",
153             
154             "\\b", "Backspace (0x08) (only if in a range specification)",
155             
156             "\\b", "Word boundary (if not in a range specification)",
157             
158             "\\B", "Non-word boundary",
159             
160             "*", "Zero or more repetitions of the preceding",
161             
162             "+", "One or more repetitions of the preceding",
163             
164             "{m,n}", "At least m and at most n repetitions of the preceding",
165             
166             "?", "At most one repetition of the preceding; same as {0,1}",
167             
168             "|", "Either preceding or next expression may match",
169             
170             "()", "Grouping",
171             
172             "[:alnum:]", "Alphanumeric character class",
173             
174             "[:alpha:]", "Uppercase or lowercase letter",
175             
176             "[:blank:]", "Blank and tab",
177             
178             "[:cntrl:]", "Control characters (at least 0x00-0x1f,0x7f)",
179             
180             "[:digit:]", "Digit",
181             
182             "[:graph:]", "Printable character excluding space",
183             
184             "[:lower:]", "Lowecase letter",
185             
186             "[:print:]", "Any printable letter (including space)",
187             
188             "[:punct:]", "Printable character excluding space and alphanumeric",
189             
190             "[:space:]", "Whitespace (same as \\s)",
191             
192             "[:upper:]", "Uppercase letter",
193             
194             "[:xdigit:]", "Hex digit (0-9, a-f, A-F)",
195         };
196     private static final String JavaDoc[] RUBY_PERCENT_WORDS =
197         new String JavaDoc[] {
198             // Dbl-space lines to keep formatter from collapsing pairs into a block
199
"%q", "String (single-quoting rules)",
200             
201             "%Q", "String (double-quoting rules)",
202             
203             "%r", "Regular Expression",
204             
205             "%x", "Commands",
206             
207             "%W", "String Array (double quoting rules)",
208             
209             "%w", "String Array (single quoting rules)",
210             
211             "%s", "Symbol",
212         };
213     private static final String JavaDoc[] RUBY_STRING_PAIRS =
214         new String JavaDoc[] {
215             // Dbl-space lines to keep formatter from collapsing pairs into a block
216
"(", "(delimiters)",
217             
218             "{", "{delimiters}",
219             
220             "[", "[delimiters]",
221             
222             "x", "<i>x</i>delimiters<i>x</i>",
223         };
224     private static final String JavaDoc[] RUBY_DOLLAR_VARIABLES =
225         new String JavaDoc[] {
226             // From http://www.ruby-doc.org/docs/UsersGuide/rg/globalvars.html
227
"$!", "Latest error message",
228             
229             "$@", "Location of error",
230             
231             "$_", "String last read by gets",
232             
233             "$.", "Line number last read by interpreter",
234             
235             "$&", "String last matched by regexp",
236             
237             "$~", "The last regexp match, as an array of subexpressions",
238             
239             "$n", "The nth subexpression in the last match (same as $~[n])",
240             
241             "$=", "Case-insensitivity flag",
242             
243             "$/", "Input record separator",
244             
245             "$\\", "Output record separator",
246             
247             "$0", "The name of the ruby script file",
248             
249             "$*", "The command line arguments",
250             
251             "$$", "Interpreter's process ID",
252             
253             "$?", "Exit status of last executed child process",
254         };
255     private static final String JavaDoc[] RUBY_QUOTED_STRING_ESCAPES =
256         new String JavaDoc[] {
257             "\\a", "Bell/alert (0x07)",
258             
259             "\\b", "Backspace (0x08)",
260             
261             "\\x", "\\x<i>nn</i>: Hex <i>nn</i>",
262             
263             "\\e", "Escape (0x1b)",
264             
265             "\\c", "Control-<i>x</i>",
266             
267             "\\C-", "Control-<i>x</i>",
268             
269             "\\f", "Formfeed (0x0c)",
270             
271             "\\n", "Newline (0x0a)",
272             
273             "\\M-", "\\M-<i>x</i>: Meta-<i>x</i>",
274             
275             "\\r", "Return (0x0d)",
276             
277             "\\M-\\C-", "Meta-control-<i>x</i>",
278             
279             "\\s", "Space (0x20)",
280             
281             "\\", "\\nnn Octal <i>nnn</i>",
282             
283             //"\\", "<i>x</i>",
284
"\\t", "Tab (0x09)",
285             
286             "#{", "#{expr}: Value of expr",
287             
288             "\\v", "Vertical tab (0x0b)",
289         };
290     private static ImageIcon JavaDoc keywordIcon;
291     private boolean caseSensitive;
292     private int anchor;
293     private HtmlFormatter formatter;
294
295     public CodeCompleter() {
296     }
297
298     private boolean startsWith(String JavaDoc theString, String JavaDoc prefix) {
299         if ((prefix == null) || (prefix.length() == 0)) {
300             return true;
301         }
302
303         return caseSensitive ? theString.startsWith(prefix)
304                              : theString.toLowerCase().startsWith(prefix.toLowerCase());
305     }
306
307     /**
308      * Compute an appropriate prefix to use for code completion.
309      * In Strings, we want to return the -whole- string if you're in a
310      * require-statement string, otherwise we want to return simply "" or the previous "\"
311      * for quoted strings, and ditto for regular expressions.
312      * For non-string contexts, just return null to let the default identifier-computation
313      * kick in.
314      */

315     @SuppressWarnings JavaDoc("unchecked")
316     public String JavaDoc getPrefix(CompilationInfo info, int offset) {
317         try {
318             BaseDocument doc = (BaseDocument)info.getDocument();
319
320             TokenHierarchy<Document JavaDoc> th = TokenHierarchy.get((Document JavaDoc)doc);
321
322             int requireStart = LexUtilities.getRequireStringOffset(offset, th);
323
324             if (requireStart != -1) {
325                 return doc.getText(requireStart, offset - requireStart);
326             }
327
328             TokenSequence<?extends GsfTokenId> ts = th.tokenSequence(RubyTokenId.language());
329
330             if (ts == null) {
331                 return null;
332             }
333
334             ts.move(offset);
335
336             if (!ts.moveNext() && !ts.movePrevious()) {
337                 return null;
338             }
339
340             if (ts.offset() == offset) {
341                 // We're looking at the offset to the RIGHT of the caret
342
// and here I care about what's on the left
343
ts.movePrevious();
344             }
345
346             Token<?extends GsfTokenId> token = ts.token();
347
348             if (token != null) {
349                 TokenId id = token.id();
350
351                 // We're within a String that has embedded Ruby. Drop into the
352
// embedded language and see if we're within a literal string there.
353
if (id == RubyTokenId.EMBEDDED_RUBY) {
354                     ts = (TokenSequence)ts.embedded();
355                     assert ts != null;
356                     ts.move(offset);
357
358                     if (!ts.moveNext() && !ts.movePrevious()) {
359                         return null;
360                     }
361
362                     token = ts.token();
363                     id = token.id();
364                 }
365
366                 String JavaDoc tokenText = token.text().toString();
367
368                 if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
369                         ((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
370                     int currOffset = ts.offset();
371
372                     // Percent completion
373
if ((currOffset == (offset - 1)) && (tokenText.length() > 0) &&
374                             (tokenText.charAt(0) == '%')) {
375                         return "%";
376                     }
377                 }
378             }
379
380             int doubleQuotedOffset = LexUtilities.getDoubleQuotedStringOffset(offset, th);
381
382             if (doubleQuotedOffset != -1) {
383                 // Tokenize the string and offer the current token portion as the text
384
if (doubleQuotedOffset == offset) {
385                     return "";
386                 } else if (doubleQuotedOffset < offset) {
387                     String JavaDoc text = doc.getText(doubleQuotedOffset, offset - doubleQuotedOffset);
388                     TokenHierarchy hi =
389                         TokenHierarchy.create(text, RubyStringTokenId.languageDouble());
390
391                     TokenSequence seq = hi.tokenSequence();
392
393                     seq.move(offset - doubleQuotedOffset);
394
395                     if (!seq.moveNext() && !seq.movePrevious()) {
396                         return "";
397                     }
398
399                     TokenId id = seq.token().id();
400                     String JavaDoc s = seq.token().text().toString();
401
402                     if ((id == RubyStringTokenId.STRING_ESCAPE) ||
403                             (id == RubyStringTokenId.STRING_INVALID)) {
404                         return s;
405                     } else if (s.startsWith("\\")) {
406                         return s;
407                     } else {
408                         return "";
409                     }
410                 } else {
411                     // The String offset is greater than the caret position.
412
// This means that we're inside the string-begin section,
413
// for example here: %q|(
414
// In this case, report no prefix
415
return "";
416                 }
417             }
418
419             int singleQuotedOffset = LexUtilities.getSingleQuotedStringOffset(offset, th);
420
421             if (singleQuotedOffset != -1) {
422                 if (singleQuotedOffset == offset) {
423                     return "";
424                 } else if (singleQuotedOffset < offset) {
425                     String JavaDoc text = doc.getText(singleQuotedOffset, offset - singleQuotedOffset);
426                     TokenHierarchy hi =
427                         TokenHierarchy.create(text, RubyStringTokenId.languageSingle());
428
429                     TokenSequence seq = hi.tokenSequence();
430
431                     seq.move(offset - singleQuotedOffset);
432
433                     if (!seq.moveNext() && !seq.movePrevious()) {
434                         return "";
435                     }
436
437                     TokenId id = seq.token().id();
438                     String JavaDoc s = seq.token().text().toString();
439
440                     if ((id == RubyStringTokenId.STRING_ESCAPE) ||
441                             (id == RubyStringTokenId.STRING_INVALID)) {
442                         return s;
443                     } else if (s.startsWith("\\")) {
444                         return s;
445                     } else {
446                         return "";
447                     }
448                 } else {
449                     // The String offset is greater than the caret position.
450
// This means that we're inside the string-begin section,
451
// for example here: %q|(
452
// In this case, report no prefix
453
return "";
454                 }
455             }
456
457             // Regular expression
458
int regexpOffset = LexUtilities.getRegexpOffset(offset, th);
459
460             if ((regexpOffset != -1) && (regexpOffset <= offset)) {
461                 // This is not right... I need to actually parse the regexp
462
// (I should use my Regexp lexer tokens which will be embedded here)
463
// such that escaping sequences (/\\\\\/) will work right, or
464
// character classes (/[foo\]). In both cases the \ may not mean escape.
465
String JavaDoc tokenText = token.text().toString();
466                 int index = offset - ts.offset();
467
468                 if ((index > 0) && (index <= tokenText.length()) &&
469                         (tokenText.charAt(index - 1) == '\\')) {
470                     return "\\";
471                 } else {
472                     // No prefix for regexps unless it's \
473
return "";
474                 }
475
476                 //return doc.getText(regexpOffset, offset-regexpOffset);
477
}
478
479             // Else: normal identifier: just return null and let the machinery do the rest
480
} catch (IOException JavaDoc ioe) {
481             Exceptions.printStackTrace(ioe);
482         } catch (BadLocationException JavaDoc ble) {
483             Exceptions.printStackTrace(ble);
484         }
485
486         // Default behavior
487
return null;
488     }
489
490     private boolean completeKeywords(List JavaDoc<CompletionProposal> proposals, String JavaDoc prefix,
491         boolean isSymbol) {
492         // Keywords
493
if ((prefix != null) && prefix.equals("$")) {
494             // Show dollar variable matches (global vars from the user's
495
// code will also be shown
496
for (int i = 0, n = RUBY_DOLLAR_VARIABLES.length; i < n; i += 2) {
497                 String JavaDoc word = RUBY_DOLLAR_VARIABLES[i];
498                 String JavaDoc desc = RUBY_DOLLAR_VARIABLES[i + 1];
499
500                 KeywordItem item = new KeywordItem(word, desc, anchor);
501
502                 if (isSymbol) {
503                     item.setSymbol(true);
504                 }
505
506                 proposals.add(item);
507             }
508         }
509
510         for (String JavaDoc keyword : RUBY_BUILTINS) {
511             if (startsWith(keyword, prefix)) {
512                 KeywordItem item = new KeywordItem(keyword, null, anchor);
513
514                 if (isSymbol) {
515                     item.setSymbol(true);
516                 }
517
518                 proposals.add(item);
519             }
520         }
521
522         return false;
523     }
524
525     private boolean completeRegexps(List JavaDoc<CompletionProposal> proposals, String JavaDoc prefix) {
526         // Regular expression matching. {
527
for (int i = 0, n = RUBY_REGEXP_WORDS.length; i < n; i += 2) {
528             String JavaDoc word = RUBY_REGEXP_WORDS[i];
529             String JavaDoc desc = RUBY_REGEXP_WORDS[i + 1];
530
531             if (startsWith(word, prefix)) {
532                 KeywordItem item = new KeywordItem(word, desc, anchor);
533                 proposals.add(item);
534             }
535         }
536
537         return true;
538     }
539
540     private boolean completePercentWords(List JavaDoc<CompletionProposal> proposals, String JavaDoc prefix) {
541         for (int i = 0, n = RUBY_PERCENT_WORDS.length; i < n; i += 2) {
542             String JavaDoc word = RUBY_PERCENT_WORDS[i];
543             String JavaDoc desc = RUBY_PERCENT_WORDS[i + 1];
544
545             if (startsWith(word, prefix)) {
546                 KeywordItem item = new KeywordItem(word, desc, anchor);
547                 proposals.add(item);
548             }
549         }
550
551         return true;
552     }
553
554     private boolean completeStringBegins(List JavaDoc<CompletionProposal> proposals) {
555         for (int i = 0, n = RUBY_STRING_PAIRS.length; i < n; i += 2) {
556             String JavaDoc word = RUBY_STRING_PAIRS[i];
557             String JavaDoc desc = RUBY_STRING_PAIRS[i + 1];
558
559             KeywordItem item = new KeywordItem(word, desc, anchor);
560             proposals.add(item);
561         }
562
563         return true;
564     }
565
566     /** Determine if we're trying to complete the name for a "def" (in which case
567      * we'd show the inherited methods) */

568     private boolean completeDefMethod(List JavaDoc<CompletionProposal> proposals, RubyIndex index,
569         String JavaDoc prefix, CompilationInfo info, int offset, TokenHierarchy<Document JavaDoc> th,
570         String JavaDoc thisUrl, String JavaDoc fqn, AstPath path, Node node) {
571         TokenSequence<?extends GsfTokenId> ts = th.tokenSequence(RubyTokenId.language());
572
573         if ((index != null) && (ts != null)) {
574             ts.move(offset);
575
576             if (!ts.moveNext() && !ts.movePrevious()) {
577                 return false;
578             }
579
580             if (ts.offset() == offset) {
581                 // We're looking at the offset to the RIGHT of the caret
582
// position, which could be whitespace, e.g.
583
// "def fo| " <-- looking at the whitespace
584
ts.movePrevious();
585             }
586
587             Token<?extends GsfTokenId> token = ts.token();
588
589             if (token != null) {
590                 TokenId id = token.id();
591
592                 // See if we're in the identifier - "foo" in "def foo"
593
// I could also be a keyword in case the prefix happens to currently
594
// match a keyword, such as "next"
595
if ((id == RubyTokenId.IDENTIFIER) || id.primaryCategory().equals("keyword")) {
596                     if (!ts.movePrevious()) {
597                         return false;
598                     }
599
600                     token = ts.token();
601                     id = token.id();
602                 }
603
604                 // If we're not in the identifier we need to be in the whitespace after "def"
605
if (id != RubyTokenId.WHITESPACE) {
606                     return false;
607                 }
608
609                 // There may be more than one whitespace; skip them
610
while (ts.movePrevious()) {
611                     token = ts.token();
612
613                     if (token.id() != RubyTokenId.WHITESPACE) {
614                         break;
615                     }
616                 }
617
618                 if (token.id() == RubyTokenId.DEF) {
619                     Set JavaDoc<IndexedMethod> methods = index.getInheritedMethods(fqn, prefix);
620
621                     for (IndexedMethod method : methods) {
622                         if ((prefix != null) && (prefix.length() > 0) &&
623                                 !method.getName().startsWith(prefix)) {
624                             continue;
625                         }
626
627                         // For def completion, skip local methods, only include superclass and included
628
if ((fqn != null) && fqn.equals(method.getClz())) {
629                             continue;
630                         }
631
632                         // If a method is an "initialize" method I should do something special so that
633
// it shows up as a "constructor" (in a new() statement) but not as a directly
634
// callable initialize method (it should already be culled because it's private)
635
MethodItem item = new MethodItem(method, anchor);
636                         proposals.add(item);
637                     }
638
639                     return true;
640                 }
641             }
642         }
643
644         return false;
645     }
646
647     /** Determine if we're trying to complete the name of a method on another object rather
648      * than an inherited or local one. These should list ALL known methods, unless of course
649      * we know the type of the method we're operating on (such as strings or regexps),
650      * or types inferred through data flow analysis
651      *
652      * @todo Look for self or this or super; these should be limited to inherited.
653      */

654     private boolean completeObjectMethod(List JavaDoc<CompletionProposal> proposals, RubyIndex index,
655         String JavaDoc prefix, CompilationInfo info, int offset, Document JavaDoc doc, TokenHierarchy<Document JavaDoc> th,
656         String JavaDoc thisUrl, String JavaDoc fqn, AstPath path, Node node) {
657         TokenSequence<?extends GsfTokenId> ts = th.tokenSequence(RubyTokenId.language());
658
659         // Look in the token stream for constructs of the type
660
// foo.x^
661
// or
662
// foo.^
663
// and if found, add all methods
664
// (no keywords etc. are possible matches)
665
if ((index != null) && (ts != null)) {
666             Call call = LexUtilities.getCallType(doc, th, offset);
667             boolean skipPrivate = true;
668
669             if ((call == Call.LOCAL) || (call == Call.NONE)) {
670                 return false;
671             }
672
673             boolean skipInstanceMethods = call.isStatic();
674
675             Set JavaDoc<IndexedMethod> methods = Collections.emptySet();
676
677             String JavaDoc type = call.getType();
678             String JavaDoc lhs = call.getLhs();
679             
680             if (type == null && lhs != null && node != null && call.isSimpleIdentifier()) {
681                 Node method = AstUtilities.findLocalScope(node, path);
682                 if (method != null) {
683                     // TODO - if the lhs is "foo.bar." I need to split this
684
// up and do it a bit more cleverly
685
TypeAnalyzer analyzer = new TypeAnalyzer(method, offset);
686                     type = analyzer.getType(lhs);
687                 }
688             }
689
690             // I'm not doing any data flow analysis at this point, so
691
// I can't do anything with a LHS like "foo.". Only actual types.
692
if ((type != null) && (type.length() > 0)) {
693
694                 if ("self".equals(lhs)) {
695                     type = fqn;
696                     skipPrivate = false;
697                 } else if ("super".equals(lhs)) {
698                     skipPrivate = false;
699
700                     IndexedClass sc = index.getSuperclass(fqn);
701
702                     if (sc != null) {
703                         type = sc.getFqn();
704                     } else {
705                         ClassNode cls = AstUtilities.findClass(node, path);
706
707                         if (cls != null) {
708                             type = AstUtilities.getSuperclass(cls);
709                         }
710                     }
711                 }
712
713                 if ((type != null) && (type.length() > 0)) {
714                     // Possibly a class on the left hand side: try searching with the class as a qualifier.
715
// Try with the LHS + current FQN recursively. E.g. if we're in
716
// Test::Unit when there's a call to Foo.x, we'll try
717
// Test::Unit::Foo, and Test::Foo
718
while (methods.size() == 0) {
719                         methods = index.getInheritedMethods(fqn + "::" + type,
720                                 (prefix != null) ? prefix : EMPTY);
721
722                         int f = fqn.lastIndexOf("::");
723
724                         if (f == -1) {
725                             break;
726                         } else {
727                             fqn = fqn.substring(0, f);
728                         }
729                     }
730
731                     // Add methods in the class (without an FQN)
732
methods = index.getInheritedMethods(type, (prefix != null) ? prefix : EMPTY);
733                 }
734             }
735
736             // Try just the method call (e.g. across all classes). This is ignoring the
737
// left hand side because we can't resolve it.
738
if ((methods.size() == 0)) {
739                 methods = index.getMethods((prefix != null) ? prefix : EMPTY, null,
740                         caseSensitive ? NameKind.PREFIX : NameKind.CASE_INSENSITIVE_PREFIX);
741             }
742
743             for (IndexedMethod method : methods) {
744                 // Don't include private or protected methods on other objects
745
if (skipPrivate && (method.isPrivate() && !"new".equals(method.getName()))) {
746                     // TODO - "initialize" removal here should not be necessary since they should
747
// be marked as private, but index doesn't contain that yet
748
continue;
749                 }
750
751                 // We can only call static methods
752
if (skipInstanceMethods && !method.isStatic()) {
753                     continue;
754                 }
755
756                 // If a method is an "initialize" method I should do something special so that
757
// it shows up as a "constructor" (in a new() statement) but not as a directly
758
// callable initialize method (it should already be culled because it's private)
759
//proposals.add(new DefaultCompletionProposal(method.getComObject(), anchor));
760
MethodItem methodItem = new MethodItem(method, anchor);
761                 proposals.add(methodItem);
762             }
763
764             return true;
765         }
766
767         return false;
768     }
769
770     @SuppressWarnings JavaDoc("unchecked")
771     private boolean completeStrings(List JavaDoc<CompletionProposal> proposals, RubyIndex index,
772         String JavaDoc prefix, int caretOffset, TokenHierarchy<Document JavaDoc> th) {
773         TokenSequence<?extends GsfTokenId> ts = th.tokenSequence(RubyTokenId.language());
774
775         if ((index != null) && (ts != null)) {
776             ts.move(caretOffset);
777
778             if (!ts.moveNext() && !ts.movePrevious()) {
779                 return false;
780             }
781
782             if (ts.offset() == caretOffset) {
783                 // We're looking at the offset to the RIGHT of the caret
784
// and here I care about what's on the left
785
ts.movePrevious();
786             }
787
788             Token<?extends GsfTokenId> token = ts.token();
789
790             if (token != null) {
791                 TokenId id = token.id();
792
793                 // We're within a String that has embedded Ruby. Drop into the
794
// embedded language and see if we're within a literal string there.
795
if (id == RubyTokenId.EMBEDDED_RUBY) {
796                     ts = (TokenSequence)ts.embedded();
797                     assert ts != null;
798                     ts.move(caretOffset);
799
800                     if (!ts.moveNext() && !ts.movePrevious()) {
801                         return false;
802                     }
803
804                     token = ts.token();
805                     id = token.id();
806                 }
807
808                 boolean inString = false;
809                 boolean isQuoted = false;
810                 boolean inRegexp = false;
811                 String JavaDoc tokenText = token.text().toString();
812
813                 // Percent completion
814
if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
815                         ((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
816                     int offset = ts.offset();
817
818                     if ((offset == (caretOffset - 1)) && (tokenText.length() > 0) &&
819                             (tokenText.charAt(0) == '%')) {
820                         if (completePercentWords(proposals, prefix)) {
821                             return true;
822                         }
823                     }
824                 }
825
826                 // Incomplete String/Regexp marker: %x|{
827
if (((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
828                         (id == RubyTokenId.REGEXP_BEGIN)) &&
829                         ((token.length() == 3) && (caretOffset == (ts.offset() + 2)))) {
830                     if (Character.isLetter(tokenText.charAt(1))) {
831                         completeStringBegins(proposals);
832
833                         return true;
834                     }
835                 }
836
837                 // Skip back to the beginning of the String. I have to loop since I
838
// may have embedded Ruby segments.
839
while ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) ||
840                         (id == RubyTokenId.QUOTED_STRING_LITERAL) ||
841                         (id == RubyTokenId.REGEXP_LITERAL) || (id == RubyTokenId.EMBEDDED_RUBY)) {
842                     if (!ts.movePrevious()) {
843                         return false;
844                     }
845
846                     token = ts.token();
847                     id = token.id();
848                 }
849
850                 if (id == RubyTokenId.STRING_BEGIN) {
851                     inString = true;
852                 } else if (id == RubyTokenId.QUOTED_STRING_BEGIN) {
853                     inString = true;
854                     isQuoted = true;
855                 } else if (id == RubyTokenId.REGEXP_BEGIN) {
856                     inRegexp = true;
857                 }
858
859                 if (inRegexp) {
860                     if (completeRegexps(proposals, prefix)) {
861                         return true;
862                     }
863                 } else if (inString) {
864                     // Completion of literal strings within require calls
865
while (ts.movePrevious()) {
866                         token = ts.token();
867
868                         if ((token.id() == RubyTokenId.WHITESPACE) ||
869                                 (token.id() == RubyTokenId.LPAREN) ||
870                                 (token.id() == RubyTokenId.STRING_LITERAL) ||
871                                 (token.id() == RubyTokenId.QUOTED_STRING_LITERAL) ||
872                                 (token.id() == RubyTokenId.STRING_BEGIN) ||
873                                 (token.id() == RubyTokenId.QUOTED_STRING_BEGIN)) {
874                             continue;
875                         }
876
877                         if (token.id() == RubyTokenId.IDENTIFIER) {
878                             String JavaDoc text = token.text().toString();
879
880                             if (text.equals("require") || text.equals("load")) {
881                                 // Do require-completion
882
Set JavaDoc<String JavaDoc[]> requires =
883                                     index.getRequires((prefix != null) ? prefix : EMPTY,
884                                         caseSensitive ? NameKind.PREFIX
885                                                       : NameKind.CASE_INSENSITIVE_PREFIX);
886
887                                 for (String JavaDoc[] require : requires) {
888                                     assert require.length == 2;
889
890                                     // If a method is an "initialize" method I should do something special so that
891
// it shows up as a "constructor" (in a new() statement) but not as a directly
892
// callable initialize method (it should already be culled because it's private)
893
KeywordItem item =
894                                         new KeywordItem(require[0], require[1], anchor);
895                                     proposals.add(item);
896                                 }
897
898                                 return true;
899                             } else {
900                                 break;
901                             }
902                         } else {
903                             break;
904                         }
905                     }
906
907                     if (inString && isQuoted) {
908                         for (int i = 0, n = RUBY_QUOTED_STRING_ESCAPES.length; i < n; i += 2) {
909                             String JavaDoc word = RUBY_QUOTED_STRING_ESCAPES[i];
910                             String JavaDoc desc = RUBY_QUOTED_STRING_ESCAPES[i + 1];
911
912                             if ((prefix != null) && !word.startsWith(prefix)) {
913                                 continue;
914                             }
915
916                             KeywordItem item = new KeywordItem(word, desc, anchor);
917                             proposals.add(item);
918                         }
919
920                         return true;
921                     } else if (inString) {
922                         // No completions for single quoted strings
923
return true;
924                     }
925                 }
926             }
927         }
928
929         return false;
930     }
931
932     // TODO: Move to the top
933
public List JavaDoc<CompletionProposal> complete(CompilationInfo info, int caretOffset, String JavaDoc prefix,
934         boolean caseSensitive, HtmlFormatter formatter) {
935         this.caseSensitive = caseSensitive;
936         this.formatter = formatter;
937
938         // Let's stick the keywords in there...
939
List JavaDoc<CompletionProposal> proposals = new ArrayList JavaDoc<CompletionProposal>();
940
941         anchor = caretOffset - ((prefix == null) ? 0 : prefix.length());
942
943         RubyIndex index = RubyIndex.get(info.getIndex());
944
945         Document JavaDoc doc = null;
946
947         try {
948             doc = info.getDocument();
949         } catch (Exception JavaDoc e) {
950             Exceptions.printStackTrace(e);
951         }
952
953         // If you invoke code completion while indexing is in progress, the
954
// completion job (which stores the caret offset) will be delayed until
955
// indexing is complete - potentially minutes later. When the job
956
// is finally run we need to make sure the caret position is still valid.
957
int length = doc.getLength();
958
959         if (caretOffset > length) {
960             caretOffset = length;
961         }
962
963         // Discover whether we're in a require statement, and if so, use special completion
964
TokenHierarchy<Document JavaDoc> th = TokenHierarchy.get(doc);
965
966         // See if we're inside a string or regular expression and if so,
967
// do completions applicable to strings - require-completion,
968
// escape codes for quoted strings and regular expressions, etc.
969
if (completeStrings(proposals, index, prefix, caretOffset, th)) {
970             return proposals;
971         }
972
973         boolean showLower = true;
974         boolean showUpper = true;
975         boolean showSymbols = false;
976         char first = 0;
977
978         if ((prefix != null) && (prefix.length() > 0)) {
979             first = prefix.charAt(0);
980             showLower = Character.isLowerCase(first);
981             // showLower is not necessarily !showUpper - prefix can be ":foo" for example
982
showUpper = Character.isUpperCase(first);
983
984             if (first == ':') {
985                 showSymbols = true;
986
987                 if (prefix.length() > 1) {
988                     char second = prefix.charAt(1);
989                     prefix = prefix.substring(1);
990                     showLower = Character.isLowerCase(second);
991                     showUpper = Character.isUpperCase(second);
992                 }
993             }
994         }
995
996         // Fields
997
// This is a bit stupid at the moment, not looking at the current typing context etc.
998
Node root = AstUtilities.getRoot(info);
999
1000        if (root == null) {
1001            completeKeywords(proposals, prefix, showSymbols);
1002
1003            return proposals;
1004        }
1005
1006        // Compute the bounds of the line that the caret is on, and suppress nodes overlapping the line.
1007
// This will hide not only paritally typed identifiers, but surrounding contents like the current class and module
1008
int lineBegin = -1;
1009        int lineEnd = -1;
1010
1011        try {
1012            if (doc instanceof BaseDocument) {
1013                BaseDocument bdoc = (BaseDocument)doc;
1014                lineBegin = Utilities.getRowStart(bdoc, caretOffset);
1015                lineEnd = Utilities.getRowEnd(bdoc, caretOffset);
1016            }
1017        } catch (BadLocationException JavaDoc ble) {
1018            Exceptions.printStackTrace(ble);
1019        }
1020
1021        AstPath path = new AstPath(root, caretOffset);
1022
1023        Map JavaDoc<String JavaDoc, Node> variables = new HashMap JavaDoc<String JavaDoc, Node>();
1024        Map JavaDoc<String JavaDoc, Node> methods = new HashMap JavaDoc<String JavaDoc, Node>();
1025        Map JavaDoc<String JavaDoc, Node> fields = new HashMap JavaDoc<String JavaDoc, Node>();
1026        Map JavaDoc<String JavaDoc, Node> globals = new HashMap JavaDoc<String JavaDoc, Node>();
1027        Map JavaDoc<String JavaDoc, Node> constants = new HashMap JavaDoc<String JavaDoc, Node>();
1028        Map JavaDoc<String JavaDoc, Node> classes = new HashMap JavaDoc<String JavaDoc, Node>();
1029
1030        Node closest = path.leaf();
1031
1032        if (showLower && (closest != null)) {
1033            Node block = AstUtilities.findDynamicScope(closest, path);
1034            addDynamic(block, variables);
1035
1036            Node method = AstUtilities.findLocalScope(closest, path);
1037            addLocals(method, variables);
1038        }
1039
1040        // TODO: should only include fields etc. down to caret location??? Decide. (Depends on language semantics. Can I have forward referemces?
1041
Node top = path.root();
1042
1043        // XXX shouldn't top == root? I can wipe this out!
1044
assert root == top;
1045
1046        if (showUpper || showSymbols) {
1047            addClasses(top, classes);
1048            addConstants(top, constants);
1049        }
1050
1051        if (showLower || showSymbols) {
1052            addMethods(top, methods);
1053        }
1054
1055        // $ is neither upper nor lower
1056
if ((first == '@') || showSymbols) {
1057            addFields(top, fields);
1058        }
1059
1060        if ((first == '$') || showSymbols) {
1061            addGlobals(top, globals);
1062        }
1063
1064        // Code completion from the index.
1065
if (index != null) {
1066            String JavaDoc thisUrl = "";
1067
1068            try {
1069                thisUrl = info.getFileObject().getURL().toExternalForm();
1070            } catch (FileStateInvalidException fse) {
1071                Exceptions.printStackTrace(fse);
1072            }
1073
1074            if (showLower || showSymbols) {
1075                String JavaDoc fqn = AstUtilities.getFqnName(path);
1076
1077                if ((fqn != null) &&
1078                        completeDefMethod(proposals, index, prefix, info, caretOffset, th, thisUrl,
1079                            fqn, path, closest)) {
1080                    return proposals;
1081                }
1082
1083                if ((fqn != null) &&
1084                        completeObjectMethod(proposals, index, prefix, info, caretOffset, doc, th,
1085                            thisUrl, fqn, path, closest)) {
1086                    return proposals;
1087                }
1088
1089                // Complete inherited methods or local methods only (plus keywords) since there
1090
// is no receiver so it must be a local or inherited method call
1091
Set JavaDoc<IndexedMethod> inheritedMethods = index.getInheritedMethods(fqn, prefix);
1092
1093                for (IndexedMethod method : inheritedMethods) {
1094                    if ((prefix != null) && (prefix.length() > 0) &&
1095                            !method.getName().startsWith(prefix)) {
1096                        continue;
1097                    }
1098
1099                    if (thisUrl.equals(method.getFileUrl())) {
1100                        continue;
1101                    }
1102
1103                    // If a method is an "initialize" method I should do something special so that
1104
// it shows up as a "constructor" (in a new() statement) but not as a directly
1105
// callable initialize method (it should already be culled because it's private)
1106
MethodItem item = new MethodItem(method, anchor);
1107
1108                    if (showSymbols) {
1109                        item.setSymbol(true);
1110                    }
1111
1112                    proposals.add(item);
1113                }
1114            }
1115
1116            if (showUpper || showSymbols) {
1117                int classAnchor = anchor;
1118                int fqnIndex = (prefix != null) ? prefix.lastIndexOf("::") : (-1);
1119
1120                if (fqnIndex != -1) {
1121                    classAnchor += (fqnIndex + 2);
1122                }
1123
1124                for (IndexedClass cls : index.getClasses((prefix != null) ? prefix : EMPTY,
1125                        caseSensitive ? NameKind.PREFIX : NameKind.CASE_INSENSITIVE_PREFIX, false,
1126                        false, false)) {
1127                    // Exclude classes scanned from the current file - see comment under method section above
1128
if (thisUrl.equals(cls.getFileUrl())) {
1129                        continue;
1130                    }
1131
1132                    ClassItem item = new ClassItem(cls, classAnchor);
1133
1134                    if (showSymbols) {
1135                        item.setSymbol(true);
1136                    }
1137
1138                    proposals.add(item);
1139                }
1140            }
1141        }
1142
1143        // TODO
1144
// Remove fields and variables whose names are already taken, e.g. do a fields.removeAll(variables) etc.
1145
for (String JavaDoc variable : variables.keySet()) {
1146            if (startsWith(variable, prefix)) {
1147                Node node = variables.get(variable);
1148
1149                if (!overlapsLine(node, lineBegin, lineEnd)) {
1150                    AstVariableElement co = new AstVariableElement(node, variable);
1151                    PlainItem item = new PlainItem(co, anchor);
1152
1153                    if (showSymbols) {
1154                        item.setSymbol(true);
1155                    }
1156
1157                    proposals.add(item);
1158                }
1159            }
1160        }
1161
1162        for (String JavaDoc method : methods.keySet()) {
1163            if (isOperator(method)) {
1164                continue;
1165            }
1166
1167            if (startsWith(method, prefix)) {
1168                Node node = methods.get(method);
1169
1170                if (overlapsLine(node, lineBegin, lineEnd)) {
1171                    continue;
1172                }
1173
1174                Element co = AstElement.create(node);
1175                assert co != null;
1176
1177                MethodItem item = new MethodItem(co, anchor);
1178
1179                if (showSymbols) {
1180                    item.setSymbol(true);
1181                }
1182
1183                proposals.add(item);
1184            }
1185        }
1186
1187        for (String JavaDoc field : fields.keySet()) {
1188            if (startsWith(field, prefix)) {
1189                Node node = fields.get(field);
1190
1191                if (overlapsLine(node, lineBegin, lineEnd)) {
1192                    continue;
1193                }
1194
1195                Element co = new AstFieldElement(node);
1196                FieldItem item = new FieldItem(co, anchor);
1197
1198                if (showSymbols) {
1199                    item.setSymbol(true);
1200                }
1201
1202                proposals.add(item);
1203            }
1204        }
1205
1206        // TODO - model globals and constants using different icons / etc.
1207
for (String JavaDoc variable : globals.keySet()) {
1208            if (startsWith(variable, prefix) ||
1209                    (showSymbols && startsWith(variable.substring(1), prefix))) {
1210                Node node = globals.get(variable);
1211
1212                if (overlapsLine(node, lineBegin, lineEnd)) {
1213                    continue;
1214                }
1215
1216                AstElement co = new AstVariableElement(node, variable);
1217                PlainItem item = new PlainItem(co, anchor);
1218
1219                if (showSymbols) {
1220                    item.setSymbol(true);
1221                }
1222
1223                proposals.add(item);
1224            }
1225        }
1226
1227        // TODO - model globals and constants using different icons / etc.
1228
for (String JavaDoc variable : constants.keySet()) {
1229            if (startsWith(variable, prefix)) {
1230                // Skip constants that are known to be classes
1231
Node node = classes.get(variable);
1232
1233                if (node != null) {
1234                    continue;
1235                }
1236
1237                node = constants.get(variable);
1238
1239                if (overlapsLine(node, lineBegin, lineEnd)) {
1240                    continue;
1241                }
1242
1243                // ComObject co;
1244
// if (isClassName(variable)) {
1245
// co = JRubyNode.create(node, null);
1246
// if (co == null) {
1247
// continue;
1248
// }
1249
// } else {
1250
// co = new DefaultComVariable(variable, false, -1, -1);
1251
// ((DefaultComVariable)co).setNode(node);
1252
AstElement co = new AstVariableElement(node, variable);
1253                PlainItem item = new PlainItem(co, anchor);
1254
1255                if (showSymbols) {
1256                    item.setSymbol(true);
1257                }
1258
1259                proposals.add(item);
1260            }
1261        }
1262
1263        for (String JavaDoc clz : classes.keySet()) {
1264            if (startsWith(clz, prefix)) {
1265                Node node = classes.get(clz);
1266
1267                if (overlapsLine(node, lineBegin, lineEnd)) {
1268                    continue;
1269                }
1270                
1271                ClassItem item;
1272                if (node instanceof ClassNode) {
1273                    AstClassElement co = new AstClassElement(node);
1274                    item = new ClassItem(co, anchor);
1275                } else {
1276                    assert node instanceof ModuleNode;
1277                    AstModuleElement co = new AstModuleElement(node);
1278                    item = new ClassItem(co, anchor);
1279                }
1280
1281
1282                if (showSymbols) {
1283                    item.setSymbol(true);
1284                }
1285
1286                proposals.add(item);
1287            }
1288        }
1289
1290        if (completeKeywords(proposals, prefix, showSymbols)) {
1291            return proposals;
1292        }
1293
1294        return proposals;
1295    }
1296
1297    // private boolean isClassName(String s) {
1298
// // Initial capital letter, second letter is not
1299
// if (s.length() == 1) {
1300
// return Character.isUpperCase(s.charAt(0));
1301
// }
1302
//
1303
// if (Character.isLowerCase(s.charAt(0))) {
1304
// return false;
1305
// }
1306
//
1307
// return Character.isLowerCase(s.charAt(1));
1308
// }
1309
private boolean overlapsLine(Node node, int lineBegin, int lineEnd) {
1310        ISourcePosition pos = node.getPosition();
1311
1312        //return (((pos.getStartOffset() <= lineEnd) && (pos.getEndOffset() >= lineBegin)));
1313
// Don't look to see if the line is within the node. See if the node is started on this line (where
1314
// the declaration is, e.g. it might be an incomplete line.
1315
return ((pos.getStartOffset() >= lineBegin) && (pos.getStartOffset() <= lineEnd));
1316    }
1317
1318    /** Return true iff the name looks like an operator name */
1319    private boolean isOperator(String JavaDoc name) {
1320        // If a name contains not a single letter, it is probably an operator - especially
1321
// if it is a short name
1322
int n = name.length();
1323
1324        if (n > 2) {
1325            return false;
1326        }
1327
1328        for (int i = 0; i < n; i++) {
1329            if (Character.isLetter(name.charAt(i))) {
1330                return false;
1331            }
1332        }
1333
1334        return true;
1335    }
1336
1337    @SuppressWarnings JavaDoc("unchecked")
1338    private void addLocals(Node node, Map JavaDoc<String JavaDoc, Node> variables) {
1339        if (node instanceof LocalVarNode) {
1340            variables.put(((INameNode)node).getName(), node);
1341        } else if (node instanceof LocalAsgnNode) {
1342            variables.put(((INameNode)node).getName(), node);
1343        } else if (node instanceof ArgsNode) {
1344            ArgsNode an = (ArgsNode)node;
1345
1346            if (an.getArgsCount() > 0) {
1347                List JavaDoc<Node> args = (List JavaDoc<Node>)an.childNodes();
1348
1349                for (Node arg : args) {
1350                    if (arg instanceof ListNode) {
1351                        List JavaDoc<Node> args2 = (List JavaDoc<Node>)arg.childNodes();
1352
1353                        for (Node arg2 : args2) {
1354                            if (arg2 instanceof ArgumentNode) {
1355                                variables.put(((ArgumentNode)arg2).getName(), arg2);
1356                            } else if (arg2 instanceof LocalAsgnNode) {
1357                                variables.put(((INameNode)arg2).getName(), arg2);
1358                            }
1359                        }
1360                    }
1361                }
1362            }
1363
1364            // } else if (node instanceof AliasNode) {
1365
// AliasNode an = (AliasNode)node;
1366
// Tricky -- which NODE do we add here? Completion creator needs to be aware of new name etc. Do later.
1367
// Besides, do we show it as a field or a method or what?
1368

1369            // variab
1370
// if (an.getNewName().equals(name)) {
1371
// OffsetRange range = AstUtilities.getAliasNewRange(an);
1372
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1373
// } else if (an.getOldName().equals(name)) {
1374
// OffsetRange range = AstUtilities.getAliasOldRange(an);
1375
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1376
// }
1377
}
1378
1379        List JavaDoc<Node> list = node.childNodes();
1380
1381        for (Node child : list) {
1382            addLocals(child, variables);
1383        }
1384    }
1385
1386    private void addDynamic(Node node, Map JavaDoc<String JavaDoc, Node> variables) {
1387        if (node instanceof DVarNode) {
1388            variables.put(((DVarNode)node).getName(), node);
1389        } else if (node instanceof DAsgnNode) {
1390            variables.put(((INameNode)node).getName(), node);
1391
1392            //} else if (node instanceof ArgsNode) {
1393
// ArgsNode an = (ArgsNode)node;
1394
//
1395
// if (an.getArgsCount() > 0) {
1396
// List<Node> args = (List<Node>)an.childNodes();
1397
// List<String> parameters = null;
1398
//
1399
// for (Node arg : args) {
1400
// if (arg instanceof ListNode) {
1401
// List<Node> args2 = (List<Node>)arg.childNodes();
1402
// parameters = new ArrayList<String>(args2.size());
1403
//
1404
// for (Node arg2 : args2) {
1405
// if (arg2 instanceof ArgumentNode) {
1406
// OffsetRange range = AstUtilities.getRange(arg2);
1407
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1408
// } else if (arg2 instanceof LocalAsgnNode) {
1409
// OffsetRange range = AstUtilities.getRange(arg2);
1410
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1411
// }
1412
// }
1413
// }
1414
// }
1415
// }
1416
// } else if (!ignoreAlias && node instanceof AliasNode) {
1417
// AliasNode an = (AliasNode)node;
1418
//
1419
// if (an.getNewName().equals(name)) {
1420
// OffsetRange range = AstUtilities.getAliasNewRange(an);
1421
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1422
// } else if (an.getOldName().equals(name)) {
1423
// OffsetRange range = AstUtilities.getAliasOldRange(an);
1424
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1425
// }
1426
}
1427
1428        @SuppressWarnings JavaDoc("unchecked")
1429        List JavaDoc<Node> list = node.childNodes();
1430
1431        for (Node child : list) {
1432            addDynamic(child, variables);
1433        }
1434    }
1435
1436    private void addFields(Node node, Map JavaDoc<String JavaDoc, Node> fields) {
1437        if (node instanceof InstVarNode) {
1438            String JavaDoc name = ((INameNode)node).getName();
1439
1440            if (!fields.containsKey(name)) { // prefer declarations for doc purposes
1441
fields.put(name, node);
1442            }
1443        } else if (node instanceof InstAsgnNode) {
1444            fields.put(((INameNode)node).getName(), node);
1445        } else if (node instanceof ClassVarNode) {
1446            String JavaDoc name = ((ClassVarNode)node).getName(); // Should be INameNode!
1447

1448            if (!fields.containsKey(name)) { // prefer declarations over references for doc purposes
1449
fields.put(name, node);
1450            }
1451        } else if (node instanceof ClassVarDeclNode) {
1452            fields.put(((INameNode)node).getName(), node);
1453
1454            // } else if (!ignoreAlias && node instanceof AliasNode) {
1455
// AliasNode an = (AliasNode)node;
1456
//
1457
// if (an.getNewName().equals(name)) {
1458
// OffsetRange range = AstUtilities.getAliasNewRange(an);
1459
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1460
// } else if (an.getOldName().equals(name)) {
1461
// OffsetRange range = AstUtilities.getAliasOldRange(an);
1462
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1463
// }
1464
}
1465
1466        @SuppressWarnings JavaDoc("unchecked")
1467        List JavaDoc<Node> list = node.childNodes();
1468
1469        for (Node child : list) {
1470            addFields(child, fields);
1471        }
1472    }
1473
1474    private void addClasses(Node node, Map JavaDoc<String JavaDoc, Node> classes) {
1475        if (node instanceof ClassNode || node instanceof ModuleNode) {
1476            String JavaDoc name = AstUtilities.getClassOrModuleName((IScopingNode)node);
1477            classes.put(name, node);
1478        }
1479
1480        @SuppressWarnings JavaDoc("unchecked")
1481        List JavaDoc<Node> list = node.childNodes();
1482
1483        for (Node child : list) {
1484            addClasses(child, classes);
1485        }
1486    }
1487
1488    private void addGlobals(Node node, Map JavaDoc<String JavaDoc, Node> globals) {
1489        /* This is duplicating global vars I already have in the index!
1490        if (node instanceof GlobalVarNode) {
1491            //if (((INameNode)node).getName().equals(name)) { // GlobalVarNode does not implement INameNode
1492            String name = ((GlobalVarNode)node).getName();
1493
1494            if (!globals.containsKey(name)) { // Prefer declarations over references
1495                globals.put(name, node);
1496            }
1497        } else*/

1498        if (node instanceof GlobalAsgnNode) {
1499            globals.put(((INameNode)node).getName(), node);
1500
1501            // } else if (!ignoreAlias && node instanceof AliasNode) {
1502
// AliasNode an = (AliasNode)node;
1503
//
1504
// if (an.getNewName().equals(name)) {
1505
// OffsetRange range = AstUtilities.getAliasNewRange(an);
1506
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1507
// } else if (an.getOldName().equals(name)) {
1508
// OffsetRange range = AstUtilities.getAliasOldRange(an);
1509
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1510
// }
1511
}
1512
1513        @SuppressWarnings JavaDoc("unchecked")
1514        List JavaDoc<Node> list = node.childNodes();
1515
1516        for (Node child : list) {
1517            addGlobals(child, globals);
1518        }
1519    }
1520
1521    private void addMethods(Node node, Map JavaDoc<String JavaDoc, Node> methods) {
1522        // Recursively search for methods or method calls that match the name and arity
1523
if (node instanceof DefnNode) {
1524            // DefnNode doesn't implement INameNode
1525
String JavaDoc name = ((DefnNode)node).getName();
1526
1527            methods.put(name, node);
1528        } else if (node instanceof DefsNode) {
1529            // DefsNode doesn't implement INameNode
1530
String JavaDoc name = ((DefsNode)node).getName();
1531
1532            methods.put(name, node);
1533
1534            // Now that I'm actually indexing all other files there is no reason
1535
// to look at function calls to "guess" existence of new methods.
1536
// (This is only partially true... I might want to look at these existing
1537
// calls to guess types)
1538
// } else if (node instanceof FCallNode || node instanceof CallNode ||
1539
// node instanceof VCallNode) {
1540
// // DefsNode doesn't implement INameNode
1541
// String name = ((INameNode)node).getName();
1542
//
1543
// // Favor declarations over references, since definitions may have associated
1544
// // documentation
1545
// if (!methods.containsKey(name)) {
1546
// methods.put(name, node);
1547
// }
1548
//
1549
// // TODO - add parametrs
1550
// // } else if (!ignoreAlias && node instanceof AliasNode) {
1551
// // AliasNode an = (AliasNode)node;
1552
// //
1553
// // if (an.getNewName().equals(name)) {
1554
// // OffsetRange range = AstUtilities.getAliasNewRange(an);
1555
// // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1556
// // } else if (an.getOldName().equals(name)) {
1557
// // OffsetRange range = AstUtilities.getAliasOldRange(an);
1558
// // highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
1559
// // }
1560
}
1561
1562        @SuppressWarnings JavaDoc("unchecked")
1563        List JavaDoc<Node> list = node.childNodes();
1564
1565        for (Node child : list) {
1566            addMethods(child, methods);
1567        }
1568    }
1569
1570    private void addConstants(Node node, Map JavaDoc<String JavaDoc, Node> constants) {
1571        // TODO - favor assignments over references
1572
if (node instanceof ConstDeclNode) {
1573            constants.put(((INameNode)node).getName(), node);
1574        }
1575
1576        @SuppressWarnings JavaDoc("unchecked")
1577        List JavaDoc<Node> list = node.childNodes();
1578
1579        for (Node child : list) {
1580            addConstants(child, constants);
1581        }
1582    }
1583
1584    private String JavaDoc loadResource(String JavaDoc basename) {
1585        // TODO: I18N
1586
InputStream JavaDoc is =
1587            new BufferedInputStream JavaDoc(CodeCompleter.class.getResourceAsStream("resources/" +
1588                    basename));
1589        StringBuilder JavaDoc sb = new StringBuilder JavaDoc();
1590
1591        try {
1592            //while (is)
1593
while (true) {
1594                int c = is.read();
1595
1596                if (c == -1) {
1597                    break;
1598                }
1599
1600                sb.append((char)c);
1601            }
1602
1603            if (sb.length() > 0) {
1604                return sb.toString();
1605            }
1606        } catch (IOException JavaDoc ie) {
1607            Exceptions.printStackTrace(ie);
1608
1609            try {
1610                is.close();
1611            } catch (IOException JavaDoc ie2) {
1612                Exceptions.printStackTrace(ie2);
1613            }
1614        }
1615
1616        return null;
1617    }
1618
1619    private String JavaDoc getKeywordHelp(String JavaDoc keyword) {
1620        // Difficulty here with context; "else" is used for both the ifelse.html and case.html both define it.
1621
// End is even more used.
1622
if (keyword.equals("if") || keyword.equals("elsif") || keyword.equals("else") ||
1623                keyword.equals("then") || keyword.equals("unless")) { // NOI18N
1624

1625            return loadResource("ifelse.html"); // NOI18N
1626
} else if (keyword.equals("case") || keyword.equals("when") || keyword.equals("else")) { // NOI18N
1627

1628            return loadResource("case.html"); // NOI18N
1629
} else if (keyword.equals("rescue") || keyword.equals("ensure")) { // NOI18N
1630

1631            return loadResource("rescue.html"); // NOI18N
1632
} else if (keyword.equals("yield")) { // NOI18N
1633

1634            return loadResource("yield.html"); // NOI18N
1635
}
1636
1637        return null;
1638    }
1639
1640    /**
1641     * Find the best possible documentation match for the given IndexedClass or IndexedMethod.
1642     * This involves looking at index to see which instances of this class or method
1643     * definition have associated rdoc, as well as choosing between them based on the
1644     * require statements in the file.
1645     */

1646    private IndexedElement findDocumentationEntry(Node root, IndexedElement obj) {
1647        // 1. Find entries known to have documentation
1648
String JavaDoc fqn = obj.getSignature();
1649        Set JavaDoc<?extends IndexedElement> result = obj.getIndex().getDocumented(fqn);
1650
1651        if ((result == null) || (result.size() == 0)) {
1652            return null;
1653        } else if (result.size() == 1) {
1654            return result.iterator().next();
1655        }
1656
1657        // 2. There are multiple matches so try to disambiguate them by the imports in this file.
1658
// For example, for "File" we usually show the standard (builtin) documentation,
1659
// unless you have required "ftools", which redefines File with new docs.
1660
Set JavaDoc<IndexedElement> candidates = new HashSet JavaDoc<IndexedElement>();
1661        Set JavaDoc<String JavaDoc> requires = AstUtilities.getRequires(root);
1662
1663        for (IndexedElement o : result) {
1664            String JavaDoc require = o.getRequire();
1665
1666            if (requires.contains(require)) {
1667                candidates.add(o);
1668            }
1669        }
1670
1671        if (candidates.size() == 1) {
1672            return candidates.iterator().next();
1673        } else if (!candidates.isEmpty()) {
1674            result = candidates;
1675        }
1676
1677        // 3. Prefer builtin (kernel) docs over other docs.
1678
candidates = new HashSet JavaDoc<IndexedElement>();
1679
1680        for (IndexedElement o : result) {
1681            String JavaDoc url = o.getFileUrl();
1682
1683            if (url.indexOf("rubystubs") != -1) {
1684                candidates.add(o);
1685            }
1686        }
1687
1688        if (candidates.size() == 1) {
1689            return candidates.iterator().next();
1690        } else if (!candidates.isEmpty()) {
1691            result = candidates;
1692        }
1693
1694        // 4. Consider other heuristics, like picking the "larger" documentation
1695
// (more lines)
1696

1697        // 5. Just pick an arbitrary one.
1698
return result.iterator().next();
1699    }
1700
1701    public String JavaDoc document(CompilationInfo info, Element element) {
1702        if (element == null) {
1703            return null;
1704        }
1705
1706        Node node = null;
1707
1708        if (element instanceof KeywordElement) {
1709            return getKeywordHelp(((KeywordElement)element).getName());
1710        } else if (element instanceof AstElement) {
1711            node = ((AstElement)element).getNode();
1712        } else if (element instanceof IndexedElement) {
1713            IndexedElement com = (IndexedElement)element;
1714            Node root = AstUtilities.getRoot(info);
1715            IndexedElement match = findDocumentationEntry(root, com);
1716
1717            if (match != null) {
1718                com = match;
1719                element = com;
1720            }
1721
1722            node = AstUtilities.getForeignNode(com);
1723
1724            if (node == null) {
1725                return null;
1726            }
1727        } else {
1728            assert false : element;
1729
1730            return null;
1731        }
1732
1733        ParserResult parseResult = info.getParserResult();
1734
1735        if (parseResult == null) {
1736            return null;
1737        }
1738
1739        // Initially, I implemented this by using RubyParserResult.getCommentNodes.
1740
// However, I -still- had to rely on looking in the Document itself, since
1741
// the CommentNodes are not attached to the AST, and to do things the way
1742
// RDoc does, I have to (for example) look to see if a comment is at the
1743
// beginning of a line or on the same line as something else, or if two
1744
// comments have any empty lines between them, and so on.
1745
// When I started looking in the document itself, I realized I might as well
1746
// do all the manipulation on the document, since having the Comment nodes
1747
// don't particularly help.
1748
Document JavaDoc doc = null;
1749        BaseDocument baseDoc = null;
1750
1751        try {
1752            if (element instanceof IndexedElement) {
1753                doc = ((IndexedElement)element).getDocument();
1754            } else {
1755                doc = info.getDocument();
1756            }
1757
1758            if (doc instanceof BaseDocument) {
1759                baseDoc = (BaseDocument)doc;
1760            } else {
1761                return null;
1762            }
1763        } catch (IOException JavaDoc ioe) {
1764            ErrorManager.getDefault().notify(ioe);
1765
1766            return null;
1767        }
1768
1769        List JavaDoc<String JavaDoc> comments = null;
1770
1771        // Check for RubyComObject: These are external files (like Ruby lib) where I need to check many files
1772
if (node instanceof ClassNode && !(element instanceof IndexedElement)) {
1773            String JavaDoc className = AstUtilities.getClassOrModuleName((ClassNode)node);
1774            List JavaDoc<ClassNode> classes = AstUtilities.getClasses(AstUtilities.getRoot(info));
1775
1776            // Iterate backwards through the list because the most recent documentation
1777
// should be chosen, if any
1778
for (int i = classes.size() - 1; i >= 0; i--) {
1779                ClassNode clz = classes.get(i);
1780                String JavaDoc name = AstUtilities.getClassOrModuleName(clz);
1781
1782                if (name.equals(className)) {
1783                    comments = AstUtilities.gatherDocumentation(baseDoc, clz);
1784
1785                    if ((comments != null) && (comments.size() > 0)) {
1786                        break;
1787                    }
1788                }
1789            }
1790        } else {
1791            comments = AstUtilities.gatherDocumentation(baseDoc, node);
1792        }
1793
1794        if ((comments == null) || (comments.size() == 0)) {
1795            return null;
1796        }
1797
1798        RDocFormatter formatter = new RDocFormatter();
1799
1800        for (String JavaDoc text : comments) {
1801            formatter.appendLine(text);
1802        }
1803
1804        return getSignature(element) + "<br>" + formatter.toHtml();
1805    }
1806
1807    private String JavaDoc getSignature(Element element) {
1808        StringBuilder JavaDoc sb = new StringBuilder JavaDoc();
1809        // TODO:
1810
sb.append("<pre>");
1811
1812        if (element instanceof MethodElement) {
1813            MethodElement executable = (MethodElement)element;
1814            // TODO - share this between Navigator implementation and here...
1815
sb.append(executable.getName());
1816
1817            Collection JavaDoc<String JavaDoc> parameters = executable.getParameters();
1818
1819            if ((parameters != null) && (parameters.size() > 0)) {
1820                sb.append("(");
1821
1822                sb.append("<font color=\"#808080\">");
1823
1824                for (Iterator JavaDoc<String JavaDoc> it = parameters.iterator(); it.hasNext();) {
1825                    String JavaDoc ve = it.next();
1826                    // TODO - if I know types, list the type here instead. For now, just use the parameter name instead
1827
sb.append(ve);
1828
1829                    if (it.hasNext()) {
1830                        sb.append(", ");
1831                    }
1832                }
1833
1834                sb.append("</font>");
1835
1836                sb.append(")");
1837            }
1838        } else {
1839            sb.append(element.getName());
1840        }
1841
1842        sb.append("</pre>");
1843
1844        return sb.toString();
1845    }
1846
1847    private abstract class RubyCompletionItem implements CompletionProposal {
1848        protected Element element;
1849        protected int anchorOffset;
1850        protected boolean symbol;
1851
1852        private RubyCompletionItem(Element element, int anchorOffset) {
1853            this.element = element;
1854            this.anchorOffset = anchorOffset;
1855        }
1856
1857        public int getAnchorOffset() {
1858            return anchorOffset;
1859        }
1860
1861        public String JavaDoc getName() {
1862            return element.getName();
1863        }
1864
1865        public void setSymbol(boolean symbol) {
1866            this.symbol = symbol;
1867        }
1868
1869        public String JavaDoc getInsertPrefix() {
1870            if (symbol) {
1871                return ":" + getName();
1872            } else {
1873                return getName();
1874            }
1875        }
1876
1877        public String JavaDoc getSortText() {
1878            return getName();
1879        }
1880
1881        public Element getElement() {
1882            return element;
1883        }
1884
1885        public ElementKind getKind() {
1886            return element.getKind();
1887        }
1888
1889        public ImageIcon JavaDoc getIcon() {
1890            return null;
1891        }
1892
1893        public String JavaDoc getLhsHtml() {
1894            ElementKind kind = getKind();
1895            formatter.reset();
1896            formatter.name(kind, true);
1897            formatter.appendText(getName());
1898            formatter.name(kind, false);
1899
1900            return formatter.getText();
1901        }
1902
1903        public Set JavaDoc<Modifier> getModifiers() {
1904            return element.getModifiers();
1905        }
1906        
1907        public String JavaDoc toString() {
1908            String JavaDoc cls = getClass().getName();
1909            cls = cls.substring(cls.lastIndexOf('.')+1);
1910            return cls + "(" + getKind() + "): " + getName();
1911        }
1912    }
1913
1914    private class MethodItem extends RubyCompletionItem {
1915        MethodItem(Element element, int anchorOffset) {
1916            super(element, anchorOffset);
1917        }
1918
1919        public String JavaDoc getLhsHtml() {
1920            ElementKind kind = getKind();
1921            formatter.reset();
1922            formatter.name(kind, true);
1923            formatter.appendText(getName());
1924            formatter.name(kind, false);
1925
1926            Collection JavaDoc<String JavaDoc> parameters = ((MethodElement)element).getParameters();
1927
1928            if ((parameters != null) && (parameters.size() > 0)) {
1929                formatter.appendHtml("("); // NOI18N
1930

1931                Iterator JavaDoc<String JavaDoc> it = parameters.iterator();
1932
1933                while (it.hasNext()) { // && tIt.hasNext()) {
1934
formatter.parameters(true);
1935                    formatter.appendText(it.next());
1936                    formatter.parameters(false);
1937
1938                    if (it.hasNext()) {
1939                        formatter.appendText(", "); // NOI18N
1940
}
1941                }
1942
1943                formatter.appendHtml(")"); // NOI18N
1944
}
1945
1946            return formatter.getText();
1947        }
1948
1949        public String JavaDoc getRhsHtml() {
1950            formatter.reset();
1951
1952            String JavaDoc in = ((MethodElement)element).getIn();
1953
1954            if (in != null) {
1955                formatter.appendText(in);
1956            } else {
1957                return null;
1958            }
1959
1960            return formatter.getText();
1961        }
1962    }
1963
1964    private class KeywordItem extends RubyCompletionItem {
1965        private static final String JavaDoc RUBY_KEYWORD = "org/netbeans/modules/ruby/jruby.png"; //NOI18N
1966
private String JavaDoc keyword;
1967        private String JavaDoc description;
1968
1969        KeywordItem(String JavaDoc keyword, String JavaDoc description, int anchorOffset) {
1970            super(null, anchorOffset);
1971            this.keyword = keyword;
1972            this.description = description;
1973        }
1974
1975        public String JavaDoc getName() {
1976            return keyword;
1977        }
1978
1979        public ElementKind getKind() {
1980            return ElementKind.KEYWORD;
1981        }
1982
1983        public String JavaDoc getRhsHtml() {
1984            if (description != null) {
1985                formatter.reset();
1986                formatter.appendText(description);
1987
1988                return formatter.getText();
1989            } else {
1990                return null;
1991            }
1992        }
1993
1994        public ImageIcon JavaDoc getIcon() {
1995            if (keywordIcon == null) {
1996                keywordIcon = new ImageIcon JavaDoc(org.openide.util.Utilities.loadImage(RUBY_KEYWORD));
1997            }
1998
1999            return keywordIcon;
2000        }
2001
2002        public Element getElement() {
2003            // For completion documentation
2004
return new KeywordElement(keyword);
2005        }
2006    }
2007
2008    private class ClassItem extends RubyCompletionItem {
2009        ClassItem(Element element, int anchorOffset) {
2010            super(element, anchorOffset);
2011        }
2012
2013        public String JavaDoc getRhsHtml() {
2014            formatter.reset();
2015
2016            String JavaDoc in = ((ClassElement)element).getIn();
2017
2018            if (in != null) {
2019                formatter.appendText(in);
2020            } else {
2021                return null;
2022            }
2023
2024            return formatter.getText();
2025        }
2026    }
2027
2028    private class PlainItem extends RubyCompletionItem {
2029        PlainItem(Element element, int anchorOffset) {
2030            super(element, anchorOffset);
2031        }
2032
2033        public String JavaDoc getRhsHtml() {
2034            return null;
2035        }
2036    }
2037
2038    private class FieldItem extends RubyCompletionItem {
2039        FieldItem(Element element, int anchorOffset) {
2040            super(element, anchorOffset);
2041        }
2042
2043        public String JavaDoc getRhsHtml() {
2044            return null;
2045        }
2046
2047        public String JavaDoc getInsertPrefix() {
2048            if (symbol) {
2049                return ":" + getName();
2050            }
2051
2052            if (element.getModifiers().contains(Modifier.STATIC)) {
2053                return "@@" + getName();
2054            } else {
2055                return "@" + getName();
2056            }
2057        }
2058    }
2059}
2060
Popular Tags