/* * Copyright 2015 Nokia Solutions and Networks * Licensed under the Apache License, Version 2.0, * see license.txt file for details. */ package org.robotframework.ide.eclipse.main.plugin.tableeditor.source; import static com.google.common.collect.Sets.newHashSet; import java.util.Optional; import java.util.Stack; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.preferences.ConfigurationScope; import org.eclipse.core.runtime.preferences.DefaultScope; import org.eclipse.core.runtime.preferences.IScopeContext; import org.eclipse.core.runtime.preferences.InstanceScope; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IRegion; import org.eclipse.jface.text.Region; import com.google.common.collect.Range; /** * @author Michal Anglart * */ public class DocumentUtilities { /** * Returns region around offset which constitutes a single robot variable. * * @param document * Document in which variable should be find * @param offset * Current offset at which search should start * @return The region describing location of variable or absent if offset lies outside variable * @throws BadLocationException */ public static Optional<IRegion> findVariable(final IDocument document, final boolean isTsv, final int offset) throws BadLocationException { final Optional<IRegion> cellRegion = findCellRegion(document, isTsv, offset); if (cellRegion.isPresent()) { final String cellContent = document.get(cellRegion.get().getOffset(), cellRegion.get().getLength()); final int projectedOffset = offset - cellRegion.get().getOffset(); return findVariable(cellContent, projectedOffset) .map(reg -> new Region(reg.getOffset() + cellRegion.get().getOffset(), reg.getLength())); } return Optional.empty(); } public static Optional<IRegion> findVariable(final String cellContent, final int offset) { final Stack<Integer> positions = new Stack<>(); int stackLevel = -1; int lastIndex = 0; for (int i = 0; i < cellContent.length(); i++) { if (varStartDetected(cellContent, i)) { positions.push(i); } if (i == offset) { stackLevel = positions.size() - 1; } if (varEndDetected(cellContent, i) && !positions.isEmpty()) { lastIndex = positions.pop(); if (stackLevel == positions.size() && i >= offset) { return Optional.<IRegion> of(new Region(lastIndex, i - lastIndex + 1)); } } } return Optional.empty(); } private static boolean varStartDetected(final String cellContent, final int i) { if (i + 1 < cellContent.length()) { return newHashSet("${", "@{", "%{", "&{").contains(cellContent.substring(i, i + 2)); } return false; } private static boolean varEndDetected(final String cellContent, final int i) { return i < cellContent.length() && cellContent.charAt(i) == '}'; } public static Optional<IRegion> findLiveVariable(final IDocument document, final boolean isTsv, final int offset) throws BadLocationException { final Optional<IRegion> cellRegion = findLiveCellRegion(document, isTsv, offset); if (cellRegion.isPresent()) { final String cellContent = document.get(cellRegion.get().getOffset(), cellRegion.get().getLength()); return findLiveVariable(cellContent, offset - cellRegion.get().getOffset()) .map(reg -> new Region(reg.getOffset() + cellRegion.get().getOffset(), reg.getLength())); } return Optional.empty(); } public static Optional<IRegion> findLiveVariable(final String cellContent, final int offset) { final Matcher matcher = Pattern.compile("[@$&%][^@$%&]*").matcher(cellContent); while (matcher.find()) { final int start = matcher.start(); final int closingBracketIndex = cellContent.indexOf('}', start + 1); final int end = closingBracketIndex == -1 ? matcher.end() : Math.min(matcher.end(), closingBracketIndex); if (Range.closed(start, end).contains(offset)) { return Optional.<IRegion> of(new Region(start, end - start)); } } return Optional.empty(); } /** * Returns region around offset which constitutes a cell in robot file table. The region * is surrounded with file begin or cells separator on the left and by the file end or another * cells separator on right. * Cell separator is at least 2 spaces, tabulator or newline character * * @param document * Document in which cell should be find * @param offset * Current offset at which search should start * @return The region describing whole cell or absent if offset is inside cell separator. If returned region * is present then it is always true that: * region.getOffset() <= offset <= region.getOffset() + region.getLength() * @throws BadLocationException */ public static Optional<IRegion> findCellRegion(final IDocument document, final boolean isTsv, final int offset) throws BadLocationException { final String prev = offset > 0 ? document.get(offset - 1, 1) : ""; final String next = offset < document.getLength() ? document.get(offset, 1) : ""; if (prev.equals("\n") && next.equals("\n")) { return Optional.<IRegion> of(new Region(offset, 0)); } if (isInsideSeparator(prev, next, isTsv)) { return Optional.empty(); } final int beginOffset = offset - calculateCellRegionBegin(document, isTsv, offset); final int endOffset = offset + calculateCellRegionEnd(document, isTsv, offset); return Optional.<IRegion> of(new Region(beginOffset, endOffset - beginOffset)); } /** * Returns region around offset which consitutues a cell during live editing. This is very * similar to {@link #findCellRegion(IDocument, int)} method with a single exception that * there can be a single space just before offset prefixed with whole cell content. * * @param document * @param offset * @return * @throws BadLocationException */ public static Optional<IRegion> findLiveCellRegion(final IDocument document, final boolean isTsv, final int offset) throws BadLocationException { final Optional<IRegion> firstCandidate = findCellRegion(document, isTsv, offset); if (!firstCandidate.isPresent() && (offset > 0 ? document.get(offset - 1, 1) : "").equals("\t")) { return firstCandidate; } if (!firstCandidate.isPresent() && offset > 0 && document.getChar(offset - 1) == '\n') { return Optional.<IRegion> of(new Region(offset, 0)); } final Optional<IRegion> region = firstCandidate.isPresent() ? firstCandidate : findCellRegion(document, isTsv, offset - 1); if (region.isPresent()) { final int length = Math.max(offset - region.get().getOffset(), region.get().getLength()); return Optional.<IRegion>of(new Region(region.get().getOffset(), length)); } return region; } private static boolean isInsideSeparator(final String prev, final String next, final Boolean isTsv) { if (isTsv) { return !prev.isEmpty() && (prev.charAt(0) == '\t' || prev.charAt(0) == '\n' || prev.charAt(0) == '\r') && !next.isEmpty() && (next.charAt(0) == '\t' || next.charAt(0) == '\n' || prev.charAt(0) == '\r'); } else { return !prev.isEmpty() && (Character.isWhitespace(prev.charAt(0)) || prev.charAt(0) == '|') && !next.isEmpty() && (Character.isWhitespace(next.charAt(0)) || next.charAt(0) == '|'); } } private static int calculateCellRegionBegin(final IDocument document, final boolean isTsv, final int caretOffset) throws BadLocationException { int j = 1; while (true) { if (caretOffset - j < 0) { break; } final char prev = document.get(caretOffset - j, 1).charAt(0); if (prev == '\t' || prev == '\r' || prev == '\n') { break; } if (caretOffset - j - 1 < 0) { if (prev != ' ') { j++; } break; } if (prev == ' ' && !isTsv) { final char lookBack = document.get(caretOffset - j - 1, 1).charAt(0); if (Character.isWhitespace(lookBack) || lookBack == '|') { break; } } j++; } return j - 1; } private static int calculateCellRegionEnd(final IDocument document, final boolean isTsv, final int caretOffset) throws BadLocationException { int i = 0; while (true) { if (caretOffset + i >= document.getLength()) { break; } final char next = document.get(caretOffset + i, 1).charAt(0); if (next == '\t' || next == '\r' || next == '\n') { break; } if (caretOffset + i + 1 >= document.getLength()) { if (next != ' ') { i++; } break; } if (next == ' ' && !isTsv) { final char lookAhead = document.get(caretOffset + i + 1, 1).charAt(0); if (Character.isWhitespace(lookAhead) || lookAhead == '|') { break; } } i++; } return i; } public static String lineContentBeforeCurrentPosition(final IDocument document, final int offset) { try { final IRegion lineInfo = document.getLineInformationOfOffset(offset); return document.get(lineInfo.getOffset(), offset - lineInfo.getOffset()); } catch (final BadLocationException e) { throw new IllegalStateException("Unable to get line content at offset " + offset, e); } } public static boolean isInLastCellOfLine(final IDocument document, final int offset, final boolean isTsv) { try { final IRegion lineInfo = document.getLineInformationOfOffset(offset); return offset + calculateCellRegionEnd(document, isTsv, offset) == lineInfo.getOffset() + lineInfo.getLength(); } catch (final BadLocationException e) { throw new IllegalStateException("Unable to get line content at offset " + offset, e); } } public static String getPrefix(final IDocument document, final Optional<IRegion> optional, final int offset) throws BadLocationException { if (!optional.isPresent()) { return ""; } return document.get(optional.get().getOffset(), offset - optional.get().getOffset()); } public static int getNumberOfCellSeparators(final String lineContentBefore, final boolean isTsv) { return isTsv ? getNumberOfCellSeparatorsInTsv(lineContentBefore) : getNumberOfCellsSeparators(lineContentBefore); } private static int getNumberOfCellsSeparators(final String lineContentBefore) { if (lineContentBefore.isEmpty()) { return 0; } final String withoutTabs = lineContentBefore.replaceAll("\t", " ") .replaceAll(" \\| ", " ") .replaceFirst("^\\| ", " "); int spacesRegions = 0; int currentNumberOfSpaces = 0; for (int i = 0; i < withoutTabs.length(); i++) { if (withoutTabs.charAt(i) == ' ') { currentNumberOfSpaces++; } else if (currentNumberOfSpaces == 1) { currentNumberOfSpaces = 0; } else if (currentNumberOfSpaces > 1) { spacesRegions++; currentNumberOfSpaces = 0; } } // maybe spaces were suffix of line content if (currentNumberOfSpaces > 1) { spacesRegions++; } return spacesRegions; } private static int getNumberOfCellSeparatorsInTsv(final String lineContentBefore) { int separators = 0; for (final char ch : lineContentBefore.toCharArray()) { if (ch == '\t') { separators++; } } return separators; } public static String getDelimiter(final IDocument document) { try { final String delimiter = document.getLineDelimiter(0); if (delimiter != null) { return delimiter; } } catch (final BadLocationException e) { // ok just get it from preferences } final IScopeContext[] context = new IScopeContext[] { InstanceScope.INSTANCE, ConfigurationScope.INSTANCE, DefaultScope.INSTANCE }; final String delimiter = Platform.getPreferencesService().getString(Platform.PI_RUNTIME, Platform.PREF_LINE_SEPARATOR, null, context); return delimiter != null ? delimiter : System.lineSeparator(); } public static int getLine(final IDocument document, final int offset) { try { return document.getLineOfOffset(offset); } catch (final BadLocationException e) { return -1; } } public static Optional<IRegion> getSnippet(final IDocument document, final int offset, final int noOfLinesBeforeAndAfter) { if (noOfLinesBeforeAndAfter < 0) { return Optional.empty(); } try { final int line = document.getLineOfOffset(offset); final int firstLine = Math.max(0, line - noOfLinesBeforeAndAfter); final int lastLine = Math.min(document.getNumberOfLines() - 1, line + noOfLinesBeforeAndAfter); final IRegion firstLineRegion = document.getLineInformation(firstLine); final IRegion lastLineRegion = document.getLineInformation(lastLine); return Optional.<IRegion> of(new Region(firstLineRegion.getOffset(), lastLineRegion.getOffset() + lastLineRegion.getLength() - firstLineRegion.getOffset())); } catch (final BadLocationException e) { return Optional.empty(); } } }