tor@1
|
1 |
/*
|
phrebejk@559
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
tor@1
|
3 |
*
|
jglick@4117
|
4 |
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
|
jglick@4117
|
5 |
*
|
jglick@4117
|
6 |
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
|
jglick@4117
|
7 |
* Other names may be trademarks of their respective owners.
|
tor@1
|
8 |
*
|
phrebejk@559
|
9 |
* The contents of this file are subject to the terms of either the GNU
|
phrebejk@559
|
10 |
* General Public License Version 2 only ("GPL") or the Common
|
phrebejk@559
|
11 |
* Development and Distribution License("CDDL") (collectively, the
|
phrebejk@559
|
12 |
* "License"). You may not use this file except in compliance with the
|
phrebejk@559
|
13 |
* License. You can obtain a copy of the License at
|
phrebejk@559
|
14 |
* http://www.netbeans.org/cddl-gplv2.html
|
phrebejk@559
|
15 |
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
|
phrebejk@559
|
16 |
* specific language governing permissions and limitations under the
|
phrebejk@559
|
17 |
* License. When distributing the software, include this License Header
|
phrebejk@559
|
18 |
* Notice in each file and include the License file at
|
jglick@4117
|
19 |
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
|
phrebejk@559
|
20 |
* particular file as subject to the "Classpath" exception as provided
|
jglick@4117
|
21 |
* by Oracle in the GPL Version 2 section of the License file that
|
phrebejk@559
|
22 |
* accompanied this code. If applicable, add the following below the
|
phrebejk@559
|
23 |
* License Header, with the fields enclosed by brackets [] replaced by
|
phrebejk@559
|
24 |
* your own identifying information:
|
tor@1
|
25 |
* "Portions Copyrighted [year] [name of copyright owner]"
|
tor@1
|
26 |
*
|
phrebejk@559
|
27 |
* Contributor(s):
|
phrebejk@559
|
28 |
*
|
tor@1
|
29 |
* The Original Software is NetBeans. The Initial Developer of the Original
|
mkrauskopf@2702
|
30 |
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
|
tor@1
|
31 |
* Microsystems, Inc. All Rights Reserved.
|
phrebejk@559
|
32 |
*
|
phrebejk@559
|
33 |
* If you wish your version of this file to be governed by only the CDDL
|
phrebejk@559
|
34 |
* or only the GPL Version 2, indicate your decision by adding
|
phrebejk@559
|
35 |
* "[Contributor] elects to include this software in this distribution
|
phrebejk@559
|
36 |
* under the [CDDL or GPL Version 2] license." If you do not indicate a
|
phrebejk@559
|
37 |
* single choice of license, a recipient has the option to distribute
|
phrebejk@559
|
38 |
* your version of this file under either the CDDL, the GPL Version 2 or
|
phrebejk@559
|
39 |
* to extend the choice of license to its licensees as provided above.
|
phrebejk@559
|
40 |
* However, if you add GPL Version 2 code and therefore, elected the GPL
|
phrebejk@559
|
41 |
* Version 2 license, then the option applies only if the new code is
|
phrebejk@559
|
42 |
* made subject to such option by the copyright holder.
|
tor@1
|
43 |
*/
|
tor@1
|
44 |
package org.netbeans.modules.ruby;
|
tor@1
|
45 |
|
tor@1
|
46 |
import java.io.BufferedInputStream;
|
tor@1
|
47 |
import java.io.IOException;
|
tor@1
|
48 |
import java.io.InputStream;
|
tor@1
|
49 |
import java.util.ArrayList;
|
tor@1
|
50 |
import java.util.Collections;
|
tor@1
|
51 |
import java.util.HashMap;
|
tor@1
|
52 |
import java.util.HashSet;
|
tor@1
|
53 |
import java.util.List;
|
tor@1
|
54 |
import java.util.Map;
|
tor@1
|
55 |
import java.util.Set;
|
tor@1
|
56 |
import javax.swing.text.BadLocationException;
|
tor@1
|
57 |
import javax.swing.text.Document;
|
tor@70
|
58 |
import javax.swing.text.JTextComponent;
|
emononen@3259
|
59 |
import org.jrubyparser.SourcePosition;
|
emononen@3259
|
60 |
import org.jrubyparser.ast.ArgsNode;
|
emononen@3259
|
61 |
import org.jrubyparser.ast.ArgumentNode;
|
emononen@3259
|
62 |
import org.jrubyparser.ast.ClassNode;
|
enebo@4519
|
63 |
import org.jrubyparser.ast.ILocalScope;
|
emononen@3259
|
64 |
import org.jrubyparser.ast.ListNode;
|
emononen@3259
|
65 |
import org.jrubyparser.ast.LocalAsgnNode;
|
emononen@3259
|
66 |
import org.jrubyparser.ast.MethodDefNode;
|
emononen@3259
|
67 |
import org.jrubyparser.ast.Node;
|
emononen@3259
|
68 |
import org.jrubyparser.ast.NodeType;
|
emononen@3259
|
69 |
import org.jrubyparser.ast.INameNode;
|
tor@1
|
70 |
import org.netbeans.api.lexer.Token;
|
tor@1
|
71 |
import org.netbeans.api.lexer.TokenHierarchy;
|
tor@1
|
72 |
import org.netbeans.api.lexer.TokenId;
|
tor@1
|
73 |
import org.netbeans.api.lexer.TokenSequence;
|
jskrivanek@2662
|
74 |
import org.netbeans.api.ruby.platform.RubyInstallation;
|
tor@1
|
75 |
import org.netbeans.editor.BaseDocument;
|
tor@1
|
76 |
import org.netbeans.editor.Utilities;
|
mkrauskopf@3030
|
77 |
import org.netbeans.modules.csl.api.CodeCompletionContext;
|
mkrauskopf@3030
|
78 |
import org.netbeans.modules.csl.api.CodeCompletionHandler;
|
mkrauskopf@3030
|
79 |
import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType;
|
mkrauskopf@3030
|
80 |
import org.netbeans.modules.csl.api.CodeCompletionResult;
|
mkrauskopf@3030
|
81 |
import org.netbeans.modules.csl.api.CompletionProposal;
|
mkrauskopf@3030
|
82 |
import org.netbeans.modules.csl.api.DeclarationFinder.DeclarationLocation;
|
mkrauskopf@3030
|
83 |
import org.netbeans.modules.csl.api.ElementHandle;
|
mkrauskopf@3030
|
84 |
import org.netbeans.modules.csl.api.ElementKind;
|
mkrauskopf@3030
|
85 |
import org.netbeans.modules.csl.api.ParameterInfo;
|
mkrauskopf@3030
|
86 |
import org.netbeans.modules.csl.spi.DefaultCompletionResult;
|
mkrauskopf@3030
|
87 |
import org.netbeans.modules.csl.spi.ParserResult;
|
vstejskal@3086
|
88 |
import org.netbeans.modules.parsing.api.Snapshot;
|
vstejskal@3086
|
89 |
import org.netbeans.modules.parsing.api.Source;
|
mkrauskopf@3030
|
90 |
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
|
mkrauskopf@2743
|
91 |
import org.netbeans.modules.ruby.RubyCompletionItem.CallItem;
|
mkrauskopf@2743
|
92 |
import org.netbeans.modules.ruby.RubyCompletionItem.ClassItem;
|
mkrauskopf@2743
|
93 |
import org.netbeans.modules.ruby.RubyCompletionItem.FieldItem;
|
mkrauskopf@2743
|
94 |
import org.netbeans.modules.ruby.RubyCompletionItem.MethodItem;
|
mkrauskopf@2743
|
95 |
import org.netbeans.modules.ruby.RubyCompletionItem.ParameterItem;
|
tor@1
|
96 |
import org.netbeans.modules.ruby.elements.AstElement;
|
tor@1
|
97 |
import org.netbeans.modules.ruby.elements.AstFieldElement;
|
tor@1867
|
98 |
import org.netbeans.modules.ruby.elements.AstNameElement;
|
mkrauskopf@3030
|
99 |
import org.netbeans.modules.ruby.elements.CommentElement;
|
mkrauskopf@3030
|
100 |
import org.netbeans.modules.ruby.elements.Element;
|
tor@1
|
101 |
import org.netbeans.modules.ruby.elements.IndexedClass;
|
tor@1
|
102 |
import org.netbeans.modules.ruby.elements.IndexedElement;
|
mkrauskopf@3030
|
103 |
import org.netbeans.modules.ruby.elements.IndexedField;
|
tor@1
|
104 |
import org.netbeans.modules.ruby.elements.IndexedMethod;
|
tor@2324
|
105 |
import org.netbeans.modules.ruby.elements.IndexedVariable;
|
tor@1
|
106 |
import org.netbeans.modules.ruby.elements.KeywordElement;
|
tor@1205
|
107 |
import org.netbeans.modules.ruby.elements.RubyElement;
|
mkrauskopf@3030
|
108 |
import org.netbeans.modules.ruby.lexer.Call;
|
tor@1
|
109 |
import org.netbeans.modules.ruby.lexer.LexUtilities;
|
tor@1
|
110 |
import org.netbeans.modules.ruby.lexer.RubyStringTokenId;
|
tor@1
|
111 |
import org.netbeans.modules.ruby.lexer.RubyTokenId;
|
tor@1
|
112 |
import org.openide.filesystems.FileObject;
|
tor@1
|
113 |
import org.openide.filesystems.FileUtil;
|
tor@1
|
114 |
import org.openide.util.Exceptions;
|
tor@581
|
115 |
import org.openide.util.NbBundle;
|
tor@1
|
116 |
|
emononen@3948
|
117 |
import static org.netbeans.modules.ruby.RubyUtils.*;
|
emononen@3948
|
118 |
|
tor@1
|
119 |
/**
|
tor@1
|
120 |
* Code completion handler for Ruby.
|
mkrauskopf@3030
|
121 |
*
|
tor@1
|
122 |
* Bug: I add lists of fields etc. But if these -overlap- the current line,
|
tor@1
|
123 |
* I throw them away. The problem is that there may be other references
|
tor@1
|
124 |
* to the field that I should -not- throw away, elsewhere!
|
tor@1
|
125 |
* @todo Ensure that I prefer assignment over reference such that javadoc is
|
tor@1
|
126 |
* more likely to be there!
|
tor@1
|
127 |
*
|
tor@1
|
128 |
*
|
tor@1
|
129 |
* @todo Handle this case: {@code class HTTPBadResponse < StandardError; end}
|
tor@450
|
130 |
* @todo Code completion should automatically suggest "initialize()" for def completion! (if I'm in a class)
|
tor@70
|
131 |
* @todo It would be nice if you select a method that takes a block, such as Array.each, if we could
|
tor@70
|
132 |
* insert a { ^ } suffix
|
tor@1
|
133 |
* @todo Use lexical tokens to avoid attempting code completion within comments,
|
tor@1
|
134 |
* literal strings and regexps
|
tor@1
|
135 |
* @todo Percent-completion doesn't work if you at this to the end of the
|
tor@1
|
136 |
* document: x = % and try to complete.
|
tor@1
|
137 |
* @todo Handle more completion scenarios: Classes (no keywords) after "class Foo <",
|
tor@1
|
138 |
* classes after "::", parameter completion (!!!), .new() completion (initialize), etc.
|
tor@1
|
139 |
* @todo Make sure completion works during a "::"
|
tor@1
|
140 |
* @todo I need to move the smart-determination from just checking in=Object/Class/Module
|
tor@1
|
141 |
* to the code which computes matches, since we have for example ObjectMixin in pretty printer
|
tor@1
|
142 |
* which adds mixin methods to Object.
|
tor@1
|
143 |
* @todo Handle Rails methods that deal with hashes:
|
tor@1
|
144 |
* - Try figuring out whether the method should take parameters by looking for examples;
|
tor@1
|
145 |
* lines that start with the method name and looks like it might have arguments
|
tor@1
|
146 |
* - Try to figure out what the different parameters are if there are hashes
|
tor@1
|
147 |
* - <tt>: looks like a parameter, e.g. "<tt>:filename</tt>"
|
tor@1
|
148 |
* and to see which parameter it might correspond to, see the
|
tor@1
|
149 |
* label; see if any of the parameter names are listed there (possibly in the args list)
|
tor@1
|
150 |
* A fallback is to look for args that look like they may be hashes, e.g.
|
tor@1
|
151 |
* def(foo1, foo2, foo3={}) - the third one is obviously a hash
|
tor@1
|
152 |
* @todo Make code completion when we're in a parameter list include the parameters as well!
|
tor@1
|
153 |
* @todo For .rjs files, insert an object named "page" of type
|
tor@1
|
154 |
* ActionView::Helpers::PrototypeHelper::JavaScriptGenerator::GeneratorMethods
|
tor@1
|
155 |
* (#105088)
|
tor@1
|
156 |
* @todo For .builder files, insert an object named "xml" of type
|
tor@1
|
157 |
* Builder::XmlMarkup
|
tor@1
|
158 |
* @todo For .rhtml/.html.erb files, insert fields etc. as documented in actionpack's lib/action_view/base.rb
|
tor@1
|
159 |
* (#105095)
|
tor@1
|
160 |
* @todo For test files in Rails, get testing context (#105043). In particular, actionpack's
|
tor@1
|
161 |
* ActionController::Assertions needs to be pulled in. This happens in action_controller/assertions.rb.
|
tor@1
|
162 |
* @todo Require-completion should handle ruby gems; it should provide the "preferred" (entry-point) files for
|
tor@1
|
163 |
* all the ruby gems, and it should hide all the files that are inside the gem
|
tor@1
|
164 |
* @todo Rakefiles files should inherit Rakefile context
|
tor@411
|
165 |
* @todo See http://blog.diegodoval.com/2007/09/ruby_on_os_x_some_useful_links.html
|
tor@411
|
166 |
* @todo Documentation completion in a rdoc should preview that rdoc section
|
tor@411
|
167 |
* @todo Make a dedicated completion item which I return on documentation completion if I want to
|
tor@411
|
168 |
* complete the CURRENT element; it basically just wraps the desired comment so we can pull it
|
tor@411
|
169 |
* out in the document() method
|
tor@480
|
170 |
* @todo Provide code completion for "3|" or "3 |" - show available overloaded operators! This
|
tor@480
|
171 |
* shouldn't just apply to numbers - any class you've overridden
|
tor@480
|
172 |
* @todo Digest http://blogs.sun.com/coolstuff/entry/using_java_classes_in_jruby
|
tor@480
|
173 |
* to fix require'java' etc.
|
tor@480
|
174 |
* @todo http://www.innovationontherun.com/scraping-dynamic-websites-using-jruby-and-htmlunit/
|
tor@480
|
175 |
* Idea: Use a quicktip to require all the jars in the project?
|
tor@497
|
176 |
* @todo The "h" method in <%= %> doesn't show up in RHTML files... where is it?
|
tor@669
|
177 |
* @todo Completion AFTER a method which takes a block (optional or required) should offer
|
tor@669
|
178 |
* { } and do/end !!
|
tor@1
|
179 |
* @author Tor Norbye
|
tor@1
|
180 |
*/
|
tor@1719
|
181 |
public class RubyCodeCompleter implements CodeCompletionHandler {
|
mkrauskopf@2482
|
182 |
|
mkrauskopf@2482
|
183 |
// Another good logical parameter would be SINGLE_WHITESPACE which would
|
mkrauskopf@2482
|
184 |
// insert a whitespace separator IF NEEDED
|
mkrauskopf@2482
|
185 |
|
tor@1
|
186 |
/** Live code template parameter: require the given file, if not already done so */
|
tor@1
|
187 |
private static final String KEY_REQUIRE = "require"; // NOI18N
|
tor@1
|
188 |
|
tor@1
|
189 |
/** Live code template parameter: find a name in scope that is known to be of the given type */
|
tor@1
|
190 |
private static final String KEY_INSTANCEOF = "instanceof"; // NOI18N
|
tor@1
|
191 |
|
tor@1
|
192 |
/** Live code template parameter: compute an unused local variable name */
|
tor@671
|
193 |
private static final String ATTR_UNUSEDLOCAL = "unusedlocal"; // NOI18N
|
tor@1
|
194 |
|
tor@1
|
195 |
/** Live code template parameter: pipe variable, since | is a bit mishandled in the UI for editing abbrevs */
|
tor@1
|
196 |
private static final String KEY_PIPE = "pipe"; // NOI18N
|
tor@1
|
197 |
|
tor@1
|
198 |
/** Live code template parameter: compute the method name */
|
tor@1
|
199 |
private static final String KEY_METHOD = "method"; // NOI18N
|
tor@1
|
200 |
|
tor@1
|
201 |
/** Live code template parameter: compute the method signature */
|
tor@1
|
202 |
private static final String KEY_METHOD_FQN = "methodfqn"; // NOI18N
|
tor@1
|
203 |
|
tor@1
|
204 |
/** Live code template parameter: compute the class name (not including the module prefix) */
|
tor@1
|
205 |
private static final String KEY_CLASS = "class"; // NOI18N
|
tor@1
|
206 |
|
tor@1
|
207 |
/** Live code template parameter: compute the class fully qualified name */
|
tor@1
|
208 |
private static final String KEY_CLASS_FQN = "classfqn"; // NOI18N
|
tor@1
|
209 |
|
tor@1
|
210 |
/** Live code template parameter: compute the superclass of the current class */
|
tor@1
|
211 |
private static final String KEY_SUPERCLASS = "superclass"; // NOI18N
|
tor@1
|
212 |
|
tor@1
|
213 |
/** Live code template parameter: compute the filename (not including the path) of the file */
|
tor@1
|
214 |
private static final String KEY_FILE = "file"; // NOI18N
|
tor@1
|
215 |
|
tor@1
|
216 |
/** Live code template parameter: compute the full path of the source directory */
|
tor@1
|
217 |
private static final String KEY_PATH = "path"; // NOI18N
|
tor@1
|
218 |
|
tor@671
|
219 |
/** Default name values for ATTR_UNUSEDLOCAL and friends */
|
tor@1
|
220 |
private static final String ATTR_DEFAULTS = "defaults"; // NOI18N
|
mkrauskopf@2481
|
221 |
|
tor@1
|
222 |
private static final Set<String> selectionTemplates = new HashSet<String>();
|
tor@1
|
223 |
|
tor@1
|
224 |
static {
|
tor@1
|
225 |
selectionTemplates.add("begin"); // NOI18N
|
tor@1
|
226 |
selectionTemplates.add("do"); // NOI18N
|
tor@553
|
227 |
selectionTemplates.add("doc"); // NOI18N
|
tor@507
|
228 |
//selectionTemplates.add("dop"); // NOI18N
|
tor@1
|
229 |
selectionTemplates.add("if"); // NOI18N
|
tor@484
|
230 |
selectionTemplates.add("ife"); // NOI18N
|
tor@1
|
231 |
}
|
tor@1
|
232 |
|
tor@1
|
233 |
private boolean caseSensitive;
|
tor@1
|
234 |
private int anchor;
|
tor@1
|
235 |
|
tor@1719
|
236 |
public RubyCodeCompleter() {
|
tor@1
|
237 |
}
|
tor@1
|
238 |
|
mkrauskopf@2743
|
239 |
static boolean startsWith(String theString, String prefix, boolean caseSensitive) {
|
enebo@4542
|
240 |
if (prefix.length() == 0) return true;
|
tor@1
|
241 |
|
tor@1
|
242 |
return caseSensitive ? theString.startsWith(prefix)
|
tor@1
|
243 |
: theString.toLowerCase().startsWith(prefix.toLowerCase());
|
tor@1
|
244 |
}
|
tor@1
|
245 |
|
mkrauskopf@2743
|
246 |
private boolean startsWith(String theString, String prefix) {
|
mkrauskopf@2743
|
247 |
return RubyCodeCompleter.startsWith(theString, prefix, caseSensitive);
|
mkrauskopf@2743
|
248 |
}
|
mkrauskopf@2743
|
249 |
|
tor@1
|
250 |
/**
|
tor@1
|
251 |
* Compute an appropriate prefix to use for code completion.
|
tor@1
|
252 |
* In Strings, we want to return the -whole- string if you're in a
|
tor@1
|
253 |
* require-statement string, otherwise we want to return simply "" or the previous "\"
|
tor@1
|
254 |
* for quoted strings, and ditto for regular expressions.
|
tor@1
|
255 |
* For non-string contexts, just return null to let the default identifier-computation
|
tor@1
|
256 |
* kick in.
|
tor@1
|
257 |
*/
|
tor@1
|
258 |
@SuppressWarnings("unchecked")
|
enebo@4542
|
259 |
@Override
|
mkrauskopf@3030
|
260 |
public String getPrefix(ParserResult info, int lexOffset, boolean upToOffset) {
|
emononen@3859
|
261 |
try {
|
emononen@3859
|
262 |
BaseDocument doc = RubyUtils.getDocument(info);
|
enebo@4542
|
263 |
if (doc == null) return null;
|
emononen@3810
|
264 |
|
emononen@3859
|
265 |
TokenHierarchy<Document> th = TokenHierarchy.get((Document)doc);
|
emononen@3859
|
266 |
doc.readLock(); // Read-lock due to token hierarchy use
|
emononen@3859
|
267 |
try {
|
tor@1
|
268 |
int requireStart = LexUtilities.getRequireStringOffset(lexOffset, th);
|
tor@1
|
269 |
|
enebo@4542
|
270 |
// XXX todo - do upToOffset
|
enebo@4542
|
271 |
if (requireStart != -1) return doc.getText(requireStart, lexOffset - requireStart);
|
tor@1
|
272 |
|
emononen@3810
|
273 |
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
|
enebo@4542
|
274 |
if (ts == null) return null;
|
tor@1
|
275 |
|
tor@1
|
276 |
ts.move(lexOffset);
|
tor@1
|
277 |
|
enebo@4542
|
278 |
if (!ts.moveNext() && !ts.movePrevious()) return null;
|
tor@1
|
279 |
|
enebo@4542
|
280 |
// We're looking at the offset to the RIGHT of the caret and here I care about what's on the left
|
enebo@4542
|
281 |
if (ts.offset() == lexOffset) ts.movePrevious();
|
tor@1
|
282 |
|
emononen@3810
|
283 |
Token<? extends RubyTokenId> token = ts.token();
|
tor@1
|
284 |
|
tor@1
|
285 |
if (token != null) {
|
tor@1
|
286 |
TokenId id = token.id();
|
tor@1
|
287 |
|
tor@1
|
288 |
// We're within a String that has embedded Ruby. Drop into the
|
tor@1
|
289 |
// embedded language and see if we're within a literal string there.
|
tor@1
|
290 |
if (id == RubyTokenId.EMBEDDED_RUBY) {
|
emononen@3810
|
291 |
ts = (TokenSequence) ts.embedded();
|
tor@1
|
292 |
assert ts != null;
|
tor@1
|
293 |
ts.move(lexOffset);
|
tor@1
|
294 |
|
enebo@4542
|
295 |
if (!ts.moveNext() && !ts.movePrevious()) return null;
|
tor@1
|
296 |
|
tor@1
|
297 |
token = ts.token();
|
tor@1
|
298 |
id = token.id();
|
tor@1
|
299 |
}
|
tor@1
|
300 |
|
tor@1
|
301 |
String tokenText = token.text().toString();
|
tor@1
|
302 |
|
tor@1
|
303 |
if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) ||
|
tor@1
|
304 |
((id == RubyTokenId.ERROR) && tokenText.equals("%"))) {
|
tor@1
|
305 |
int currOffset = ts.offset();
|
tor@1
|
306 |
|
tor@1
|
307 |
// Percent completion
|
tor@1
|
308 |
if ((currOffset == (lexOffset - 1)) && (tokenText.length() > 0) &&
|
tor@1
|
309 |
(tokenText.charAt(0) == '%')) {
|
tor@1
|
310 |
return "%";
|
tor@1
|
311 |
}
|
tor@1
|
312 |
}
|
tor@1
|
313 |
}
|
tor@1
|
314 |
|
tor@1
|
315 |
int doubleQuotedOffset = LexUtilities.getDoubleQuotedStringOffset(lexOffset, th);
|
tor@1
|
316 |
|
tor@1
|
317 |
if (doubleQuotedOffset != -1) {
|
tor@1
|
318 |
// Tokenize the string and offer the current token portion as the text
|
tor@1
|
319 |
if (doubleQuotedOffset == lexOffset) {
|
tor@1
|
320 |
return "";
|
tor@1
|
321 |
} else if (doubleQuotedOffset < lexOffset) {
|
tor@1
|
322 |
String text = doc.getText(doubleQuotedOffset, lexOffset - doubleQuotedOffset);
|
tor@1
|
323 |
TokenHierarchy hi =
|
emononen@3810
|
324 |
TokenHierarchy.create(text, RubyStringTokenId.languageDouble());
|
tor@1
|
325 |
|
tor@1
|
326 |
TokenSequence seq = hi.tokenSequence();
|
tor@1
|
327 |
|
tor@1
|
328 |
seq.move(lexOffset - doubleQuotedOffset);
|
tor@1
|
329 |
|
tor@1
|
330 |
if (!seq.moveNext() && !seq.movePrevious()) {
|
tor@1
|
331 |
return "";
|
tor@1
|
332 |
}
|
tor@1
|
333 |
|
tor@1
|
334 |
TokenId id = seq.token().id();
|
tor@1
|
335 |
String s = seq.token().text().toString();
|
tor@1
|
336 |
|
tor@1
|
337 |
if ((id == RubyStringTokenId.STRING_ESCAPE) ||
|
tor@1
|
338 |
(id == RubyStringTokenId.STRING_INVALID)) {
|
tor@1
|
339 |
return s;
|
tor@1
|
340 |
} else if (s.startsWith("\\")) {
|
tor@1
|
341 |
return s;
|
tor@1
|
342 |
} else {
|
tor@1
|
343 |
return "";
|
tor@1
|
344 |
}
|
tor@1
|
345 |
} else {
|
tor@1
|
346 |
// The String offset is greater than the caret position.
|
tor@1
|
347 |
// This means that we're inside the string-begin section,
|
tor@1
|
348 |
// for example here: %q|(
|
tor@1
|
349 |
// In this case, report no prefix
|
tor@1
|
350 |
return "";
|
tor@1
|
351 |
}
|
tor@1
|
352 |
}
|
tor@1
|
353 |
|
tor@1
|
354 |
int singleQuotedOffset = LexUtilities.getSingleQuotedStringOffset(lexOffset, th);
|
tor@1
|
355 |
|
tor@1
|
356 |
if (singleQuotedOffset != -1) {
|
tor@1
|
357 |
if (singleQuotedOffset == lexOffset) {
|
tor@1
|
358 |
return "";
|
tor@1
|
359 |
} else if (singleQuotedOffset < lexOffset) {
|
tor@1
|
360 |
String text = doc.getText(singleQuotedOffset, lexOffset - singleQuotedOffset);
|
tor@1
|
361 |
TokenHierarchy hi =
|
emononen@3810
|
362 |
TokenHierarchy.create(text, RubyStringTokenId.languageSingle());
|
tor@1
|
363 |
|
tor@1
|
364 |
TokenSequence seq = hi.tokenSequence();
|
tor@1
|
365 |
|
tor@1
|
366 |
seq.move(lexOffset - singleQuotedOffset);
|
tor@1
|
367 |
|
tor@1
|
368 |
if (!seq.moveNext() && !seq.movePrevious()) {
|
tor@1
|
369 |
return "";
|
tor@1
|
370 |
}
|
tor@1
|
371 |
|
tor@1
|
372 |
TokenId id = seq.token().id();
|
tor@1
|
373 |
String s = seq.token().text().toString();
|
tor@1
|
374 |
|
tor@1
|
375 |
if ((id == RubyStringTokenId.STRING_ESCAPE) ||
|
tor@1
|
376 |
(id == RubyStringTokenId.STRING_INVALID)) {
|
tor@1
|
377 |
return s;
|
tor@1
|
378 |
} else if (s.startsWith("\\")) {
|
tor@1
|
379 |
return s;
|
tor@1
|
380 |
} else {
|
tor@1
|
381 |
return "";
|
tor@1
|
382 |
}
|
tor@1
|
383 |
} else {
|
tor@1
|
384 |
// The String offset is greater than the caret position.
|
tor@1
|
385 |
// This means that we're inside the string-begin section,
|
tor@1
|
386 |
// for example here: %q|(
|
tor@1
|
387 |
// In this case, report no prefix
|
tor@1
|
388 |
return "";
|
tor@1
|
389 |
}
|
tor@1
|
390 |
}
|
tor@1
|
391 |
|
tor@1
|
392 |
// Regular expression
|
tor@1
|
393 |
int regexpOffset = LexUtilities.getRegexpOffset(lexOffset, th);
|
tor@1
|
394 |
|
tor@1
|
395 |
if ((regexpOffset != -1) && (regexpOffset <= lexOffset)) {
|
tor@1
|
396 |
// This is not right... I need to actually parse the regexp
|
tor@1
|
397 |
// (I should use my Regexp lexer tokens which will be embedded here)
|
tor@1
|
398 |
// such that escaping sequences (/\\\\\/) will work right, or
|
tor@1
|
399 |
// character classes (/[foo\]). In both cases the \ may not mean escape.
|
tor@1
|
400 |
String tokenText = token.text().toString();
|
tor@1
|
401 |
int index = lexOffset - ts.offset();
|
tor@1
|
402 |
|
tor@1
|
403 |
if ((index > 0) && (index <= tokenText.length()) &&
|
tor@1
|
404 |
(tokenText.charAt(index - 1) == '\\')) {
|
tor@1
|
405 |
return "\\";
|
tor@1
|
406 |
} else {
|
tor@1
|
407 |
// No prefix for regexps unless it's \
|
tor@1
|
408 |
return "";
|
tor@1
|
409 |
}
|
tor@1
|
410 |
|
tor@1
|
411 |
//return doc.getText(regexpOffset, offset-regexpOffset);
|
tor@1
|
412 |
}
|
tor@1
|
413 |
|
tor@103
|
414 |
int lineBegin = Utilities.getRowStart(doc, lexOffset);
|
tor@103
|
415 |
if (lineBegin != -1) {
|
tor@103
|
416 |
int lineEnd = Utilities.getRowEnd(doc, lexOffset);
|
emononen@3810
|
417 |
String line = doc.getText(lineBegin, lineEnd - lineBegin);
|
emononen@3810
|
418 |
int lineOffset = lexOffset - lineBegin;
|
tor@103
|
419 |
int start = lineOffset;
|
tor@103
|
420 |
if (lineOffset > 0) {
|
emononen@3810
|
421 |
for (int i = lineOffset - 1; i >= 0; i--) {
|
tor@103
|
422 |
char c = line.charAt(i);
|
tor@103
|
423 |
if (!RubyUtils.isIdentifierChar(c)) {
|
tor@103
|
424 |
break;
|
tor@103
|
425 |
} else {
|
tor@103
|
426 |
start = i;
|
tor@103
|
427 |
}
|
tor@103
|
428 |
}
|
tor@103
|
429 |
}
|
emononen@3810
|
430 |
|
tor@103
|
431 |
// Find identifier end
|
tor@581
|
432 |
String prefix;
|
emononen@3810
|
433 |
if (upToOffset) {
|
tor@103
|
434 |
prefix = line.substring(start, lineOffset);
|
tor@103
|
435 |
} else {
|
tor@103
|
436 |
if (lineOffset == line.length()) {
|
tor@103
|
437 |
prefix = line.substring(start);
|
tor@1
|
438 |
} else {
|
tor@103
|
439 |
int n = line.length();
|
tor@103
|
440 |
int end = lineOffset;
|
tor@103
|
441 |
for (int j = lineOffset; j < n; j++) {
|
tor@103
|
442 |
char d = line.charAt(j);
|
tor@103
|
443 |
// Try to accept Foo::Bar as well
|
tor@103
|
444 |
if (!RubyUtils.isStrictIdentifierChar(d)) {
|
tor@103
|
445 |
break;
|
tor@103
|
446 |
} else {
|
emononen@3810
|
447 |
end = j + 1;
|
tor@103
|
448 |
}
|
tor@103
|
449 |
}
|
tor@103
|
450 |
prefix = line.substring(start, end);
|
tor@103
|
451 |
}
|
tor@103
|
452 |
}
|
emononen@3810
|
453 |
|
tor@103
|
454 |
if (prefix.length() > 0) {
|
tor@450
|
455 |
if (prefix.endsWith("::")) {
|
tor@450
|
456 |
return "";
|
tor@450
|
457 |
}
|
tor@103
|
458 |
|
tor@103
|
459 |
if (prefix.endsWith(":") && prefix.length() > 1) {
|
tor@103
|
460 |
return null;
|
tor@1
|
461 |
}
|
tor@1
|
462 |
|
tor@1
|
463 |
// Strip out LHS if it's a qualified method, e.g. Benchmark::measure -> measure
|
tor@1
|
464 |
int q = prefix.lastIndexOf("::");
|
tor@1
|
465 |
|
tor@1
|
466 |
if (q != -1) {
|
tor@1
|
467 |
prefix = prefix.substring(q + 2);
|
tor@1
|
468 |
}
|
emononen@3810
|
469 |
|
tor@82
|
470 |
// The identifier chars identified by RubyLanguage are a bit too permissive;
|
tor@82
|
471 |
// they include things like "=", "!" and even "&" such that double-clicks will
|
tor@82
|
472 |
// pick up the whole "token" the user is after. But "=" is only allowed at the
|
tor@82
|
473 |
// end of identifiers for example.
|
tor@103
|
474 |
if (prefix.length() == 1) {
|
tor@103
|
475 |
char c = prefix.charAt(0);
|
tor@103
|
476 |
if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$' || c == ':')) {
|
tor@103
|
477 |
return null;
|
tor@103
|
478 |
}
|
tor@103
|
479 |
} else {
|
emononen@3810
|
480 |
for (int i = prefix.length() - 2; i >= 0; i--) { // -2: the last position (-1) can legally be =, ! or ?
|
tor@103
|
481 |
char c = prefix.charAt(i);
|
emononen@3810
|
482 |
if (i == 0 && c == ':') {
|
tor@103
|
483 |
// : is okay at the begining of prefixes
|
tor@103
|
484 |
} else if (!(Character.isJavaIdentifierPart(c) || c == '@' || c == '$')) {
|
emononen@3810
|
485 |
prefix = prefix.substring(i + 1);
|
tor@103
|
486 |
break;
|
tor@103
|
487 |
}
|
tor@82
|
488 |
}
|
tor@82
|
489 |
}
|
tor@1
|
490 |
|
tor@1
|
491 |
return prefix;
|
tor@1
|
492 |
}
|
tor@1
|
493 |
}
|
emononen@3859
|
494 |
} finally {
|
emononen@3859
|
495 |
doc.readUnlock();
|
emononen@3859
|
496 |
}
|
tor@1
|
497 |
// Else: normal identifier: just return null and let the machinery do the rest
|
tor@1
|
498 |
} catch (BadLocationException ble) {
|
emononen@3691
|
499 |
// do nothing - see #154991;
|
tor@1
|
500 |
}
|
tor@1
|
501 |
|
tor@1
|
502 |
// Default behavior
|
tor@1
|
503 |
return null;
|
tor@1
|
504 |
}
|
tor@1
|
505 |
|
tor@1
|
506 |
/** Determine if we're trying to complete the name for a "def" (in which case
|
tor@1
|
507 |
* we'd show the inherited methods).
|
tor@1
|
508 |
* This needs to be enhanced to handle "Foo." prefixes, e.g. def self.foo
|
tor@1
|
509 |
*/
|
tor@712
|
510 |
private boolean completeDefOrInclude(List<CompletionProposal> proposals, CompletionRequest request, String fqn) {
|
tor@14
|
511 |
RubyIndex index = request.index;
|
tor@14
|
512 |
String prefix = request.prefix;
|
tor@14
|
513 |
int lexOffset = request.lexOffset;
|
emononen@3859
|
514 |
TokenHierarchy<Document> th = request.th;
|
mkrauskopf@3030
|
515 |
QuerySupport.Kind kind = request.kind;
|
tor@14
|
516 |
|
tor@874
|
517 |
TokenSequence<?extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset);
|
tor@1
|
518 |
|
tor@1
|
519 |
if ((index != null) && (ts != null)) {
|
tor@1
|
520 |
ts.move(lexOffset);
|
tor@1
|
521 |
|
tor@1
|
522 |
if (!ts.moveNext() && !ts.movePrevious()) {
|
tor@1
|
523 |
return false;
|
tor@1
|
524 |
}
|
tor@1
|
525 |
|
tor@1
|
526 |
if (ts.offset() == lexOffset) {
|
tor@1
|
527 |
// We're looking at the offset to the RIGHT of the caret
|
tor@1
|
528 |
// position, which could be whitespace, e.g.
|
tor@1
|
529 |
// "def fo| " <-- looking at the whitespace
|
tor@1
|
530 |
ts.movePrevious();
|
tor@1
|
531 |
}
|
tor@1
|
532 |
|
tor@874
|
533 |
Token<?extends RubyTokenId> token = ts.token();
|
tor@1
|
534 |
|
tor@1
|
535 |
if (token != null) {
|
tor@1
|
536 |
TokenId id = token.id();
|
tor@1
|
537 |
|
tor@1
|
538 |
// See if we're in the identifier - "foo" in "def foo"
|
tor@1
|
539 |
// I could also be a keyword in case the prefix happens to currently
|
tor@1
|
540 |
// match a keyword, such as "next"
|
tor@712
|
541 |
if ((id == RubyTokenId.IDENTIFIER) || (id == RubyTokenId.CONSTANT) || id.primaryCategory().equals("keyword")) {
|
tor@1
|
542 |
if (!ts.movePrevious()) {
|
tor@1
|
543 |
return false;
|
tor@1
|
544 |
}
|
tor@1
|
545 |
|
tor@1
|
546 |
token = ts.token();
|
tor@1
|
547 |
id = token.id();
|
tor@1
|
548 |
}
|
tor@1
|
549 |
|
tor@1
|
550 |
// If we're not in the identifier we need to be in the whitespace after "def"
|
tor@1
|
551 |
if (id != RubyTokenId.WHITESPACE) {
|
tor@373
|
552 |
// Do something about http://www.netbeans.org/issues/show_bug.cgi?id=100452 here
|
tor@373
|
553 |
// In addition to checking for whitespace I should look for "Foo." here
|
tor@1
|
554 |
return false;
|
tor@1
|
555 |
}
|
tor@1
|
556 |
|
tor@1
|
557 |
// There may be more than one whitespace; skip them
|
tor@1
|
558 |
while (ts.movePrevious()) {
|
tor@1
|
559 |
token = ts.token();
|
tor@1
|
560 |
|
tor@1
|
561 |
if (token.id() != RubyTokenId.WHITESPACE) {
|
tor@1
|
562 |
break;
|
tor@1
|
563 |
}
|
tor@1
|
564 |
}
|
tor@1
|
565 |
|
tor@1
|
566 |
if (token.id() == RubyTokenId.DEF) {
|
tor@1
|
567 |
Set<IndexedMethod> methods = index.getInheritedMethods(fqn, prefix, kind);
|
tor@1
|
568 |
|
tor@1
|
569 |
for (IndexedMethod method : methods) {
|
tor@1
|
570 |
// Hmmm, is this necessary? Filtering should happen in the getInheritedMEthods call
|
tor@1
|
571 |
if ((prefix.length() > 0) && !method.getName().startsWith(prefix)) {
|
tor@1
|
572 |
continue;
|
tor@1
|
573 |
}
|
tor@1
|
574 |
|
tor@1
|
575 |
// For def completion, skip local methods, only include superclass and included
|
tor@1
|
576 |
if ((fqn != null) && fqn.equals(method.getClz())) {
|
tor@1
|
577 |
continue;
|
tor@1
|
578 |
}
|
tor@581
|
579 |
|
tor@581
|
580 |
if (method.isNoDoc()) {
|
tor@581
|
581 |
continue;
|
tor@581
|
582 |
}
|
tor@1
|
583 |
|
tor@1
|
584 |
// If a method is an "initialize" method I should do something special so that
|
tor@1
|
585 |
// it shows up as a "constructor" (in a new() statement) but not as a directly
|
tor@1
|
586 |
// callable initialize method (it should already be culled because it's private)
|
tor@14
|
587 |
MethodItem item = new MethodItem(method, anchor, request);
|
tor@1
|
588 |
// Exact matches
|
tor@1
|
589 |
item.setSmart(method.isSmart());
|
tor@1
|
590 |
proposals.add(item);
|
tor@1
|
591 |
}
|
tor@1
|
592 |
|
tor@1
|
593 |
return true;
|
tor@712
|
594 |
} else if (token.id() == RubyTokenId.IDENTIFIER && "include".equals(token.text().toString())) {
|
tor@712
|
595 |
// Module completion
|
tor@712
|
596 |
Set<IndexedClass> classes = index.getClasses(prefix, kind, false, true, false);
|
tor@712
|
597 |
for (IndexedClass clz : classes) {
|
tor@712
|
598 |
if (clz.isNoDoc()) {
|
tor@712
|
599 |
continue;
|
tor@712
|
600 |
}
|
tor@712
|
601 |
|
tor@712
|
602 |
ClassItem item = new ClassItem(clz, anchor, request);
|
tor@712
|
603 |
item.setSmart(true);
|
tor@712
|
604 |
proposals.add(item);
|
tor@712
|
605 |
}
|
tor@712
|
606 |
|
tor@712
|
607 |
return true;
|
tor@1
|
608 |
}
|
tor@1
|
609 |
}
|
tor@1
|
610 |
}
|
tor@1
|
611 |
|
tor@1
|
612 |
return false;
|
tor@1
|
613 |
}
|
tor@2324
|
614 |
|
tor@2324
|
615 |
private void completeGlobals(List<CompletionProposal> proposals, CompletionRequest request, boolean showSymbols) {
|
tor@2324
|
616 |
RubyIndex index = request.index;
|
tor@2324
|
617 |
String prefix = request.prefix;
|
mkrauskopf@3030
|
618 |
QuerySupport.Kind kind = request.kind;
|
tor@2324
|
619 |
|
tor@2324
|
620 |
Set<IndexedVariable> globals = index.getGlobals(prefix, kind);
|
tor@2324
|
621 |
for (IndexedVariable global : globals) {
|
tor@2458
|
622 |
RubyCompletionItem item = new RubyCompletionItem(global, anchor, request);
|
tor@2324
|
623 |
item.setSmart(true);
|
tor@2324
|
624 |
|
tor@2324
|
625 |
if (showSymbols) {
|
tor@2324
|
626 |
item.setSymbol(true);
|
tor@2324
|
627 |
}
|
tor@2324
|
628 |
|
tor@2324
|
629 |
proposals.add(item);
|
tor@2324
|
630 |
}
|
tor@2324
|
631 |
}
|
tor@1
|
632 |
|
tor@581
|
633 |
private boolean addParameters(List<CompletionProposal> proposals, CompletionRequest request) {
|
tor@581
|
634 |
IndexedMethod[] methodHolder = new IndexedMethod[1];
|
tor@797
|
635 |
@SuppressWarnings("unchecked")
|
tor@797
|
636 |
Set<IndexedMethod>[] alternatesHolder = new Set[1];
|
tor@581
|
637 |
int[] paramIndexHolder = new int[1];
|
tor@581
|
638 |
int[] anchorOffsetHolder = new int[1];
|
mkrauskopf@3030
|
639 |
ParserResult info = request.parserResult;
|
tor@581
|
640 |
int lexOffset = request.lexOffset;
|
tor@581
|
641 |
int astOffset = request.astOffset;
|
mkrauskopf@2743
|
642 |
if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
|
tor@2553
|
643 |
methodHolder, paramIndexHolder, anchorOffsetHolder, alternatesHolder, request.kind)) {
|
tor@581
|
644 |
|
tor@581
|
645 |
return false;
|
tor@581
|
646 |
}
|
tor@581
|
647 |
|
tor@581
|
648 |
IndexedMethod targetMethod = methodHolder[0];
|
tor@581
|
649 |
int index = paramIndexHolder[0];
|
tor@797
|
650 |
|
tor@1551
|
651 |
CallItem callItem = new CallItem(targetMethod, index, anchor, request);
|
tor@1551
|
652 |
proposals.add(callItem);
|
tor@1551
|
653 |
// Also show other documented, not nodoc'ed items (except for those
|
tor@1551
|
654 |
// with identical signatures, such as overrides of the same method)
|
tor@1551
|
655 |
if (alternatesHolder[0] != null) {
|
tor@1551
|
656 |
Set<String> signatures = new HashSet<String>();
|
tor@1551
|
657 |
signatures.add(targetMethod.getSignature().substring(targetMethod.getSignature().indexOf('#')+1));
|
tor@1551
|
658 |
for (IndexedMethod m : alternatesHolder[0]) {
|
tor@1551
|
659 |
if (m != targetMethod && m.isDocumented() && !m.isNoDoc()) {
|
tor@1551
|
660 |
String sig = m.getSignature().substring(m.getSignature().indexOf('#')+1);
|
tor@1551
|
661 |
if (!signatures.contains(sig)) {
|
tor@1551
|
662 |
CallItem item = new CallItem(m, index, anchor, request);
|
tor@1551
|
663 |
proposals.add(item);
|
tor@1551
|
664 |
signatures.add(sig);
|
tor@797
|
665 |
}
|
tor@797
|
666 |
}
|
tor@797
|
667 |
}
|
tor@797
|
668 |
}
|
tor@797
|
669 |
|
tor@581
|
670 |
List<String> params = targetMethod.getParameters();
|
mkrauskopf@2482
|
671 |
if (params == null || params.isEmpty()) {
|
tor@581
|
672 |
return false;
|
tor@581
|
673 |
}
|
tor@581
|
674 |
|
tor@989
|
675 |
if (params.size() <= index) {
|
tor@989
|
676 |
// Just use the last parameter in these cases
|
tor@989
|
677 |
// See for example the TableDefinition.binary dynamic method where
|
tor@989
|
678 |
// you can add a number of parameter names and the options parameter
|
tor@989
|
679 |
// is always the last one
|
tor@989
|
680 |
index = params.size()-1;
|
tor@989
|
681 |
}
|
tor@989
|
682 |
|
tor@581
|
683 |
boolean isLastArg = index < params.size()-1;
|
tor@581
|
684 |
|
tor@581
|
685 |
String attrs = targetMethod.getEncodedAttributes();
|
tor@581
|
686 |
if (attrs != null && attrs.length() > 0) {
|
tor@581
|
687 |
int offset = -1;
|
tor@581
|
688 |
for (int i = 0; i < 3; i++) {
|
tor@581
|
689 |
offset = attrs.indexOf(';', offset+1);
|
tor@581
|
690 |
if (offset == -1) {
|
tor@581
|
691 |
break;
|
tor@581
|
692 |
}
|
tor@581
|
693 |
}
|
tor@602
|
694 |
if (offset == -1) {
|
tor@602
|
695 |
Node root = null;
|
tor@602
|
696 |
if (info != null) {
|
tor@602
|
697 |
root = AstUtilities.getRoot(info);
|
tor@602
|
698 |
}
|
tor@602
|
699 |
|
tor@602
|
700 |
IndexedElement match = findDocumentationEntry(root, targetMethod);
|
tor@602
|
701 |
if (match == targetMethod || !(match instanceof IndexedMethod)) {
|
tor@602
|
702 |
return false;
|
tor@602
|
703 |
}
|
tor@602
|
704 |
targetMethod = (IndexedMethod)match;
|
tor@602
|
705 |
attrs = targetMethod.getEncodedAttributes();
|
tor@602
|
706 |
if (attrs != null && attrs.length() > 0) {
|
tor@602
|
707 |
offset = -1;
|
tor@602
|
708 |
for (int i = 0; i < 3; i++) {
|
tor@602
|
709 |
offset = attrs.indexOf(';', offset+1);
|
tor@602
|
710 |
if (offset == -1) {
|
tor@602
|
711 |
break;
|
tor@602
|
712 |
}
|
tor@602
|
713 |
}
|
tor@602
|
714 |
}
|
tor@602
|
715 |
}
|
tor@581
|
716 |
String currentName = params.get(index);
|
tor@581
|
717 |
if (currentName.startsWith("*")) {
|
tor@581
|
718 |
// * and & are part of the sig
|
tor@581
|
719 |
currentName = currentName.substring(1);
|
tor@581
|
720 |
} else if (currentName.startsWith("&")) {
|
tor@581
|
721 |
currentName = currentName.substring(1);
|
tor@581
|
722 |
}
|
tor@581
|
723 |
if (offset != -1) {
|
tor@581
|
724 |
// Pick apart
|
tor@581
|
725 |
attrs = attrs.substring(offset+1);
|
tor@581
|
726 |
if (attrs.length() == 0) {
|
tor@581
|
727 |
return false;
|
tor@581
|
728 |
}
|
tor@581
|
729 |
String[] argEntries = attrs.split(",");
|
tor@581
|
730 |
for (String entry : argEntries) {
|
tor@581
|
731 |
int parenIndex = entry.indexOf('(');
|
tor@581
|
732 |
assert parenIndex != -1 : attrs;
|
tor@581
|
733 |
String name = entry.substring(0, parenIndex);
|
tor@581
|
734 |
if (currentName.equals(name)) {
|
tor@581
|
735 |
// Found a special parameter desc entry for this
|
tor@581
|
736 |
// parameter - decode it and create completion items
|
tor@581
|
737 |
// Decode
|
tor@581
|
738 |
int endIndex = entry.indexOf(')', parenIndex);
|
tor@581
|
739 |
assert endIndex != -1;
|
tor@581
|
740 |
String data = entry.substring(parenIndex+1, endIndex);
|
tor@581
|
741 |
if (data.length() > 0 && data.charAt(0) == '-') {
|
tor@581
|
742 |
// It's a plain item (e.g. not a hash etc) where
|
tor@581
|
743 |
// we have some logical types to complete
|
tor@581
|
744 |
if ("-table".equals(data)) {
|
tor@581
|
745 |
completeDbTables(proposals, targetMethod, request, isLastArg);
|
tor@581
|
746 |
// Not exiting - I may have other entries here too
|
tor@581
|
747 |
} else if ("-column".equals(data)) {
|
tor@581
|
748 |
completeDbColumns(proposals, targetMethod, request, isLastArg);
|
tor@581
|
749 |
// Not exiting - I may have other entries here too
|
tor@581
|
750 |
} else if ("-model".equals(data)) {
|
tor@581
|
751 |
completeModels(proposals, targetMethod, request, isLastArg);
|
tor@581
|
752 |
}
|
tor@581
|
753 |
} else if (data.startsWith("=>")) {
|
tor@581
|
754 |
// It's a hash; show the given keys
|
tor@581
|
755 |
// TODO: Determine if the caret is in the
|
tor@581
|
756 |
// value part, and if so, show the values instead
|
tor@581
|
757 |
// Uhm... what about fields and such?
|
tor@581
|
758 |
completeHash(proposals, request, targetMethod, data, isLastArg);
|
tor@581
|
759 |
// Not exiting - I may have a non-hash entry here too!
|
tor@581
|
760 |
} else {
|
tor@581
|
761 |
// Just show a fixed set of values
|
tor@581
|
762 |
completeFixed(proposals, request, targetMethod, data, isLastArg);
|
tor@581
|
763 |
// Not exiting - I may have other entries here too
|
tor@1
|
764 |
}
|
tor@1
|
765 |
}
|
tor@1
|
766 |
}
|
tor@1
|
767 |
}
|
tor@1
|
768 |
}
|
tor@1
|
769 |
|
tor@1
|
770 |
return true;
|
tor@1
|
771 |
}
|
tor@1
|
772 |
|
tor@581
|
773 |
// /** Handle insertion of :action, :controller, etc. even for methods without
|
tor@581
|
774 |
// * actual method signatures. Operate at the lexical level.
|
tor@581
|
775 |
// */
|
tor@581
|
776 |
// private void handleRailsKeys(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
|
tor@581
|
777 |
// TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
|
tor@581
|
778 |
// if (ts == null) {
|
tor@581
|
779 |
// return;
|
tor@581
|
780 |
// }
|
tor@581
|
781 |
// boolean inValue = false;
|
tor@581
|
782 |
// ts.move(anchor);
|
tor@581
|
783 |
// String line = null;
|
tor@581
|
784 |
// while (ts.movePrevious()) {
|
tor@581
|
785 |
// final Token token = ts.token();
|
tor@581
|
786 |
// if (token.id() == RubyTokenId.WHITESPACE) {
|
tor@581
|
787 |
// continue;
|
tor@581
|
788 |
// } else if (token.id() == RubyTokenId.NONUNARY_OP &&
|
tor@581
|
789 |
// (token.text().toString().equals("=>"))) { // NOI18N
|
tor@581
|
790 |
// inValue = true;
|
tor@581
|
791 |
// // TODO - continue on to find out what the key is
|
tor@581
|
792 |
// try {
|
tor@581
|
793 |
// BaseDocument doc = request.doc;
|
tor@581
|
794 |
// int lineStart = Utilities.getRowStart(doc, ts.offset());
|
tor@581
|
795 |
// line = doc.getText(lineStart, ts.offset()-lineStart).trim();
|
tor@581
|
796 |
// } catch (BadLocationException ble) {
|
tor@581
|
797 |
// Exceptions.printStackTrace(ble);
|
tor@581
|
798 |
// return;
|
tor@581
|
799 |
// }
|
tor@581
|
800 |
// } else {
|
tor@581
|
801 |
// break;
|
tor@581
|
802 |
// }
|
tor@581
|
803 |
// }
|
tor@581
|
804 |
//
|
tor@581
|
805 |
// if (inValue) {
|
tor@581
|
806 |
// if (line.endsWith(":action")) {
|
tor@581
|
807 |
// // TODO
|
tor@581
|
808 |
// } else if (line.endsWith(":controller")) {
|
tor@581
|
809 |
// // Dynamically produce controllers
|
tor@581
|
810 |
// List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
|
tor@581
|
811 |
// String prefix = request.prefix;
|
tor@581
|
812 |
// for (String n : controllers) {
|
tor@581
|
813 |
// n = "'" + n + "'";
|
tor@581
|
814 |
// if (startsWith(n, prefix)) {
|
tor@581
|
815 |
// String insert = n;
|
tor@581
|
816 |
// if (!isLastArg) {
|
tor@581
|
817 |
// insert = insert + ", ";
|
tor@581
|
818 |
// }
|
tor@581
|
819 |
// ParameterItem item = new ParameterItem(target, n, null, insert, anchor, request);
|
tor@581
|
820 |
// item.setSymbol(true);
|
tor@581
|
821 |
// item.setSmart(true);
|
tor@581
|
822 |
// proposals.add(item);
|
tor@581
|
823 |
// }
|
tor@581
|
824 |
// }
|
tor@581
|
825 |
// } else if (line.endsWith(":partial")) {
|
tor@581
|
826 |
// // TODO
|
tor@581
|
827 |
// }
|
tor@581
|
828 |
// }
|
tor@581
|
829 |
// }
|
tor@581
|
830 |
|
tor@581
|
831 |
private boolean completeHash(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
|
tor@581
|
832 |
assert data.startsWith("=>");
|
tor@581
|
833 |
data = data.substring(2);
|
tor@581
|
834 |
String prefix = request.prefix;
|
tor@581
|
835 |
|
tor@581
|
836 |
// Determine if we're in the key part or the value part when completing
|
tor@581
|
837 |
boolean inValue = false;
|
tor@581
|
838 |
TokenSequence ts = LexUtilities.getRubyTokenSequence(request.doc, anchor);
|
tor@581
|
839 |
if (ts == null) {
|
tor@581
|
840 |
return false;
|
tor@581
|
841 |
}
|
tor@581
|
842 |
ts.move(anchor);
|
tor@581
|
843 |
String line = null;
|
tor@581
|
844 |
|
tor@581
|
845 |
while (ts.movePrevious()) {
|
tor@581
|
846 |
final Token token = ts.token();
|
tor@581
|
847 |
if (token.id() == RubyTokenId.WHITESPACE) {
|
tor@581
|
848 |
continue;
|
tor@581
|
849 |
} else if (token.id() == RubyTokenId.NONUNARY_OP &&
|
tor@581
|
850 |
(token.text().toString().equals("=>"))) { // NOI18N
|
tor@581
|
851 |
inValue = true;
|
tor@581
|
852 |
// TODO - continue on to find out what the key is
|
tor@581
|
853 |
try {
|
tor@581
|
854 |
BaseDocument doc = request.doc;
|
tor@581
|
855 |
int lineStart = Utilities.getRowStart(doc, ts.offset());
|
tor@581
|
856 |
line = doc.getText(lineStart, ts.offset()-lineStart).trim();
|
tor@581
|
857 |
} catch (BadLocationException ble) {
|
tor@581
|
858 |
return false;
|
tor@581
|
859 |
}
|
tor@581
|
860 |
} else {
|
tor@581
|
861 |
break;
|
tor@581
|
862 |
}
|
tor@581
|
863 |
}
|
tor@581
|
864 |
|
tor@581
|
865 |
List<String> suggestions = new ArrayList<String>();
|
tor@581
|
866 |
|
tor@581
|
867 |
String key = null;
|
tor@581
|
868 |
String[] values = data.split("\\|");
|
tor@581
|
869 |
if (inValue) {
|
tor@581
|
870 |
// Find the key and see if we have a type to offer for it
|
tor@581
|
871 |
for (String value : values) {
|
tor@581
|
872 |
int typeIndex = value.indexOf(':');
|
tor@581
|
873 |
if (typeIndex != -1) {
|
tor@581
|
874 |
String name = value.substring(0, typeIndex);
|
tor@581
|
875 |
if (line.endsWith(name)) {
|
tor@581
|
876 |
key = name;
|
tor@581
|
877 |
// Score - it appears we're using the
|
tor@581
|
878 |
// key for this item
|
tor@581
|
879 |
String type = value.substring(typeIndex+1);
|
tor@581
|
880 |
if ("nil".equals(type)) { // NOI18N
|
tor@581
|
881 |
suggestions.add("nil"); // NOI18N
|
tor@581
|
882 |
} else if ("bool".equals(type)) { // NOI18N
|
tor@581
|
883 |
suggestions.add("true"); // NOI18N
|
tor@581
|
884 |
suggestions.add("false"); // NOI18N
|
tor@581
|
885 |
} else if ("submitmethod".equals(type)) { // NOI18N
|
tor@581
|
886 |
suggestions.add("post"); // NOI18N
|
tor@581
|
887 |
suggestions.add("get"); // NOI18N
|
tor@581
|
888 |
} else if ("validationactive".equals(type)) { // NOI18N
|
tor@581
|
889 |
suggestions.add(":save"); // NOI18N
|
tor@581
|
890 |
suggestions.add(":create"); // NOI18N
|
tor@581
|
891 |
suggestions.add(":update"); // NOI18N
|
tor@581
|
892 |
} else if ("string".equals(type)) { // NOI18N
|
tor@581
|
893 |
suggestions.add("\""); // NOI18N
|
tor@581
|
894 |
} else if ("hash".equals(type)) { // NOI18N
|
tor@581
|
895 |
suggestions.add("{"); // NOI18N
|
tor@581
|
896 |
} else if ("controller".equals(type)) {
|
tor@581
|
897 |
// Dynamically produce controllers
|
tor@581
|
898 |
List<String> controllers = RubyUtils.getControllerNames(request.fileObject, true);
|
tor@581
|
899 |
for (String n : controllers) {
|
tor@581
|
900 |
suggestions.add("'" + n + "'");
|
tor@581
|
901 |
}
|
tor@581
|
902 |
} else if ("action".equals(type)) {
|
tor@581
|
903 |
// Dynamically produce actions
|
tor@581
|
904 |
// This would need to be scoped by the current
|
tor@581
|
905 |
// context - look at the hash, find the specified
|
tor@581
|
906 |
// controller and limit it to that
|
tor@607
|
907 |
List<String> actions = getActionNames(request);
|
tor@607
|
908 |
for (String n : actions) {
|
tor@607
|
909 |
suggestions.add("'" + n + "'");
|
tor@607
|
910 |
}
|
emononen@3863
|
911 |
} else if ("status".equals(type)) {
|
emononen@3863
|
912 |
return RubyHttpStatusCodeCompleter.complete(proposals, request, anchor, caseSensitive, target);
|
tor@581
|
913 |
}
|
tor@581
|
914 |
}
|
tor@581
|
915 |
}
|
tor@581
|
916 |
}
|
tor@581
|
917 |
} else {
|
tor@581
|
918 |
for (String value : values) {
|
tor@581
|
919 |
int typeIndex = value.indexOf(':');
|
tor@581
|
920 |
if (typeIndex != -1) {
|
tor@581
|
921 |
value = value.substring(0, typeIndex);
|
tor@581
|
922 |
}
|
tor@581
|
923 |
value = ":" + value + " => ";
|
tor@581
|
924 |
suggestions.add(value);
|
tor@581
|
925 |
}
|
tor@581
|
926 |
}
|
tor@581
|
927 |
|
tor@581
|
928 |
// I've gotta clean up the colon handling in complete()
|
tor@581
|
929 |
// I originally stripped ":" to make direct (INameNode)getName()
|
tor@581
|
930 |
// comparisons on symbols work directly but it's becoming a liability now
|
tor@581
|
931 |
String colonPrefix = ":" + prefix;
|
tor@581
|
932 |
for (String suggestion : suggestions) {
|
tor@581
|
933 |
if (startsWith(suggestion, prefix) || startsWith(suggestion, colonPrefix)) {
|
tor@581
|
934 |
String insert = suggestion;
|
tor@581
|
935 |
String desc = null;
|
tor@581
|
936 |
if (inValue) {
|
tor@581
|
937 |
if (!isLastArg) {
|
tor@581
|
938 |
insert = insert + ", ";
|
tor@581
|
939 |
}
|
tor@581
|
940 |
if (key != null) {
|
tor@581
|
941 |
desc = ":" + key + " = " + suggestion;
|
tor@581
|
942 |
}
|
tor@581
|
943 |
}
|
tor@581
|
944 |
ParameterItem item = new ParameterItem(target, suggestion, desc, insert, anchor, request);
|
tor@581
|
945 |
item.setSymbol(true);
|
tor@581
|
946 |
item.setSmart(true);
|
tor@581
|
947 |
proposals.add(item);
|
tor@581
|
948 |
}
|
tor@581
|
949 |
}
|
tor@581
|
950 |
|
tor@581
|
951 |
return true;
|
tor@581
|
952 |
}
|
tor@581
|
953 |
|
tor@607
|
954 |
/** Get the actions for the given file. If the file is a controller, list the actions within it,
|
tor@607
|
955 |
* otherwise, if the file is a view, list the actions for the corresponding controller.
|
tor@607
|
956 |
*
|
tor@607
|
957 |
* @param fileInProject the file we're looking up
|
tor@607
|
958 |
* @return A List of action names
|
tor@607
|
959 |
*/
|
tor@607
|
960 |
private List<String> getActionNames(CompletionRequest request) {
|
tor@607
|
961 |
FileObject file = request.fileObject;
|
tor@607
|
962 |
FileObject controllerFile = null;
|
tor@607
|
963 |
if (file.getNameExt().endsWith("_controller.rb")) {
|
tor@607
|
964 |
controllerFile = file;
|
tor@607
|
965 |
} else {
|
tor@607
|
966 |
controllerFile = RubyUtils.getRailsControllerFor(file);
|
tor@607
|
967 |
}
|
tor@607
|
968 |
// TODO - check for other :controller-> settings in the hashmap and if present, use it
|
tor@607
|
969 |
if (controllerFile == null) {
|
tor@607
|
970 |
return Collections.emptyList();
|
tor@607
|
971 |
}
|
tor@607
|
972 |
|
tor@607
|
973 |
String controllerClass = RubyUtils.getControllerClass(controllerFile);
|
tor@607
|
974 |
if (controllerClass != null) {
|
tor@607
|
975 |
String prefix = request.prefix;
|
tor@607
|
976 |
Set<IndexedMethod> methods = request.index.getMethods(prefix, controllerClass, request.kind);
|
tor@607
|
977 |
List<String> actions = new ArrayList<String>();
|
tor@607
|
978 |
for (IndexedMethod method : methods) {
|
tor@607
|
979 |
if (method.isPublic() && method.getArgs() == null || method.getArgs().length == 0) {
|
tor@607
|
980 |
actions.add(method.getName());
|
tor@607
|
981 |
}
|
tor@607
|
982 |
}
|
tor@607
|
983 |
|
tor@607
|
984 |
return actions;
|
tor@607
|
985 |
}
|
tor@607
|
986 |
|
tor@607
|
987 |
// TODO - pull out the methods or this class
|
tor@607
|
988 |
|
tor@607
|
989 |
return Collections.emptyList();
|
tor@607
|
990 |
}
|
tor@607
|
991 |
|
tor@581
|
992 |
private void completeFixed(List<CompletionProposal> proposals, CompletionRequest request, IndexedMethod target, String data, boolean isLastArg) {
|
tor@581
|
993 |
String[] values = data.split("\\|");
|
tor@581
|
994 |
String prefix = request.prefix;
|
tor@604
|
995 |
// I originally stripped ":" to make direct (INameNode)getName()
|
tor@604
|
996 |
// comparisons on symbols work directly but it's becoming a liability now
|
tor@604
|
997 |
String colonPrefix = ":" + prefix;
|
tor@581
|
998 |
for (String value : values) {
|
tor@604
|
999 |
if (startsWith(value, prefix) || startsWith(value, colonPrefix)) {
|
tor@581
|
1000 |
String insert = isLastArg ? value : (value + ", ");
|
tor@581
|
1001 |
ParameterItem item = new ParameterItem(target, value, null, insert, anchor, request);
|
tor@581
|
1002 |
item.setSymbol(true);
|
tor@581
|
1003 |
item.setSmart(true);
|
tor@581
|
1004 |
proposals.add(item);
|
tor@581
|
1005 |
}
|
tor@581
|
1006 |
}
|
tor@581
|
1007 |
}
|
tor@581
|
1008 |
|
tor@581
|
1009 |
private void completeDbTables(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
|
tor@581
|
1010 |
// Add in the eligible database tables found in this project
|
tor@581
|
1011 |
// Assumes this is a Rails project
|
tor@604
|
1012 |
String p = request.prefix;
|
tor@604
|
1013 |
String colonPrefix = p;
|
tor@604
|
1014 |
if (":".equals(p)) { // NOI18N
|
tor@604
|
1015 |
p = "";
|
tor@604
|
1016 |
} else {
|
tor@604
|
1017 |
colonPrefix = ":" + p; // NOI18N
|
tor@604
|
1018 |
}
|
tor@604
|
1019 |
Set<String> tables = request.index.getDatabaseTables(p, request.kind);
|
tor@581
|
1020 |
|
tor@604
|
1021 |
// I originally stripped ":" to make direct (INameNode)getName()
|
tor@604
|
1022 |
// comparisons on symbols work directly but it's becoming a liability now
|
tor@581
|
1023 |
String prefix = request.prefix;
|
tor@581
|
1024 |
for (String table : tables) {
|
tor@607
|
1025 |
// PENDING: Should I insert :tablename or 'tablename' or "tablename" ?
|
tor@604
|
1026 |
String tableName = ":" + table;
|
tor@604
|
1027 |
if (startsWith(tableName, prefix) || startsWith(tableName, colonPrefix)) {
|
tor@581
|
1028 |
String insert = isLastArg ? tableName : (tableName + ", ");
|
tor@581
|
1029 |
ParameterItem item = new ParameterItem(target, tableName, null, insert, anchor, request);
|
tor@581
|
1030 |
item.setSymbol(true);
|
tor@581
|
1031 |
item.setSmart(true);
|
tor@581
|
1032 |
proposals.add(item);
|
tor@581
|
1033 |
}
|
tor@581
|
1034 |
}
|
tor@581
|
1035 |
}
|
tor@581
|
1036 |
|
tor@581
|
1037 |
private void completeModels(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
|
emononen@3340
|
1038 |
Set<IndexedClass> clz = request.index.getSubClasses(request.prefix, RubyIndex.ACTIVE_RECORD_BASE, request.kind);
|
tor@581
|
1039 |
|
tor@581
|
1040 |
String prefix = request.prefix;
|
tor@607
|
1041 |
// I originally stripped ":" to make direct (INameNode)getName()
|
tor@607
|
1042 |
// comparisons on symbols work directly but it's becoming a liability now
|
tor@607
|
1043 |
String colonPrefix = ":" + prefix;
|
tor@581
|
1044 |
for (IndexedClass c : clz) {
|
tor@581
|
1045 |
String name = c.getName();
|
tor@581
|
1046 |
String symbol = ":"+RubyUtils.camelToUnderlinedName(name);
|
tor@607
|
1047 |
if (startsWith(symbol, prefix) || startsWith(symbol, colonPrefix)) {
|
tor@581
|
1048 |
String insert = isLastArg ? symbol : (symbol + ", ");
|
tor@581
|
1049 |
ParameterItem item = new ParameterItem(target, symbol, name, insert, anchor, request);
|
tor@581
|
1050 |
item.setSymbol(true);
|
tor@581
|
1051 |
item.setSmart(true);
|
tor@581
|
1052 |
proposals.add(item);
|
tor@581
|
1053 |
}
|
tor@581
|
1054 |
}
|
tor@581
|
1055 |
}
|
tor@581
|
1056 |
|
tor@581
|
1057 |
private void completeDbColumns(List<CompletionProposal> proposals, IndexedMethod target, CompletionRequest request, boolean isLastArg) {
|
tor@581
|
1058 |
// Add in the eligible database tables found in this project
|
tor@581
|
1059 |
// Assumes this is a Rails project
|
mkrauskopf@2482
|
1060 |
// Set<String> tables = request.index.getDatabaseTables(request.prefix, request.kind);
|
tor@581
|
1061 |
|
tor@581
|
1062 |
// TODO
|
tor@581
|
1063 |
// for (String table : tables) {
|
tor@581
|
1064 |
// if (startsWith(table, prefix)) {
|
tor@581
|
1065 |
// SymbolHashItem item = new SymbolHashItem(target, ":" + table, null, anchor, request);
|
tor@581
|
1066 |
// item.setSymbol(true);
|
tor@581
|
1067 |
// proposals.add(item);
|
tor@581
|
1068 |
// }
|
tor@581
|
1069 |
// }
|
tor@581
|
1070 |
}
|
tor@581
|
1071 |
|
enebo@4519
|
1072 |
private boolean isEmpty(String value) {
|
enebo@4519
|
1073 |
return value == null || value.length() == 0;
|
enebo@4519
|
1074 |
}
|
tor@581
|
1075 |
|
tor@1
|
1076 |
// TODO: Move to the top
|
enebo@4519
|
1077 |
@Override
|
mkrauskopf@3030
|
1078 |
public CodeCompletionResult complete(final CodeCompletionContext context) {
|
mkrauskopf@3030
|
1079 |
ParserResult ir = context.getParserResult();
|
tor@1663
|
1080 |
int lexOffset = context.getCaretOffset();
|
tor@1663
|
1081 |
String prefix = context.getPrefix();
|
mkrauskopf@3030
|
1082 |
QuerySupport.Kind kind = context.isPrefixMatch() ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.EXACT;
|
tor@1663
|
1083 |
QueryType queryType = context.getQueryType();
|
tor@1663
|
1084 |
this.caseSensitive = context.isCaseSensitive();
|
tor@1
|
1085 |
|
mkrauskopf@3030
|
1086 |
final int astOffset = AstUtilities.getAstOffset(ir, lexOffset);
|
enebo@4519
|
1087 |
if (astOffset == -1) return null;
|
tor@1
|
1088 |
|
tor@1
|
1089 |
// Avoid all those annoying null checks
|
enebo@4519
|
1090 |
if (prefix == null) prefix = "";
|
tor@1
|
1091 |
|
tor@1
|
1092 |
List<CompletionProposal> proposals = new ArrayList<CompletionProposal>();
|
tor@1663
|
1093 |
DefaultCompletionResult completionResult = new DefaultCompletionResult(proposals, false);
|
tor@1
|
1094 |
|
tor@1
|
1095 |
anchor = lexOffset - prefix.length();
|
tor@1
|
1096 |
|
mkrauskopf@3030
|
1097 |
final RubyIndex index = RubyIndex.get(ir);
|
tor@103
|
1098 |
|
mkrauskopf@3030
|
1099 |
final Document document = RubyUtils.getDocument(ir);
|
enebo@4519
|
1100 |
if (document == null) return CodeCompletionResult.NONE;
|
tor@1
|
1101 |
|
tor@1
|
1102 |
// TODO - move to LexUtilities now that this applies to the lexing offset?
|
mkrauskopf@3030
|
1103 |
lexOffset = AstUtilities.boundCaretOffset(ir, lexOffset);
|
tor@1
|
1104 |
|
tor@1
|
1105 |
// Discover whether we're in a require statement, and if so, use special completion
|
emononen@3859
|
1106 |
final TokenHierarchy<Document> th = TokenHierarchy.get(document);
|
tor@103
|
1107 |
final BaseDocument doc = (BaseDocument)document;
|
mkrauskopf@3030
|
1108 |
final FileObject fileObject = RubyUtils.getFileObject(ir);
|
tor@2324
|
1109 |
|
tor@103
|
1110 |
boolean showLower = true;
|
tor@103
|
1111 |
boolean showUpper = true;
|
tor@103
|
1112 |
boolean showSymbols = false;
|
tor@103
|
1113 |
char first = 0;
|
tor@103
|
1114 |
|
emononen@3859
|
1115 |
doc.readLock(); // Read-lock due to Token hierarchy use
|
emononen@3859
|
1116 |
try {
|
tor@103
|
1117 |
if (prefix.length() > 0) {
|
tor@103
|
1118 |
first = prefix.charAt(0);
|
tor@103
|
1119 |
|
tor@103
|
1120 |
// Foo::bar --> first char is "b" - we're looking for a method
|
tor@103
|
1121 |
int qualifier = prefix.lastIndexOf("::");
|
enebo@4519
|
1122 |
if (qualifier != -1 && qualifier < prefix.length() - 2) first = prefix.charAt(qualifier + 2);
|
tor@103
|
1123 |
|
tor@103
|
1124 |
showLower = Character.isLowerCase(first);
|
tor@103
|
1125 |
// showLower is not necessarily !showUpper - prefix can be ":foo" for example
|
tor@103
|
1126 |
showUpper = Character.isUpperCase(first);
|
tor@103
|
1127 |
|
tor@103
|
1128 |
if (first == ':') {
|
tor@103
|
1129 |
showSymbols = true;
|
tor@103
|
1130 |
|
tor@103
|
1131 |
if (prefix.length() > 1) {
|
tor@103
|
1132 |
char second = prefix.charAt(1);
|
tor@103
|
1133 |
prefix = prefix.substring(1);
|
tor@103
|
1134 |
showLower = Character.isLowerCase(second);
|
tor@103
|
1135 |
showUpper = Character.isUpperCase(second);
|
tor@103
|
1136 |
}
|
tor@103
|
1137 |
}
|
tor@103
|
1138 |
}
|
emononen@3810
|
1139 |
|
tor@14
|
1140 |
// Carry completion context around since this logic is split across lots of methods
|
tor@14
|
1141 |
// and I don't want to pass dozens of parameters from method to method; just pass
|
mkrauskopf@3030
|
1142 |
// a request context with supporting parserResult needed by the various completion helpers.
|
mkrauskopf@2743
|
1143 |
CompletionRequest request = new CompletionRequest(
|
mkrauskopf@3030
|
1144 |
completionResult, th, ir, lexOffset, astOffset,
|
mkrauskopf@2743
|
1145 |
doc, prefix, index, kind, queryType, fileObject);
|
emononen@3810
|
1146 |
|
tor@1
|
1147 |
// See if we're inside a string or regular expression and if so,
|
tor@1
|
1148 |
// do completions applicable to strings - require-completion,
|
tor@1
|
1149 |
// escape codes for quoted strings and regular expressions, etc.
|
mkrauskopf@2743
|
1150 |
if (RubyStringCompleter.complete(proposals, request, anchor, caseSensitive)) {
|
tor@2061
|
1151 |
completionResult.setFilterable(false);
|
tor@1663
|
1152 |
return completionResult;
|
tor@1
|
1153 |
}
|
emononen@3810
|
1154 |
|
tor@450
|
1155 |
Call call = Call.getCallType(doc, th, lexOffset);
|
tor@450
|
1156 |
|
tor@1
|
1157 |
// Fields
|
tor@1
|
1158 |
// This is a bit stupid at the moment, not looking at the current typing context etc.
|
mkrauskopf@3030
|
1159 |
Node root = AstUtilities.getRoot(ir);
|
tor@1
|
1160 |
|
tor@1
|
1161 |
if (root == null) {
|
mkrauskopf@2743
|
1162 |
RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
|
tor@1663
|
1163 |
return completionResult;
|
tor@1
|
1164 |
}
|
tor@1
|
1165 |
|
tor@1
|
1166 |
// Compute the bounds of the line that the caret is on, and suppress nodes overlapping the line.
|
tor@1
|
1167 |
// This will hide not only paritally typed identifiers, but surrounding contents like the current class and module
|
tor@103
|
1168 |
final int astLineBegin;
|
tor@103
|
1169 |
final int astLineEnd;
|
tor@1
|
1170 |
|
tor@1
|
1171 |
try {
|
mkrauskopf@3030
|
1172 |
astLineBegin = AstUtilities.getAstOffset(ir, Utilities.getRowStart(doc, lexOffset));
|
mkrauskopf@3030
|
1173 |
astLineEnd = AstUtilities.getAstOffset(ir, Utilities.getRowEnd(doc, lexOffset));
|
tor@1
|
1174 |
} catch (BadLocationException ble) {
|
tor@2496
|
1175 |
return CodeCompletionResult.NONE;
|
tor@1
|
1176 |
}
|
tor@1
|
1177 |
|
tor@103
|
1178 |
final AstPath path = new AstPath(root, astOffset);
|
tor@14
|
1179 |
request.path = path;
|
tor@1
|
1180 |
|
tor@1
|
1181 |
Map<String, Node> variables = new HashMap<String, Node>();
|
tor@1
|
1182 |
Map<String, Node> fields = new HashMap<String, Node>();
|
tor@1
|
1183 |
Map<String, Node> globals = new HashMap<String, Node>();
|
tor@1
|
1184 |
Map<String, Node> constants = new HashMap<String, Node>();
|
tor@1
|
1185 |
|
tor@103
|
1186 |
final Node closest = path.leaf();
|
mkrauskopf@2834
|
1187 |
request.target = closest;
|
tor@1
|
1188 |
|
tor@1
|
1189 |
// Don't try to add local vars, globals etc. as part of calls or class fqns
|
tor@1
|
1190 |
if (call.getLhs() == null) {
|
enebo@4519
|
1191 |
if (showLower && closest != null) {
|
enebo@4519
|
1192 |
for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
|
tor@681
|
1193 |
addDynamic(block, variables);
|
tor@1
|
1194 |
}
|
emononen@3810
|
1195 |
|
enebo@4519
|
1196 |
for (Node child : AstUtilities.findLocalScope(closest, path).childNodes()) {
|
tor@1
|
1197 |
addLocals(child, variables);
|
tor@1
|
1198 |
}
|
tor@1
|
1199 |
}
|
tor@1
|
1200 |
|
emononen@3582
|
1201 |
boolean inAttrCall = isInAttr(closest, path);
|
emononen@3582
|
1202 |
|
emononen@3582
|
1203 |
if (prefix.length() == 0 || first == '@' || showSymbols || inAttrCall) {
|
tor@411
|
1204 |
String fqn = AstUtilities.getFqnName(path);
|
tor@411
|
1205 |
|
enebo@4519
|
1206 |
if (isEmpty(fqn)) {
|
emononen@4102
|
1207 |
String fileName = RubyUtils.getFileObject(context.getParserResult()).getName();
|
emononen@4102
|
1208 |
if (fileName.endsWith("_spec")) { //NOI18N
|
emononen@4102
|
1209 |
// use the virtual class created for the spec file
|
emononen@4102
|
1210 |
fqn = RubyUtils.underlinedNameToCamel(fileName);
|
emononen@4102
|
1211 |
} else {
|
emononen@4102
|
1212 |
fqn = "Object"; // NOI18N
|
emononen@4102
|
1213 |
}
|
tor@411
|
1214 |
}
|
tor@411
|
1215 |
|
tor@411
|
1216 |
// TODO - if fqn has multiple ::'s, try various combinations? or is
|
tor@411
|
1217 |
// add inherited already doing that?
|
emononen@3810
|
1218 |
|
tor@480
|
1219 |
Set<IndexedField> f;
|
tor@480
|
1220 |
if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
|
tor@480
|
1221 |
f = new HashSet<IndexedField>();
|
tor@480
|
1222 |
addActionViewFields(f, fileObject, index, prefix, kind);
|
tor@480
|
1223 |
} else {
|
emononen@3810
|
1224 |
//strip out ':' when querying fields for cases like 'attr_reader :^'
|
emononen@3582
|
1225 |
if (inAttrCall && first == ':' && prefix.length() == 1) {
|
emononen@3582
|
1226 |
f = index.getInheritedFields(fqn, "", kind, false);
|
emononen@3582
|
1227 |
} else {
|
emononen@3582
|
1228 |
f = index.getInheritedFields(fqn, prefix, kind, false);
|
emononen@3582
|
1229 |
}
|
tor@480
|
1230 |
}
|
tor@480
|
1231 |
|
tor@411
|
1232 |
for (IndexedField field : f) {
|
emononen@3582
|
1233 |
String insertPrefix = inAttrCall ? ":" : null;
|
emononen@3582
|
1234 |
FieldItem item = new FieldItem(field, anchor, request, insertPrefix);
|
tor@411
|
1235 |
|
tor@411
|
1236 |
item.setSmart(field.isSmart());
|
tor@411
|
1237 |
|
enebo@4519
|
1238 |
if (showSymbols) item.setSymbol(true);
|
tor@411
|
1239 |
|
tor@411
|
1240 |
proposals.add(item);
|
tor@1
|
1241 |
}
|
emononen@3582
|
1242 |
|
emononen@3582
|
1243 |
// return just the fields for attr_
|
enebo@4519
|
1244 |
if (inAttrCall) return completionResult;
|
tor@1
|
1245 |
}
|
tor@1
|
1246 |
|
tor@1
|
1247 |
// $ is neither upper nor lower
|
enebo@4519
|
1248 |
if (prefix.length() == 0 || first == '$' || showSymbols) {
|
tor@2324
|
1249 |
if (prefix.startsWith("$") || showSymbols) {
|
tor@2324
|
1250 |
completeGlobals(proposals, request, showSymbols);
|
tor@2355
|
1251 |
// Dollar variables too
|
mkrauskopf@2743
|
1252 |
RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols);
|
enebo@4519
|
1253 |
|
enebo@4519
|
1254 |
if (!showSymbols) return completionResult;
|
tor@1
|
1255 |
}
|
tor@1
|
1256 |
}
|
tor@1
|
1257 |
}
|
tor@1
|
1258 |
|
tor@1
|
1259 |
// TODO: should only include fields etc. down to caret location??? Decide. (Depends on language semantics. Can I have forward referemces?
|
mkrauskopf@2768
|
1260 |
if (call.isConstantExpected()) {
|
mkrauskopf@2744
|
1261 |
RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
|
emononen@3749
|
1262 |
RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
|
emononen@3749
|
1263 |
RubyType type = call.getType();
|
emononen@3749
|
1264 |
if (type.isKnown() && type.isSingleton()) {
|
emononen@3749
|
1265 |
RubyMethodCompleter.complete(proposals, request, type.first(), call, anchor, caseSensitive);
|
emononen@3749
|
1266 |
}
|
emononen@3603
|
1267 |
return completionResult;
|
tor@1
|
1268 |
}
|
emononen@3810
|
1269 |
|
mkrauskopf@3030
|
1270 |
// If we're in a call, add in some parserResult and help for the code completion call
|
tor@581
|
1271 |
boolean inCall = addParameters(proposals, request);
|
tor@1
|
1272 |
|
tor@1
|
1273 |
// Code completion from the index.
|
tor@1
|
1274 |
if (index != null) {
|
tor@1
|
1275 |
if (showLower || showSymbols) {
|
tor@1
|
1276 |
String fqn = AstUtilities.getFqnName(path);
|
enebo@4519
|
1277 |
if (isEmpty(fqn)) fqn = "Object"; // NOI18N
|
tor@1
|
1278 |
|
enebo@4519
|
1279 |
// doesn't apply to (or work with) documentation/tooltip help
|
enebo@4519
|
1280 |
if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, fqn)) {
|
tor@1663
|
1281 |
return completionResult;
|
tor@1
|
1282 |
}
|
tor@1
|
1283 |
|
enebo@4519
|
1284 |
if (RubyMethodCompleter.complete(proposals, request, fqn, call, anchor, caseSensitive)) {
|
tor@1663
|
1285 |
return completionResult;
|
tor@1
|
1286 |
}
|
tor@1
|
1287 |
|
tor@1
|
1288 |
// Only call local and inherited methods if we don't have an LHS, such as Foo::
|
tor@1
|
1289 |
if (call.getLhs() == null) {
|
tor@1
|
1290 |
// TODO - pull this into a completeInheritedMethod call
|
tor@1
|
1291 |
// Complete inherited methods or local methods only (plus keywords) since there
|
tor@1
|
1292 |
// is no receiver so it must be a local or inherited method call
|
enebo@4519
|
1293 |
Set<IndexedMethod> inheritedMethods = index.getInheritedMethods(fqn, prefix, kind);
|
tor@581
|
1294 |
|
emononen@3351
|
1295 |
inheritedMethods = RubyDynamicFindersCompleter.proposeDynamicMethods(inheritedMethods, proposals, request, anchor);
|
tor@1
|
1296 |
// Handle action view completion for RHTML and Markaby files
|
tor@1
|
1297 |
if (RubyUtils.isRhtmlFile(fileObject) || RubyUtils.isMarkabyFile(fileObject)) {
|
tor@480
|
1298 |
addActionViewMethods(inheritedMethods, fileObject, index, prefix, kind);
|
tor@480
|
1299 |
} else if (fileObject.getName().endsWith("_spec")) { // NOI18N
|
tor@480
|
1300 |
// RSpec
|
emononen@3810
|
1301 |
|
tor@581
|
1302 |
/* My spec object had the following extras methods over a plain Object:
|
emononen@3810
|
1303 |
x = self.class.methods
|
emononen@3810
|
1304 |
x.each {|c|
|
emononen@3810
|
1305 |
puts c
|
emononen@3810
|
1306 |
}
|
emononen@3810
|
1307 |
> args_and_options
|
emononen@3810
|
1308 |
> context
|
emononen@3810
|
1309 |
> copy_instance_variables_from
|
emononen@3810
|
1310 |
> describe
|
emononen@3810
|
1311 |
> gem
|
emononen@3810
|
1312 |
> metaclass
|
emononen@3810
|
1313 |
> require
|
emononen@3810
|
1314 |
> require_gem
|
emononen@3810
|
1315 |
> respond_to
|
emononen@3810
|
1316 |
> should
|
emononen@3810
|
1317 |
> should_not
|
tor@581
|
1318 |
*/
|
emononen@3810
|
1319 |
String includes[] = {
|
tor@480
|
1320 |
// "describe" should be in Kernel already, from spec/runner/extensions/kernel.rb
|
tor@480
|
1321 |
"Spec::Matchers",
|
tor@480
|
1322 |
// This one shouldn't be necessary since there's a
|
tor@480
|
1323 |
// "class Object; include xxx::ObjectExpectations; end" in rspec's object.rb
|
emononen@3810
|
1324 |
"Spec::Expectations::ObjectExpectations",
|
emononen@3810
|
1325 |
"Spec::DSL::BehaviourEval::InstanceMethods"}; // NOI18N
|
tor@480
|
1326 |
for (String fqns : includes) {
|
enebo@4519
|
1327 |
inheritedMethods.addAll(index.getInheritedMethods(fqns, prefix, kind));
|
tor@480
|
1328 |
}
|
tor@480
|
1329 |
}
|
tor@1
|
1330 |
|
tor@1
|
1331 |
for (IndexedMethod method : inheritedMethods) {
|
tor@1
|
1332 |
// This should not be necessary - filtering happens in getInheritedMethods right?
|
enebo@4519
|
1333 |
if (prefix.length() > 0 && !method.getName().startsWith(prefix)) continue;
|
enebo@4519
|
1334 |
if (method.isNoDoc()) continue;
|
emononen@3810
|
1335 |
|
tor@1
|
1336 |
// If a method is an "initialize" method I should do something special so that
|
tor@1
|
1337 |
// it shows up as a "constructor" (in a new() statement) but not as a directly
|
tor@1
|
1338 |
// callable initialize method (it should already be culled because it's private)
|
tor@14
|
1339 |
MethodItem item = new MethodItem(method, anchor, request);
|
tor@1
|
1340 |
|
tor@1
|
1341 |
item.setSmart(method.isSmart());
|
tor@1
|
1342 |
|
enebo@4519
|
1343 |
if (showSymbols) item.setSymbol(true);
|
tor@1
|
1344 |
|
tor@1
|
1345 |
proposals.add(item);
|
tor@1
|
1346 |
}
|
tor@1
|
1347 |
}
|
tor@1
|
1348 |
}
|
tor@1
|
1349 |
|
tor@712
|
1350 |
if (showUpper) {
|
enebo@4519
|
1351 |
// doesn't apply to (or work with) documentation/tooltip help
|
enebo@4519
|
1352 |
if (queryType == QueryType.COMPLETION && completeDefOrInclude(proposals, request, "")) {
|
tor@1663
|
1353 |
return completionResult;
|
tor@712
|
1354 |
}
|
tor@712
|
1355 |
}
|
tor@731
|
1356 |
if ((showUpper && ((prefix != null && prefix.length() > 0) ||
|
emononen@3810
|
1357 |
(!call.isMethodExpected() && call.getLhs() != null && call.getLhs().length() > 0))) || (showSymbols && !inCall)) {
|
tor@712
|
1358 |
// TODO - allow method calls if you're already entered the first char!
|
emononen@3801
|
1359 |
RubyConstantCompleter.complete(proposals, request, anchor, caseSensitive, call);
|
mkrauskopf@2743
|
1360 |
RubyClassCompleter.complete(proposals, request, anchor, caseSensitive, call, showSymbols);
|
tor@1
|
1361 |
}
|
tor@1
|
1362 |
}
|
mkrauskopf@3030
|
1363 |
assert (kind == QuerySupport.Kind.PREFIX) || (kind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX) ||
|
emononen@3810
|
1364 |
(kind == QuerySupport.Kind.EXACT);
|
tor@1
|
1365 |
|
tor@1
|
1366 |
// TODO
|
tor@1
|
1367 |
// Remove fields and variables whose names are already taken, e.g. do a fields.removeAll(variables) etc.
|
tor@1
|
1368 |
for (String variable : variables.keySet()) {
|
mkrauskopf@3030
|
1369 |
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
|
mkrauskopf@3030
|
1370 |
((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
|
tor@1
|
1371 |
Node node = variables.get(variable);
|
tor@1
|
1372 |
|
tor@1
|
1373 |
if (!overlapsLine(node, astLineBegin, astLineEnd)) {
|
mkrauskopf@3030
|
1374 |
AstElement co = new AstNameElement(ir, node, variable,
|
tor@1867
|
1375 |
ElementKind.VARIABLE);
|
tor@2458
|
1376 |
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
|
tor@1
|
1377 |
item.setSmart(true);
|
tor@1
|
1378 |
|
tor@1
|
1379 |
if (showSymbols) {
|
tor@1
|
1380 |
item.setSymbol(true);
|
tor@1
|
1381 |
}
|
tor@1
|
1382 |
|
tor@1
|
1383 |
proposals.add(item);
|
tor@1
|
1384 |
}
|
tor@1
|
1385 |
}
|
tor@1
|
1386 |
}
|
tor@1
|
1387 |
|
tor@1
|
1388 |
for (String field : fields.keySet()) {
|
mkrauskopf@3030
|
1389 |
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(field)) ||
|
mkrauskopf@3030
|
1390 |
((kind != QuerySupport.Kind.EXACT) && startsWith(field, prefix))) {
|
tor@1
|
1391 |
Node node = fields.get(field);
|
tor@1
|
1392 |
|
tor@1
|
1393 |
if (overlapsLine(node, astLineBegin, astLineEnd)) {
|
tor@1
|
1394 |
continue;
|
tor@1
|
1395 |
}
|
tor@1
|
1396 |
|
mkrauskopf@3030
|
1397 |
Element co = new AstFieldElement(ir, node);
|
tor@14
|
1398 |
FieldItem item = new FieldItem(co, anchor, request);
|
tor@1
|
1399 |
item.setSmart(true);
|
tor@1
|
1400 |
|
tor@1
|
1401 |
if (showSymbols) {
|
tor@1
|
1402 |
item.setSymbol(true);
|
tor@1
|
1403 |
}
|
tor@1
|
1404 |
|
tor@1
|
1405 |
proposals.add(item);
|
tor@1
|
1406 |
}
|
tor@1
|
1407 |
}
|
tor@1
|
1408 |
|
tor@1
|
1409 |
// TODO - model globals and constants using different icons / etc.
|
tor@1
|
1410 |
for (String variable : globals.keySet()) {
|
mkrauskopf@3030
|
1411 |
// TODO - kind.EXACT
|
tor@1
|
1412 |
if (startsWith(variable, prefix) ||
|
tor@1
|
1413 |
(showSymbols && startsWith(variable.substring(1), prefix))) {
|
tor@1
|
1414 |
Node node = globals.get(variable);
|
tor@1
|
1415 |
|
tor@1
|
1416 |
if (overlapsLine(node, astLineBegin, astLineEnd)) {
|
tor@1
|
1417 |
continue;
|
tor@1
|
1418 |
}
|
tor@1
|
1419 |
|
mkrauskopf@3030
|
1420 |
AstElement co = new AstNameElement(ir, node, variable,
|
tor@1867
|
1421 |
ElementKind.VARIABLE);
|
tor@2458
|
1422 |
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
|
tor@1
|
1423 |
item.setSmart(true);
|
tor@1
|
1424 |
|
tor@1
|
1425 |
if (showSymbols) {
|
tor@1
|
1426 |
item.setSymbol(true);
|
tor@1
|
1427 |
}
|
tor@1
|
1428 |
|
tor@1
|
1429 |
proposals.add(item);
|
tor@1
|
1430 |
}
|
tor@1
|
1431 |
}
|
emononen@3810
|
1432 |
|
tor@1
|
1433 |
// TODO - model globals and constants using different icons / etc.
|
tor@1
|
1434 |
for (String variable : constants.keySet()) {
|
mkrauskopf@3030
|
1435 |
if (((kind == QuerySupport.Kind.EXACT) && prefix.equals(variable)) ||
|
mkrauskopf@3030
|
1436 |
((kind != QuerySupport.Kind.EXACT) && startsWith(variable, prefix))) {
|
tor@1
|
1437 |
// Skip constants that are known to be classes
|
tor@1
|
1438 |
Node node = constants.get(variable);
|
tor@1
|
1439 |
|
enebo@4519
|
1440 |
if (overlapsLine(node, astLineBegin, astLineEnd)) continue;
|
tor@1
|
1441 |
|
tor@1
|
1442 |
// ComObject co;
|
tor@1
|
1443 |
// if (isClassName(variable)) {
|
mkrauskopf@2834
|
1444 |
// co = JRubyNode.create(target, null);
|
tor@1
|
1445 |
// if (co == null) {
|
tor@1
|
1446 |
// continue;
|
tor@1
|
1447 |
// }
|
tor@1
|
1448 |
// } else {
|
tor@1
|
1449 |
// co = new DefaultComVariable(variable, false, -1, -1);
|
mkrauskopf@2834
|
1450 |
// ((DefaultComVariable)co).setNode(target);
|
enebo@4519
|
1451 |
AstElement co = new AstNameElement(ir, node, variable, ElementKind.VARIABLE);
|
tor@1867
|
1452 |
|
tor@2458
|
1453 |
RubyCompletionItem item = new RubyCompletionItem(co, anchor, request);
|
tor@1
|
1454 |
item.setSmart(true);
|
tor@1
|
1455 |
|
tor@1
|
1456 |
if (showSymbols) {
|
tor@1
|
1457 |
item.setSymbol(true);
|
tor@1
|
1458 |
}
|
tor@1
|
1459 |
|
tor@1
|
1460 |
proposals.add(item);
|
tor@1
|
1461 |
}
|
tor@1
|
1462 |
}
|
tor@1
|
1463 |
|
mkrauskopf@2743
|
1464 |
if (RubyKeywordCompleter.complete(proposals, request, anchor, caseSensitive, showSymbols)) {
|
tor@1663
|
1465 |
return completionResult;
|
tor@1
|
1466 |
}
|
tor@1
|
1467 |
|
tor@1
|
1468 |
if (queryType == QueryType.DOCUMENTATION) {
|
mkrauskopf@3030
|
1469 |
proposals = filterDocumentation(proposals, root, doc, ir, astOffset, lexOffset, prefix, path,
|
tor@1
|
1470 |
index);
|
tor@1
|
1471 |
}
|
emononen@3859
|
1472 |
} finally {
|
emononen@3859
|
1473 |
doc.readUnlock();
|
emononen@3859
|
1474 |
}
|
emononen@3859
|
1475 |
|
tor@1663
|
1476 |
return completionResult;
|
tor@1
|
1477 |
}
|
tor@1
|
1478 |
|
emononen@3582
|
1479 |
|
emononen@3582
|
1480 |
private boolean isInAttr(Node closest, AstPath path) {
|
emononen@3582
|
1481 |
if (closest != null) {
|
emononen@3582
|
1482 |
// first argument in attr_*
|
emononen@3582
|
1483 |
for (Node child : closest.childNodes()) {
|
emononen@3582
|
1484 |
if (AstUtilities.isAttr(child)) {
|
emononen@3582
|
1485 |
return true;
|
emononen@3582
|
1486 |
}
|
emononen@3582
|
1487 |
}
|
emononen@3582
|
1488 |
// others, e.g. attr_reader :foo, :ba^r
|
emononen@3582
|
1489 |
if (AstUtilities.isAttr(path.leafParent()) || AstUtilities.isAttr(path.leafGrandParent())) {
|
emononen@3582
|
1490 |
return true;
|
emononen@3582
|
1491 |
}
|
emononen@3582
|
1492 |
}
|
emononen@3582
|
1493 |
return false;
|
emononen@3582
|
1494 |
}
|
emononen@3582
|
1495 |
|
emononen@3582
|
1496 |
private void addActionViewMethods(Set<IndexedMethod> inheritedMethods, FileObject fileObject, RubyIndex index, String prefix,
|
mkrauskopf@3030
|
1497 |
QuerySupport.Kind kind) {
|
tor@1
|
1498 |
// RHTML and Markaby: Add in the helper methods etc. from the associated files
|
tor@1
|
1499 |
boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
|
tor@1
|
1500 |
if (isMarkaby) {
|
tor@1
|
1501 |
Set<IndexedMethod> actionView = index.getInheritedMethods("ActionView::Base", prefix, kind); // NOI18N
|
tor@1
|
1502 |
inheritedMethods.addAll(actionView);
|
tor@1
|
1503 |
}
|
tor@1
|
1504 |
|
emononen@3948
|
1505 |
if (isRhtmlFile(fileObject) || isMarkaby) {
|
tor@1
|
1506 |
// Hack - include controller and helper files as well
|
tor@1
|
1507 |
FileObject f = fileObject.getParent();
|
emononen@3948
|
1508 |
// name of the controller w/o the "controller" suffix
|
tor@1
|
1509 |
String controllerName = null;
|
tor@1
|
1510 |
// XXX Will this work for .mab files? Where do they go?
|
tor@1
|
1511 |
while (f != null && !f.getName().equals("views")) { // todo - make sure grandparent is app
|
emononen@3948
|
1512 |
String n = underlinedNameToCamel(f.getName());
|
tor@1
|
1513 |
if (controllerName == null) {
|
tor@1
|
1514 |
controllerName = n;
|
tor@1
|
1515 |
} else {
|
tor@1
|
1516 |
controllerName = n + "::" + controllerName;
|
tor@1
|
1517 |
}
|
tor@1
|
1518 |
f = f.getParent();
|
tor@1
|
1519 |
}
|
emononen@3948
|
1520 |
|
emononen@3948
|
1521 |
// // add in all methods from the associated helper and inherited helpers. this will
|
emononen@3948
|
1522 |
// add also ApplicationHelper, which is global
|
emononen@3948
|
1523 |
Set<String> helperNames = new HashSet<String>();
|
emononen@3948
|
1524 |
helperNames.add(helperName(controllerName));
|
emononen@3948
|
1525 |
for (IndexedClass superClass : index.getSuperClasses(controllerName(controllerName))) {
|
emononen@3948
|
1526 |
if ("ActionController::Base".equals(superClass.getFqn())) { //NOI18N
|
emononen@3948
|
1527 |
break;
|
emononen@3948
|
1528 |
}
|
emononen@3948
|
1529 |
helperNames.add(helperName(superClass.getFqn()));
|
emononen@3948
|
1530 |
}
|
emononen@3948
|
1531 |
for (String helper : helperNames) {
|
emononen@3948
|
1532 |
inheritedMethods.addAll(index.getInheritedMethods(helper, prefix, kind));
|
emononen@3948
|
1533 |
}
|
emononen@3948
|
1534 |
|
emononen@3948
|
1535 |
index.getSuperClasses(controllerName);
|
tor@1
|
1536 |
// TODO - pull in the fields (NOT THE METHODS) from the controller
|
tor@1
|
1537 |
//Set<IndexedMethod> controller = index.getInheritedMethods(controllerName+"Controller", prefix, kind);
|
tor@1
|
1538 |
//inheritedMethods.addAll(controller);
|
tor@1
|
1539 |
}
|
emononen@3948
|
1540 |
}
|
tor@1
|
1541 |
|
tor@480
|
1542 |
private void addActionViewFields(Set<IndexedField> inheritedFields, FileObject fileObject, RubyIndex index, String prefix,
|
mkrauskopf@3030
|
1543 |
QuerySupport.Kind kind) {
|
tor@480
|
1544 |
// RHTML and Markaby: Add in the helper methods etc. from the associated files
|
tor@480
|
1545 |
boolean isMarkaby = RubyUtils.isMarkabyFile(fileObject);
|
tor@480
|
1546 |
if (isMarkaby) {
|
tor@581
|
1547 |
Set<IndexedField> actionView = index.getInheritedFields("ActionView::Base", prefix, kind, true); // NOI18N
|
tor@480
|
1548 |
inheritedFields.addAll(actionView);
|
tor@480
|
1549 |
}
|
tor@480
|
1550 |
|
tor@480
|
1551 |
if (RubyUtils.isRhtmlFile(fileObject) || isMarkaby) {
|
tor@480
|
1552 |
// Hack - include controller and helper files as well
|
tor@480
|
1553 |
FileObject f = fileObject.getParent();
|
tor@480
|
1554 |
String controllerName = null;
|
tor@480
|
1555 |
// XXX Will this work for .mab files? Where do they go?
|
tor@480
|
1556 |
while (f != null && !f.getName().equals("views")) { // NOI18N // todo - make sure grandparent is app
|
tor@480
|
1557 |
String n = RubyUtils.underlinedNameToCamel(f.getName());
|
tor@480
|
1558 |
if (controllerName == null) {
|
tor@480
|
1559 |
controllerName = n;
|
tor@480
|
1560 |
} else {
|
tor@480
|
1561 |
controllerName = n + "::" + controllerName; // NOI18N
|
tor@480
|
1562 |
}
|
tor@480
|
1563 |
f = f.getParent();
|
tor@480
|
1564 |
}
|
tor@480
|
1565 |
|
tor@480
|
1566 |
String fqn = controllerName+"Controller"; // NOI18N
|
tor@581
|
1567 |
Set<IndexedField> controllerFields = index.getInheritedFields(fqn, prefix, kind, true);
|
tor@480
|
1568 |
for (IndexedField field : controllerFields) {
|
tor@480
|
1569 |
if ("ActionController::Base".equals(field.getIn())) { // NOI18N
|
tor@480
|
1570 |
continue;
|
tor@480
|
1571 |
}
|
tor@480
|
1572 |
inheritedFields.add(field);
|
tor@480
|
1573 |
}
|
tor@480
|
1574 |
}
|
tor@480
|
1575 |
}
|
tor@480
|
1576 |
|
tor@1
|
1577 |
/** If we're doing documentation completion, try to drop the list down to a single alternative
|
tor@1
|
1578 |
* (since the framework will just use the first produced result), and in particular, the -best-
|
tor@1
|
1579 |
* alternative
|
tor@1
|
1580 |
*/
|
tor@1198
|
1581 |
// TODO - pass in request object here!
|
tor@1
|
1582 |
private List<CompletionProposal> filterDocumentation(List<CompletionProposal> proposals,
|
mkrauskopf@3030
|
1583 |
Node root, BaseDocument doc, ParserResult parserResult, int astOffset, int lexOffset, String name,
|
tor@1
|
1584 |
AstPath path, RubyIndex index) {
|
tor@1
|
1585 |
// Look to see if this symbol is either a "class Foo" or a "def foo", and if we invoke
|
tor@1
|
1586 |
// completion on it, prefer this element provided it has documentation
|
tor@1
|
1587 |
List<CompletionProposal> candidates = new ArrayList<CompletionProposal>();
|
mkrauskopf@3030
|
1588 |
FileObject fo = RubyUtils.getFileObject(parserResult);
|
tor@1
|
1589 |
Map<IndexedElement, CompletionProposal> elementMap =
|
tor@1
|
1590 |
new HashMap<IndexedElement, CompletionProposal>();
|
tor@1
|
1591 |
Set<IndexedMethod> methods = new HashSet<IndexedMethod>();
|
tor@1
|
1592 |
Set<IndexedClass> classes = new HashSet<IndexedClass>();
|
tor@1
|
1593 |
|
tor@1
|
1594 |
for (CompletionProposal proposal : proposals) {
|
tor@1205
|
1595 |
RubyElement e = (RubyElement) proposal.getElement();
|
tor@1
|
1596 |
|
tor@1
|
1597 |
if (e instanceof IndexedElement) {
|
tor@1
|
1598 |
IndexedElement ie = (IndexedElement)e;
|
tor@1
|
1599 |
|
tor@1
|
1600 |
if (ie instanceof IndexedClass) {
|
tor@1
|
1601 |
classes.add((IndexedClass)ie);
|
tor@1
|
1602 |
elementMap.put(ie, proposal);
|
tor@1
|
1603 |
} else if (ie instanceof IndexedMethod) {
|
tor@1
|
1604 |
methods.add((IndexedMethod)ie);
|
tor@1
|
1605 |
elementMap.put(ie, proposal);
|
tor@1
|
1606 |
}
|
tor@1
|
1607 |
|
tor@1
|
1608 |
if (ie.getFileObject() == fo) {
|
tor@1
|
1609 |
// The class is in this file - if it has documentation, prefer it
|
tor@1
|
1610 |
candidates.add(proposal);
|
tor@1
|
1611 |
}
|
tor@1
|
1612 |
}
|
tor@1
|
1613 |
}
|
tor@1
|
1614 |
|
tor@1
|
1615 |
// Check the candidates to see if one of them is actually -defined-
|
tor@1
|
1616 |
// under the caret; e.g. if you have "class File" with documentation,
|
tor@1
|
1617 |
// and you ctrl-space on it, you always want to show THIS documentation
|
tor@1
|
1618 |
// for File, not the standard one defined elsewhere.
|
tor@1
|
1619 |
for (CompletionProposal candidate : candidates) {
|
tor@1
|
1620 |
// See if the candidate corresponds to the caret position
|
tor@1205
|
1621 |
RubyElement re = (RubyElement) candidate.getElement();
|
tor@1205
|
1622 |
if (!(re instanceof IndexedElement)) {
|
tor@1205
|
1623 |
continue;
|
tor@1205
|
1624 |
}
|
tor@1198
|
1625 |
IndexedElement e = (IndexedElement)re;
|
tor@1
|
1626 |
String signature = e.getSignature();
|
tor@1
|
1627 |
Node node = AstUtilities.findBySignature(root, signature);
|
tor@1
|
1628 |
|
tor@1
|
1629 |
if (node != null) {
|
emononen@3259
|
1630 |
SourcePosition pos = node.getPosition();
|
mkrauskopf@3030
|
1631 |
int startPos = LexUtilities.getLexerOffset(parserResult, pos.getStartOffset());
|
tor@1
|
1632 |
|
tor@1
|
1633 |
try {
|
mkrauskopf@3030
|
1634 |
int lineBegin = AstUtilities.getAstOffset(parserResult, Utilities.getRowFirstNonWhite(doc, startPos));
|
mkrauskopf@3030
|
1635 |
int lineEnd = AstUtilities.getAstOffset(parserResult, Utilities.getRowEnd(doc, startPos));
|
tor@1
|
1636 |
|
tor@1
|
1637 |
if ((astOffset >= lineBegin) && (astOffset <= lineEnd)) {
|
tor@1
|
1638 |
// Look for documentation
|
vstejskal@3086
|
1639 |
List<String> rdoc = AstUtilities.gatherDocumentation(parserResult.getSnapshot(), node);
|
tor@1
|
1640 |
|
mkrauskopf@2482
|
1641 |
if (rdoc != null && !rdoc.isEmpty()) {
|
tor@1
|
1642 |
return Collections.singletonList(candidate);
|
tor@1
|
1643 |
}
|
tor@1
|
1644 |
}
|
tor@1
|
1645 |
} catch (BadLocationException ble) {
|
tor@1
|
1646 |
// The parse information is too old - the document has shrunk. Do nothing, the
|
tor@1
|
1647 |
// AST nodes are pointing into the old contents.
|
tor@1
|
1648 |
}
|
tor@1
|
1649 |
}
|
tor@1
|
1650 |
}
|
tor@1
|
1651 |
|
tor@1
|
1652 |
// Try to pick the best match among many documentation entries: Heuristic time.
|
tor@1
|
1653 |
// Similar to heuristics used for Go To Declaration: Prefer long documentation,
|
tor@1
|
1654 |
// prefer documentation related to the require-statements in this file, etc.
|
tor@1
|
1655 |
IndexedElement candidate = null;
|
tor@1
|
1656 |
|
mkrauskopf@2482
|
1657 |
if (!classes.isEmpty()) {
|
mkrauskopf@3030
|
1658 |
RubyClassDeclarationFinder cdf = new RubyClassDeclarationFinder(parserResult, null, path, index, path.leaf());
|
mkrauskopf@2766
|
1659 |
candidate = cdf.findBestElementMatch(classes);
|
mkrauskopf@2482
|
1660 |
} else if (!methods.isEmpty()) {
|
mkrauskopf@2765
|
1661 |
RubyDeclarationFinder finder = new RubyDeclarationFinder();
|
tor@1
|
1662 |
candidate = finder.findBestMethodMatch(name, methods, doc, astOffset, lexOffset, path,
|
tor@1
|
1663 |
path.leaf(), index);
|
tor@1
|
1664 |
}
|
tor@1
|
1665 |
|
tor@1
|
1666 |
if (candidate != null) {
|
tor@1
|
1667 |
CompletionProposal proposal = elementMap.get(candidate);
|
tor@1
|
1668 |
|
tor@1
|
1669 |
if (proposal != null) {
|
tor@1
|
1670 |
return Collections.singletonList(proposal);
|
tor@1
|
1671 |
}
|
tor@1
|
1672 |
}
|
tor@1
|
1673 |
|
tor@1
|
1674 |
return proposals;
|
tor@1
|
1675 |
}
|
tor@1
|
1676 |
|
tor@1
|
1677 |
// private boolean isClassName(String s) {
|
tor@1
|
1678 |
// // Initial capital letter, second letter is not
|
tor@1
|
1679 |
// if (s.length() == 1) {
|
tor@1
|
1680 |
// return Character.isUpperCase(s.charAt(0));
|
tor@1
|
1681 |
// }
|
tor@1
|
1682 |
//
|
tor@1
|
1683 |
// if (Character.isLowerCase(s.charAt(0))) {
|
tor@1
|
1684 |
// return false;
|
tor@1
|
1685 |
// }
|
tor@1
|
1686 |
//
|
tor@1
|
1687 |
// return Character.isLowerCase(s.charAt(1));
|
tor@1
|
1688 |
// }
|
tor@1
|
1689 |
private boolean overlapsLine(Node node, int lineBegin, int lineEnd) {
|
emononen@3259
|
1690 |
SourcePosition pos = node.getPosition();
|
tor@1
|
1691 |
|
tor@1
|
1692 |
//return (((pos.getStartOffset() <= lineEnd) && (pos.getEndOffset() >= lineBegin)));
|
mkrauskopf@2834
|
1693 |
// Don't look to see if the line is within the target. See if the target is started on this line (where
|
tor@1
|
1694 |
// the declaration is, e.g. it might be an incomplete line.
|
tor@1
|
1695 |
return ((pos.getStartOffset() >= lineBegin) && (pos.getStartOffset() <= lineEnd));
|
tor@1
|
1696 |
}
|
tor@1
|
1697 |
|
tor@93
|
1698 |
// /** Return true iff the name looks like an operator name */
|
tor@93
|
1699 |
// private boolean isOperator(String name) {
|
tor@93
|
1700 |
// // If a name contains not a single letter, it is probably an operator - especially
|
tor@93
|
1701 |
// // if it is a short name
|
tor@93
|
1702 |
// int n = name.length();
|
tor@93
|
1703 |
//
|
tor@93
|
1704 |
// if (n > 2) {
|
tor@93
|
1705 |
// return false;
|
tor@93
|
1706 |
// }
|
tor@93
|
1707 |
//
|
tor@93
|
1708 |
// for (int i = 0; i < n; i++) {
|
tor@93
|
1709 |
// if (Character.isLetter(name.charAt(i))) {
|
tor@93
|
1710 |
// return false;
|
tor@93
|
1711 |
// }
|
tor@93
|
1712 |
// }
|
tor@93
|
1713 |
//
|
tor@93
|
1714 |
// return true;
|
tor@93
|
1715 |
// }
|
tor@1
|
1716 |
|
tor@874
|
1717 |
static void addLocals(Node node, Map<String, Node> variables) {
|
emononen@3259
|
1718 |
switch (node.getNodeType()) {
|
tor@1291
|
1719 |
case LOCALASGNNODE: {
|
tor@1
|
1720 |
String name = ((INameNode)node).getName();
|
tor@1
|
1721 |
|
tor@1
|
1722 |
if (!variables.containsKey(name)) {
|
tor@1
|
1723 |
variables.put(name, node);
|
tor@1
|
1724 |
}
|
tor@669
|
1725 |
break;
|
tor@669
|
1726 |
}
|
tor@1291
|
1727 |
case ARGSNODE: {
|
tor@1
|
1728 |
// TODO - use AstUtilities.getDefArgs here - but avoid hitting them twice!
|
tor@1
|
1729 |
//List<String> parameters = AstUtilities.getDefArgs(def, true);
|
tor@1
|
1730 |
// However, I've gotta find the parameter nodes themselves too!
|
tor@1
|
1731 |
ArgsNode an = (ArgsNode)node;
|
tor@1
|
1732 |
|
emononen@3259
|
1733 |
if (an.getRequiredCount() > 0) {
|
enebo@4519
|
1734 |
for (Node arg : an.childNodes()) {
|
tor@1
|
1735 |
if (arg instanceof ListNode) {
|
enebo@4519
|
1736 |
for (Node arg2 : arg.childNodes()) {
|
tor@1
|
1737 |
if (arg2 instanceof ArgumentNode) {
|
tor@1
|
1738 |
variables.put(((ArgumentNode)arg2).getName(), arg2);
|
tor@1
|
1739 |
} else if (arg2 instanceof LocalAsgnNode) {
|
tor@1
|
1740 |
variables.put(((INameNode)arg2).getName(), arg2);
|
tor@1
|
1741 |
}
|
tor@1
|
1742 |
}
|
tor@1
|
1743 |
}
|
tor@1
|
1744 |
}
|
tor@1
|
1745 |
}
|
tor@1
|
1746 |
|
tor@1
|
1747 |
// Rest args
|
emononen@3259
|
1748 |
if (an.getRest() != null) {
|
emononen@3259
|
1749 |
String name = an.getRest().getName();
|
emononen@3259
|
1750 |
variables.put(name, an.getRest());
|
tor@1
|
1751 |
}
|
tor@1
|
1752 |
|
tor@1
|
1753 |
// Block args
|
emononen@3259
|
1754 |
if (an.getBlock() != null) {
|
emononen@3259
|
1755 |
String name = an.getBlock().getName();
|
emononen@3259
|
1756 |
variables.put(name, an.getBlock());
|
tor@1
|
1757 |
}
|
tor@669
|
1758 |
|
tor@669
|
1759 |
break;
|
tor@1
|
1760 |
}
|
tor@1
|
1761 |
|
mkrauskopf@2834
|
1762 |
// } else if (target instanceof AliasNode) {
|
mkrauskopf@2834
|
1763 |
// AliasNode an = (AliasNode)target;
|
tor@669
|
1764 |
// Tricky -- which NODE do we add here? Completion creator needs to be aware of new name etc. Do later.
|
tor@669
|
1765 |
// Besides, do we show it as a field or a method or what?
|
tor@669
|
1766 |
|
tor@669
|
1767 |
// variab
|
tor@669
|
1768 |
// if (an.getNewName().equals(name)) {
|
tor@669
|
1769 |
// OffsetRange range = AstUtilities.getAliasNewRange(an);
|
tor@669
|
1770 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@669
|
1771 |
// } else if (an.getOldName().equals(name)) {
|
tor@669
|
1772 |
// OffsetRange range = AstUtilities.getAliasOldRange(an);
|
tor@669
|
1773 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@669
|
1774 |
// }
|
tor@669
|
1775 |
// break;
|
tor@669
|
1776 |
}
|
tor@669
|
1777 |
|
enebo@4519
|
1778 |
for (Node child : node.childNodes()) {
|
enebo@4542
|
1779 |
if (child instanceof ILocalScope) continue; // ignore nested local scopes
|
tor@1
|
1780 |
|
tor@1
|
1781 |
addLocals(child, variables);
|
tor@1
|
1782 |
}
|
tor@1
|
1783 |
}
|
tor@1
|
1784 |
|
tor@874
|
1785 |
static void addDynamic(Node node, Map<String, Node> variables) {
|
emononen@3259
|
1786 |
if (node.getNodeType() == NodeType.DASGNNODE) {
|
tor@1
|
1787 |
String name = ((INameNode)node).getName();
|
tor@1
|
1788 |
|
tor@1
|
1789 |
if (!variables.containsKey(name)) {
|
tor@1
|
1790 |
variables.put(name, node);
|
tor@1
|
1791 |
}
|
tor@1
|
1792 |
|
mkrauskopf@2834
|
1793 |
//} else if (target instanceof ArgsNode) {
|
mkrauskopf@2834
|
1794 |
// ArgsNode an = (ArgsNode)target;
|
tor@1
|
1795 |
//
|
tor@1
|
1796 |
// if (an.getArgsCount() > 0) {
|
tor@2503
|
1797 |
// List<Node> args = an.childNodes();
|
tor@1
|
1798 |
// List<String> parameters = null;
|
tor@1
|
1799 |
//
|
tor@1
|
1800 |
// for (Node arg : args) {
|
tor@1
|
1801 |
// if (arg instanceof ListNode) {
|
tor@2503
|
1802 |
// List<Node> args2 = arg.childNodes();
|
tor@1
|
1803 |
// parameters = new ArrayList<String>(args2.size());
|
tor@1
|
1804 |
//
|
tor@1
|
1805 |
// for (Node arg2 : args2) {
|
tor@1
|
1806 |
// if (arg2 instanceof ArgumentNode) {
|
tor@1
|
1807 |
// OffsetRange range = AstUtilities.getRange(arg2);
|
tor@1
|
1808 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@1
|
1809 |
// } else if (arg2 instanceof LocalAsgnNode) {
|
tor@1
|
1810 |
// OffsetRange range = AstUtilities.getRange(arg2);
|
tor@1
|
1811 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@1
|
1812 |
// }
|
tor@1
|
1813 |
// }
|
tor@1
|
1814 |
// }
|
tor@1
|
1815 |
// }
|
tor@1
|
1816 |
// }
|
mkrauskopf@2834
|
1817 |
// } else if (!ignoreAlias && target instanceof AliasNode) {
|
mkrauskopf@2834
|
1818 |
// AliasNode an = (AliasNode)target;
|
tor@1
|
1819 |
//
|
tor@1
|
1820 |
// if (an.getNewName().equals(name)) {
|
tor@1
|
1821 |
// OffsetRange range = AstUtilities.getAliasNewRange(an);
|
tor@1
|
1822 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@1
|
1823 |
// } else if (an.getOldName().equals(name)) {
|
tor@1
|
1824 |
// OffsetRange range = AstUtilities.getAliasOldRange(an);
|
tor@1
|
1825 |
// highlights.put(range, ColoringAttributes.MARK_OCCURRENCES);
|
tor@1
|
1826 |
// }
|
tor@1
|
1827 |
}
|
tor@1
|
1828 |
|
enebo@4519
|
1829 |
for (Node child : node.childNodes()) {
|
enebo@4542
|
1830 |
if (child instanceof ILocalScope || child.getNodeType() == NodeType.ITERNODE) continue;
|
tor@1
|
1831 |
|
tor@1
|
1832 |
addDynamic(child, variables);
|
tor@1
|
1833 |
}
|
tor@1
|
1834 |
}
|
tor@1
|
1835 |
|
tor@1
|
1836 |
private String loadResource(String basename) {
|
tor@1
|
1837 |
// TODO: I18N
|
tor@93
|
1838 |
InputStream is = null;
|
enebo@4519
|
1839 |
|
tor@93
|
1840 |
try {
|
enebo@4519
|
1841 |
StringBuilder sb = new StringBuilder();
|
tor@1719
|
1842 |
is = new BufferedInputStream(RubyCodeCompleter.class.getResourceAsStream("resources/" +
|
tor@1
|
1843 |
basename));
|
tor@1
|
1844 |
//while (is)
|
tor@1
|
1845 |
while (true) {
|
tor@1
|
1846 |
int c = is.read();
|
tor@1
|
1847 |
|
enebo@4519
|
1848 |
if (c == -1) break;
|
tor@1
|
1849 |
|
tor@1
|
1850 |
sb.append((char)c);
|
tor@1
|
1851 |
}
|
tor@1
|
1852 |
|
enebo@4519
|
1853 |
if (sb.length() > 0) return sb.toString();
|
tor@1
|
1854 |
} catch (IOException ie) {
|
tor@1
|
1855 |
Exceptions.printStackTrace(ie);
|
tor@93
|
1856 |
} finally {
|
tor@1
|
1857 |
try {
|
enebo@4519
|
1858 |
if (is != null) is.close();
|
tor@93
|
1859 |
} catch (IOException ie) {
|
tor@93
|
1860 |
Exceptions.printStackTrace(ie);
|
tor@1
|
1861 |
}
|
tor@1
|
1862 |
}
|
tor@1
|
1863 |
|
tor@1
|
1864 |
return null;
|
tor@1
|
1865 |
}
|
tor@1
|
1866 |
|
tor@1
|
1867 |
private String getKeywordHelp(String keyword) {
|
tor@1
|
1868 |
// Difficulty here with context; "else" is used for both the ifelse.html and case.html both define it.
|
tor@1
|
1869 |
// End is even more used.
|
tor@1
|
1870 |
if (keyword.equals("if") || keyword.equals("elsif") || keyword.equals("else") ||
|
tor@1
|
1871 |
keyword.equals("then") || keyword.equals("unless")) { // NOI18N
|
tor@1
|
1872 |
|
tor@1
|
1873 |
return loadResource("ifelse.html"); // NOI18N
|
tor@1
|
1874 |
} else if (keyword.equals("case") || keyword.equals("when") || keyword.equals("else")) { // NOI18N
|
tor@1
|
1875 |
|
tor@1
|
1876 |
return loadResource("case.html"); // NOI18N
|
tor@1
|
1877 |
} else if (keyword.equals("rescue") || keyword.equals("ensure")) { // NOI18N
|
tor@1
|
1878 |
|
tor@1
|
1879 |
return loadResource("rescue.html"); // NOI18N
|
tor@1
|
1880 |
} else if (keyword.equals("yield")) { // NOI18N
|
tor@1
|
1881 |
|
tor@1
|
1882 |
return loadResource("yield.html"); // NOI18N
|
tor@1
|
1883 |
}
|
tor@1
|
1884 |
|
tor@1
|
1885 |
return null;
|
tor@1
|
1886 |
}
|
tor@1
|
1887 |
|
tor@1
|
1888 |
/**
|
tor@1
|
1889 |
* Find the best possible documentation match for the given IndexedClass or IndexedMethod.
|
tor@1
|
1890 |
* This involves looking at index to see which instances of this class or method
|
tor@1
|
1891 |
* definition have associated rdoc, as well as choosing between them based on the
|
tor@1
|
1892 |
* require statements in the file.
|
tor@1
|
1893 |
*/
|
mkrauskopf@2743
|
1894 |
static IndexedElement findDocumentationEntry(Node root, IndexedElement obj) {
|
tor@1
|
1895 |
// 1. Find entries known to have documentation
|
tor@1
|
1896 |
String fqn = obj.getSignature();
|
tor@1
|
1897 |
Set<?extends IndexedElement> result = obj.getIndex().getDocumented(fqn);
|
tor@1
|
1898 |
|
mkrauskopf@2482
|
1899 |
if ((result == null) || (result.isEmpty())) {
|
tor@1
|
1900 |
return null;
|
tor@1
|
1901 |
} else if (result.size() == 1) {
|
tor@1
|
1902 |
return result.iterator().next();
|
tor@1
|
1903 |
}
|
tor@1
|
1904 |
|
tor@1
|
1905 |
// 2. There are multiple matches so try to disambiguate them by the imports in this file.
|
tor@1
|
1906 |
// For example, for "File" we usually show the standard (builtin) documentation,
|
tor@1
|
1907 |
// unless you have required "ftools", which redefines File with new docs.
|
tor@1
|
1908 |
Set<IndexedElement> candidates;
|
tor@1
|
1909 |
if (root != null) {
|
tor@1
|
1910 |
candidates = new HashSet<IndexedElement>();
|
tor@1
|
1911 |
Set<String> requires = AstUtilities.getRequires(root);
|
tor@1
|
1912 |
|
tor@1
|
1913 |
for (IndexedElement o : result) {
|
tor@1
|
1914 |
String require = o.getRequire();
|
tor@1
|
1915 |
|
tor@1
|
1916 |
if (requires.contains(require)) {
|
tor@1
|
1917 |
candidates.add(o);
|
tor@1
|
1918 |
}
|
tor@1
|
1919 |
}
|
tor@1
|
1920 |
|
tor@1
|
1921 |
if (candidates.size() == 1) {
|
tor@1
|
1922 |
return candidates.iterator().next();
|
tor@1
|
1923 |
} else if (!candidates.isEmpty()) {
|
tor@1
|
1924 |
result = candidates;
|
tor@1
|
1925 |
}
|
tor@1
|
1926 |
}
|
tor@1
|
1927 |
|
tor@1
|
1928 |
// 3. Prefer builtin (kernel) docs over other docs.
|
tor@1
|
1929 |
candidates = new HashSet<IndexedElement>();
|
tor@1
|
1930 |
|
tor@1
|
1931 |
for (IndexedElement o : result) {
|
tor@1
|
1932 |
String url = o.getFileUrl();
|
tor@1
|
1933 |
|
mkrauskopf@3059
|
1934 |
if (RubyUtils.isRubyStubsURL(url)) {
|
tor@1
|
1935 |
candidates.add(o);
|
tor@1
|
1936 |
}
|
tor@1
|
1937 |
}
|
tor@1
|
1938 |
|
tor@1
|
1939 |
if (candidates.size() == 1) {
|
tor@1
|
1940 |
return candidates.iterator().next();
|
tor@1
|
1941 |
} else if (!candidates.isEmpty()) {
|
tor@1
|
1942 |
result = candidates;
|
tor@1
|
1943 |
}
|
tor@1
|
1944 |
|
tor@1
|
1945 |
// 4. Consider other heuristics, like picking the "larger" documentation
|
tor@1
|
1946 |
// (more lines)
|
tor@1
|
1947 |
|
tor@1
|
1948 |
// 5. Just pick an arbitrary one.
|
tor@1
|
1949 |
return result.iterator().next();
|
tor@1
|
1950 |
}
|
tor@1
|
1951 |
|
tor@1
|
1952 |
/**
|
tor@1
|
1953 |
* @todo If you invoke this on top of a symbol, I should really just show
|
tor@1
|
1954 |
* the documentation for that symbol!
|
tor@1
|
1955 |
*
|
tor@1
|
1956 |
* @param element The element we want to look up comments for
|
mkrauskopf@3030
|
1957 |
* @param parserResult The (optional) compilation parserResult for a document referencing the element.
|
tor@1
|
1958 |
* This is used to consult require-statements in the given compilation context etc.
|
tor@1
|
1959 |
* to choose among many alternatives. May be null, in which case the element had
|
tor@1
|
1960 |
* better be an IndexedElement.
|
tor@1
|
1961 |
*/
|
mkrauskopf@3030
|
1962 |
static List<String> getComments(ParserResult info, Element element) {
|
tor@1
|
1963 |
assert info != null || element instanceof IndexedElement;
|
tor@1
|
1964 |
|
tor@1
|
1965 |
if (element == null) {
|
tor@1
|
1966 |
return null;
|
tor@1
|
1967 |
}
|
tor@1
|
1968 |
|
enebo@4542
|
1969 |
Node node;
|
tor@1
|
1970 |
|
tor@1
|
1971 |
if (element instanceof AstElement) {
|
tor@1
|
1972 |
node = ((AstElement)element).getNode();
|
tor@1
|
1973 |
} else if (element instanceof IndexedElement) {
|
tor@1
|
1974 |
IndexedElement com = (IndexedElement)element;
|
tor@1
|
1975 |
Node root = null;
|
tor@1
|
1976 |
if (info != null) {
|
tor@1
|
1977 |
root = AstUtilities.getRoot(info);
|
tor@1
|
1978 |
}
|
tor@1
|
1979 |
|
tor@1
|
1980 |
IndexedElement match = findDocumentationEntry(root, com);
|
tor@1
|
1981 |
|
tor@1
|
1982 |
if (match != null) {
|
tor@1
|
1983 |
com = match;
|
tor@1
|
1984 |
element = com;
|
tor@1
|
1985 |
}
|
tor@1
|
1986 |
|
mkrauskopf@3030
|
1987 |
node = AstUtilities.getForeignNode(com);
|
tor@1
|
1988 |
|
tor@1
|
1989 |
if (node == null) {
|
tor@1
|
1990 |
return null;
|
tor@1
|
1991 |
}
|
tor@1
|
1992 |
} else {
|
tor@1
|
1993 |
assert false : element;
|
tor@1
|
1994 |
|
tor@1
|
1995 |
return null;
|
tor@1
|
1996 |
}
|
tor@1
|
1997 |
|
tor@1
|
1998 |
// Initially, I implemented this by using RubyParserResult.getCommentNodes.
|
tor@1
|
1999 |
// However, I -still- had to rely on looking in the Document itself, since
|
tor@1
|
2000 |
// the CommentNodes are not attached to the AST, and to do things the way
|
tor@1
|
2001 |
// RDoc does, I have to (for example) look to see if a comment is at the
|
tor@1
|
2002 |
// beginning of a line or on the same line as something else, or if two
|
tor@1
|
2003 |
// comments have any empty lines between them, and so on.
|
tor@1
|
2004 |
// When I started looking in the document itself, I realized I might as well
|
tor@1
|
2005 |
// do all the manipulation on the document, since having the Comment nodes
|
tor@1
|
2006 |
// don't particularly help.
|
vstejskal@3086
|
2007 |
Snapshot snapshot;
|
tor@1716
|
2008 |
if (element instanceof IndexedElement) {
|
vstejskal@3086
|
2009 |
FileObject f = ((IndexedElement) element).getFileObject();
|
vstejskal@3086
|
2010 |
snapshot = Source.create(f).createSnapshot();
|
tor@1716
|
2011 |
} else if (info != null) {
|
vstejskal@3086
|
2012 |
snapshot = info.getSnapshot();
|
tor@1716
|
2013 |
} else {
|
tor@1
|
2014 |
return null;
|
tor@1
|
2015 |
}
|
tor@1
|
2016 |
|
tor@1
|
2017 |
List<String> comments = null;
|
tor@1
|
2018 |
|
tor@1
|
2019 |
// Check for RubyComObject: These are external files (like Ruby lib) where I need to check many files
|
tor@1
|
2020 |
if (node instanceof ClassNode && !(element instanceof IndexedElement)) {
|
tor@1
|
2021 |
String className = AstUtilities.getClassOrModuleName((ClassNode)node);
|
tor@1
|
2022 |
List<ClassNode> classes = AstUtilities.getClasses(AstUtilities.getRoot(info));
|
tor@1
|
2023 |
|
tor@1
|
2024 |
// Iterate backwards through the list because the most recent documentation
|
tor@1
|
2025 |
// should be chosen, if any
|
tor@1
|
2026 |
for (int i = classes.size() - 1; i >= 0; i--) {
|
tor@1
|
2027 |
ClassNode clz = classes.get(i);
|
tor@1
|
2028 |
String name = AstUtilities.getClassOrModuleName(clz);
|
tor@1
|
2029 |
|
tor@1
|
2030 |
if (name.equals(className)) {
|
vstejskal@3086
|
2031 |
comments = AstUtilities.gatherDocumentation(snapshot, clz);
|
tor@1
|
2032 |
|
mkrauskopf@2482
|
2033 |
if ((comments != null) && (!comments.isEmpty())) {
|
tor@1
|
2034 |
break;
|
tor@1
|
2035 |
}
|
tor@1
|
2036 |
}
|
tor@1
|
2037 |
}
|
tor@1
|
2038 |
} else {
|
vstejskal@3086
|
2039 |
comments = AstUtilities.gatherDocumentation(snapshot, node);
|
tor@1
|
2040 |
}
|
tor@1
|
2041 |
|
mkrauskopf@2482
|
2042 |
if ((comments == null) || (comments.isEmpty())) {
|
tor@1
|
2043 |
return null;
|
tor@1
|
2044 |
}
|
tor@1
|
2045 |
|
tor@1
|
2046 |
return comments;
|
tor@1
|
2047 |
}
|
tor@1
|
2048 |
|
enebo@4519
|
2049 |
@Override
|
mkrauskopf@3030
|
2050 |
public String document(ParserResult info, ElementHandle handle) {
|
tor@1198
|
2051 |
Element element = null;
|
tor@1198
|
2052 |
if (handle instanceof ElementHandle.UrlHandle) {
|
tor@1198
|
2053 |
String url = ((ElementHandle.UrlHandle)handle).getUrl();
|
tor@1719
|
2054 |
DeclarationLocation loc = new RubyDeclarationFinder().findLinkedMethod(info, url);
|
tor@1198
|
2055 |
if (loc != DeclarationLocation.NONE) {
|
enebo@4519
|
2056 |
element = RubyParser.resolveHandle(info, loc.getElement());
|
enebo@4519
|
2057 |
if (element == null) return null;
|
tor@1198
|
2058 |
}
|
tor@1198
|
2059 |
} else {
|
tor@1198
|
2060 |
element = RubyParser.resolveHandle(info, handle);
|
tor@1198
|
2061 |
}
|
enebo@4519
|
2062 |
if (element == null) return null;
|
enebo@4519
|
2063 |
if (element instanceof KeywordElement) return getKeywordHelp(((KeywordElement)element).getName());
|
enebo@4519
|
2064 |
if (element instanceof CommentElement) {
|
tor@507
|
2065 |
// Text is packaged as the name
|
tor@507
|
2066 |
String comment = element.getName();
|
tor@507
|
2067 |
RDocFormatter formatter = new RDocFormatter();
|
tor@507
|
2068 |
String[] comments = comment.split("\n");
|
tor@507
|
2069 |
for (String text : comments) {
|
tor@507
|
2070 |
// Truncate off leading whitespace before # on comment lines
|
tor@507
|
2071 |
for (int i = 0, n = text.length(); i < n; i++) {
|
tor@507
|
2072 |
char c = text.charAt(i);
|
tor@507
|
2073 |
if (c == '#') {
|
tor@507
|
2074 |
if (i > 0) {
|
tor@507
|
2075 |
text = text.substring(i);
|
tor@507
|
2076 |
break;
|
tor@507
|
2077 |
}
|
tor@507
|
2078 |
} else if (!Character.isWhitespace(c)) {
|
tor@507
|
2079 |
break;
|
tor@507
|
2080 |
}
|
tor@507
|
2081 |
}
|
tor@507
|
2082 |
formatter.appendLine(text);
|
tor@507
|
2083 |
}
|
mkrauskopf@2482
|
2084 |
return formatter.toHtml();
|
tor@1
|
2085 |
}
|
tor@1
|
2086 |
|
tor@1
|
2087 |
List<String> comments = getComments(info, element);
|
tor@1
|
2088 |
if (comments == null) {
|
emononen@3982
|
2089 |
if (FindersHelper.isFinderMethod(element.getName(), false)) {
|
tor@1719
|
2090 |
return new RDocFormatter().getSignature(element) + NbBundle.getMessage(RubyCodeCompleter.class, "DynamicMethod");
|
tor@581
|
2091 |
}
|
tor@1719
|
2092 |
String html = new RDocFormatter().getSignature(element) + "\n<hr>\n<i>" + NbBundle.getMessage(RubyCodeCompleter.class, "NoCommentFound") +"</i>";
|
tor@581
|
2093 |
|
tor@581
|
2094 |
return html;
|
tor@1
|
2095 |
}
|
tor@581
|
2096 |
|
tor@1
|
2097 |
RDocFormatter formatter = new RDocFormatter();
|
tor@1
|
2098 |
String name = element.getName();
|
tor@1
|
2099 |
if (name != null && name.length() > 0) {
|
tor@1
|
2100 |
formatter.setSeqName(name);
|
tor@1
|
2101 |
}
|
tor@1
|
2102 |
|
tor@1
|
2103 |
for (String text : comments) {
|
tor@1
|
2104 |
formatter.appendLine(text);
|
tor@1
|
2105 |
}
|
tor@1
|
2106 |
|
tor@1
|
2107 |
String html = formatter.toHtml();
|
enebo@4519
|
2108 |
if (!formatter.wroteSignature()) html = formatter.getSignature(element) + "\n<hr>\n" + html;
|
tor@1
|
2109 |
|
tor@1
|
2110 |
return html;
|
tor@1
|
2111 |
}
|
tor@1
|
2112 |
|
emononen@4021
|
2113 |
@Override
|
tor@884
|
2114 |
public ElementHandle resolveLink(String link, ElementHandle elementHandle) {
|
enebo@4519
|
2115 |
if (elementHandle == null) return null;
|
enebo@4519
|
2116 |
|
jskrivanek@2662
|
2117 |
if (link.indexOf('#') != -1 && elementHandle.getMimeType().equals(RubyInstallation.RUBY_MIME_TYPE)) {
|
tor@884
|
2118 |
if (link.startsWith("#")) {
|
tor@884
|
2119 |
// Put the current class etc. in front of the method call if necessary
|
tor@1540
|
2120 |
Element surrounding = RubyParser.resolveHandle(null, elementHandle);
|
tor@884
|
2121 |
if (surrounding != null && surrounding.getKind() != ElementKind.KEYWORD) {
|
tor@884
|
2122 |
String name = surrounding.getName();
|
tor@884
|
2123 |
ElementKind kind = surrounding.getKind();
|
tor@884
|
2124 |
if (!(kind == ElementKind.CLASS || kind == ElementKind.MODULE)) {
|
tor@884
|
2125 |
String in = surrounding.getIn();
|
tor@884
|
2126 |
if (in != null && in.length() > 0) {
|
tor@884
|
2127 |
name = in;
|
tor@884
|
2128 |
} else if (name != null) {
|
tor@884
|
2129 |
int index = name.indexOf('#');
|
enebo@4519
|
2130 |
if (index > 0) name = name.substring(0, index);
|
tor@884
|
2131 |
}
|
tor@884
|
2132 |
}
|
enebo@4519
|
2133 |
if (name != null) link = name + link;
|
tor@884
|
2134 |
}
|
tor@884
|
2135 |
}
|
tor@884
|
2136 |
return new ElementHandle.UrlHandle(link);
|
tor@884
|
2137 |
}
|
tor@884
|
2138 |
|
tor@884
|
2139 |
return null;
|
tor@884
|
2140 |
}
|
jglick@4348
|
2141 |
|
jglick@4348
|
2142 |
// before csl.api 2.11:
|
jglick@4348
|
2143 |
public Set<String> getApplicableTemplates(ParserResult info, int selectionBegin, int selectionEnd) {
|
jglick@4348
|
2144 |
return getApplicableTemplates(RubyUtils.getDocument(info), selectionBegin, selectionEnd);
|
jglick@4348
|
2145 |
}
|
jglick@4348
|
2146 |
// after csl.api 2.11:
|
enebo@4519
|
2147 |
@Override
|
enebo@4519
|
2148 |
public Set<String> getApplicableTemplates(Document d, int selectionBegin, int selectionEnd) {
|
tor@484
|
2149 |
|
tor@1
|
2150 |
// TODO - check the code at the AST path and determine whether it makes sense to
|
tor@1
|
2151 |
// wrap it in a begin block etc.
|
tor@1
|
2152 |
// TODO - I'd like to be able to pass any selection-based templates I'm not familiar with
|
tor@484
|
2153 |
|
tor@484
|
2154 |
boolean valid = false;
|
tor@484
|
2155 |
|
tor@484
|
2156 |
if (selectionEnd != -1) {
|
enebo@4519
|
2157 |
if (d == null || !(d instanceof BaseDocument) || selectionBegin == selectionEnd) return Collections.emptySet();
|
enebo@4519
|
2158 |
|
enebo@4519
|
2159 |
BaseDocument doc = (BaseDocument) d;
|
tor@484
|
2160 |
try {
|
tor@2818
|
2161 |
doc.readLock();
|
enebo@4519
|
2162 |
if (selectionEnd < selectionBegin) {
|
tor@497
|
2163 |
int temp = selectionBegin;
|
tor@497
|
2164 |
selectionBegin = selectionEnd;
|
tor@497
|
2165 |
selectionEnd = temp;
|
tor@497
|
2166 |
}
|
tor@484
|
2167 |
boolean startLineIsEmpty = Utilities.isRowEmpty(doc, selectionBegin);
|
tor@484
|
2168 |
boolean endLineIsEmpty = Utilities.isRowEmpty(doc, selectionEnd);
|
tor@484
|
2169 |
|
tor@484
|
2170 |
if ((startLineIsEmpty || selectionBegin <= Utilities.getRowFirstNonWhite(doc, selectionBegin)) &&
|
tor@484
|
2171 |
(endLineIsEmpty || selectionEnd > Utilities.getRowLastNonWhite(doc, selectionEnd))) {
|
tor@484
|
2172 |
// I have no text to the left of the beginning or text to the right of the end, but I might
|
tor@484
|
2173 |
// have just selected whitespace - check that
|
tor@484
|
2174 |
String text = doc.getText(selectionBegin, selectionEnd-selectionBegin);
|
tor@484
|
2175 |
for (int i = 0; i < text.length(); i++) {
|
tor@484
|
2176 |
if (!Character.isWhitespace(text.charAt(i))) {
|
tor@484
|
2177 |
|
tor@484
|
2178 |
// Make sure that we're not in a string etc
|
mmetelka@806
|
2179 |
Token<?> token = LexUtilities.getToken(doc, selectionBegin);
|
tor@484
|
2180 |
if (token != null) {
|
tor@484
|
2181 |
TokenId id = token.id();
|
tor@484
|
2182 |
if (id != RubyTokenId.STRING_LITERAL && id != RubyTokenId.LINE_COMMENT &&
|
tor@484
|
2183 |
id != RubyTokenId.QUOTED_STRING_LITERAL && id != RubyTokenId.REGEXP_LITERAL &&
|
tor@484
|
2184 |
id != RubyTokenId.DOCUMENTATION) {
|
tor@484
|
2185 |
// Yes - allow surround with here
|
tor@484
|
2186 |
|
tor@484
|
2187 |
// TODO - make this smarter by looking at the AST and see if
|
tor@484
|
2188 |
// we have a complete set of nodes
|
tor@484
|
2189 |
valid = true;
|
tor@484
|
2190 |
}
|
tor@484
|
2191 |
}
|
tor@484
|
2192 |
|
tor@484
|
2193 |
break;
|
tor@484
|
2194 |
}
|
tor@484
|
2195 |
}
|
tor@484
|
2196 |
}
|
tor@484
|
2197 |
} catch (BadLocationException ble) {
|
emononen@3691
|
2198 |
// do nothing - see #154991
|
tor@2818
|
2199 |
} finally {
|
tor@2818
|
2200 |
doc.readUnlock();
|
tor@484
|
2201 |
}
|
tor@484
|
2202 |
} else {
|
tor@484
|
2203 |
valid = true;
|
tor@484
|
2204 |
}
|
tor@484
|
2205 |
|
enebo@4519
|
2206 |
return valid ? selectionTemplates : Collections.<String>emptySet();
|
tor@1
|
2207 |
}
|
tor@1
|
2208 |
|
mkrauskopf@3030
|
2209 |
private String suggestName(ParserResult info, int caretOffset, String prefix, Map params) {
|
tor@1
|
2210 |
// Look at the given context, compute fields and see if I can find a free name
|
tor@1
|
2211 |
caretOffset = AstUtilities.boundCaretOffset(info, caretOffset);
|
tor@1
|
2212 |
|
tor@1
|
2213 |
Node root = AstUtilities.getRoot(info);
|
enebo@4557
|
2214 |
if (root == null) return null;
|
tor@1
|
2215 |
|
tor@1
|
2216 |
AstPath path = new AstPath(root, caretOffset);
|
tor@1
|
2217 |
Node closest = path.leaf();
|
enebo@4519
|
2218 |
if (closest == null) return null;
|
tor@1
|
2219 |
|
enebo@4557
|
2220 |
// TODO: Look for a unique {global,class,instance} variable -- this requires looking at the index
|
enebo@4557
|
2221 |
if (prefix.startsWith("$") || prefix.startsWith("@")) return null;
|
enebo@4519
|
2222 |
|
enebo@4557
|
2223 |
// Look for a local variable in the given scope
|
enebo@4557
|
2224 |
Node method = AstUtilities.findLocalScope(closest, path);
|
enebo@4557
|
2225 |
Map<String, Node> variables = new HashMap<String, Node>();
|
enebo@4557
|
2226 |
addLocals(method, variables);
|
tor@1
|
2227 |
|
enebo@4557
|
2228 |
for (Node block : AstUtilities.getApplicableBlocks(path, false)) {
|
enebo@4557
|
2229 |
addDynamic(block, variables);
|
enebo@4557
|
2230 |
}
|
enebo@4557
|
2231 |
|
enebo@4557
|
2232 |
// See if we have any name suggestions
|
enebo@4557
|
2233 |
String suggestions = (String) params.get(ATTR_DEFAULTS);
|
enebo@4557
|
2234 |
|
enebo@4557
|
2235 |
// Check the suggestions
|
enebo@4557
|
2236 |
if (suggestions != null && !suggestions.isEmpty()) {
|
enebo@4557
|
2237 |
String[] names = suggestions.split(",");
|
enebo@4557
|
2238 |
|
enebo@4557
|
2239 |
for (String suggestion : names) {
|
enebo@4557
|
2240 |
if (!variables.containsKey(suggestion)) return suggestion;
|
enebo@4557
|
2241 |
}
|
enebo@4557
|
2242 |
|
enebo@4557
|
2243 |
// Try some variations of the name
|
enebo@4557
|
2244 |
for (String suggestion : names) {
|
enebo@4557
|
2245 |
for (int number = 2; number < 5; number++) {
|
enebo@4557
|
2246 |
String name = suggestion + number;
|
enebo@4557
|
2247 |
|
enebo@4557
|
2248 |
if (!variables.containsKey(name)) return name;
|
tor@681
|
2249 |
}
|
enebo@4557
|
2250 |
}
|
enebo@4557
|
2251 |
}
|
tor@1
|
2252 |
|
enebo@4557
|
2253 |
// Try the prefix
|
enebo@4557
|
2254 |
if (!prefix.isEmpty() && !variables.containsKey(prefix)) return prefix;
|
tor@1
|
2255 |
|
enebo@4557
|
2256 |
// TODO: What's the right algorithm for uniqueifying a variable name in Ruby?
|
enebo@4557
|
2257 |
// For now, will just append a number
|
enebo@4557
|
2258 |
if (isEmpty(prefix)) prefix = "var";
|
tor@1
|
2259 |
|
enebo@4557
|
2260 |
for (int number = 1; number < 15; number++) {
|
enebo@4557
|
2261 |
String name = number == 1 ? prefix : (prefix + number);
|
tor@1
|
2262 |
|
enebo@4557
|
2263 |
if (!variables.containsKey(name)) return name;
|
enebo@4557
|
2264 |
}
|
tor@1
|
2265 |
|
enebo@4557
|
2266 |
return null;
|
tor@1
|
2267 |
}
|
tor@1
|
2268 |
|
enebo@4519
|
2269 |
@Override
|
mkrauskopf@3030
|
2270 |
public String resolveTemplateVariable(String variable, ParserResult result, int caretOffset,
|
tor@1
|
2271 |
String name, Map params) {
|
enebo@4519
|
2272 |
if (variable.equals(KEY_PIPE)) return "||";
|
tor@671
|
2273 |
|
tor@671
|
2274 |
// Old-style format - support temporarily
|
tor@671
|
2275 |
if (variable.equals(ATTR_UNUSEDLOCAL)) { // TODO REMOVEME
|
mkrauskopf@3030
|
2276 |
return suggestName(result, caretOffset, name, params);
|
tor@671
|
2277 |
}
|
tor@671
|
2278 |
|
tor@671
|
2279 |
if (params != null && params.containsKey(ATTR_UNUSEDLOCAL)) {
|
mkrauskopf@3030
|
2280 |
return suggestName(result, caretOffset, name, params);
|
tor@1
|
2281 |
}
|
tor@1
|
2282 |
|
tor@1
|
2283 |
if ((!(variable.equals(KEY_METHOD) || variable.equals(KEY_METHOD_FQN) ||
|
tor@1
|
2284 |
variable.equals(KEY_CLASS) || variable.equals(KEY_CLASS_FQN) ||
|
tor@1
|
2285 |
variable.equals(KEY_SUPERCLASS) || variable.equals(KEY_PATH) ||
|
tor@1
|
2286 |
variable.equals(KEY_FILE)))) {
|
tor@1
|
2287 |
return null;
|
tor@1
|
2288 |
}
|
tor@1
|
2289 |
|
mkrauskopf@3030
|
2290 |
caretOffset = AstUtilities.boundCaretOffset(result, caretOffset);
|
tor@1
|
2291 |
|
mkrauskopf@3030
|
2292 |
Node root = AstUtilities.getRoot(result);
|
enebo@4519
|
2293 |
if (root == null) return null;
|
tor@1
|
2294 |
|
tor@1
|
2295 |
AstPath path = new AstPath(root, caretOffset);
|
tor@1
|
2296 |
|
tor@1
|
2297 |
if (variable.equals(KEY_METHOD)) {
|
enebo@4549
|
2298 |
MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
|
tor@1
|
2299 |
|
enebo@4549
|
2300 |
if (method != null) return method.getName();
|
tor@1
|
2301 |
} else if (variable.equals(KEY_METHOD_FQN)) {
|
enebo@4549
|
2302 |
MethodDefNode method = AstUtilities.findMethodAtOffset(root, caretOffset);
|
tor@1
|
2303 |
|
enebo@4549
|
2304 |
if (method != null) {
|
tor@1
|
2305 |
String ctx = AstUtilities.getFqnName(path);
|
tor@1
|
2306 |
|
enebo@4549
|
2307 |
return !ctx.isEmpty() ? ctx + "#" + method.getName() : method.getName();
|
tor@1
|
2308 |
}
|
tor@1
|
2309 |
} else if (variable.equals(KEY_CLASS)) {
|
tor@1
|
2310 |
ClassNode node = AstUtilities.findClass(path);
|
tor@1
|
2311 |
|
enebo@4519
|
2312 |
if (node != null) return node.getCPath().getName();
|
tor@1
|
2313 |
} else if (variable.equals(KEY_SUPERCLASS)) {
|
tor@1
|
2314 |
ClassNode node = AstUtilities.findClass(path);
|
tor@1
|
2315 |
|
tor@1
|
2316 |
if (node != null) {
|
mkrauskopf@3030
|
2317 |
RubyIndex index = RubyIndex.get(result);
|
mkrauskopf@3030
|
2318 |
if (index != null) {
|
tor@1
|
2319 |
IndexedClass cls = index.getSuperclass(AstUtilities.getFqnName(path));
|
tor@1
|
2320 |
|
enebo@4519
|
2321 |
if (cls != null) return cls.getFqn();
|
tor@1
|
2322 |
}
|
tor@1
|
2323 |
|
tor@1
|
2324 |
String superCls = AstUtilities.getSuperclass(node);
|
enebo@4519
|
2325 |
|
enebo@4519
|
2326 |
return superCls != null ? superCls : "Object";
|
tor@1
|
2327 |
}
|
tor@1
|
2328 |
} else if (variable.equals(KEY_CLASS_FQN)) {
|
tor@1
|
2329 |
return AstUtilities.getFqnName(path);
|
tor@1
|
2330 |
} else if (variable.equals(KEY_FILE)) {
|
mkrauskopf@3030
|
2331 |
return FileUtil.toFile(result.getSnapshot().getSource().getFileObject()).getName();
|
tor@1
|
2332 |
} else if (variable.equals(KEY_PATH)) {
|
mkrauskopf@3030
|
2333 |
return FileUtil.toFile(RubyUtils.getFileObject(result)).getPath();
|
tor@1
|
2334 |
}
|
tor@1
|
2335 |
|
tor@1
|
2336 |
return null;
|
tor@1
|
2337 |
}
|
tor@1
|
2338 |
|
enebo@4519
|
2339 |
@Override
|
mkrauskopf@3030
|
2340 |
public ParameterInfo parameters(ParserResult info, int lexOffset, CompletionProposal proposal) {
|
tor@581
|
2341 |
IndexedMethod[] methodHolder = new IndexedMethod[1];
|
tor@581
|
2342 |
int[] paramIndexHolder = new int[1];
|
tor@581
|
2343 |
int[] anchorOffsetHolder = new int[1];
|
tor@581
|
2344 |
int astOffset = AstUtilities.getAstOffset(info, lexOffset);
|
mkrauskopf@2743
|
2345 |
if (!RubyMethodCompleter.computeMethodCall(info, lexOffset, astOffset,
|
mkrauskopf@3030
|
2346 |
methodHolder, paramIndexHolder, anchorOffsetHolder, null, QuerySupport.Kind.PREFIX)) {
|
tor@581
|
2347 |
|
tor@1
|
2348 |
return ParameterInfo.NONE;
|
tor@1
|
2349 |
}
|
tor@1
|
2350 |
|
tor@581
|
2351 |
IndexedMethod method = methodHolder[0];
|
tor@581
|
2352 |
if (method == null) {
|
tor@1
|
2353 |
return ParameterInfo.NONE;
|
tor@1
|
2354 |
}
|
tor@581
|
2355 |
int index = paramIndexHolder[0];
|
tor@2423
|
2356 |
int astAnchorOffset = anchorOffsetHolder[0];
|
tor@2423
|
2357 |
int anchorOffset = LexUtilities.getLexerOffset(info, astAnchorOffset);
|
tor@581
|
2358 |
|
tor@1
|
2359 |
|
tor@1
|
2360 |
// TODO: Make sure the caret offset is inside the arguments portion
|
tor@1
|
2361 |
// (parameter hints shouldn't work on the method call name itself
|
tor@1
|
2362 |
|
tor@581
|
2363 |
// See if we can find the method corresponding to this call
|
tor@581
|
2364 |
// if (proposal != null) {
|
tor@581
|
2365 |
// Element element = proposal.getElement();
|
tor@581
|
2366 |
// if (element instanceof IndexedMethod) {
|
tor@581
|
2367 |
// method = ((IndexedMethod)element);
|
tor@581
|
2368 |
// }
|
tor@581
|
2369 |
// }
|
tor@1
|
2370 |
|
tor@1
|
2371 |
List<String> params = method.getParameters();
|
tor@1
|
2372 |
|
mkrauskopf@2482
|
2373 |
if ((params != null) && (!params.isEmpty())) {
|
tor@1
|
2374 |
return new ParameterInfo(params, index, anchorOffset);
|
tor@1
|
2375 |
}
|
tor@1
|
2376 |
|
tor@1
|
2377 |
return ParameterInfo.NONE;
|
tor@1
|
2378 |
}
|
tor@14
|
2379 |
|
tor@14
|
2380 |
/** Return true if we always want to use parentheses
|
tor@14
|
2381 |
* @todo Make into a user-configurable option
|
tor@14
|
2382 |
* @todo Avoid doing this if there's possible ambiguity (e.g. nested method calls
|
tor@14
|
2383 |
* without spaces
|
tor@14
|
2384 |
*/
|
tor@14
|
2385 |
|
enebo@4519
|
2386 |
@Override
|
tor@70
|
2387 |
public QueryType getAutoQuery(JTextComponent component, String typedText) {
|
tor@70
|
2388 |
char c = typedText.charAt(0);
|
tor@70
|
2389 |
|
enebo@4519
|
2390 |
if (c == '\n' || c == '(' || c == '[' || c == '{') return QueryType.STOP;
|
enebo@4519
|
2391 |
if (c != '.' && c != ':') return QueryType.NONE;
|
tor@70
|
2392 |
|
tor@70
|
2393 |
int offset = component.getCaretPosition();
|
mkrauskopf@3030
|
2394 |
BaseDocument doc = (BaseDocument) component.getDocument();
|
tor@70
|
2395 |
|
tor@145
|
2396 |
if (".".equals(typedText)) { // NOI18N
|
tor@70
|
2397 |
// See if we're in Ruby context
|
tor@874
|
2398 |
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
|
enebo@4519
|
2399 |
if (ts == null) return QueryType.NONE;
|
enebo@4519
|
2400 |
|
tor@70
|
2401 |
ts.move(offset);
|
enebo@4519
|
2402 |
|
enebo@4519
|
2403 |
if (!ts.moveNext() && !ts.movePrevious()) return QueryType.NONE;
|
enebo@4519
|
2404 |
|
enebo@4519
|
2405 |
if (ts.offset() == offset && !ts.movePrevious()) return QueryType.NONE;
|
enebo@4519
|
2406 |
|
tor@874
|
2407 |
Token<? extends RubyTokenId> token = ts.token();
|
tor@70
|
2408 |
TokenId id = token.id();
|
tor@70
|
2409 |
|
enebo@4519
|
2410 |
if (id == RubyTokenId.RANGE) return QueryType.NONE; // ".." is a range, not dot completion
|
tor@70
|
2411 |
|
tor@70
|
2412 |
// TODO - handle embedded ruby
|
tor@145
|
2413 |
if ("comment".equals(id.primaryCategory()) || // NOI18N
|
tor@145
|
2414 |
"string".equals(id.primaryCategory()) || // NOI18N
|
tor@145
|
2415 |
"regexp".equals(id.primaryCategory())) { // NOI18N
|
tor@70
|
2416 |
return QueryType.NONE;
|
tor@70
|
2417 |
}
|
tor@70
|
2418 |
|
tor@70
|
2419 |
return QueryType.COMPLETION;
|
tor@70
|
2420 |
}
|
tor@70
|
2421 |
|
tor@70
|
2422 |
if (":".equals(typedText)) { // NOI18N
|
tor@70
|
2423 |
// See if it was "::" and we're in ruby context
|
tor@70
|
2424 |
int dot = component.getSelectionStart();
|
tor@70
|
2425 |
try {
|
tor@145
|
2426 |
if ((dot > 1 && component.getText(dot-2, 1).charAt(0) == ':') && // NOI18N
|
tor@70
|
2427 |
isRubyContext(doc, dot-1)) {
|
tor@70
|
2428 |
return QueryType.COMPLETION;
|
tor@70
|
2429 |
}
|
tor@70
|
2430 |
} catch (BadLocationException ble) {
|
emononen@3691
|
2431 |
// do nothing - see #154991
|
tor@70
|
2432 |
}
|
tor@70
|
2433 |
}
|
tor@70
|
2434 |
|
tor@70
|
2435 |
return QueryType.NONE;
|
tor@70
|
2436 |
}
|
tor@14
|
2437 |
|
tor@70
|
2438 |
public static boolean isRubyContext(BaseDocument doc, int offset) {
|
tor@874
|
2439 |
TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(doc, offset);
|
tor@70
|
2440 |
|
enebo@4519
|
2441 |
if (ts == null) return false;
|
tor@70
|
2442 |
|
tor@70
|
2443 |
ts.move(offset);
|
tor@70
|
2444 |
|
enebo@4519
|
2445 |
if (!ts.movePrevious() && !ts.moveNext()) return true;
|
tor@70
|
2446 |
|
tor@70
|
2447 |
TokenId id = ts.token().id();
|
tor@145
|
2448 |
if ("comment".equals(id.primaryCategory()) || "string".equals(id.primaryCategory()) || // NOI18N
|
tor@145
|
2449 |
"regexp".equals(id.primaryCategory())) { // NOI18N
|
tor@70
|
2450 |
return false;
|
tor@70
|
2451 |
}
|
tor@70
|
2452 |
|
tor@70
|
2453 |
return true;
|
tor@70
|
2454 |
}
|
mkrauskopf@2736
|
2455 |
|
tor@1
|
2456 |
}
|