package com.redhat.ceylon.eclipse.code.editor;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.ASTRING_LITERAL;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.AVERBATIM_STRING;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.LINE_COMMENT;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.MULTI_COMMENT;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.STRING_LITERAL;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.VERBATIM_STRING;
import static com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer.WS;
import static com.redhat.ceylon.eclipse.code.parse.TreeLifecycleListener.Stage.SYNTACTIC_ANALYSIS;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.AUTO_FOLD_COMMENTS;
import static com.redhat.ceylon.eclipse.code.preferences.CeylonPreferenceInitializer.AUTO_FOLD_IMPORTS;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.antlr.runtime.CommonToken;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.jface.preference.IPreferenceStore;
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.IProjectionListener;
import org.eclipse.jface.text.source.projection.ProjectionAnnotation;
import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import com.redhat.ceylon.compiler.typechecker.parser.CeylonLexer;
import com.redhat.ceylon.compiler.typechecker.tree.Node;
import com.redhat.ceylon.compiler.typechecker.tree.Tree;
import com.redhat.ceylon.compiler.typechecker.tree.Visitor;
import com.redhat.ceylon.eclipse.code.parse.CeylonParseController;
import com.redhat.ceylon.eclipse.code.parse.TreeLifecycleListener;
import com.redhat.ceylon.eclipse.ui.CeylonPlugin;
public class ProjectionAnnotationManager
implements TreeLifecycleListener,
IProjectionListener {
private static final Annotation[] NO_ANNOTATIONS =
new Annotation[0];
private CeylonEditor editor;
private boolean firstTime = true;
private final HashMap<Annotation,Position> newAnnotations =
new HashMap<Annotation, Position>();
private HashMap<Annotation,Position> oldAnnotations =
new HashMap<Annotation, Position>();
public ProjectionAnnotationManager(CeylonEditor editor) {
this.editor = editor;
}
@Override
public void projectionEnabled() {
reset();
// editor.scheduleParsing();
CeylonParseController pc =
editor.getParseController();
if (pc.getDocument()
== editor.getCeylonSourceViewer()
.getDocument()) {
update(pc, new NullProgressMonitor());
}
}
@Override
public void projectionDisabled() {}
public Stage getStage() {
return SYNTACTIC_ANALYSIS;
}
public void update(CeylonParseController parseController,
IProgressMonitor monitor) {
if (parseController.getStage().ordinal()
>= getStage().ordinal()) {
Tree.CompilationUnit rn =
parseController.getParsedRootNode();
if (rn!=null) { // can be null if file is outside workspace
try {
updateFoldingStructure(rn,
parseController.getTokens());
}
catch (Exception e) {
e.printStackTrace();
}
}
}
}
void reset() {
firstTime=true;
oldAnnotations.clear();
}
/**
* Make a folding annotation that corresponds to the extent of text
* represented by a given program entity. Usually, this will be an
* AST node, but it can be anything for which the language's
* ISourcePositionLocator can produce an offset/end offset.
*
* @param n an Object representing a program entity
*/
// public void makeAnnotation(Object n) {
// makeAnnotation(n, false);
// }
/**
* Make a folding annotation that corresponds to the extent of text
* represented by a given program entity. Usually, this will be an
* AST node, but it can be anything for which the language's
* ISourcePositionLocator can produce an offset/end offset.
*
* @param n an Object representing a program entity
*/
// public void makeAnnotation(Object n, boolean collapsed) {
// makeAnnotation(getStartOffset(n), getLength(n), collapsed);
// }
/**
* Make a folding annotation that corresponds to the given range of text.
*
* @param start The starting offset of the text range
* @param len The length of the text range
*/
private ProjectionAnnotation makeAnnotation(
int start, int len, int tokenType) {
ProjectionAnnotation annotation =
new CeylonProjectionAnnotation(tokenType);
newAnnotations.put(annotation,
new Position(start, len));
return annotation;
}
protected int advanceToEndOfLine(int offset, int len) {
IDocument doc =
editor.getCeylonSourceViewer()
.getDocument();
try {
int line = doc.getLineOfOffset(offset+len);
while (offset+len<doc.getLength() &&
// Character.isWhitespace(doc.getChar(offset+len)) &&
doc.getLineOfOffset(offset+len)==line) {
len++;
}
}
catch (BadLocationException e) {
e.printStackTrace();
}
return len;
}
/**
* Update the folding structure for a source text, where the text and its
* AST are represented by a given parse controller and the folding structure
* is represented by annotations in a given annotation model.
*
* This is the principal routine of the folding updater.
*
* The implementation provided here makes use of a local class
* FoldingUpdateStrategy, to which the task of updating the folding
* structure is delegated.
*
* updateFoldingStructure is synchronized because, at least on file opening,
* it can be called more than once before the first invocation has completed.
* This can lead to inconsistent calculations resulting in the absence of
* folding annotations in newly opened files.
*
* @param ast The AST for the source text
* @param annotationModel A structure of projection annotations that
* represent the foldable elements in the source
* text
*/
public synchronized void updateFoldingStructure(
Tree.CompilationUnit ast,
List<CommonToken> tokens) {
try {
ProjectionAnnotationModel annotationModel =
editor.getCeylonSourceViewer()
.getProjectionAnnotationModel();
if (ast==null||annotationModel==null) {
// We can't create annotations without an AST
return;
}
// But, since here we have the AST ...
createAnnotations(ast, tokens);
/*
// Update the annotation model if there have been changes
// but not otherwise (since update leads to redrawing of the
// source in the editor, which is likely to be unwelcome if
// there haven't been any changes relevant to folding)
boolean updateNeeded = false;
if (firstTime) {
// Should just be the first time through
updateNeeded = true;
}
else {
// Check to see whether the current and previous annotations
// differ in any significant way; if not, then there's no
// reason to update the annotation model.
// Note: This test may be implemented in various ways that may
// be more or less simple, efficient, correct, etc. (The
// default test provided below is simplistic although quick and
// usually effective.)
updateNeeded = differ(oldAnnotations, newAnnotations);
}
// Need to curtail calls to modifyAnnotations() because these lead to calls
// to fireModelChanged(), which eventually lead to calls to updateFoldingStructure,
// which lead back here, which would lead to another call to modifyAnnotations()
// (unless those were curtailed)
if (updateNeeded) {*/
List<Annotation> deletions =
new ArrayList<Annotation>
(oldAnnotations.size());
for (Map.Entry<Annotation,Position> e:
oldAnnotations.entrySet()) {
if (!newAnnotations.containsValue(e.getValue())) {
deletions.add(e.getKey());
}
}
Map<Annotation, Position> additions =
new HashMap<Annotation,Position>
(newAnnotations.size());
for (Map.Entry<Annotation,Position> e:
newAnnotations.entrySet()) {
if (!oldAnnotations.containsValue(e.getValue())) {
additions.put(e.getKey(), e.getValue());
}
}
if (!deletions.isEmpty() || !additions.isEmpty()) {
annotationModel.modifyAnnotations(
deletions.toArray(NO_ANNOTATIONS),
additions, null);
}
// Capture the latest set of annotations in a form that can be used the next
// time that it is necessary to modify the annotations
for (Annotation a: deletions) {
oldAnnotations.remove(a);
}
oldAnnotations.putAll(additions);
//}
newAnnotations.clear();
}
catch (Exception e) {
e.printStackTrace();
}
}
/**
* Send a visitor to an AST representing a program in order to construct the
* folding annotations. Both the visitor type and the AST node type are language-
* dependent, so this method is abstract.
*
* @param newAnnotations A map of annotations to text positions
* @param annotations A listing of the annotations in newAnnotations, that is,
* a listing of keys to the map of text positions
* @param ast An Object that will be taken to represent an AST node
*/
public void createAnnotations(Tree.CompilationUnit ast,
List<CommonToken> tokens) {
final boolean autofoldImports;
final boolean autofoldComments;
if (firstTime) {
IPreferenceStore store =
CeylonPlugin.getPreferences();
autofoldImports =
store.getBoolean(AUTO_FOLD_IMPORTS);
autofoldComments =
store.getBoolean(AUTO_FOLD_COMMENTS);
firstTime = false;
}
else {
autofoldImports = false;
autofoldComments = false;
}
for (int i=0; i<tokens.size(); i++) {
CommonToken token = tokens.get(i);
int type = token.getType();
if (type==MULTI_COMMENT ||
type==STRING_LITERAL ||
type==ASTRING_LITERAL ||
type==VERBATIM_STRING ||
type==AVERBATIM_STRING) {
if (isMultilineToken(token)) {
ProjectionAnnotation ann =
makeAnnotation(token, token);
if (autofoldComments && ann!=null
&& type==MULTI_COMMENT) {
ann.markCollapsed();
}
}
}
if (type==LINE_COMMENT) {
CommonToken until = token;
int j=i+1;
CommonToken next = tokens.get(j);
while (next.getType()==LINE_COMMENT ||
next.getType()==WS) {
if (next.getType()==LINE_COMMENT) {
until = next;
i = j;
}
next = tokens.get(++j);
}
ProjectionAnnotation ann =
foldIfNecessary(token, until);
if (ann!=null && autofoldComments) {
ann.markCollapsed();
}
}
}
Tree.CompilationUnit cu =
(Tree.CompilationUnit) ast;
new Visitor() {
@Override
public void visit(Tree.ImportList importList) {
super.visit(importList);
if (!importList.getImports().isEmpty()) {
ProjectionAnnotation ann =
foldIfNecessary(importList);
if (autofoldImports && ann!=null) {
ann.markCollapsed();
}
}
}
/*@Override
public void visit(Tree.Import that) {
super.visit(that);
foldIfNecessary(that);
}*/
@Override
public void visit(Tree.Body that) {
super.visit(that);
if (that.getToken()!=null) { //for "else if"
foldIfNecessary(that);
}
}
@Override
public void visit(Tree.NamedArgumentList that) {
super.visit(that);
foldIfNecessary(that);
}
@Override
public void visit(Tree.ModuleDescriptor that) {
super.visit(that);
foldIfNecessary(that);
}
}.visit(cu);
}
private ProjectionAnnotation foldIfNecessary(Node node) {
CommonToken token =
(CommonToken) node.getToken();
CommonToken endToken =
(CommonToken) node.getEndToken();
if (token!=null && endToken!=null &&
endToken.getLine()-token.getLine()>0) {
return makeAnnotation(token, endToken);
}
else {
return null;
}
}
private ProjectionAnnotation foldIfNecessary(
CommonToken start, CommonToken end) {
if (end.getLine()>start.getLine()) {
return makeAnnotation(start, end);
}
else {
return null;
}
}
private boolean isMultilineToken(CommonToken token) {
return token.getText().indexOf('\n')>0 ||
token.getText().indexOf('\r')>0;
}
private ProjectionAnnotation makeAnnotation(
CommonToken start, CommonToken end) {
int offset = start.getStartIndex();
int len = end.getStopIndex()-start.getStartIndex()+1;
if (end.getType()!=CeylonLexer.LINE_COMMENT) {
len = advanceToEndOfLine(offset, len);
}
return makeAnnotation(offset, len, start.getType());
}
}