/* * 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.Iterables.filter; import static com.google.common.collect.Iterables.transform; import static com.google.common.collect.Sets.newHashSet; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import org.eclipse.jface.text.BadLocationException; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.Position; import org.eclipse.jface.text.source.Annotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotation; import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel; import org.rf.ide.core.testdata.model.ModelType; import org.robotframework.ide.eclipse.main.plugin.RedPlugin; import org.robotframework.ide.eclipse.main.plugin.RedPreferences; import org.robotframework.ide.eclipse.main.plugin.RedPreferences.FoldableElements; import org.robotframework.ide.eclipse.main.plugin.model.RobotCase; import org.robotframework.ide.eclipse.main.plugin.model.RobotCasesSection; import org.robotframework.ide.eclipse.main.plugin.model.RobotCodeHoldingElement; import org.robotframework.ide.eclipse.main.plugin.model.RobotDefinitionSetting; import org.robotframework.ide.eclipse.main.plugin.model.RobotFileInternalElement; import org.robotframework.ide.eclipse.main.plugin.model.RobotKeywordCall; import org.robotframework.ide.eclipse.main.plugin.model.RobotKeywordDefinition; import org.robotframework.ide.eclipse.main.plugin.model.RobotKeywordsSection; import org.robotframework.ide.eclipse.main.plugin.model.RobotSettingsSection; import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFile; import org.robotframework.ide.eclipse.main.plugin.model.RobotSuiteFileSection; import org.robotframework.red.swt.StyledTextWrapper; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.collect.Lists; class SuiteSourceEditorFoldingSupport { private final RedPreferences preferences; private final StyledTextWrapper textControl; private final ProjectionAnnotationModel annotationsModel; private Map<Position, Annotation> oldFoldingAnnotations; @VisibleForTesting SuiteSourceEditorFoldingSupport(final StyledTextWrapper textControl, final ProjectionAnnotationModel annotationsModel) { this(RedPlugin.getDefault().getPreferences(), textControl, annotationsModel); } SuiteSourceEditorFoldingSupport(final RedPreferences preferences, final StyledTextWrapper textControl, final ProjectionAnnotationModel annotationsModel) { this.preferences = preferences; this.textControl = textControl; this.annotationsModel = annotationsModel; this.oldFoldingAnnotations = new HashMap<>(); } public void reset() { if (annotationsModel != null) { final Iterator<Annotation> annotationIterator = annotationsModel.getAnnotationIterator(); while (annotationIterator.hasNext()) { final Annotation next = annotationIterator.next(); if (next instanceof ProjectionAnnotation) { annotationsModel.removeAnnotation(next); } } } oldFoldingAnnotations.clear(); } Collection<Position> calculateFoldingPositions(final RobotSuiteFile model, final IDocument document) { final Collection<Position> positions = new HashSet<>(); final EnumSet<FoldableElements> foldableElements = preferences.getFoldableElements(); if (foldableElements.contains(FoldableElements.SECTIONS)) { positions.addAll(calculateSectionsFoldingPositions(model)); } if (foldableElements.contains(FoldableElements.CASES)) { positions.addAll(calculateTestCasesFoldingPositions(model)); } if (foldableElements.contains(FoldableElements.KEYWORDS)) { positions.addAll(calculateKeywordsFoldingPositions(model)); } if (foldableElements.contains(FoldableElements.DOCUMENTATION)) { positions.addAll(calculateDocumentationFoldingPositions(model)); } final Iterable<Position> positionsSpanningOverLimit = filter(positions, onlyPositionsSpanning(document, preferences.getFoldingLineLimit())); return newHashSet(transform(positionsSpanningOverLimit, nextLineShiftedPosition(document))); } private Collection<Position> calculateSectionsFoldingPositions(final RobotSuiteFile model) { final Collection<Position> positions = new HashSet<>(); for (final RobotSuiteFileSection section : model.getChildren()) { final Map<Position, Set<Position>> positionsGroupedByHeaders = new HashMap<>(); final List<Position> headerOffsets = new ArrayList<>(); for (final Position headerPosition : section.getPositions()) { headerOffsets.add(headerPosition); positionsGroupedByHeaders.put(headerPosition, new HashSet<Position>()); } Collections.sort(headerOffsets, positionsComparator()); for (final RobotFileInternalElement child : section.getChildren()) { final Position position = child.getPosition(); positionsGroupedByHeaders.get(findHeader(headerOffsets, position)).add(position); } positions.addAll(toSectionPosition(positionsGroupedByHeaders)); } return positions; } private Collection<Position> toSectionPosition(final Map<Position, Set<Position>> positionsGroupedByHeaders) { final Set<Position> positions = new HashSet<>(); for (final Position key : positionsGroupedByHeaders.keySet()) { if (positionsGroupedByHeaders.get(key).isEmpty()) { positions.add(key); } else { final Position maxPosition = Collections.max(positionsGroupedByHeaders.get(key), positionsComparator()); final int offset = key.getOffset(); final int length = maxPosition.getOffset() + maxPosition.getLength() - offset; positions.add(new Position(offset, length)); } } return positions; } private static Position findHeader(final List<Position> headerPositions, final Position position) { Position previous = null; for (final Position headerPosition : headerPositions) { if (headerPosition.getOffset() > position.getOffset()) { break; } previous = headerPosition; } return previous; } private Collection<Position> calculateTestCasesFoldingPositions(final RobotSuiteFile model) { final Optional<RobotCasesSection> casesSection = model.findSection(RobotCasesSection.class); if (casesSection.isPresent()) { return Lists.transform(casesSection.get().getChildren(), toPositions()); } return new HashSet<>(); } private Collection<Position> calculateKeywordsFoldingPositions(final RobotSuiteFile model) { final Optional<RobotKeywordsSection> keywordSection = model.findSection(RobotKeywordsSection.class); if (keywordSection.isPresent()) { return Lists.transform(keywordSection.get().getChildren(), toPositions()); } return new HashSet<>(); } private Collection<Position> calculateDocumentationFoldingPositions(final RobotSuiteFile model) { final Collection<Position> positions = new HashSet<>(); positions.addAll(calculateSuiteDocumentationFoldingPositions(model)); positions.addAll(calculateCasesDocumentationFoldingPositions(model)); positions.addAll(calculateKeywordsDocumentationFoldingPositions(model)); return positions; } private Collection<Position> calculateSuiteDocumentationFoldingPositions(final RobotSuiteFile model) { final Collection<Position> positions = new HashSet<>(); final Optional<RobotSettingsSection> settingsSection = model.findSection(RobotSettingsSection.class); if (settingsSection.isPresent()) { for (final RobotKeywordCall setting : settingsSection.get().getChildren()) { if (setting.getLinkedElement().getModelType() == ModelType.SUITE_DOCUMENTATION) { positions.add(setting.getPosition()); } } } return positions; } private Collection<Position> calculateCasesDocumentationFoldingPositions(final RobotSuiteFile model) { final Collection<Position> positions = new HashSet<>(); final Optional<RobotCasesSection> casesSection = model.findSection(RobotCasesSection.class); if (casesSection.isPresent()) { for (final RobotCase test : casesSection.get().getChildren()) { for (final RobotKeywordCall call : test.getChildren()) { if (call instanceof RobotDefinitionSetting && ((RobotDefinitionSetting) call).isDocumentation()) { positions.add(call.getPosition()); } } } } return positions; } private Collection<Position> calculateKeywordsDocumentationFoldingPositions(final RobotSuiteFile model) { final Collection<Position> positions = new HashSet<>(); final Optional<RobotKeywordsSection> keywordsSection = model.findSection(RobotKeywordsSection.class); if (keywordsSection.isPresent()) { for (final RobotKeywordDefinition keyword : keywordsSection.get().getChildren()) { for (final RobotKeywordCall call : keyword.getChildren()) { if (call instanceof RobotDefinitionSetting && ((RobotDefinitionSetting) call).isDocumentation()) { positions.add(call.getPosition()); } } } } return positions; } private static Comparator<Position> positionsComparator() { return new Comparator<Position>() { @Override public int compare(final Position p1, final Position p2) { return Integer.compare(p1.getOffset(), p2.getOffset()); } }; } private static Predicate<Position> onlyPositionsSpanning(final IDocument document, final int linesToSpan) { return new Predicate<Position>() { @Override public boolean apply(final Position position) { final int startLine = DocumentUtilities.getLine(document, position.getOffset()); final int endLine; if (position.getOffset() + position.getLength() == document.getLength()) { endLine = DocumentUtilities.getLine(document, position.getOffset() + position.getLength() - 1); } else { endLine = DocumentUtilities.getLine(document, position.getOffset() + position.getLength()); } return endLine - startLine + 1 >= linesToSpan; } }; } private static Function<RobotCodeHoldingElement<?>, Position> toPositions() { return new Function<RobotCodeHoldingElement<?>, Position>() { @Override public Position apply(final RobotCodeHoldingElement<?> element) { return element.getPosition(); } }; } private static Function<Position, Position> nextLineShiftedPosition(final IDocument document) { return new Function<Position, Position>() { @Override public Position apply(final Position position) { try { final int line = document.getLineOfOffset(position.getOffset() + position.getLength()); final int nextLine = line + 1; if (nextLine >= document.getNumberOfLines()) { return new Position(position.getOffset(), Math.max(0, document.getLength() - position.getOffset())); } else { final int nextLineOffset = document.getLineInformation(nextLine).getOffset(); return new Position(position.getOffset(), nextLineOffset - position.getOffset()); } } catch (final BadLocationException e) { return position; } } }; } void updateFoldingStructure(final Collection<Position> positions) { if (annotationsModel == null) { return; } final List<Annotation> annotationsToRemove = new ArrayList<>(); final HashMap<ProjectionAnnotation, Position> annotationsToAdd = new HashMap<>(); final List<Annotation> annotationsToChange = new ArrayList<>(); final Map<Position, Annotation> newFoldingAnnotations = new HashMap<>(); for (final Position position : positions) { if (oldFoldingAnnotations.containsKey(position)) { final Annotation annotation = oldFoldingAnnotations.get(position); annotationsToChange.add(annotation); newFoldingAnnotations.put(new Position(position.getOffset(), position.getLength()), annotation); } else { final ProjectionAnnotation annotation = new ProjectionAnnotation(); annotationsToAdd.put(annotation, position); newFoldingAnnotations.put(new Position(position.getOffset(), position.getLength()), annotation); } } for (final Position position : oldFoldingAnnotations.keySet()) { if (!newFoldingAnnotations.containsKey(position)) { annotationsToRemove.add(oldFoldingAnnotations.get(position)); } } try { if (!textControl.isDisposed()) { textControl.setRedraw(false); } annotationsModel.modifyAnnotations(annotationsToRemove.toArray(new Annotation[0]), annotationsToAdd, annotationsToChange.toArray(new Annotation[0])); // workaround : without this the horizontal scrollbar is reset to 0 position when // writing at the end of long line if (!textControl.isDisposed()) { textControl.showSelection(); } } finally { if (!textControl.isDisposed()) { textControl.setRedraw(true); } oldFoldingAnnotations = newFoldingAnnotations; } } }