/*******************************************************************************
* Copyright (c) 2011 Sebastian Benz.
* 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:
* Sebastian Benz - initial API and implementation
******************************************************************************/
package de.sebastianbenz.task.ui.highlighting;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Iterables.toArray;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.NOTE_DONE_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.PROJECT1_DONE_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.PROJECT2_DONE_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.PROJECT2_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.PROJECT3_DONE_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.PROJECT3_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.TAG_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.TASK_DONE_ID;
import static de.sebastianbenz.task.ui.highlighting.HighlightingConfiguration.TASK_URL_ID;
import static de.sebastianbenz.task.util.Contents.offset;
import static de.sebastianbenz.task.util.Contents.region;
import static java.util.Collections.sort;
import static org.eclipse.xtext.util.Strings.isEmpty;
import java.util.Iterator;
import java.util.List;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.xtext.resource.XtextResource;
import org.eclipse.xtext.ui.editor.syntaxcoloring.IHighlightedPositionAcceptor;
import org.eclipse.xtext.ui.editor.syntaxcoloring.ISemanticHighlightingCalculator;
import com.google.common.collect.Lists;
import com.google.inject.Inject;
import de.sebastianbenz.task.Code;
import de.sebastianbenz.task.Content;
import de.sebastianbenz.task.Link;
import de.sebastianbenz.task.Note;
import de.sebastianbenz.task.Project;
import de.sebastianbenz.task.Tag;
import de.sebastianbenz.task.Task;
import de.sebastianbenz.task.impl.CodeImplCustom;
import de.sebastianbenz.task.tagging.Region;
import de.sebastianbenz.task.util.Contents;
import de.sebastianbenz.task.util.TaskSwitch;
public class SemanticHighlightingCalculator implements ISemanticHighlightingCalculator{
private static final int CODE_SEPARATOR = CodeImplCustom.PREFIX.length();
private static class Position implements Comparable<Position>{
public final int offset;
public final int length;
public final String[] id;
public Position(int offset, int length, String[] id) {
this.offset = offset;
this.length = length;
this.id = id;
}
public int compareTo(Position other) {
return this.offset < other.offset ? -1 : this.offset == other.offset ? 0 : 1;
}
@Override
public String toString() {
return offset + "-" + length + " " + id;
}
}
public static class FilteringAcceptor implements IHighlightedPositionAcceptor {
private final int baseOffset;
private List<Position> matches = Lists.newArrayList();
private final IHighlightedPositionAcceptor acceptor;
private FilteringAcceptor(IHighlightedPositionAcceptor acceptor, int base) {
this.acceptor = acceptor;
this.baseOffset = base;
}
public void addPosition(int offset, int length, String... id) {
matches.add(new Position(offset, length, id));
}
public void flush() {
Position[] positions = filterHiddenMatches();
for (Position match : positions) {
if(match != null){
acceptor.addPosition(baseOffset + match.offset, match.length, match.id);
}
}
}
private Position[] filterHiddenMatches() {
sort(matches);
Position[] positions = toArray(matches, Position.class);
for(int i = 0; i < positions.length; i++){
Position current = positions[i];
if(isInsideOtherMatch(positions, current, i)){
positions[i] = null;
}
}
return positions;
}
private boolean isInsideOtherMatch(Position[] positions, Position current, int i) {
for(int j = 0; j < i; j++){
Position other = positions[j];
if(other != null && current.offset < other.offset + other.length){
return true;
}
}
return false;
}
}
private class Implementation extends TaskSwitch<Boolean>{
private final IHighlightedPositionAcceptor acceptor;
public Implementation(IHighlightedPositionAcceptor acceptor) {
this.acceptor = acceptor;
}
@Override
public Boolean caseTask(Task content) {
return applyHighlighting(content, TASK_DONE_ID, TASK_URL_ID);
}
private Boolean applyHighlighting(Content content, String doneStyle, String urlStyle) {
Iterable<Tag> allTags = content.getTags();
if(content.isDone()){
markAsDone(content, allTags, doneStyle);
}else{
highlightUrls(content, urlStyle);
}
highlightTags(content, allTags);
return Boolean.TRUE;
}
private void highlightUrls(Content content, String style) {
int offset = offset(content);
for (Link link : concat(content.getLinks(), content.getImages())) {
int linkOffset;
int linkLength;
if(isEmpty(link.getDescription())){
linkOffset = offset + link.getOffset();
linkLength = link.getLength();
}else{
int descriptionLength = link.getDescription().length();
linkOffset = offset + link.getOffset() + descriptionLength + 3;
linkLength = link.getLength() - descriptionLength - 4;
}
acceptor.addPosition(linkOffset, linkLength, style);
}
}
@Override
public Boolean caseNote(Note content) {
return applyHighlighting(content, NOTE_DONE_ID, HighlightingConfiguration.NOTE_URL_ID);
}
private void markAsDone(Content content, Iterable<Tag> allTags, String doneStyle) {
Region region = Contents.region(content);
int begin = 0;
int lastTagEnd = region.getOffset();
int taskOffset = lastTagEnd;
for (Tag tag : allTags) {
int length = tag.getOffset() - begin - whiteSpaces(content, tag.getOffset());
if(length > 0){
int offset = taskOffset + begin;
if(isNotWhiteSpace(content, taskOffset, length, offset)){
acceptor.addPosition(offset, length, doneStyle);
}
lastTagEnd = offset + length + tag.getLength();
}
begin = tag.getOffset() + tag.getLength() + 1;
}
int taskLength = content.getText().trim().length();
if(lastTagEnd < taskOffset + taskLength){
acceptor.addPosition(lastTagEnd, taskLength-begin, doneStyle);
}
}
private int whiteSpaces(Content content, int offset) {
int whiteSpaceCount = 0;
for(int i = offset-1; content.getText().charAt(i) == ' '; i--){
whiteSpaceCount++;
}
return whiteSpaceCount;
}
private boolean isNotWhiteSpace(Content content, int taskOffset,
int length, int offset) {
int beginIndex = offset - taskOffset;
return content.getText().substring(beginIndex, beginIndex + length).trim().length() > 0;
}
private void highlightTags(Content content, Iterable<Tag> allTags) {
for (Tag tag : allTags) {
acceptor.addPosition(offset(content) + tag.getOffset(), tag.getLength(), TAG_ID);
}
}
@Override
public Boolean caseProject(Project project) {
int level = project.getLevel();
if(level == 1){
highlight(project, PROJECT2_ID, PROJECT2_DONE_ID);
}else if(level >= 2) {
highlight(project, PROJECT3_ID, PROJECT3_DONE_ID);
}else if(project.isDone()){
highlight(project, PROJECT1_DONE_ID);
}
return Boolean.TRUE;
}
private void highlight(Project project, String normal,
String done) {
String id = project.isDone() ? done : normal;
highlight(project, id);
}
protected void highlight(Content content, String id) {
Region region = region(content);
acceptor.addPosition(region.getOffset(), region.getLength(), id);
}
@Override
public Boolean caseCode(Code code) {
applyBrushes(code);
return Boolean.TRUE;
}
protected void applyBrushes(Code code) {
FilteringAcceptor filter = new FilteringAcceptor(acceptor, codeOffset(code));
Brush brush = brushFor(code);
brush.apply(textOf(code), filter);
filter.flush();
}
protected String textOf(Code code) {
return code.getText();
}
protected Brush brushFor(Code code) {
return configurationRegistry.get(code.getLang());
}
protected int codeOffset(Code code) {
return offset(code) + CODE_SEPARATOR;
}
}
@Inject
public SemanticHighlightingCalculator(BrushRegistry configurationRegistry){
this.configurationRegistry = configurationRegistry;
}
private final BrushRegistry configurationRegistry;
public void provideHighlightingFor(XtextResource resource,
IHighlightedPositionAcceptor acceptor) {
if (noNodeModel(resource)){
return;
}
Implementation highlighter = new Implementation(acceptor);
Iterator<EObject> contents = resource.getAllContents();
while (contents.hasNext()) {
highlighter.doSwitch((EObject) contents.next());
}
}
protected EObject root(XtextResource resource) {
return resource.getParseResult().getRootASTElement();
}
protected boolean noNodeModel(XtextResource resource) {
return resource == null || resource.getParseResult() == null || root(resource) == null;
}
}