IDEA-58743 Formatter: Add ability to configure formatter's 'white space symbols'

Dedicated extension is added - com.intellij.formatting.WhiteSpaceFormattingStrategy
This commit is contained in:
Denis Zhdanov
2010-09-21 10:39:36 +04:00
parent 99f09738f9
commit 4fa361ecd2
10 changed files with 367 additions and 14 deletions
@@ -54,4 +54,23 @@ public interface FormattingDocumentModel {
int getTextLength();
Document getDocument();
/**
* Allows to answer if all document symbols from <code>[startOffset; endOffset)</code> region are treated as white spaces by formatter.
*
* @param startOffset target start document offset (inclusive)
* @param endOffset target end document offset (exclusive)
* @return <code>true</code> if all document symbols from <code>[startOffset; endOffset)</code> region are treated
* as white spaces by formatter; <code>false</code> otherwise
*/
boolean containsWhiteSpaceSymbolsOnly(int startOffset, int endOffset);
/**
* Allows to answer if given symbol is treated by the current model as white space symbol during formatting.
*
* @param symbol symbols to check
* @return <code>true</code> if given symbol is treated by the current model as white space symbol during formatting;
* <code>false</code> otherwise
*/
boolean isWhiteSpaceSymbol(char symbol);
}
@@ -0,0 +1,36 @@
/*
* Copyright 2000-2010 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.formatting;
import com.intellij.lang.LanguageExtension;
import java.util.Collection;
/**
* Exposes pre-configured {@link WhiteSpaceFormattingStrategy} objects to use in a per-language manner.
*
* @author Denis Zhdanov
* @since Sep 20, 2010 7:41:55 PM
*/
public class LanguageWhiteSpaceFormattingStrategy extends LanguageExtension<Collection<WhiteSpaceFormattingStrategy>> {
public static final String EP_NAME = "lang.whiteSpaceFormattingStrategy";
public static final LanguageWhiteSpaceFormattingStrategy INSTANCE = new LanguageWhiteSpaceFormattingStrategy();
private LanguageWhiteSpaceFormattingStrategy() {
super(EP_NAME);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2000-2010 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.formatting;
import org.jetbrains.annotations.NotNull;
/**
* Defines common contract for strategy that determines if particular symbol or sequence of symbols may be treated as
* white space during formatting.
* <p/>
* <code>'Treated as white space'</code> here means that formatter is free to remove any number of such symbols or replace them
* by 'pure' white spaces.
*
* @author Denis Zhdanov
* @since Sep 20, 2010 5:05:08 PM
*/
public interface WhiteSpaceFormattingStrategy {
/**
* Checks if given sub-sequence of the given text contains symbols that may be treated as white spaces.
*
* @param text text to check
* @param start start offset to use with the given text (inclusive)
* @param end end offset to use with the given text (exclusive)
* @return offset of the first symbol that belongs to <code>[startOffset; endOffset)</code> range
* and is not treated as white space by the current strategy <b>or</b> value that is greater
* or equal to the given <code>'end'</code> parameter if all target sub-sequence symbols
* can be treated as white spaces
*/
int check(@NotNull CharSequence text, int start, int end);
}
@@ -23,7 +23,6 @@ import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiWhiteSpace;
import com.intellij.psi.codeStyle.CodeStyleSettings;
import com.intellij.psi.formatter.FormattingDocumentModelImpl;
import org.jetbrains.annotations.NonNls;
import java.util.ArrayList;
@@ -66,8 +65,6 @@ class WhiteSpace {
private static final byte CONTAINS_SPACES_INITIALLY = 0x40;
private static final int LF_COUNT_SHIFT = 7;
private static final int MAX_LF_COUNT = 1 << 24;
@NonNls private static final String CDATA_START = "<![CDATA[";
@NonNls private static final String CDATA_END = "]]>";
/**
* Creates new <code>WhiteSpace</code> object with the given start offset and a flag that shows if current white space is
@@ -158,19 +155,13 @@ class WhiteSpace {
*/
private boolean coveredByBlock(final FormattingDocumentModel model) {
if (myInitial == null) return true;
String s = myInitial.toString().trim();
if (s.length() == 0) return true;
if (model.containsWhiteSpaceSymbolsOnly(myStart, myEnd)) return true;
if (!(model instanceof FormattingDocumentModelImpl)) return false;
PsiFile psiFile = ((FormattingDocumentModelImpl)model).getFile();
if (psiFile == null) return false;
PsiElement start = psiFile.findElementAt(myStart);
PsiElement end = psiFile.findElementAt(myEnd-1);
// CDATA usage at common white space stuff smells because it makes no sense in context, for example, white space for pure java block.
if (s.startsWith(CDATA_START)) s = s.substring(CDATA_START.length());
if (s.endsWith(CDATA_END)) s = s.substring(0, s.length() - CDATA_END.length());
s = s.trim();
if (s.length() == 0) return true;
return start == end && start instanceof PsiWhiteSpace; // there maybe non-white text inside CDATA-encoded injected elements
}
@@ -183,12 +174,10 @@ class WhiteSpace {
mySpaces = 0;
myIndentSpaces = 0;
break;
case ' ':
mySpaces++;
break;
case '\t':
myIndentSpaces += tabSize;
break;
default: mySpaces++;
}
}
}
@@ -0,0 +1,51 @@
/*
* Copyright 2000-2010 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.WhiteSpaceFormattingStrategy;
import com.intellij.util.text.CharArrayUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
/**
* {@link WhiteSpaceFormattingStrategy} implementation that considers to be white spaces all symbols from
* standard XML CDATA section ('{@code <![CDATA[...]]>}').
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since Sep 20, 2010 5:39:50 PM
*/
public class CdataWhiteSpaceDefinitionStrategy implements WhiteSpaceFormattingStrategy {
@NonNls public static final String CDATA_START = "<![CDATA[";
@NonNls public static final String CDATA_END = "]]>";
@Override
public int check(@NotNull CharSequence text, int start, int end) {
if (CharArrayUtil.indexOf(text, CDATA_START, start, end) != start) {
return start;
}
int i = CharArrayUtil.indexOf(text, CDATA_END, start, end);
if (i < 0) {
return start;
}
int result = i + CDATA_END.length();
return result > end ? start : result;
}
}
@@ -17,6 +17,9 @@
package com.intellij.psi.formatter;
import com.intellij.formatting.FormattingDocumentModel;
import com.intellij.formatting.LanguageWhiteSpaceFormattingStrategy;
import com.intellij.formatting.WhiteSpaceFormattingStrategy;
import com.intellij.lang.Language;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.impl.DocumentImpl;
@@ -28,15 +31,31 @@ import com.intellij.psi.impl.PsiDocumentManagerImpl;
import com.intellij.psi.impl.PsiToDocumentSynchronizer;
import org.jetbrains.annotations.NotNull;
import java.nio.CharBuffer;
import java.util.*;
public class FormattingDocumentModelImpl implements FormattingDocumentModel{
private static final List<WhiteSpaceFormattingStrategy> SHARED_STRATEGIES = Arrays.asList(
new StaticWhiteSpaceDefinitionStrategy(' ', '\t'), new CdataWhiteSpaceDefinitionStrategy()
);
private final Set<WhiteSpaceFormattingStrategy> myWhiteSpaceStrategies = new HashSet<WhiteSpaceFormattingStrategy>(SHARED_STRATEGIES);
private final Document myDocument;
private final PsiFile myFile;
private static final Logger LOG = Logger.getInstance("#com.intellij.psi.formatter.FormattingDocumentModelImpl");
public FormattingDocumentModelImpl(final Document document, PsiFile file) {
myDocument = document;
myFile = file;
if (file != null) {
Language language = file.getLanguage();
Collection<WhiteSpaceFormattingStrategy> strategies = LanguageWhiteSpaceFormattingStrategy.INSTANCE.forLanguage(language);
if (strategies != null) {
myWhiteSpaceStrategies.addAll(strategies);
}
}
}
public static FormattingDocumentModelImpl createOn(PsiFile file) {
@@ -68,23 +87,28 @@ public class FormattingDocumentModelImpl implements FormattingDocumentModel{
return document;
}
@Override
public int getLineNumber(int offset) {
LOG.assertTrue (offset <= myDocument.getTextLength());
return myDocument.getLineNumber(offset);
}
@Override
public int getLineStartOffset(int line) {
return myDocument.getLineStartOffset(line);
}
@Override
public CharSequence getText(final TextRange textRange) {
return myDocument.getCharsSequence().subSequence(textRange.getStartOffset(), textRange.getEndOffset());
}
@Override
public int getTextLength() {
return myDocument.getTextLength();
}
@Override
public Document getDocument() {
return myDocument;
}
@@ -93,6 +117,33 @@ public class FormattingDocumentModelImpl implements FormattingDocumentModel{
return myFile;
}
@Override
public boolean containsWhiteSpaceSymbolsOnly(int startOffset, int endOffset) {
return containsWhiteSpaceSymbolsOnly(myDocument.getCharsSequence(), startOffset, endOffset);
}
@Override
public boolean isWhiteSpaceSymbol(char symbol) {
return containsWhiteSpaceSymbolsOnly(CharBuffer.wrap(new char[] {symbol}), 0, 1);
}
private boolean containsWhiteSpaceSymbolsOnly(CharSequence text, int startOffset, int endOffset) {
int offset = startOffset;
while (offset < endOffset) {
int oldOffset = offset;
for (WhiteSpaceFormattingStrategy strategy : myWhiteSpaceStrategies) {
offset = strategy.check(text, offset, endOffset);
if (offset > oldOffset) {
break;
}
}
if (offset == oldOffset) {
return false;
}
}
return offset >= endOffset;
}
public static boolean canUseDocumentModel(@NotNull Document document,@NotNull PsiFile file) {
PsiDocumentManager psiDocumentManager = PsiDocumentManager.getInstance(file.getProject());
return !psiDocumentManager.isUncommited(document) &&
@@ -0,0 +1,56 @@
/*
* Copyright 2000-2010 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.WhiteSpaceFormattingStrategy;
import gnu.trove.TIntHashSet;
import org.jetbrains.annotations.NotNull;
/**
* {@link WhiteSpaceFormattingStrategy} implementation that is pre-configured with the set of symbols that may
* be treated as white spaces.
* <p/>
* Thread-safe.
*
* @author Denis Zhdanov
* @since Sep 20, 2010 5:11:49 PM
*/
public class StaticWhiteSpaceDefinitionStrategy implements WhiteSpaceFormattingStrategy {
private final TIntHashSet myWhiteSpaceSymbols = new TIntHashSet();
/**
* Creates new <code>StaticWhiteSpaceDefinitionStrategy</code> object with the symbols that should be treated as white spaces.
*
* @param whiteSpaceSymbols symbols that should be treated as white spaces by the current strategy
*/
public StaticWhiteSpaceDefinitionStrategy(char ... whiteSpaceSymbols) {
for (char symbol : whiteSpaceSymbols) {
myWhiteSpaceSymbols.add(symbol);
}
}
@Override
public int check(@NotNull CharSequence text, int start, int end) {
for (int i = start; i < end; i++) {
char c = text.charAt(i);
if (!myWhiteSpaceSymbols.contains(c)) {
return i;
}
}
return end;
}
}
@@ -0,0 +1,61 @@
/*
* Copyright 2000-2010 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 static org.junit.Assert.*;
import com.intellij.psi.formatter.CdataWhiteSpaceDefinitionStrategy;
import org.junit.Test;
import org.junit.Before;
import org.junit.After;
import org.junit.runner.RunWith;
/**
* @author Denis Zhdanov
* @since 09/20/2010
*/
public class CdataWhiteSpaceDefinitionStrategyTest {
private CdataWhiteSpaceDefinitionStrategy myStrategy;
@Before
public void setUp() {
myStrategy = new CdataWhiteSpaceDefinitionStrategy();
}
@Test
public void withoutClosingSection() {
String text = "<![CDATA[xxx]>"; // Doesn't contain double ']'
assertSame(0, myStrategy.check(text, 0, text.length()));
text = "<![CDATA[xxx]]>";
assertSame(0, myStrategy.check(text, 0, text.length() - 1)); // Last symbol is out of scope
}
@Test
public void cdataNotAtFirstOffset() {
String text = " <![CDATA[xxx]]>";
assertSame(0, myStrategy.check(text, 0, text.length()));
assertSame(1, myStrategy.check(text, 1, text.length()));
}
@Test
public void partialMatch() {
String text = "<![CDATA[xxx]]> ";
int expectedOffset = text.indexOf(">") + 1;
assertSame(expectedOffset, myStrategy.check(text, 0, text.length()));
assertSame(expectedOffset + 1, myStrategy.check(" " + text, 1, text.length()));
}
}
@@ -0,0 +1,44 @@
package com.intellij.psi.formatter;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.assertSame;
/**
* @author Denis Zhdanov
* @since 09/20/2010
*/
public class StaticWhiteSpaceDefinitionStrategyTest {
private StaticWhiteSpaceDefinitionStrategy myStrategy;
@Before
public void setUp() {
myStrategy = new StaticWhiteSpaceDefinitionStrategy('a', 'b', 'c');
}
@Test
public void failOnTheFirstSymbol() {
assertSame(0, myStrategy.check("def", 0, 2));
assertSame(1, myStrategy.check("defghi", 1, 2));
}
@Test
public void failInTheMiddle() {
assertSame(1, myStrategy.check("adef", 0, 3));
assertSame(2, myStrategy.check("daefghi", 1, 3));
}
@Test
public void failOnTheLastSymbol() {
assertSame(2, myStrategy.check("abe", 0, 3));
assertSame(3, myStrategy.check("dabefghi", 1, 4));
}
@Test
public void successfulMatch() {
assertSame(3, myStrategy.check("abc", 0, 3));
assertSame(4, myStrategy.check("dabcefg", 1, 4));
}
}
@@ -280,6 +280,8 @@
beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.lineWrapStrategy"
beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="lang.whiteSpaceFormattingStrategy"
beanClass="com.intellij.lang.LanguageExtensionPoint"/>
<extensionPoint name="gotoDeclarationHandler"
interface="com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler"/>