// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package com.google.collide.client.code.autocomplete.codegraph; import static com.google.collide.client.code.autocomplete.codegraph.ParseUtils.Context.IN_CODE; import static com.google.collide.codemirror2.Token.LITERAL_PERIOD; import static com.google.collide.codemirror2.TokenType.KEYWORD; import static com.google.collide.codemirror2.TokenType.NULL; import static com.google.collide.codemirror2.TokenType.VARIABLE; import static com.google.collide.codemirror2.TokenType.VARIABLE2; import static com.google.collide.codemirror2.TokenType.WHITESPACE; import com.google.collide.client.code.autocomplete.AutocompleteProposal; import com.google.collide.client.code.autocomplete.AutocompleteProposals; import com.google.collide.client.code.autocomplete.AutocompleteResult; import com.google.collide.client.code.autocomplete.Autocompleter; import com.google.collide.client.code.autocomplete.DefaultAutocompleteResult; import com.google.collide.client.code.autocomplete.PrefixIndex; import com.google.collide.client.code.autocomplete.AutocompleteProposals.Context; import com.google.collide.client.code.autocomplete.codegraph.ParseUtils.ExtendedParseResult; import com.google.collide.client.documentparser.DocumentParser; import com.google.collide.client.documentparser.ParseResult; import com.google.collide.client.editor.selection.SelectionModel; import com.google.collide.client.util.PathUtil; import com.google.collide.codemirror2.State; import com.google.collide.codemirror2.SyntaxType; import com.google.collide.codemirror2.Token; import com.google.collide.codemirror2.TokenType; import com.google.collide.json.client.JsoStringSet; import com.google.collide.json.shared.JsonArray; import com.google.collide.json.shared.JsonStringSet; import com.google.collide.shared.document.Position; import com.google.collide.shared.util.JsonCollections; import com.google.collide.shared.util.StringUtils; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; import javax.annotation.Nonnull; // TODO: Implement autocompletion-session end notification. /** * Builds CompletionContext and proposals list. * * @param <T> language-specific {@link State} type. */ public abstract class ProposalBuilder<T extends State> { // TODO: Fix wording. private static final String HINT = "Press Ctrl-Shift-Space for alternate completion"; private final Class<T> stateClass; protected ProposalBuilder(Class<T> stateClass) { this.stateClass = stateClass; } /** * Add more proposals prefixes based on language specifics. */ protected abstract void addShortcutsTo(CompletionContext<T> context, JsonStringSet prefixes); /** * Returns language-specific templates. * * <p>Only lower-case items will match in case-insensitive mode. */ protected abstract PrefixIndex<TemplateProposal> getTemplatesIndex(); /** * Returns local variables visible in the current scope. */ protected abstract JsonArray<String> getLocalVariables(ParseResult<T> parseResult); /** * Checks if the given prefix denotes "this"/"self" context. * * <p>Prefix is the beginning of the statement to the last period (including). * * <p>In case implementation returns {@code true} - * {@link CompletionContext#previousContext} is turned to empty in a purpose * of shortcutting. * * TODO: I think we should move this implicit shortcutting to a more * proper place. */ protected abstract boolean checkIsThisPrefix(String prefix); /** * Constructs context based on text around current cursor position. */ @VisibleForTesting public CompletionContext<T> buildContext( SelectionModel selection, @Nonnull DocumentParser parser) { ExtendedParseResult<T> parseResult = ParseUtils.getExtendedParseResult( stateClass, parser, selection.getCursorPosition()); if (parseResult.getContext() != IN_CODE) { return null; } return buildContext(parseResult); } protected CompletionContext<T> buildContext(ExtendedParseResult<T> extendedParseResult) { Preconditions.checkArgument(extendedParseResult.getContext() == IN_CODE); ParseResult<T> parseResult = extendedParseResult.getParseResult(); JsonArray<Token> tokens = parseResult.getTokens(); if (tokens.isEmpty()) { return new CompletionContext<T>("", "", false, CompletionType.GLOBAL, parseResult, 0); } int indent = 0; if (TokenType.WHITESPACE == tokens.get(0).getType()) { indent = tokens.get(0).getValue().length(); } Token lastToken = tokens.pop(); TokenType lastTokenType = lastToken.getType(); if (lastTokenType == WHITESPACE) { return new CompletionContext<T>("", "", false, CompletionType.GLOBAL, parseResult, indent); } String lastTokenValue = lastToken.getValue(); if (lastTokenType == KEYWORD) { return new CompletionContext<T>( "", lastTokenValue, false, CompletionType.GLOBAL, parseResult, indent); } boolean expectingPeriod = true; String triggeringString; // Property autocompletion only when cursor stands after period or id. if (lastTokenType == VARIABLE || lastTokenType == VARIABLE2 || lastTokenType == TokenType.PROPERTY) { triggeringString = lastTokenValue; } else if ((lastTokenType == NULL) && LITERAL_PERIOD.equals(lastTokenValue)) { triggeringString = ""; expectingPeriod = false; } else { return new CompletionContext<T>("", "", false, CompletionType.GLOBAL, parseResult, indent); } JsonArray<String> contextParts = JsonCollections.createArray(); expectingPeriod = ParseUtils .buildInvocationSequenceContext(tokens, expectingPeriod, contextParts); contextParts.reverse(); // If there were no more ids. if (contextParts.isEmpty() && expectingPeriod) { return new CompletionContext<T>( "", triggeringString, false, CompletionType.GLOBAL, parseResult, indent); } // TODO: What if expectingPeriod == false? String previousContext = contextParts.join(".") + "."; boolean isThisContext = checkIsThisPrefix(previousContext); return new CompletionContext<T>(previousContext, triggeringString, isThisContext, CompletionType.PROPERTY, parseResult, indent); } // TODO: Implement multiline context building. /** * Build {@link AutocompleteResult} according to current context and * selected proposal. */ AutocompleteResult computeAutocompletionResult( CodeGraphProposal selectedProposal, String triggeringString) { String name = selectedProposal.getName(); int tailOffset = 0; if (selectedProposal.isFunction()) { tailOffset = 1; name += "()"; } return new DefaultAutocompleteResult(name, triggeringString, name.length() - tailOffset); } public AutocompleteProposals getProposals(SyntaxType mode, @Nonnull DocumentParser parser, SelectionModel selection, ScopeTrieBuilder scopeTrieBuilder) { CompletionContext<T> context = buildContext(selection, parser); if (context == null) { return AutocompleteProposals.EMPTY; } String triggeringString = context.getTriggeringString(); JsonArray<AutocompleteProposal> items = doGetProposals( context, selection.getCursorPosition(), scopeTrieBuilder); return new AutocompleteProposals( mode, new Context(triggeringString, context.getIndent()), items, HINT); } @VisibleForTesting JsonArray<AutocompleteProposal> doGetProposals( CompletionContext<T> context, Position cursorPosition, ScopeTrieBuilder scopeTrieBuilder) { String itemPrefix = context.getTriggeringString(); boolean ignoreCase = Autocompleter.CASE_INSENSITIVE; if (ignoreCase) { itemPrefix = itemPrefix.toLowerCase(); } // A set used to avoid duplicates. JsoStringSet uniqueNames = JsoStringSet.create(); // This array will be filled with proposals form different sources: // templates; visible names found by parser; matches from code graph. JsonArray<AutocompleteProposal> result = JsonCollections.createArray(); // This also means previousContext == "" if (CompletionType.GLOBAL == context.getCompletionType()) { // Add templates. JsonArray<? extends TemplateProposal> templates = getTemplatesIndex().search(itemPrefix); result.addAll(templates); for (TemplateProposal template : templates.asIterable()) { uniqueNames.add(template.getName()); } // Add visible names found by parser. JsonArray<String> localVariables = getLocalVariables(context.getParseResult()); for (String localVariable : localVariables.asIterable()) { if (StringUtils.startsWith(itemPrefix, localVariable, ignoreCase)) { if (!uniqueNames.contains(localVariable)) { uniqueNames.add(localVariable); result.add(new CodeGraphProposal(localVariable, PathUtil.EMPTY_PATH, false)); } } } } // Now use the knowledge about current scope and calculate possible // shortcuts in code graph. JsonStringSet prefixes = scopeTrieBuilder.calculateScopePrefixes(context, cursorPosition); // Let language-specific modifications. addShortcutsTo(context, prefixes); PrefixIndex<CodeGraphProposal> codeGraphTrie = scopeTrieBuilder.getCodeGraphTrie(); JsonArray<AutocompleteProposal> codeProposals = JsonCollections.createArray(); // We're iterate found shortcuts... for (String prefix : prefixes.getKeys().asIterable()) { JsonArray<? extends CodeGraphProposal> proposals = codeGraphTrie.search(prefix + itemPrefix); // Distill raw proposals. int prefixLength = prefix.length(); for (CodeGraphProposal proposal : proposals.asIterable()) { // Take part of string between prefix and period. String proposalName = proposal.getName(); int nameEndIndex = proposalName.length(); int periodIndex = proposalName.indexOf('.', prefixLength); if (periodIndex != -1) { // TODO: Do we need this? nameEndIndex = periodIndex; } proposalName = proposalName.substring(prefixLength, nameEndIndex); if (!uniqueNames.contains(proposalName)) { uniqueNames.add(proposalName); codeProposals.add( new CodeGraphProposal(proposalName, proposal.getPath(), proposal.isFunction())); } } } result.addAll(codeProposals); return result; } }