| /******************************************************************************* |
| * Copyright (c) 2005, 2016 IBM Corporation 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 |
| * |
| *******************************************************************************/ |
| package org.eclipse.dltk.ruby.internal.ui.text.rules; |
| |
| import java.util.Arrays; |
| import java.util.Comparator; |
| |
| import org.eclipse.dltk.ruby.core.utils.RubySyntaxUtils; |
| import org.eclipse.core.runtime.Assert; |
| import org.eclipse.jface.text.rules.ICharacterScanner; |
| import org.eclipse.jface.text.rules.IPredicateRule; |
| import org.eclipse.jface.text.rules.IToken; |
| import org.eclipse.jface.text.rules.Token; |
| |
| public class RubyPercentStringRule implements IPredicateRule { |
| |
| private static final char ESCAPE = '\\'; |
| |
| /** |
| * Comparator that orders <code>char[]</code> in decreasing array lengths. |
| * |
| * @since 3.1 |
| */ |
| private static class DecreasingCharArrayLengthComparator implements Comparator<char[]> { |
| public int compare(char[] o1, char[] o2) { |
| return o2.length - o1.length; |
| } |
| } |
| |
| /** Internal setting for the un-initialized column constraint */ |
| protected static final int UNDEFINED= -1; |
| |
| /** The token to be returned on success */ |
| protected IToken fToken; |
| /** The pattern's column constrain */ |
| protected int fColumn= UNDEFINED; |
| /** |
| * Indicates whether the escape character continues a line |
| * @since 3.0 |
| */ |
| protected boolean fEscapeContinuesLine; |
| /** Indicates whether end of line terminates the pattern */ |
| protected boolean fBreaksOnEOL; |
| /** Indicates whether end of file terminates the pattern */ |
| protected boolean fBreaksOnEOF; |
| |
| /** |
| * Line delimiter comparator which orders according to decreasing delimiter length. |
| * @since 3.1 |
| */ |
| private Comparator<char[]> fLineDelimiterComparator= new DecreasingCharArrayLengthComparator(); |
| /** |
| * Cached line delimiters. |
| * @since 3.1 |
| */ |
| private char[][] fLineDelimiters; |
| /** |
| * Cached sorted {@linkplain #fLineDelimiters}. |
| * @since 3.1 |
| */ |
| private char[][] fSortedLineDelimiters; |
| |
| /** |
| * Creates a rule for the given starting and ending sequence. |
| * When these sequences are detected the rule will return the specified token. |
| * Alternatively, the sequence can also be ended by the end of the line. |
| * Any character which follows the given escapeCharacter will be ignored. |
| * |
| * @param token the token which will be returned on success |
| * @param breaksOnEOL indicates whether the end of the line also terminates the pattern |
| */ |
| public RubyPercentStringRule(IToken token, boolean breaksOnEOL) { |
| Assert.isNotNull(token); |
| |
| fToken= token; |
| fBreaksOnEOL= breaksOnEOL; |
| fBreaksOnEOF= false; |
| } |
| |
| /** |
| * Sets a column constraint for this rule. If set, the rule's token |
| * will only be returned if the pattern is detected starting at the |
| * specified column. If the column is smaller then 0, the column |
| * constraint is considered removed. |
| * |
| * @param column the column in which the pattern starts |
| */ |
| public void setColumnConstraint(int column) { |
| if (column < 0) |
| column= UNDEFINED; |
| fColumn= column; |
| } |
| |
| |
| /** |
| * Evaluates this rules without considering any column constraints. |
| * |
| * @param scanner the character scanner to be used |
| * @return the token resulting from this evaluation |
| */ |
| protected IToken doEvaluate(ICharacterScanner scanner) { |
| return doEvaluate(scanner, false); |
| } |
| |
| /** |
| * Evaluates this rules without considering any column constraints. Resumes |
| * detection, i.e. look sonly for the end sequence required by this rule if the |
| * <code>resume</code> flag is set. |
| * |
| * @param scanner the character scanner to be used |
| * @param resume <code>true</code> if detection should be resumed, <code>false</code> otherwise |
| * @return the token resulting from this evaluation |
| * @since 2.0 |
| */ |
| protected IToken doEvaluate(ICharacterScanner scanner, boolean resume) { |
| |
| if (resume) { |
| |
| // if (endSequenceDetected(scanner, (char), ']')) |
| // return fToken; |
| |
| } else { |
| |
| int c= scanner.read(); |
| if (c == '%') { |
| if (scanner.getColumn() > 1) { |
| scanner.unread(); |
| scanner.unread(); |
| int b = scanner.read(); |
| scanner.read(); |
| if (b == '<') |
| return Token.UNDEFINED; |
| } |
| char leader = startSequenceDetected(scanner); |
| char term = RubySyntaxUtils.getPercentStringTerminator((char) leader); |
| if (term != (char) 0) { |
| if (endSequenceDetected(scanner, leader, term)) |
| return fToken; |
| } |
| } |
| } |
| |
| scanner.unread(); |
| return Token.UNDEFINED; |
| } |
| |
| /* |
| * @see IRule#evaluate(ICharacterScanner) |
| */ |
| public IToken evaluate(ICharacterScanner scanner) { |
| return evaluate(scanner, false); |
| } |
| |
| protected boolean endSequenceDetected(ICharacterScanner scanner, char lead, char term) { |
| |
| char[][] originalDelimiters= scanner.getLegalLineDelimiters(); |
| int count= originalDelimiters.length; |
| if (fLineDelimiters == null || originalDelimiters.length != count) { |
| fSortedLineDelimiters= new char[count][]; |
| } else { |
| while (count > 0 && fLineDelimiters[count-1] == originalDelimiters[count-1]) |
| count--; |
| } |
| if (count != 0) { |
| fLineDelimiters= originalDelimiters; |
| System.arraycopy(fLineDelimiters, 0, fSortedLineDelimiters, 0, fLineDelimiters.length); |
| Arrays.sort(fSortedLineDelimiters, fLineDelimiterComparator); |
| } |
| |
| int c; |
| int nestCount = 1; |
| int d = 0; |
| while ((c= scanner.read()) != ICharacterScanner.EOF) { |
| d++; |
| if (c == ESCAPE) { |
| // System.out.println("ESCAPE " + (char) c); |
| // Skip escaped character(s) |
| if (fEscapeContinuesLine) { |
| c= scanner.read(); |
| for (int i= 0; i < fSortedLineDelimiters.length; i++) { |
| if (c == fSortedLineDelimiters[i][0] && sequenceDetected(scanner, fSortedLineDelimiters[i], true)) |
| break; |
| } |
| } else |
| scanner.read(); |
| |
| } else if (lead != term && c == lead) { |
| nestCount++; |
| } else if (c == (int) term) { |
| if (--nestCount <= 0) |
| return true; |
| } else if (c == '%') { |
| while (d-- > 0) |
| scanner.unread(); |
| return false; |
| } else if (fBreaksOnEOL) { |
| // System.out.println((char) c); |
| // Check for end of line since it can be used to terminate the pattern. |
| for (int i= 0; i < fSortedLineDelimiters.length; i++) { |
| if (c == fSortedLineDelimiters[i][0] && sequenceDetected(scanner, fSortedLineDelimiters[i], true)) |
| return true; |
| } |
| } |
| } |
| if (fBreaksOnEOF) return true; |
| scanner.unread(); |
| return false; |
| } |
| |
| protected char startSequenceDetected(ICharacterScanner scanner) { |
| int c = scanner.read(); |
| if (c == ICharacterScanner.EOF) { |
| scanner.unread(); |
| return (char) 0; |
| } |
| boolean letterFound = false; |
| if (RubySyntaxUtils.isValidPercentStringStarter((char) c)) { |
| letterFound = true; |
| c = scanner.read(); |
| if (c == ICharacterScanner.EOF) { |
| scanner.unread(); |
| scanner.unread(); |
| return (char) 0; |
| } |
| } |
| char term = RubySyntaxUtils.getPercentStringTerminator((char) c); |
| if (term == (char) 0) { |
| scanner.unread(); |
| if (letterFound) |
| scanner.unread(); |
| return (char) 0; |
| } |
| return (char) c; |
| } |
| |
| protected boolean sequenceDetected(ICharacterScanner scanner, char[] sequence, boolean eofAllowed) { |
| for (int i= 1; i < sequence.length; i++) { |
| int c= scanner.read(); |
| if (c == ICharacterScanner.EOF && eofAllowed) { |
| return true; |
| } else if (c != sequence[i]) { |
| // Non-matching character detected, rewind the scanner back to the start. |
| // Do not unread the first character. |
| scanner.unread(); |
| for (int j= i-1; j > 0; j--) |
| scanner.unread(); |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| public IToken evaluate(ICharacterScanner scanner, boolean resume) { |
| int c= scanner.read(); |
| scanner.unread(); |
| if (c == '%') { |
| // RubyPartitionScanner s = (RubyPartitionScanner) scanner; |
| // if (s.getCurrentContext() == RubyContext.AFTER_EXPRESSION) |
| // return Token.UNDEFINED; |
| return doEvaluate(scanner, resume); |
| } |
| return Token.UNDEFINED; |
| } |
| |
| public IToken getSuccessToken() { |
| return fToken; |
| } |
| } |