/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2010 Oracle and/or its affiliates. All rights reserved. * * Oracle and Java are registered trademarks of Oracle and/or its affiliates. * Other names may be trademarks of their respective owners. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common * Development and Distribution License("CDDL") (collectively, the * "License"). You may not use this file except in compliance with the * License. You can obtain a copy of the License at * http://www.netbeans.org/cddl-gplv2.html * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the * specific language governing permissions and limitations under the * License. When distributing the software, include this License Header * Notice in each file and include the License file at * nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the GPL Version 2 section of the License file that * accompanied this code. If applicable, add the following below the * License Header, with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * If you wish your version of this file to be governed by only the CDDL * or only the GPL Version 2, indicate your decision by adding * "[Contributor] elects to include this software in this distribution * under the [CDDL or GPL Version 2] license." If you do not indicate a * single choice of license, a recipient has the option to distribute * your version of this file under either the CDDL, the GPL Version 2 or * to extend the choice of license to its licensees as provided above. * However, if you add GPL Version 2 code and therefore, elected the GPL * Version 2 license, then the option applies only if the new code is * made subject to such option by the copyright holder. * * Contributor(s): * * Portions Copyrighted 2008 Sun Microsystems, Inc. */ package org.netbeans.modules.ruby; import java.util.List; import java.util.Set; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; import org.netbeans.api.lexer.TokenId; import org.netbeans.api.lexer.TokenSequence; import org.netbeans.api.project.FileOwnerQuery; import org.netbeans.api.project.Project; import org.netbeans.api.ruby.platform.RubyPlatform; import org.netbeans.editor.BaseDocument; import org.netbeans.modules.csl.api.CodeCompletionHandler.QueryType; import org.netbeans.modules.csl.api.CompletionProposal; import org.netbeans.modules.csl.api.OffsetRange; import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport; import org.netbeans.modules.ruby.RubyCompletionItem.ClassItem; import org.netbeans.modules.ruby.RubyCompletionItem.KeywordItem; import org.netbeans.modules.ruby.elements.CommentElement; import org.netbeans.modules.ruby.elements.Element; import org.netbeans.modules.ruby.lexer.LexUtilities; import org.netbeans.modules.ruby.lexer.RubyTokenId; import org.netbeans.modules.ruby.platform.gems.Gem; import org.netbeans.modules.ruby.platform.gems.GemManager; import org.openide.util.NbBundle; final class RubyStringCompleter extends RubyBaseCompleter { private static final String[] RUBY_PERCENT_WORDS = new String[]{ "%q", "String (single-quoting rules)", "%Q", "String (double-quoting rules)", "%r", "Regular Expression", "%x", "Commands", "%W", "String Array (double quoting rules)", "%w", "String Array (single quoting rules)", "%s", "Symbol" }; private static final String[] RUBY_REGEXP_WORDS = new String[]{ "^", "Start of line", "$", "End of line", "\\A", "Beginning of string", "\\z", "End of string", "\\Z", "End of string (except \\n)", "\\w", "Letter or digit; same as [0-9A-Za-z]", "\\W", "Neither letter or digit", "\\s", "Space character; same as [ \\t\\n\\r\\f]", "\\S", "Non-space character", "\\d", "Digit character; same as [0-9]", "\\D", "Non-digit character", "\\b", "Backspace (0x08) (only if in a range specification)", "\\b", "Word boundary (if not in a range specification)", "\\B", "Non-word boundary", "*", "Zero or more repetitions of the preceding", "+", "One or more repetitions of the preceding", "{m,n}", "At least m and at most n repetitions of the preceding", "?", "At most one repetition of the preceding; same as {0,1}", "|", "Either preceding or next expression may match", "()", "Grouping", "[:alnum:]", "Alphanumeric character class", "[:alpha:]", "Uppercase or lowercase letter", "[:blank:]", "Blank and tab", "[:cntrl:]", "Control characters (at least 0x00-0x1f,0x7f)", "[:digit:]", "Digit", "[:graph:]", "Printable character excluding space", "[:lower:]", "Lowecase letter", "[:print:]", "Any printable letter (including space)", "[:punct:]", "Printable character excluding space and alphanumeric", "[:space:]", "Whitespace (same as \\s)", "[:upper:]", "Uppercase letter", "[:xdigit:]", "Hex digit (0-9, a-f, A-F)" }; private static final String[] RUBY_STRING_PAIRS = new String[]{ "(", "(delimiters)", "{", "{delimiters}", "[", "[delimiters]", "x", "<i>x</i>delimiters<i>x</i>" }; private static final String[] RUBY_QUOTED_STRING_ESCAPES = new String[]{ "\\a", "Bell/alert (0x07)", "\\b", "Backspace (0x08)", "\\x", "\\x<i>nn</i>: Hex <i>nn</i>", "\\e", "Escape (0x1b)", "\\c", "Control-<i>x</i>", "\\C-", "Control-<i>x</i>", "\\f", "Formfeed (0x0c)", "\\n", "Newline (0x0a)", "\\M-", "\\M-<i>x</i>: Meta-<i>x</i>", "\\r", "Return (0x0d)", "\\M-\\C-", "Meta-control-<i>x</i>", "\\s", "Space (0x20)", "\\", "\\nnn Octal <i>nnn</i>", //"\\", "<i>x</i>", "\\t", "Tab (0x09)", "#{", "#{expr}: Value of expr", "\\v", "Vertical tab (0x0b)" }; static boolean complete( final List<? super CompletionProposal> proposals, final CompletionRequest request, final int anchor, final boolean caseSensitive) { RubyStringCompleter rsc = new RubyStringCompleter(proposals, request, anchor, caseSensitive); return rsc.complete(); } private RubyStringCompleter( final List<? super CompletionProposal> proposals, final CompletionRequest request, final int anchor, final boolean caseSensitive) { super(proposals, request, anchor, caseSensitive); } @SuppressWarnings("unchecked") private boolean complete() { String prefix = request.prefix; int lexOffset = request.lexOffset; TokenHierarchy<Document> th = request.th; TokenSequence<? extends RubyTokenId> ts = LexUtilities.getRubyTokenSequence(th, lexOffset); if ((getIndex() != null) && (ts != null)) { ts.move(lexOffset); if (!ts.moveNext() && !ts.movePrevious()) { return false; } if (ts.offset() == lexOffset) { // We're looking at the offset to the RIGHT of the caret // and here I care about what's on the left ts.movePrevious(); } Token<? extends RubyTokenId> token = ts.token(); if (token != null) { TokenId id = token.id(); if (id == RubyTokenId.LINE_COMMENT || id == RubyTokenId.DOCUMENTATION) { // Comment completion - rdoc tags and such if (request.queryType == QueryType.DOCUMENTATION) { BaseDocument doc = request.doc; OffsetRange commentBlock = LexUtilities.getCommentBlock(doc, lexOffset); if (commentBlock != OffsetRange.NONE) { try { String text = doc.getText(commentBlock.getStart(), commentBlock.getLength()); if (text.startsWith("=begin\n") && text.endsWith("=end")) { // NOI18N text = text.substring("=begin\n".length(), text.length() - "=end".length()); // NOI18N } Element element = new CommentElement(text); ClassItem item = new ClassItem(element, anchor, request); propose(item); return true; } catch (BadLocationException ble) { // do nothing - see #154991 } } } // No other possible completions in comments return true; } // We're within a String that has embedded Ruby. Drop into the // embedded language and see if we're within a literal string there. if (id == RubyTokenId.EMBEDDED_RUBY) { ts = (TokenSequence) ts.embedded(); assert ts != null; ts.move(lexOffset); if (!ts.moveNext() && !ts.movePrevious()) { return false; } token = ts.token(); id = token.id(); } boolean inString = false; boolean isQuoted = false; boolean inRegexp = false; String tokenText = token.text().toString(); // Percent completion if ((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) || ((id == RubyTokenId.ERROR) && tokenText.equals("%"))) { int offset = ts.offset(); if ((offset == (lexOffset - 1)) && (tokenText.length() > 0) && (tokenText.charAt(0) == '%')) { if (completePercentWords()) { return true; } } } // Incomplete String/Regexp marker: %x|{ if (((id == RubyTokenId.STRING_BEGIN) || (id == RubyTokenId.QUOTED_STRING_BEGIN) || (id == RubyTokenId.REGEXP_BEGIN)) && ((token.length() == 3) && (lexOffset == (ts.offset() + 2)))) { if (Character.isLetter(tokenText.charAt(1))) { completeStringBegins(); return true; } } // Skip back to the beginning of the String. I have to loop since I // may have embedded Ruby segments. while ((id == RubyTokenId.ERROR) || (id == RubyTokenId.STRING_LITERAL) || (id == RubyTokenId.QUOTED_STRING_LITERAL) || (id == RubyTokenId.REGEXP_LITERAL) || (id == RubyTokenId.EMBEDDED_RUBY)) { if (id == RubyTokenId.QUOTED_STRING_LITERAL) { isQuoted = true; } if (!ts.movePrevious()) { return false; } token = ts.token(); id = token.id(); } if (id == RubyTokenId.STRING_BEGIN) { inString = true; } else if (id == RubyTokenId.QUOTED_STRING_BEGIN) { inString = true; isQuoted = true; } else if (id == RubyTokenId.REGEXP_BEGIN) { inRegexp = true; } if (inRegexp) { if (completeRegexps()) { request.completionResult.setFilterable(false); return true; } } else if (inString) { // Completion of literal strings within require calls while (ts.movePrevious()) { token = ts.token(); if ((token.id() == RubyTokenId.WHITESPACE) || (token.id() == RubyTokenId.LPAREN) || (token.id() == RubyTokenId.STRING_LITERAL) || (token.id() == RubyTokenId.QUOTED_STRING_LITERAL) || (token.id() == RubyTokenId.STRING_BEGIN) || (token.id() == RubyTokenId.QUOTED_STRING_BEGIN)) { continue; } if (token.id() == RubyTokenId.IDENTIFIER) { String text = token.text().toString(); if (text.equals("require") || text.equals("load")) { // Do require-completion Set<String[]> requires = getIndex().getRequires(prefix, caseSensitive ? QuerySupport.Kind.PREFIX : QuerySupport.Kind.CASE_INSENSITIVE_PREFIX); for (String[] require : requires) { assert require.length == 2; // If a method is an "initialize" method I should do something special so that // it shows up as a "constructor" (in a new() statement) but not as a directly // callable initialize method (it should already be culled because it's private) KeywordItem item = new KeywordItem(require[0], require[1], anchor, request); propose(item); } return true; } else if ("gem".equals(text)) { proposeGems(prefix); } else { break; } } else { break; } } if (inString && isQuoted) { for (int i = 0, n = RUBY_QUOTED_STRING_ESCAPES.length; i < n; i += 2) { String word = RUBY_QUOTED_STRING_ESCAPES[i]; String desc = RUBY_QUOTED_STRING_ESCAPES[i + 1]; if (!word.startsWith(prefix)) { continue; } KeywordItem item = new KeywordItem(word, desc, anchor, request); propose(item); } return true; } else if (inString) { // No completions for single quoted strings return true; } } } } return false; } private void proposeGems(String prefix) { Project owner = FileOwnerQuery.getOwner(request.fileObject); if (owner == null) { return; } GemManager gemManager = RubyPlatform.gemManagerFor(owner); if (gemManager == null) { return; } List<Gem> gems = gemManager.getLocalGems(); for (Gem gem : gems) { if (gem.getName().startsWith(prefix)) { String versions = NbBundle.getMessage(RubyStringCompleter.class, "InstalledVersions", gem.getInstalledVersionsAsString()); KeywordItem item = new KeywordItem(gem.getName(), versions, anchor, request); propose(item); } } } private boolean completePercentWords() { String prefix = request.prefix; for (int i = 0, n = RUBY_PERCENT_WORDS.length; i < n; i += 2) { String word = RUBY_PERCENT_WORDS[i]; String desc = RUBY_PERCENT_WORDS[i + 1]; if (RubyCodeCompleter.startsWith(word, prefix, caseSensitive)) { KeywordItem item = new KeywordItem(word, desc, anchor, request); propose(item); } } return true; } private boolean completeRegexps() { String prefix = request.prefix; // Regular expression matching. { for (int i = 0, n = RUBY_REGEXP_WORDS.length; i < n; i += 2) { String word = RUBY_REGEXP_WORDS[i]; String desc = RUBY_REGEXP_WORDS[i + 1]; if (RubyCodeCompleter.startsWith(word, prefix, caseSensitive)) { KeywordItem item = new KeywordItem(word, desc, anchor, request); propose(item); } } return true; } private boolean completeStringBegins() { for (int i = 0, n = RUBY_STRING_PAIRS.length; i < n; i += 2) { String word = RUBY_STRING_PAIRS[i]; String desc = RUBY_STRING_PAIRS[i + 1]; KeywordItem item = new KeywordItem(word, desc, anchor, request); propose(item); } return true; } }