/******************************************************************************* * Copyright (c) 2015 QNX Software Systems and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * QNX Software Systems - Initial API and implementation *******************************************************************************/ package org.eclipse.cdt.internal.qt.ui.pro.parser; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.regex.MatchResult; import java.util.regex.Pattern; /** * Contains all information about a variable's representation in a Qt Project (.pro) File. This includes information about offsets, * lengths, and textual representation of various components of a variable declaration such as its: * <ul> * <li>Name, such as "SOURCES"</li> * <li>Assignment operator (= or +=)</li> * <li>Values for a particular line</li> * <li>Comments for a particular line</li> * <li>Line feeds</li> * <li>Line escapes (\)</li> * </ul> * Also contains the static method <code>findNextVariable(Scanner)</code> to perform the regular expressions lookup of the next * variable in a document. */ public class QtProjectVariable { private static final Pattern REGEX = Pattern.compile( "(?m)^\\h*((?:[_a-zA-Z][_a-zA-Z0-9]*\\.)*[_a-zA-Z][_a-zA-Z0-9]*)\\h*(=|\\+=|-=|\\*=)\\h*([^#\\v]*?)\\h*((?:(\\\\)\\h*)?(#[^\\v]*)?$)"); //$NON-NLS-1$ private static final Pattern LINE_ESCAPE_REGEX = Pattern.compile("(?m)^(\\h*)([^#\\v]*?)\\h*((?:(\\\\)\\h*)?(#[^\\v]*)?$)"); //$NON-NLS-1$ private static final int GROUP_VAR_NAME = 1; private static final int GROUP_VAR_ASSIGNMENT = 2; private static final int GROUP_VAR_CONTENTS = 3; private static final int GROUP_VAR_TERMINATOR = 4; private static final int GROUP_VAR_LINE_ESCAPE = 5; private static final int GROUP_VAR_COMMENT = 6; private static final int GROUP_LINE_INDENT = 1; private static final int GROUP_LINE_CONTENTS = 2; private static final int GROUP_LINE_TERMINATOR = 3; private static final int GROUP_LINE_LINE_ESCAPE = 4; private static final int GROUP_LINE_COMMENT = 5; /** * Finds the next Qt Project Variable within a String using the given Scanner. If there are no variables to be found, this * method will return <code>null</code>. * * @param scanner * the scanner to use for regular expressions matching * @return the next variable or <code>null</code> if none */ public static QtProjectVariable findNextVariable(Scanner scanner) { List<MatchResult> matchResults = new ArrayList<>(); // Find the start of a variable declaration String match = scanner.findWithinHorizon(REGEX, 0); if (match == null) { return null; } // Get subsequent lines if the previous one ends with '\' MatchResult matchResult = scanner.match(); matchResults.add(matchResult); if (matchResult.group(QtProjectVariable.GROUP_VAR_TERMINATOR).startsWith("\\")) { //$NON-NLS-1$ do { match = scanner.findWithinHorizon(LINE_ESCAPE_REGEX, 0); if (match == null) { // This means that we have a newline escape where another line doesn't exist break; } matchResult = scanner.match(); matchResults.add(matchResult); } while (matchResult.group(QtProjectVariable.GROUP_LINE_TERMINATOR).startsWith("\\")); //$NON-NLS-1$ } return new QtProjectVariable(matchResults); } private final int startOffset; private final int endOffset; private final String text; private final List<MatchResult> matchResults; /** * Constructs a project file variable from a list of match results obtained from a <code>Scanner</code>. This constructor is * only intended to be called from within the static method <code>findNextVariable(Scanner)</code>. * * @param matches * list of <code>MatchResult</code> */ private QtProjectVariable(List<MatchResult> matches) { this.startOffset = matches.get(0).start(); this.endOffset = matches.get(matches.size() - 1).end(); this.matchResults = matches; StringBuilder sb = new StringBuilder(); for (MatchResult m : matches) { sb.append(m.group()); } this.text = sb.toString(); } /** * Gets the offset of this variable relative to the start of its containing document. * * @return the offset of this variable */ public int getOffset() { return startOffset; } /** * Gets the length of this variable as it appears in its containing document. * * @return the total length of this variable */ public int getLength() { return endOffset - startOffset; } /** * Gets the name of this variable as it appears in the document. For example, the <code>"SOURCES"</code> variable. * * @return the name of this variable */ public String getName() { return matchResults.get(0).group(GROUP_VAR_NAME); } /** * the assignment operator of this variable (<code>+=</code> or <code>"="</code>) * * @return the assignment operator */ public String getAssignmentOperator() { return matchResults.get(0).group(GROUP_VAR_ASSIGNMENT); } /** * Returns a list of value(s) assigned to this variable. Each entry in the list represents a new line. * * @return a List containing all of the value(s) assigned to this variable */ public List<String> getValues() { List<String> values = new ArrayList<String>(); values.add(matchResults.get(0).group(GROUP_VAR_CONTENTS)); for (int i = 1; i < matchResults.size(); i++) { values.add(matchResults.get(i).group(GROUP_LINE_CONTENTS)); } return values; } /** * Returns the indentation of the given line as a String. Mainly used by the QtProjectFileWriter to write back to the Document. * * @param line * the line number to check * @return a <code>String</code> representing the indentation of the given line */ public String getIndentString(int line) { MatchResult match = matchResults.get(line); if (line == 0) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < match.start(GROUP_VAR_CONTENTS) - match.start(); i++) { sb.append(' '); } return sb.toString(); } return match.group(GROUP_LINE_INDENT); } /** * Retrieves the offset of the value portion of a given line relative to the start of its containing document. * * @param line * the line to check * @return the offset of the value */ public int getValueOffsetForLine(int line) { if (line == 0) { return matchResults.get(line).start(GROUP_VAR_CONTENTS); } return matchResults.get(line).start(GROUP_LINE_CONTENTS); } /** * Retrieves a String representing the value at a specific line of this variable. * * @param line * the line to check * @return the value */ public String getValueForLine(int line) { if (line == 0) { return matchResults.get(line).group(GROUP_VAR_CONTENTS); } return matchResults.get(line).group(GROUP_LINE_CONTENTS); } /** * Returns the ideal offset in the containing document at which a line escape can be inserted. * * @param line * the line to check * @return the ideal location for a line escape */ public int getLineEscapeReplacementOffset(int line) { if (line == 0) { return matchResults.get(line).end(GROUP_VAR_CONTENTS); } return matchResults.get(line).end(GROUP_LINE_CONTENTS); } /** * Returns the ideal String for the line escape character. This is mostly for spacing requirements and should be used in tandem * with the method <code>getLineEscapeReplacementOffset</code>. * * @param line * the line to check * @return the ideal String for the line escape character */ public String getLineEscapeReplacementString(int line) { int commentOffset = -1; int contentsOffset = -1; if (line == 0) { commentOffset = matchResults.get(line).start(GROUP_VAR_COMMENT); contentsOffset = matchResults.get(line).end(GROUP_VAR_CONTENTS); } else { commentOffset = matchResults.get(line).start(GROUP_LINE_COMMENT); contentsOffset = matchResults.get(line).end(GROUP_LINE_CONTENTS); } if (commentOffset > 0) { if (commentOffset - contentsOffset == 0) { return " \\ "; //$NON-NLS-1$ } } return " \\"; //$NON-NLS-1$ } /** * Retrieves the offset of the line escape for a given line relative to its containing document. This method takes into account * spacing and should be used to determine how to best remove a line escape character from a given line. * * @param line * the line to check * @return the offset of the line escape character */ public int getLineEscapeOffset(int line) { if (line == 0) { return matchResults.get(line).end(GROUP_VAR_CONTENTS); } return matchResults.get(line).end(GROUP_LINE_CONTENTS); } /** * Get the end position relative to the start of the containing document that contains the line escape character of the given * line. This is used for removal of the line escape character and takes into account the spacing of the line. * * @param line * the line to check * @return the end position of the line escape character */ public int getLineEscapeEnd(int line) { int end = -1; if (line == 0) { end = matchResults.get(line).end(GROUP_VAR_LINE_ESCAPE); } else { end = matchResults.get(line).end(GROUP_LINE_LINE_ESCAPE); } if (end > 0) { return end; } if (line == 0) { return matchResults.get(line).end(GROUP_VAR_TERMINATOR); } return matchResults.get(line).end(GROUP_LINE_TERMINATOR); } /** * Gets the end position of this variable relative to the containing document. * * @return the end position of this variable */ public int getEndOffset() { return matchResults.get(matchResults.size() - 1).end(); } /** * Retrieves the full text of this variable as it appears in the document. * * @return the full String of this variable as it appears in the document */ public String getText() { return text; } /** * Gets the total number of lines in this variable declaration. * * @return the total number of lines */ public int getNumberOfLines() { return matchResults.size(); } /** * Retrieves a String representing the given line as it appears in the document. * * @param line * the line to retrieve * @return a String representing the line */ public String getLine(int line) { return matchResults.get(line).group(); } /** * Retrieves the offset of the given line relative to its containing document. * * @param line * the line to retrieve * @return the line's offset in the document */ public int getLineOffset(int line) { return matchResults.get(line).start(); } /** * Returns the line at which the specified value appears. This method checks the whole line for the value and will not match a * subset of that String. This is equivalent to calling <code>getValueIndex(value,false)</code>. * * @param value * the value to search for * @return the line that the value appears on or -1 if it doesn't exist */ public int getValueIndex(String value) { return getValueIndex(value, false); } /** * Returns the line at which the specified value appears. This method checks the whole line for the value and will not match a * subset of that String. If <code>ignoreCase</code> is <code>false</code>, this method searches for the value using * <code>equalsIgnoreCase</code> instead of <code>equals</code>. * * @param value * the value to search for * @param ignoreCase * whether or not the value is case-sensitive * @return the line that the value appears on or -1 if it doesn't exist */ public int getValueIndex(String value, boolean ignoreCase) { int line = 0; for (String val : getValues()) { if (ignoreCase) { if (val.equalsIgnoreCase(value)) { return line; } } else { if (val.equals(value)) { return line; } } line++; } return -1; } /** * Gets the offset of the end of a given line relative to its containing document. * * @param line * the line to check * @return the offset of the end of the line */ public int getLineEnd(int line) { return matchResults.get(line).end(); } }