/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.maddyhome.idea.copyright.psi;
import com.intellij.lang.Commenter;
import com.intellij.lang.LanguageCommenters;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.util.IncorrectOperationException;
import com.maddyhome.idea.copyright.CopyrightManager;
import com.maddyhome.idea.copyright.CopyrightProfile;
import com.maddyhome.idea.copyright.pattern.EntityUtil;
import com.maddyhome.idea.copyright.pattern.VelocityHelper;
import com.maddyhome.idea.copyright.util.FileTypeUtil;
import consulo.copyright.config.CopyrightFileConfig;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public abstract class UpdatePsiFileCopyright<T extends CopyrightFileConfig> {
public static final Logger LOGGER = Logger.getInstance(UpdatePsiFileCopyright.class);
private final T myFileConfig;
@NotNull
private final PsiFile myPsiFile;
@NotNull
private final CopyrightProfile myCopyrightProfile;
private final Set<CommentAction> myActions = new TreeSet<CommentAction>();
private String myCommentText;
private FileType myFileType;
@SuppressWarnings("unchecked")
protected UpdatePsiFileCopyright(@NotNull PsiFile psiFile, @NotNull CopyrightProfile copyrightProfile) {
myPsiFile = psiFile;
myCopyrightProfile = copyrightProfile;
VirtualFile virtualFile = psiFile.getVirtualFile();
assert virtualFile != null;
myFileType = virtualFile.getFileType();
myFileConfig = (T)CopyrightManager.getInstance(psiFile.getProject()).getCopyrightFileConfigManager().getMergedOptions(myFileType);
}
private static CommentRange getLineCopyrightComments(List<PsiComment> comments, Document doc, int i, PsiComment comment) {
PsiElement firstComment = comment;
PsiElement lastComment = comment;
final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(PsiUtilCore.findLanguageFromElement(comment));
if (isLineComment(commenter, comment, doc)) {
int sline = doc.getLineNumber(comment.getTextRange().getStartOffset());
int eline = doc.getLineNumber(comment.getTextRange().getEndOffset());
for (int j = i - 1; j >= 0; j--) {
PsiComment cmt = comments.get(j);
if (isLineComment(commenter, cmt, doc) && doc.getLineNumber(cmt.getTextRange().getEndOffset()) == sline - 1) {
firstComment = cmt;
sline = doc.getLineNumber(cmt.getTextRange().getStartOffset());
}
else {
break;
}
}
for (int j = i + 1; j < comments.size(); j++) {
PsiComment cmt = comments.get(j);
if (isLineComment(commenter, cmt, doc) && doc.getLineNumber(cmt.getTextRange().getStartOffset()) == eline + 1) {
lastComment = cmt;
eline = doc.getLineNumber(cmt.getTextRange().getEndOffset());
}
else {
break;
}
}
}
return new CommentRange(firstComment, lastComment);
}
private static boolean isLineComment(Commenter commenter, PsiComment comment, Document doc) {
final String lineCommentPrefix = commenter.getLineCommentPrefix();
if (lineCommentPrefix != null) {
return comment.getText().startsWith(lineCommentPrefix);
}
final TextRange textRange = comment.getTextRange();
return doc.getLineNumber(textRange.getStartOffset()) == doc.getLineNumber(textRange.getEndOffset());
}
public void process() throws Exception {
if (accept()) {
scanFile();
processActions();
}
}
protected boolean accept() {
return !(myPsiFile instanceof PsiPlainTextFile);
}
protected abstract void scanFile();
protected void checkComments(PsiElement first, PsiElement last, boolean commentHere) {
List<PsiComment> comments = new ArrayList<PsiComment>();
collectComments(first, last, comments);
checkComments(last, commentHere, comments);
}
protected void collectComments(PsiElement first, PsiElement last, List<PsiComment> comments) {
if (first == last && first instanceof PsiComment) {
comments.add((PsiComment)first);
return;
}
PsiElement elem = first;
while (elem != last && elem != null) {
if (elem instanceof PsiComment) {
comments.add((PsiComment)elem);
LOGGER.debug("found comment");
}
elem = getNextSibling(elem);
}
}
protected void checkComments(PsiElement last, boolean commentHere, List<PsiComment> comments) {
try {
final String keyword = myCopyrightProfile.getKeyword();
final LinkedHashSet<CommentRange> found = new LinkedHashSet<CommentRange>();
Document doc = null;
if (!StringUtil.isEmpty(keyword)) {
Pattern pattern = Pattern.compile(keyword, Pattern.CASE_INSENSITIVE);
doc = FileDocumentManager.getInstance().getDocument(getFile().getVirtualFile());
for (int i = 0; i < comments.size(); i++) {
PsiComment comment = comments.get(i);
String text = comment.getText();
Matcher match = pattern.matcher(text);
if (match.find()) {
found.add(getLineCopyrightComments(comments, doc, i, comment));
}
}
}
// Default insertion point to just before user chosen marker (package, import, class)
PsiElement point = last;
if (commentHere && !comments.isEmpty() && myFileConfig.isRelativeBefore()) {
// Insert before first comment within this section of code.
point = comments.get(0);
}
if (commentHere && found.size() == 1) {
CommentRange range = found.iterator().next();
// Is the comment in the right place?
if (myFileConfig.isRelativeBefore() && range.getFirst() == comments.get(0) ||
!myFileConfig.isRelativeBefore() && range.getLast() == comments.get(comments.size() - 1)) {
// Check to see if current copyright comment matches new one.
String newComment = getCommentText("", "");
myCommentText = null;
String oldComment =
doc.getCharsSequence().subSequence(range.getFirst().getTextRange().getStartOffset(), range.getLast().getTextRange().getEndOffset()).toString()
.trim();
if (!StringUtil.isEmptyOrSpaces(myCopyrightProfile.getAllowReplaceKeyword()) && !oldComment.contains(myCopyrightProfile.getAllowReplaceKeyword())) {
return;
}
if (newComment.trim().equals(oldComment)) {
if (!getLanguageOptions().isAddBlankAfter()) {
// TODO - do we need option to remove blank line after?
return; // Nothing to do since the comment is the same
}
PsiElement next = getNextSibling(range.getLast());
if (next instanceof PsiWhiteSpace && StringUtil.countNewLines(next.getText()) > 1) {
return;
}
point = range.getFirst();
}
else if (!newComment.isEmpty()) {
int start = range.getFirst().getTextRange().getStartOffset();
int end = range.getLast().getTextRange().getEndOffset();
addAction(new CommentAction(CommentAction.ACTION_REPLACE, start, end));
return;
}
}
}
for (CommentRange range : found) {
// Remove the old copyright
int start = range.getFirst().getTextRange().getStartOffset();
int end = range.getLast().getTextRange().getEndOffset();
// If this is the only comment then remove the whitespace after unless there is none before
if (range.getFirst() == comments.get(0) && range.getLast() == comments.get(comments.size() - 1)) {
int startLen = 0;
if (getPreviousSibling(range.getFirst()) instanceof PsiWhiteSpace) {
startLen = StringUtil.countNewLines(getPreviousSibling(range.getFirst()).getText());
}
int endLen = 0;
if (getNextSibling(range.getLast()) instanceof PsiWhiteSpace) {
endLen = StringUtil.countNewLines(getNextSibling(range.getLast()).getText());
}
if (startLen == 1 && getPreviousSibling(range.getFirst()).getTextRange().getStartOffset() > 0) {
start = getPreviousSibling(range.getFirst()).getTextRange().getStartOffset();
}
else if (endLen > 0) {
end = getNextSibling(range.getLast()).getTextRange().getEndOffset();
}
}
// If this is the last comment then remove the whitespace before the comment
else if (range.getLast() == comments.get(comments.size() - 1)) {
if (getPreviousSibling(range.getFirst()) instanceof PsiWhiteSpace && StringUtil.countNewLines(getPreviousSibling(range.getFirst()).getText()) > 1) {
start = getPreviousSibling(range.getFirst()).getTextRange().getStartOffset();
}
}
// If this is the first or middle comment then remove the whitespace after the comment
else if (getNextSibling(range.getLast()) instanceof PsiWhiteSpace) {
end = getNextSibling(range.getLast()).getTextRange().getEndOffset();
}
addAction(new CommentAction(CommentAction.ACTION_DELETE, start, end));
}
// Finally add the comment if user chose this section.
if (commentHere) {
String suffix = "\n";
if (point != last && getPreviousSibling(point) != null && getPreviousSibling(point) instanceof PsiWhiteSpace) {
suffix = getPreviousSibling(point).getText();
if (StringUtil.countNewLines(suffix) == 1) {
suffix = '\n' + suffix;
}
}
if (point != last && getPreviousSibling(point) == null) {
suffix = "\n\n";
}
if (getLanguageOptions().isAddBlankAfter() && StringUtil.countNewLines(suffix) == 1) {
suffix += "\n";
}
String prefix = "";
if (getPreviousSibling(point) != null) {
if (getPreviousSibling(point) instanceof PsiComment) {
prefix = "\n\n";
}
if (getPreviousSibling(point) instanceof PsiWhiteSpace &&
getPreviousSibling(getPreviousSibling(point)) != null &&
getPreviousSibling(getPreviousSibling(point)) instanceof PsiComment) {
String ws = getPreviousSibling(point).getText();
int cnt = StringUtil.countNewLines(ws);
if (cnt == 1) {
prefix = "\n";
}
}
}
int pos = 0;
if (point != null) {
final TextRange textRange = point.getTextRange();
if (textRange != null) {
pos = textRange.getStartOffset();
}
}
addAction(new CommentAction(pos, prefix, suffix));
}
}
catch (Exception e) {
LOGGER.error(e);
}
}
@NotNull
public PsiFile getFile() {
return myPsiFile;
}
@NotNull
public CopyrightFileConfig getFileConfig() {
return myFileConfig;
}
@NotNull
public FileType getFileType() {
return myFileType;
}
@Nullable
public Module getModule() {
return ModuleUtilCore.findModuleForPsiElement(myPsiFile);
}
@NotNull
public Project getProject() {
return myPsiFile.getProject();
}
protected CopyrightFileConfig getLanguageOptions() {
return myFileConfig;
}
protected void addAction(CommentAction action) {
myActions.add(action);
}
protected PsiElement getPreviousSibling(PsiElement element) {
return element == null ? null : element.getPrevSibling();
}
protected PsiElement getNextSibling(PsiElement element) {
return element == null ? null : element.getNextSibling();
}
protected void processActions() throws IncorrectOperationException {
Application app = ApplicationManager.getApplication();
app.runWriteAction(new Runnable() {
@Override
public void run() {
Document doc = FileDocumentManager.getInstance().getDocument(myPsiFile.getVirtualFile());
PsiDocumentManager.getInstance(myPsiFile.getProject()).doPostponedOperationsAndUnblockDocument(doc);
for (CommentAction action : myActions) {
int start = action.getStart();
int end = action.getEnd();
switch (action.getType()) {
case CommentAction.ACTION_INSERT:
String comment = getCommentText(action.getPrefix(), action.getSuffix());
if (!comment.isEmpty()) {
doc.insertString(start, comment);
}
break;
case CommentAction.ACTION_REPLACE:
doc.replaceString(start, end, getCommentText("", ""));
break;
case CommentAction.ACTION_DELETE:
doc.deleteString(start, end);
break;
}
}
}
});
}
protected String getCommentText(String prefix, String suffix) {
if (myCommentText == null) {
String base = EntityUtil.decode(myCopyrightProfile.getNotice());
if (base.isEmpty()) {
myCommentText = "";
}
else {
String expanded = null;
try {
expanded = VelocityHelper.evaluate(myPsiFile, getProject(), getModule(), base);
}
catch (Exception e) {
expanded = "";
}
String cmt = FileTypeUtil.buildComment(myFileType, expanded, myFileConfig);
myCommentText = StringUtil.convertLineSeparators(prefix + cmt + suffix);
}
}
return myCommentText;
}
private static class CommentRange {
private final PsiElement first;
private final PsiElement last;
public CommentRange(PsiElement first, PsiElement last) {
this.first = first;
this.last = last;
}
public PsiElement getFirst() {
return first;
}
public PsiElement getLast() {
return last;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CommentRange that = (CommentRange)o;
if (first != null ? !first.equals(that.first) : that.first != null) return false;
if (last != null ? !last.equals(that.last) : that.last != null) return false;
return true;
}
@Override
public int hashCode() {
int result = first != null ? first.hashCode() : 0;
result = 31 * result + (last != null ? last.hashCode() : 0);
return result;
}
}
protected static class CommentAction implements Comparable<CommentAction> {
public static final int ACTION_INSERT = 1;
public static final int ACTION_REPLACE = 2;
public static final int ACTION_DELETE = 3;
private final int type;
private final int start;
private final int end;
private String prefix = null;
private String suffix = null;
public CommentAction(int pos, String prefix, String suffix) {
type = ACTION_INSERT;
start = pos;
end = pos;
this.prefix = prefix;
this.suffix = suffix;
}
public CommentAction(int type, int start, int end) {
this.type = type;
this.start = start;
this.end = end;
}
public int getType() {
return type;
}
public int getStart() {
return start;
}
public int getEnd() {
return end;
}
public String getPrefix() {
return prefix;
}
public String getSuffix() {
return suffix;
}
@Override
public int compareTo(CommentAction object) {
int s = object.getStart();
int diff = s - start;
if (diff == 0) {
diff = type == ACTION_INSERT ? 1 : -1;
}
return diff;
}
}
}