// 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.css; import static com.google.collide.client.code.autocomplete.AutocompleteResult.PopupAction.CLOSE; import static com.google.collide.client.code.autocomplete.AutocompleteResult.PopupAction.OPEN; import static com.google.collide.client.code.autocomplete.css.CompletionType.CLASS; import static com.google.collide.client.code.autocomplete.css.CompletionType.PROPERTY; import static com.google.collide.client.code.autocomplete.css.CompletionType.VALUE; import static com.google.collide.codemirror2.TokenType.NULL; import com.google.collide.client.code.autocomplete.AbstractTrie; import com.google.collide.client.code.autocomplete.AutocompleteController; 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.DefaultAutocompleteResult; import com.google.collide.client.code.autocomplete.LanguageSpecificAutocompleter; import com.google.collide.client.code.autocomplete.SignalEventEssence; import com.google.collide.client.code.autocomplete.AutocompleteProposals.ProposalWithContext; 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.client.util.logging.Log; import com.google.collide.codemirror2.CssState; import com.google.collide.codemirror2.CssToken; import com.google.collide.codemirror2.SyntaxType; import com.google.collide.codemirror2.Token; import com.google.collide.json.shared.JsonArray; import com.google.collide.shared.document.Line; import com.google.collide.shared.document.Position; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; /** * Autocompleter for CSS. Currently, this only supports CSS2. * * TODO: Support CSS3. * TODO: may be trigger on ":"? */ public class CssAutocompleter extends LanguageSpecificAutocompleter { private static final String PROPERTY_TERMINATOR = ";"; private static final String PROPERTY_SEPARATOR = ": "; private static final String CLASS_SEPARATOR = "{\n \n}"; private static final int CLASS_JUMPLENGTH = 4; private static final AbstractTrie<AutocompleteProposal> cssTrie = CssTrie.createTrie(); public static CssAutocompleter create() { return new CssAutocompleter(); } private static AutocompleteResult constructResult(String rawResult, String triggeringString) { int start = rawResult.indexOf('<'); int end = rawResult.indexOf('>'); if ((start >= 0) && (start < end)) { return new DefaultAutocompleteResult( rawResult, (end + 1), 0, (end + 1) - start, 0, CLOSE, triggeringString); } return new DefaultAutocompleteResult(rawResult, triggeringString, rawResult.length()); } private CssCompletionQuery completionQuery; private CssAutocompleter() { super(SyntaxType.CSS); } @Override public void attach( DocumentParser parser, AutocompleteController controller, PathUtil filePath) { super.attach(parser, controller, filePath); completionQuery = null; } @Override public AutocompleteResult computeAutocompletionResult(ProposalWithContext proposal) { AutocompleteProposal selectedProposal = proposal.getItem(); String triggeringString = proposal.getContext().getTriggeringString(); String name = selectedProposal.getName(); CompletionType completionType = completionQuery.getCompletionType(); if (CLASS == completionType) { // In this case implicit autocompletion workflow should trigger, // and so execution should never reach this point. Log.warn(getClass(), "Invocation of this method in not allowed for type CLASS"); return DefaultAutocompleteResult.EMPTY; } else if (PROPERTY == completionType) { String addend = name + PROPERTY_SEPARATOR + PROPERTY_TERMINATOR; int jumpLength = addend.length() - PROPERTY_TERMINATOR.length(); return new DefaultAutocompleteResult( addend, jumpLength, 0, 0, 0, OPEN, triggeringString); } else if (VALUE == completionType) { return constructResult(name, triggeringString); } Log.warn(getClass(), "Invocation of this method in not allowed for type NONE"); return DefaultAutocompleteResult.EMPTY; } /** * Creates a completion query from the position of the caret and the editor. * The completion query contains the string to complete and the type of * autocompletion. * * TODO: take care of quoted '{' and '}' */ @VisibleForTesting CssCompletionQuery updateOrCreateQuery(CssCompletionQuery completionQuery, Position cursor) { Line line = cursor.getLine(); int column = cursor.getColumn(); Line lineWithCursor = line; boolean parsingLineWithCursor = true; /* * textSoFar will contain the text of the CSS rule (only the stuff within * the curly braces). If we are not in an open rule, return false */ String textBefore = ""; while ((line != null) && (!textBefore.contains("{"))) { int lastOpen; int lastClose; String text; if (parsingLineWithCursor) { text = line.getText().substring(0, column); parsingLineWithCursor = false; } else { /* * Don't include the newline character; it is irrelevant for * autocompletion. */ text = line.getText().trim(); } textBefore = text + textBefore; lastOpen = text.lastIndexOf('{'); lastClose = text.lastIndexOf('}'); // Either we have only a } or the } appears after { if (lastOpen < lastClose) { return completionQuery; } else if ((lastOpen == -1) && (lastClose == -1)) { line = line.getPreviousLine(); } else { if (textBefore.endsWith("{")) { // opening a new css class, no text after to consider return new CssCompletionQuery(textBefore, ""); } else if (textBefore.endsWith(";") && completionQuery != null) { // we don't want to create a new query, otherwise we lose the // completed proposals completionQuery.setCompletionType(CompletionType.NONE); return completionQuery; } } } parsingLineWithCursor = true; String textAfter = ""; line = lineWithCursor; while ((line != null) && (!textAfter.contains("}"))) { int lastOpen; int lastClose; String text; if (parsingLineWithCursor) { text = line.getText().substring(column); parsingLineWithCursor = false; } else { /* * Don't include the newline character; it is irrelevant for * autocompletion. */ text = line.getText().trim(); } textAfter = textAfter + text; lastOpen = text.lastIndexOf('{'); lastClose = text.lastIndexOf('}'); // Either we have only a } or the } appears after { if (lastClose < lastOpen) { return completionQuery; } else if ((lastOpen == -1) && (lastClose == -1)) { line = line.getNextLine(); } else { if ((!textAfter.isEmpty()) && (textAfter.charAt(textAfter.length() - 1) == ';')) { return completionQuery; } } } if (textBefore.contains("{")) { textBefore = textBefore.substring(textBefore.indexOf('{') + 1); } if (textAfter.contains("}")) { textAfter = textAfter.substring(0, textAfter.indexOf('}')); } return new CssCompletionQuery(textBefore, textAfter); } /** * Finds autocompletions for a given completion query. * * @return an array of autocompletion proposals */ @Override public AutocompleteProposals findAutocompletions( SelectionModel selection, SignalEventEssence trigger) { if (selection.hasSelection()) { // Doesn't make much sense to autocomplete CSS when something is selected. return AutocompleteProposals.EMPTY; } completionQuery = updateOrCreateQuery(completionQuery, selection.getCursorPosition()); if (completionQuery == null) { return AutocompleteProposals.EMPTY; } String triggeringString = completionQuery.getTriggeringString(); if (triggeringString == null) { return AutocompleteProposals.EMPTY; } switch (completionQuery.getCompletionType()) { case PROPERTY: return new AutocompleteProposals(SyntaxType.CSS, triggeringString, CssTrie.findAndFilterAutocompletions( cssTrie, triggeringString, completionQuery.getCompletedProperties())); case VALUE: return new AutocompleteProposals(SyntaxType.CSS, triggeringString, CssPartialParser.getInstance().getAutocompletions( completionQuery.getProperty(), completionQuery.getValuesBefore(), triggeringString, completionQuery.getValuesAfter())); case CLASS: // TODO: Implement css-class autocompletions (pseudoclasses // and HTML elements). return AutocompleteProposals.EMPTY; default: return AutocompleteProposals.EMPTY; } } @Override public ExplicitAction getExplicitAction(SelectionModel selectionModel, SignalEventEssence signal, boolean popupIsShown) { if (signal.getChar() != '{') { return ExplicitAction.DEFAULT; } if (selectionModel.hasSelection()) { return ExplicitAction.DEFAULT; } DocumentParser parser = getParser(); // 1) Check we are not in block already. ParseResult<CssState> parseResult = parser.getState( CssState.class, selectionModel.getCursorPosition(), " "); if (parseResult == null) { return ExplicitAction.DEFAULT; } JsonArray<Token> tokens = parseResult.getTokens(); Preconditions.checkNotNull(tokens); Preconditions.checkState(tokens.size() > 0); CssToken lastToken = (CssToken) tokens.peek(); if ("{".equals(lastToken.getContext())) { return ExplicitAction.DEFAULT; } // 2) Check we will enter block. parseResult = parser.getState(CssState.class, selectionModel.getCursorPosition(), "{"); if (parseResult == null) { return ExplicitAction.DEFAULT; } tokens = parseResult.getTokens(); Preconditions.checkNotNull(tokens); Preconditions.checkState(tokens.size() > 0); lastToken = (CssToken) tokens.peek(); String context = lastToken.getContext(); boolean inBlock = context != null && context.endsWith("{"); if (inBlock && NULL == lastToken.getType()) { return new ExplicitAction( new DefaultAutocompleteResult(CLASS_SEPARATOR, "", CLASS_JUMPLENGTH)); } return ExplicitAction.DEFAULT; } @Override protected void pause() { super.pause(); completionQuery = null; } @Override public void cleanup() { } }