/** * Copyright 2016-2017 Linagora, Université Joseph Fourier, Floralis * * The present code is developed in the scope of the joint LINAGORA - * Université Joseph Fourier - Floralis research program and is designated * as a "Result" pursuant to the terms and conditions of the LINAGORA * - Université Joseph Fourier - Floralis research program. Each copyright * holder of Results enumerated here above fully & independently holds complete * ownership of the complete Intellectual Property rights applicable to the whole * of said Results, and may freely exploit it in any manner which does not infringe * the moral rights of the other copyright holders. * * 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 net.roboconf.tooling.core.autocompletion; import static net.roboconf.core.dsl.ParsingConstants.KEYWORD_FACET; import static net.roboconf.core.dsl.ParsingConstants.KEYWORD_IMPORT; import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_COMPONENT_FACETS; import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_COMPONENT_IMPORTS; import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_COMPONENT_INSTALLER; import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_GRAPH_CHILDREN; import static net.roboconf.core.dsl.ParsingConstants.PROPERTY_GRAPH_EXTENDS; import static net.roboconf.tooling.core.TextUtils.isLineBreak; import static net.roboconf.tooling.core.autocompletion.CompletionUtils.basicProposal; import static net.roboconf.tooling.core.autocompletion.CompletionUtils.buildProposalsFromMap; import static net.roboconf.tooling.core.autocompletion.CompletionUtils.findAllExportedVariables; import static net.roboconf.tooling.core.autocompletion.CompletionUtils.findTypeNames; import static net.roboconf.tooling.core.autocompletion.CompletionUtils.startsWith; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.TreeMap; import net.roboconf.core.dsl.ParsingConstants; import net.roboconf.core.utils.Utils; import net.roboconf.tooling.core.SelectionRange; import net.roboconf.tooling.core.TextUtils; /** * @author Vincent Zurczak - Linagora */ public class GraphsCompletionProposer implements ICompletionProposer { static final String FACET_BLOCK = "New facet block"; static final String COMPONENT_BLOCK = "New component block"; static final String FACET_PREFIX = KEYWORD_FACET + " "; static final String IMPORT_PREFIX = KEYWORD_IMPORT + " "; static final String[] KNOWN_INSTALLERS = { "script", "puppet", "logger" }; static final String[] COMPONENT_PROPERTY_NAMES = { ParsingConstants.PROPERTY_GRAPH_CHILDREN, ParsingConstants.PROPERTY_GRAPH_EXPORTS, ParsingConstants.PROPERTY_GRAPH_EXTENDS, ParsingConstants.PROPERTY_COMPONENT_FACETS, ParsingConstants.PROPERTY_COMPONENT_IMPORTS, ParsingConstants.PROPERTY_COMPONENT_INSTALLER }; static final String[] FACET_PROPERTY_NAMES = { ParsingConstants.PROPERTY_GRAPH_CHILDREN, ParsingConstants.PROPERTY_GRAPH_EXPORTS, ParsingConstants.PROPERTY_GRAPH_EXTENDS }; private final File appDirectory; private final File editedFile; /** * Constructor. * @param appDirectory * @param editedFile */ public GraphsCompletionProposer( File appDirectory, File editedFile ) { this.appDirectory = appDirectory; this.editedFile = editedFile; } @Override public List<RoboconfCompletionProposal> findProposals( String text ) { // Find the text to insert List<RoboconfCompletionProposal> proposals = new ArrayList<> (); Ctx ctx = findContext( text ); switch( ctx.kind ) { case NEUTRAL: // Import if( startsWith( IMPORT_PREFIX, ctx.property )) { // "import" or a part of this word if( ! ctx.property.matches( "(?i)" + KEYWORD_IMPORT + "\\s+" )) proposals.add( basicProposal( IMPORT_PREFIX, ctx.property, true )); // "import " else for( String graphImport : CompletionUtils.findGraphFilesToImport( this.appDirectory, this.editedFile, text )) { if( startsWith( graphImport, ctx.lastWord )) proposals.add( basicProposal( graphImport, "", false )); } } // From here, there cannot be any word before the offset. if( ! Utils.isEmptyOrWhitespaces( ctx.lastWord )) break; // Facet ctx.property = ctx.property.trim(); if( startsWith( FACET_PREFIX, ctx.property )) { // Basic proposal: facet proposals.add( basicProposal( FACET_PREFIX, ctx.property, true )); // More complex proposal: a full block String proposalString = FACET_PREFIX + "name {\n\t\n}"; RoboconfCompletionProposal proposal = new RoboconfCompletionProposal( proposalString, FACET_BLOCK, FACET_BLOCK + "\n\n" + proposalString.trim(), ctx.property.length()); proposal.getSelection().add( new SelectionRange( 6, 4 )); proposals.add( proposal ); } // Component if( Utils.isEmptyOrWhitespaces( ctx.property )) { // More complex proposal: a full block String proposalString = "name {\n\t\n}"; RoboconfCompletionProposal proposal = new RoboconfCompletionProposal( proposalString, COMPONENT_BLOCK, COMPONENT_BLOCK + "\n\n" + proposalString.trim(), ctx.property.length()); proposal.getSelection().add( new SelectionRange( 0, 4 )); proposals.add( proposal ); } break; case ATTRIBUTE: String[] arr = ctx.facet ? FACET_PROPERTY_NAMES : COMPONENT_PROPERTY_NAMES; for( String s : arr ) { s += ": "; if( startsWith( s, ctx.lastWord )) proposals.add( basicProposal( s, ctx.lastWord )); } break; case PROPERTY: ctx.property = ctx.property.trim(); Map<String,String> candidates = findPropertyCandidates( ctx ); proposals.addAll( buildProposalsFromMap( candidates, ctx.lastWord )); break; default: break; } return proposals; } /** * @author Vincent Zurczak - Linagora */ private static enum CtxKind { NEUTRAL, // Default: beginning of the document or between two types COMMENT, // We are inside a comment NOTHING, // Nothing special here ATTRIBUTE, // At the beginning or right after a colon or an opening curly bracket PROPERTY; // We are defining a property } /** * @author Vincent Zurczak - Linagora */ private static class Ctx { CtxKind kind = CtxKind.NEUTRAL; String lastWord, property; boolean facet = false; } /** * Finds the context from the given text. * @param text * @return a context */ private Ctx findContext( String text ) { // Are we inside a comment? Ctx ctx = new Ctx(); int n; for( n = text.length() - 1; n >= 0; n-- ) { char c = text.charAt( n ); if( isLineBreak( c )) break; if( c == '#' ) { ctx.kind = CtxKind.COMMENT; return ctx; } } // Simplify the search: remove all the comments. text = TextUtils.removeComments( text ); // Keep on simplifying the search: remove complete components and facets // Since instances can contain other instances (recursivity), we must // apply the pattern several times, until no more replacement is possible. int before = -1; int after = -2; while( before != after ) { before = text.length(); text = text.replaceAll( "(?i)(?s)(facet\\s+)?[^{]*\\{[^{}]*\\}\r?\n", "" ); after = text.length(); } // Remove white spaces at the beginning of the string. text = text.replaceAll( "^(\n|\r\n)+", "" ); // Now, find our context. // We go back until we find a '{'. Then, we keep rewinding // until we find a line break. String lastWord = null; String lastLine = null; StringBuilder sb = new StringBuilder(); boolean bracketFound = false; for( n = text.length() - 1; n >= 0; n-- ) { char c = text.charAt( n ); // White space? We have a our last word. if( Character.isWhitespace( c ) && lastWord == null ) { lastWord = sb.toString(); sb.setLength( 0 ); sb.append( c ); } // Same thing after a curly bracket else if( c == '{' ) bracketFound = true; // If we find a closing bracket, then we are at the end of a declaration else if( c == '}' ) { ctx.kind = CtxKind.NOTHING; break; } // Line break? That depends... else if( isLineBreak( c )) { if( bracketFound ) break; if( lastLine == null ) { lastLine = sb.toString(); sb.setLength( 0 ); } } else { sb.insert( 0, c ); } } // Update the context if( lastLine == null ) lastLine = sb.toString(); ctx.lastWord = lastWord == null ? "" : lastWord; ctx.property = lastLine; // Time to analyze if( ctx.property.trim().endsWith( ":" ) || ctx.property.trim().endsWith( "," )) { ctx.kind = CtxKind.PROPERTY; } else if( Utils.isEmptyOrWhitespaces( ctx.property )) { if( bracketFound && ! Utils.isEmptyOrWhitespaces( sb.toString())) ctx.kind = CtxKind.ATTRIBUTE; } if( sb.toString().matches( "(?i)\\s*" + KEYWORD_FACET + "\\s+.*" )) ctx.facet = true; return ctx; } /** * @param ctx the current context * @return a non-null map of candidate values for this property * <p> * Key = value to insert, value = description. * </p> */ private Map<String,String> findPropertyCandidates( Ctx ctx ) { Map<String,String> result = new TreeMap<> (); if( ! ctx.facet && ctx.property.matches( PROPERTY_COMPONENT_INSTALLER + "\\s*:\\s*" )) { for( String installer : KNOWN_INSTALLERS ) result.put( installer, null ); } else if( ctx.property.matches( PROPERTY_GRAPH_CHILDREN + "\\s*:\\s*.*" )) { result.putAll( findTypeNames( this.appDirectory, true, true )); } else if( ctx.property.matches( PROPERTY_GRAPH_EXTENDS + "\\s*:\\s*.*" )) { result.putAll( findTypeNames( this.appDirectory, ctx.facet, ! ctx.facet )); } else if( ! ctx.facet && ctx.property.matches( PROPERTY_COMPONENT_FACETS + "\\s*:\\s*.*" )) { result.putAll( findTypeNames( this.appDirectory, true, false )); } else if( ! ctx.facet && ctx.property.matches( PROPERTY_COMPONENT_IMPORTS + "\\s*:\\s*.*" )) { result.putAll( findAllExportedVariables( this.appDirectory )); } return result; } }