blob: 92b29cb88df7ecdc0e9d3a224efd5a2f394ba935 [file] [log] [blame]
/*
* 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.intellij.psi.formatter;
import com.intellij.formatting.Block;
import com.intellij.formatting.FormattingDocumentModel;
import com.intellij.formatting.FormattingModelEx;
import com.intellij.lang.ASTNode;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.codeStyle.CommonCodeStyleSettings;
import com.intellij.psi.impl.source.codeStyle.CodeEditUtil;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* @author lesya
*/
public class DocumentBasedFormattingModel implements FormattingModelEx {
private final Block myRootBlock;
private final FormattingDocumentModel myDocumentModel;
@NotNull private final Document myDocument;
private final Project myProject;
private final CodeStyleSettings mySettings;
private final FileType myFileType;
private final PsiFile myFile;
public DocumentBasedFormattingModel(final Block rootBlock,
@NotNull final Document document,
final Project project,
final CodeStyleSettings settings,
final FileType fileType,
final PsiFile file) {
myRootBlock = rootBlock;
myDocument = document;
myProject = project;
mySettings = settings;
myFileType = fileType;
myFile = file;
myDocumentModel = new FormattingDocumentModelImpl(document,file);
}
public DocumentBasedFormattingModel(final Block rootBlock,
final Project project,
final CodeStyleSettings settings,
final FileType fileType,
final PsiFile file) {
myRootBlock = rootBlock;
myProject = project;
mySettings = settings;
myFileType = fileType;
myFile = file;
myDocumentModel = FormattingDocumentModelImpl.createOn(file);
myDocument = myDocumentModel.getDocument();
}
@Override
@NotNull
public Block getRootBlock() {
return myRootBlock;
}
@Override
@NotNull
public FormattingDocumentModel getDocumentModel() {
return myDocumentModel;
}
@Override
public TextRange replaceWhiteSpace(TextRange textRange, String whiteSpace) {
return replaceWhiteSpace(textRange, null, whiteSpace);
}
@Override
public TextRange replaceWhiteSpace(TextRange textRange, ASTNode nodeAfter, String whiteSpace) {
boolean removesStartMarker;
String marker;
// When processing injection in cdata / comment we need not remove start / end markers that present as whitespace during check in
// com.intellij.formatting.WhiteSpace and during building formatter model = blocks in e.g. com.intellij.psi.formatter.xml.XmlTagBlock
if ((removesStartMarker = removesPattern(textRange, whiteSpace, marker = "<![CDATA[") ||
removesPattern(textRange, whiteSpace, marker ="<!--[")) ||
removesPattern(textRange, whiteSpace, marker = "]]>") ||
removesPattern(textRange, whiteSpace, marker = "]-->")
) {
String newWs = null;
if (removesStartMarker) { // TODO once we reformat comments we will need to handle their markers as well
int at = CharArrayUtil.indexOf(myDocument.getCharsSequence(), marker, textRange.getStartOffset(), textRange.getEndOffset() + 1);
String ws = myDocument.getCharsSequence().subSequence(textRange.getStartOffset(), textRange.getEndOffset()).toString();
newWs = mergeWsWithCdataMarker(whiteSpace, ws, at - textRange.getStartOffset());
if (removesPattern(textRange, newWs != null ? newWs: whiteSpace, marker = "]]>")) {
int i;
if (newWs != null && (i = newWs.lastIndexOf('\n')) > 0) {
int cdataStart = newWs.indexOf("<![CDATA[");
int i2 = newWs.lastIndexOf('\n', cdataStart);
String cdataIndent = i2 != -1 ? newWs.substring(i2 + 1, cdataStart):"";
newWs = newWs.substring(0, i) + cdataIndent + marker + newWs.substring(i);
}
}
}
if (newWs == null) return textRange;
whiteSpace = newWs;
}
CharSequence whiteSpaceToUse = getDocumentModel().adjustWhiteSpaceIfNecessary(
whiteSpace, textRange.getStartOffset(), textRange.getEndOffset(), nodeAfter, false
);
myDocument.replaceString(textRange.getStartOffset(),
textRange.getEndOffset(),
whiteSpaceToUse);
return new TextRange(textRange.getStartOffset(), textRange.getStartOffset() + whiteSpaceToUse.length());
}
private boolean removesPattern(final TextRange textRange, final String whiteSpace, final String pattern) {
return CharArrayUtil.indexOf(myDocument.getCharsSequence(), pattern, textRange.getStartOffset(), textRange.getEndOffset() + 1) >= 0 &&
CharArrayUtil.indexOf(whiteSpace, pattern, 0) < 0;
}
@Override
public TextRange shiftIndentInsideRange(TextRange range, int indent) {
final int newLength = shiftIndentInside(range, indent);
return new TextRange(range.getStartOffset(), range.getStartOffset() + newLength);
}
@Override
public void commitChanges() {
CodeEditUtil.allowToMarkNodesForPostponedFormatting(false);
try {
PsiDocumentManager.getInstance(myProject).commitDocument(myDocument);
}
finally {
CodeEditUtil.allowToMarkNodesForPostponedFormatting(true);
}
}
private int shiftIndentInside(final TextRange elementRange, final int shift) {
final StringBuilder buffer = new StringBuilder();
StringBuilder afterWhiteSpace = new StringBuilder();
int whiteSpaceLength = 0;
boolean insideWhiteSpace = true;
int line = 0;
for (int i = elementRange.getStartOffset(); i < elementRange.getEndOffset(); i++) {
final char c = myDocument.getCharsSequence().charAt(i);
switch (c) {
case '\n':
if (line > 0) {
createWhiteSpace(whiteSpaceLength + shift, buffer);
}
buffer.append(afterWhiteSpace.toString());
insideWhiteSpace = true;
whiteSpaceLength = 0;
afterWhiteSpace = new StringBuilder();
buffer.append(c);
line++;
break;
case ' ':
if (insideWhiteSpace) {
whiteSpaceLength += 1;
}
else {
afterWhiteSpace.append(c);
}
break;
case '\t':
if (insideWhiteSpace) {
whiteSpaceLength += getIndentOptions().TAB_SIZE;
}
else {
afterWhiteSpace.append(c);
}
break;
default:
insideWhiteSpace = false;
afterWhiteSpace.append(c);
}
}
if (line > 0) {
createWhiteSpace(whiteSpaceLength + shift, buffer);
}
buffer.append(afterWhiteSpace.toString());
myDocument.replaceString(elementRange.getStartOffset(), elementRange.getEndOffset(), buffer.toString());
return buffer.length();
}
private void createWhiteSpace(final int whiteSpaceLength, StringBuilder buffer) {
if (whiteSpaceLength < 0) return;
final CommonCodeStyleSettings.IndentOptions indentOptions = getIndentOptions();
if (indentOptions.USE_TAB_CHARACTER) {
int tabs = whiteSpaceLength / indentOptions.TAB_SIZE;
int spaces = whiteSpaceLength - tabs * indentOptions.TAB_SIZE;
StringUtil.repeatSymbol(buffer, '\t', tabs);
StringUtil.repeatSymbol(buffer, ' ', spaces);
}
else {
StringUtil.repeatSymbol(buffer, ' ', whiteSpaceLength);
}
}
private CommonCodeStyleSettings.IndentOptions getIndentOptions() {
return mySettings.getIndentOptions(myFileType);
}
@NotNull
public Document getDocument() {
return myDocument;
}
public Project getProject() {
return myProject;
}
public PsiFile getFile() {
return myFile;
}
@Nullable
public static String mergeWsWithCdataMarker(String whiteSpace, final String s, final int cdataPos) {
final int firstCrInGeneratedWs = whiteSpace.indexOf('\n');
final int secondCrInGeneratedWs = firstCrInGeneratedWs != -1 ? whiteSpace.indexOf('\n', firstCrInGeneratedWs + 1) : -1;
final int firstCrInPreviousWs = s.indexOf('\n');
final int secondCrInPreviousWs = firstCrInPreviousWs != -1 ? s.indexOf('\n', firstCrInPreviousWs + 1) : -1;
boolean knowHowToModifyCData = false;
if (secondCrInPreviousWs != -1 && secondCrInGeneratedWs != -1 && cdataPos > firstCrInPreviousWs && cdataPos < secondCrInPreviousWs) {
whiteSpace = whiteSpace.substring(0, secondCrInGeneratedWs) +
s.substring(firstCrInPreviousWs + 1, secondCrInPreviousWs) +
whiteSpace.substring(secondCrInGeneratedWs);
knowHowToModifyCData = true;
}
if (!knowHowToModifyCData) whiteSpace = null;
return whiteSpace;
}
}