| /* |
| * Copyright 2000-2014 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.impl.source.codeStyle; |
| |
| import com.intellij.formatting.*; |
| import com.intellij.injected.editor.DocumentWindow; |
| import com.intellij.lang.*; |
| import com.intellij.lang.injection.InjectedLanguageManager; |
| import com.intellij.openapi.application.ApplicationManager; |
| import com.intellij.openapi.diagnostic.Logger; |
| import com.intellij.openapi.editor.*; |
| import com.intellij.openapi.extensions.Extensions; |
| import com.intellij.openapi.fileTypes.FileType; |
| import com.intellij.openapi.project.Project; |
| import com.intellij.openapi.util.Computable; |
| import com.intellij.openapi.util.Pair; |
| import com.intellij.openapi.util.TextRange; |
| import com.intellij.psi.*; |
| import com.intellij.psi.codeStyle.CodeStyleManager; |
| import com.intellij.psi.codeStyle.CodeStyleSettings; |
| import com.intellij.psi.codeStyle.CodeStyleSettingsManager; |
| import com.intellij.psi.codeStyle.Indent; |
| import com.intellij.psi.formatter.FormatterUtil; |
| import com.intellij.psi.impl.CheckUtil; |
| import com.intellij.psi.impl.source.PostprocessReformattingAspect; |
| import com.intellij.psi.impl.source.SourceTreeToPsiMap; |
| import com.intellij.psi.impl.source.tree.FileElement; |
| import com.intellij.psi.impl.source.tree.RecursiveTreeElementWalkingVisitor; |
| import com.intellij.psi.impl.source.tree.TreeElement; |
| import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil; |
| import com.intellij.psi.util.PsiUtilBase; |
| import com.intellij.util.CharTable; |
| import com.intellij.util.IncorrectOperationException; |
| import com.intellij.util.ThrowableRunnable; |
| import com.intellij.util.text.CharArrayUtil; |
| import org.jetbrains.annotations.NonNls; |
| import org.jetbrains.annotations.NotNull; |
| import org.jetbrains.annotations.Nullable; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.concurrent.TimeUnit; |
| |
| public class CodeStyleManagerImpl extends CodeStyleManager { |
| private static final Logger LOG = Logger.getInstance(CodeStyleManagerImpl.class); |
| private static final ThreadLocal<ProcessingUnderProgressInfo> SEQUENTIAL_PROCESSING_ALLOWED |
| = new ThreadLocal<ProcessingUnderProgressInfo>() |
| { |
| @Override |
| protected ProcessingUnderProgressInfo initialValue() { |
| return new ProcessingUnderProgressInfo(); |
| } |
| }; |
| |
| private final FormatterTagHandler myTagHandler; |
| |
| private final Project myProject; |
| @NonNls private static final String DUMMY_IDENTIFIER = "xxx"; |
| |
| public CodeStyleManagerImpl(Project project) { |
| myProject = project; |
| myTagHandler = new FormatterTagHandler(getSettings()); |
| } |
| |
| @Override |
| @NotNull |
| public Project getProject() { |
| return myProject; |
| } |
| |
| @Override |
| @NotNull |
| public PsiElement reformat(@NotNull PsiElement element) throws IncorrectOperationException { |
| return reformat(element, false); |
| } |
| |
| @Override |
| @NotNull |
| public PsiElement reformat(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly) throws IncorrectOperationException { |
| CheckUtil.checkWritable(element); |
| if( !SourceTreeToPsiMap.hasTreeElement( element ) ) |
| { |
| return element; |
| } |
| |
| ASTNode treeElement = SourceTreeToPsiMap.psiElementToTree(element); |
| final PsiElement formatted = SourceTreeToPsiMap.treeElementToPsi(new CodeFormatterFacade(getSettings(), element.getLanguage()).processElement(treeElement)); |
| if (!canChangeWhiteSpacesOnly) { |
| return postProcessElement(formatted); |
| } |
| return formatted; |
| } |
| |
| private PsiElement postProcessElement(@NotNull final PsiElement formatted) { |
| PsiElement result = formatted; |
| if (getSettings().FORMATTER_TAGS_ENABLED && formatted instanceof PsiFile) { |
| postProcessEnabledRanges((PsiFile) formatted, formatted.getTextRange(), getSettings()); |
| } |
| else { |
| for (PostFormatProcessor postFormatProcessor : Extensions.getExtensions(PostFormatProcessor.EP_NAME)) { |
| result = postFormatProcessor.processElement(result, getSettings()); |
| } |
| } |
| return result; |
| } |
| |
| private void postProcessText(@NotNull final PsiFile file, @NotNull final TextRange textRange) { |
| if (!getSettings().FORMATTER_TAGS_ENABLED) { |
| TextRange currentRange = textRange; |
| for (final PostFormatProcessor myPostFormatProcessor : Extensions.getExtensions(PostFormatProcessor.EP_NAME)) { |
| currentRange = myPostFormatProcessor.processText(file, currentRange, getSettings()); |
| } |
| } |
| else { |
| postProcessEnabledRanges(file, textRange, getSettings()); |
| } |
| } |
| |
| @Override |
| public PsiElement reformatRange(@NotNull PsiElement element, |
| int startOffset, |
| int endOffset, |
| boolean canChangeWhiteSpacesOnly) throws IncorrectOperationException { |
| return reformatRangeImpl(element, startOffset, endOffset, canChangeWhiteSpacesOnly); |
| } |
| |
| @Override |
| public PsiElement reformatRange(@NotNull PsiElement element, int startOffset, int endOffset) |
| throws IncorrectOperationException { |
| return reformatRangeImpl(element, startOffset, endOffset, false); |
| |
| } |
| |
| private static void transformAllChildren(final ASTNode file) { |
| ((TreeElement)file).acceptTree(new RecursiveTreeElementWalkingVisitor() { |
| }); |
| } |
| |
| |
| @Override |
| public void reformatText(@NotNull PsiFile file, int startOffset, int endOffset) throws IncorrectOperationException { |
| reformatText(file, Collections.singleton(new TextRange(startOffset, endOffset))); |
| } |
| |
| @Override |
| public void reformatText(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges) |
| throws IncorrectOperationException { |
| reformatText(file, ranges, null); |
| } |
| |
| public void reformatText(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges, @Nullable Editor editor) throws IncorrectOperationException { |
| if (ranges.isEmpty()) { |
| return; |
| } |
| ApplicationManager.getApplication().assertWriteAccessAllowed(); |
| PsiDocumentManager.getInstance(getProject()).commitAllDocuments(); |
| |
| CheckUtil.checkWritable(file); |
| if (!SourceTreeToPsiMap.hasTreeElement(file)) { |
| return; |
| } |
| |
| ASTNode treeElement = SourceTreeToPsiMap.psiElementToTree(file); |
| transformAllChildren(treeElement); |
| |
| final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(), file.getLanguage()); |
| LOG.assertTrue(file.isValid(), "File name: " + file.getName() + " , class: " + file.getClass().getSimpleName()); |
| |
| if (editor == null) { |
| editor = PsiUtilBase.findEditor(file); |
| } |
| |
| CaretPositionKeeper caretKeeper = null; |
| if (editor != null) { |
| caretKeeper = new CaretPositionKeeper(editor); |
| } |
| |
| Collection<TextRange> correctedRanges = FormatterUtil.isFormatterCalledExplicitly() |
| ? removeEndingWhiteSpaceFromEachRange(file, ranges) |
| : ranges; |
| |
| final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(getProject()); |
| List<RangeFormatInfo> infos = new ArrayList<RangeFormatInfo>(); |
| for (TextRange range : correctedRanges) { |
| final PsiElement start = findElementInTreeWithFormatterEnabled(file, range.getStartOffset()); |
| final PsiElement end = findElementInTreeWithFormatterEnabled(file, range.getEndOffset()); |
| if (start != null && !start.isValid()) { |
| LOG.error("start=" + start + "; file=" + file); |
| } |
| if (end != null && !end.isValid()) { |
| LOG.error("end=" + start + "; end=" + file); |
| } |
| boolean formatFromStart = range.getStartOffset() == 0; |
| boolean formatToEnd = range.getEndOffset() == file.getTextLength(); |
| infos.add(new RangeFormatInfo( |
| start == null ? null : smartPointerManager.createSmartPsiElementPointer(start), |
| end == null ? null : smartPointerManager.createSmartPsiElementPointer(end), |
| formatFromStart, |
| formatToEnd |
| )); |
| } |
| |
| FormatTextRanges formatRanges = new FormatTextRanges(); |
| for (TextRange range : correctedRanges) { |
| formatRanges.add(range, true); |
| } |
| codeFormatter.processText(file, formatRanges, true); |
| for (RangeFormatInfo info : infos) { |
| final PsiElement startElement = info.startPointer == null ? null : info.startPointer.getElement(); |
| final PsiElement endElement = info.endPointer == null ? null : info.endPointer.getElement(); |
| if ((startElement != null || info.fromStart) && (endElement != null || info.toEnd)) { |
| postProcessText(file, new TextRange(info.fromStart ? 0 : startElement.getTextRange().getStartOffset(), |
| info.toEnd ? file.getTextLength() : endElement.getTextRange().getEndOffset())); |
| } |
| if (info.startPointer != null) smartPointerManager.removePointer(info.startPointer); |
| if (info.endPointer != null) smartPointerManager.removePointer(info.endPointer); |
| } |
| |
| if (caretKeeper != null) { |
| caretKeeper.restoreCaretPosition(); |
| } |
| } |
| |
| @NotNull |
| private Collection<TextRange> removeEndingWhiteSpaceFromEachRange(@NotNull PsiFile file, @NotNull Collection<TextRange> ranges) { |
| Collection<TextRange> result = new ArrayList<TextRange>(); |
| |
| for (TextRange range : ranges) { |
| int rangeStart = range.getStartOffset(); |
| int rangeEnd = range.getEndOffset(); |
| |
| PsiElement lastElementInRange = findElementInTreeWithFormatterEnabled(file, range.getEndOffset()); |
| if (lastElementInRange instanceof PsiWhiteSpace |
| && rangeStart < lastElementInRange.getTextRange().getStartOffset()) |
| { |
| PsiElement prev = lastElementInRange.getPrevSibling(); |
| if (prev != null) { |
| rangeEnd = prev.getTextRange().getEndOffset(); |
| } |
| } |
| |
| result.add(new TextRange(rangeStart, rangeEnd)); |
| } |
| |
| return result; |
| } |
| |
| private PsiElement reformatRangeImpl(final PsiElement element, |
| final int startOffset, |
| final int endOffset, |
| boolean canChangeWhiteSpacesOnly) throws IncorrectOperationException { |
| LOG.assertTrue(element.isValid()); |
| CheckUtil.checkWritable(element); |
| if( !SourceTreeToPsiMap.hasTreeElement( element ) ) |
| { |
| return element; |
| } |
| |
| ASTNode treeElement = SourceTreeToPsiMap.psiElementToTree(element); |
| final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(), element.getLanguage()); |
| final PsiElement formatted = SourceTreeToPsiMap.treeElementToPsi(codeFormatter.processRange(treeElement, startOffset, endOffset)); |
| |
| return canChangeWhiteSpacesOnly ? formatted : postProcessElement(formatted); |
| } |
| |
| |
| @Override |
| public void reformatNewlyAddedElement(@NotNull final ASTNode parent, @NotNull final ASTNode addedElement) throws IncorrectOperationException { |
| |
| LOG.assertTrue(addedElement.getTreeParent() == parent, "addedElement must be added to parent"); |
| |
| final PsiElement psiElement = parent.getPsi(); |
| |
| PsiFile containingFile = psiElement.getContainingFile(); |
| final FileViewProvider fileViewProvider = containingFile.getViewProvider(); |
| if (fileViewProvider instanceof MultiplePsiFilesPerDocumentFileViewProvider) { |
| containingFile = fileViewProvider.getPsi(fileViewProvider.getBaseLanguage()); |
| } |
| |
| TextRange textRange = addedElement.getTextRange(); |
| final Document document = fileViewProvider.getDocument(); |
| if (document instanceof DocumentWindow) { |
| containingFile = InjectedLanguageManager.getInstance(containingFile.getProject()).getTopLevelFile(containingFile); |
| textRange = ((DocumentWindow)document).injectedToHost(textRange); |
| } |
| |
| final FormattingModelBuilder builder = LanguageFormatting.INSTANCE.forContext(containingFile); |
| if (builder != null) { |
| final FormattingModel model = CoreFormatterUtil.buildModel(builder, containingFile, getSettings(), FormattingMode.REFORMAT); |
| FormatterEx.getInstanceEx().formatAroundRange(model, getSettings(), textRange, containingFile.getFileType()); |
| } |
| |
| adjustLineIndent(containingFile, textRange); |
| } |
| |
| @Override |
| public int adjustLineIndent(@NotNull final PsiFile file, final int offset) throws IncorrectOperationException { |
| return PostprocessReformattingAspect.getInstance(file.getProject()).disablePostprocessFormattingInside(new Computable<Integer>() { |
| @Override |
| public Integer compute() { |
| return doAdjustLineIndentByOffset(file, offset); |
| } |
| }); |
| } |
| |
| @Nullable |
| static PsiElement findElementInTreeWithFormatterEnabled(final PsiFile file, final int offset) { |
| final PsiElement bottomost = file.findElementAt(offset); |
| if (bottomost != null && LanguageFormatting.INSTANCE.forContext(bottomost) != null){ |
| return bottomost; |
| } |
| |
| final Language fileLang = file.getLanguage(); |
| if (fileLang instanceof CompositeLanguage) { |
| return file.getViewProvider().findElementAt(offset, fileLang); |
| } |
| |
| return bottomost; |
| } |
| |
| @Override |
| public int adjustLineIndent(@NotNull final Document document, final int offset) { |
| return PostprocessReformattingAspect.getInstance(getProject()).disablePostprocessFormattingInside(new Computable<Integer>() { |
| @Override |
| public Integer compute() { |
| final PsiDocumentManager documentManager = PsiDocumentManager.getInstance(myProject); |
| documentManager.commitDocument(document); |
| |
| PsiFile file = documentManager.getPsiFile(document); |
| if (file == null) return offset; |
| |
| return doAdjustLineIndentByOffset(file, offset); |
| } |
| }); |
| } |
| |
| private int doAdjustLineIndentByOffset(@NotNull PsiFile file, int offset) { |
| return new CodeStyleManagerRunnable<Integer>(this, FormattingMode.ADJUST_INDENT) { |
| @Override |
| protected Integer doPerform(int offset, TextRange range) { |
| return FormatterEx.getInstanceEx().adjustLineIndent(myModel, mySettings, myIndentOptions, offset, mySignificantRange); |
| } |
| |
| @Override |
| protected Integer computeValueInsidePlainComment(PsiFile file, int offset, Integer defaultValue) { |
| return CharArrayUtil.shiftForward(file.getViewProvider().getContents(), offset, " \t"); |
| } |
| |
| @Override |
| protected Integer adjustResultForInjected(Integer result, DocumentWindow documentWindow) { |
| return documentWindow.hostToInjected(result); |
| } |
| }.perform(file, offset, null, offset); |
| } |
| |
| @Override |
| public void adjustLineIndent(@NotNull PsiFile file, TextRange rangeToAdjust) throws IncorrectOperationException { |
| new CodeStyleManagerRunnable<Object>(this, FormattingMode.ADJUST_INDENT) { |
| @Override |
| protected Object doPerform(int offset, TextRange range) { |
| FormatterEx.getInstanceEx().adjustLineIndentsForRange(myModel, mySettings, myIndentOptions, range); |
| return null; |
| } |
| }.perform(file, -1, rangeToAdjust, null); |
| } |
| |
| @Override |
| @Nullable |
| public String getLineIndent(@NotNull PsiFile file, int offset) { |
| return new CodeStyleManagerRunnable<String>(this, FormattingMode.ADJUST_INDENT) { |
| @Override |
| protected boolean useDocumentBaseFormattingModel() { |
| return false; |
| } |
| |
| @Override |
| protected String doPerform(int offset, TextRange range) { |
| return FormatterEx.getInstanceEx().getLineIndent(myModel, mySettings, myIndentOptions, offset, mySignificantRange); |
| } |
| }.perform(file, offset, null, null); |
| } |
| |
| @Override |
| @Nullable |
| public String getLineIndent(@NotNull Document document, int offset) { |
| PsiFile file = PsiDocumentManager.getInstance(myProject).getPsiFile(document); |
| if (file == null) return ""; |
| |
| return getLineIndent(file, offset); |
| } |
| |
| @Override |
| public boolean isLineToBeIndented(@NotNull PsiFile file, int offset) { |
| if (!SourceTreeToPsiMap.hasTreeElement(file)) { |
| return false; |
| } |
| CharSequence chars = file.getViewProvider().getContents(); |
| int start = CharArrayUtil.shiftBackward(chars, offset - 1, " \t"); |
| if (start > 0 && chars.charAt(start) != '\n' && chars.charAt(start) != '\r') { |
| return false; |
| } |
| int end = CharArrayUtil.shiftForward(chars, offset, " \t"); |
| if (end >= chars.length()) { |
| return false; |
| } |
| ASTNode element = SourceTreeToPsiMap.psiElementToTree(findElementInTreeWithFormatterEnabled(file, end)); |
| if (element == null) { |
| return false; |
| } |
| if (element.getElementType() == TokenType.WHITE_SPACE) { |
| return false; |
| } |
| if (element.getElementType() == PlainTextTokenTypes.PLAIN_TEXT) { |
| return false; |
| } |
| /* |
| if( element.getElementType() instanceof IJspElementType ) |
| { |
| return false; |
| } |
| */ |
| if (getSettings().KEEP_FIRST_COLUMN_COMMENT && isCommentToken(element)) { |
| if (IndentHelper.getInstance().getIndent(myProject, file.getFileType(), element, true) == 0) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private static boolean isCommentToken(final ASTNode element) { |
| final Language language = element.getElementType().getLanguage(); |
| final Commenter commenter = LanguageCommenters.INSTANCE.forLanguage(language); |
| if (commenter instanceof CodeDocumentationAwareCommenter) { |
| final CodeDocumentationAwareCommenter documentationAwareCommenter = (CodeDocumentationAwareCommenter)commenter; |
| return element.getElementType() == documentationAwareCommenter.getBlockCommentTokenType() || |
| element.getElementType() == documentationAwareCommenter.getLineCommentTokenType(); |
| } |
| return false; |
| } |
| |
| private static boolean isWhiteSpaceSymbol(char c) { |
| return c == ' ' || c == '\t' || c == '\n'; |
| } |
| |
| /** |
| * Formatter trims line that contains white spaces symbols only, however, there is a possible case that we want |
| * to preserve them for particular line (e.g. for live template that defines blank line that contains $END$ marker). |
| * <p/> |
| * Current approach is to do the following: |
| * <pre> |
| * <ol> |
| * <li>Insert dummy text at the end of the blank line which white space symbols should be preserved;</li> |
| * <li>Perform formatting;</li> |
| * <li>Remove dummy text;</li> |
| * </ol> |
| * </pre> |
| * <p/> |
| * This method inserts that dummy comment (fallback to identifier <code>xxx</code>, see {@link CodeStyleManagerImpl#createDummy(PsiFile)}) |
| * if necessary (if target line contains white space symbols only). |
| * <p/> |
| |
| * <b>Note:</b> it's expected that the whole white space region that contains given offset is processed in a way that all |
| * {@link RangeMarker range markers} registered for the given offset are expanded to the whole white space region. |
| * E.g. there is a possible case that particular range marker serves for defining formatting range, hence, its start/end offsets |
| * are updated correspondingly after current method call and whole white space region is reformatted. |
| * |
| * @param file target PSI file |
| * @param document target document |
| * @param offset offset that defines end boundary of the target line text fragment (start boundary is the first line's symbol) |
| * @return text range that points to the newly inserted dummy text if any; <code>null</code> otherwise |
| * @throws IncorrectOperationException if given file is read-only |
| */ |
| @Nullable |
| public static TextRange insertNewLineIndentMarker(@NotNull PsiFile file, @NotNull Document document, int offset) { |
| CharSequence text = document.getCharsSequence(); |
| if (offset < 0 || offset >= text.length() || !isWhiteSpaceSymbol(text.charAt(offset))) { |
| return null; |
| } |
| |
| for (int i = offset - 1; i >= 0; i--) { |
| char c = text.charAt(i); |
| // We don't want to insert a marker if target line is not blank (doesn't consist from white space symbols only). |
| if (c == '\n') { |
| break; |
| } |
| if (!isWhiteSpaceSymbol(c)) { |
| return null; |
| } |
| } |
| |
| int end = offset; |
| for (; end < text.length(); end++) { |
| if (!isWhiteSpaceSymbol(text.charAt(end))) { |
| break; |
| } |
| } |
| |
| setSequentialProcessingAllowed(false); |
| String dummy = createDummy(file); |
| document.insertString(offset, dummy); |
| return new TextRange(offset, offset + dummy.length()); |
| } |
| |
| @NotNull |
| private static String createDummy(@NotNull PsiFile file) { |
| Language language = file.getLanguage(); |
| PsiComment comment = null; |
| try { |
| comment = PsiParserFacade.SERVICE.getInstance(file.getProject()).createLineOrBlockCommentFromText(language, ""); |
| } |
| catch (Throwable ignored) { |
| } |
| String text = comment != null ? comment.getText() : null; |
| return text != null ? text : DUMMY_IDENTIFIER; |
| } |
| |
| /** |
| * Allows to check if given offset points to white space element within the given PSI file and return that white space |
| * element in the case of positive answer. |
| * |
| * @param file target file |
| * @param offset offset that might point to white space element within the given PSI file |
| * @return target white space element for the given offset within the given file (if any); <code>null</code> otherwise |
| */ |
| @Nullable |
| public static PsiElement findWhiteSpaceNode(@NotNull PsiFile file, int offset) { |
| return doFindWhiteSpaceNode(file, offset).first; |
| } |
| |
| @NotNull |
| private static Pair<PsiElement, CharTable> doFindWhiteSpaceNode(@NotNull PsiFile file, int offset) { |
| ASTNode astNode = SourceTreeToPsiMap.psiElementToTree(file); |
| if (!(astNode instanceof FileElement)) { |
| return new Pair<PsiElement, CharTable>(null, null); |
| } |
| PsiElement elementAt = InjectedLanguageUtil.findInjectedElementNoCommit(file, offset); |
| final CharTable charTable = ((FileElement)astNode).getCharTable(); |
| if (elementAt == null) { |
| elementAt = findElementInTreeWithFormatterEnabled(file, offset); |
| } |
| |
| if( elementAt == null) { |
| return new Pair<PsiElement, CharTable>(null, charTable); |
| } |
| ASTNode node = elementAt.getNode(); |
| if (node == null || node.getElementType() != TokenType.WHITE_SPACE) { |
| return new Pair<PsiElement, CharTable>(null, charTable); |
| } |
| return Pair.create(elementAt, charTable); |
| } |
| |
| @Override |
| public Indent getIndent(String text, FileType fileType) { |
| int indent = IndentHelperImpl.getIndent(myProject, fileType, text, true); |
| int indenLevel = indent / IndentHelperImpl.INDENT_FACTOR; |
| int spaceCount = indent - indenLevel * IndentHelperImpl.INDENT_FACTOR; |
| return new IndentImpl(getSettings(), indenLevel, spaceCount, fileType); |
| } |
| |
| @Override |
| public String fillIndent(Indent indent, FileType fileType) { |
| IndentImpl indent1 = (IndentImpl)indent; |
| int indentLevel = indent1.getIndentLevel(); |
| int spaceCount = indent1.getSpaceCount(); |
| if (indentLevel < 0) { |
| spaceCount += indentLevel * getSettings().getIndentSize(fileType); |
| indentLevel = 0; |
| if (spaceCount < 0) { |
| spaceCount = 0; |
| } |
| } |
| else { |
| if (spaceCount < 0) { |
| int v = (-spaceCount + getSettings().getIndentSize(fileType) - 1) / getSettings().getIndentSize(fileType); |
| indentLevel -= v; |
| spaceCount += v * getSettings().getIndentSize(fileType); |
| if (indentLevel < 0) { |
| indentLevel = 0; |
| } |
| } |
| } |
| return IndentHelperImpl.fillIndent(myProject, fileType, indentLevel * IndentHelperImpl.INDENT_FACTOR + spaceCount); |
| } |
| |
| @Override |
| public Indent zeroIndent() { |
| return new IndentImpl(getSettings(), 0, 0, null); |
| } |
| |
| |
| @NotNull |
| private CodeStyleSettings getSettings() { |
| return CodeStyleSettingsManager.getSettings(myProject); |
| } |
| |
| @Override |
| public boolean isSequentialProcessingAllowed() { |
| return SEQUENTIAL_PROCESSING_ALLOWED.get().isAllowed(); |
| } |
| |
| /** |
| * Allows to define if {@link #isSequentialProcessingAllowed() sequential processing} should be allowed. |
| * <p/> |
| * Current approach is not allow to stop sequential processing for more than predefine amount of time (couple of seconds). |
| * That means that call to this method with <code>'true'</code> argument is not mandatory for successful processing even |
| * if this method is called with <code>'false'</code> argument before. |
| * |
| * @param allowed flag that defines if {@link #isSequentialProcessingAllowed() sequential processing} should be allowed |
| */ |
| public static void setSequentialProcessingAllowed(boolean allowed) { |
| ProcessingUnderProgressInfo info = SEQUENTIAL_PROCESSING_ALLOWED.get(); |
| if (allowed) { |
| info.decrement(); |
| } |
| else { |
| info.increment(); |
| } |
| } |
| |
| private static class ProcessingUnderProgressInfo { |
| |
| private static final long DURATION_TIME = TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS); |
| |
| private int myCount; |
| private long myEndTime; |
| |
| public void increment() { |
| if (myCount > 0 && System.currentTimeMillis() > myEndTime) { |
| myCount = 0; |
| } |
| myCount++; |
| myEndTime = System.currentTimeMillis() + DURATION_TIME; |
| } |
| |
| public void decrement() { |
| if (myCount <= 0) { |
| return; |
| } |
| myCount--; |
| } |
| |
| public boolean isAllowed() { |
| return myCount <= 0 || System.currentTimeMillis() >= myEndTime; |
| } |
| } |
| |
| @Override |
| public void performActionWithFormatterDisabled(final Runnable r) { |
| performActionWithFormatterDisabled(new Computable<Object>() { |
| @Override |
| public Object compute() { |
| r.run(); |
| return null; |
| } |
| }); |
| } |
| |
| @Override |
| public <T extends Throwable> void performActionWithFormatterDisabled(final ThrowableRunnable<T> r) throws T { |
| final Throwable[] throwable = new Throwable[1]; |
| |
| performActionWithFormatterDisabled(new Computable<Object>() { |
| @Override |
| public Object compute() { |
| try { |
| r.run(); |
| } |
| catch (Throwable t) { |
| throwable[0] = t; |
| } |
| return null; |
| } |
| }); |
| |
| if (throwable[0] != null) { |
| //noinspection unchecked |
| throw (T)throwable[0]; |
| } |
| } |
| |
| @Override |
| public <T> T performActionWithFormatterDisabled(final Computable<T> r) { |
| return ((FormatterImpl)FormatterEx.getInstance()).runWithFormattingDisabled(new Computable<T>() { |
| @Override |
| public T compute() { |
| final PostprocessReformattingAspect component = PostprocessReformattingAspect.getInstance(getProject()); |
| return component.disablePostprocessFormattingInside(r); |
| } |
| }); |
| } |
| |
| private static class RangeFormatInfo{ |
| private final SmartPsiElementPointer startPointer; |
| private final SmartPsiElementPointer endPointer; |
| private final boolean fromStart; |
| private final boolean toEnd; |
| |
| RangeFormatInfo(@Nullable SmartPsiElementPointer startPointer, |
| @Nullable SmartPsiElementPointer endPointer, |
| boolean fromStart, |
| boolean toEnd) |
| { |
| this.startPointer = startPointer; |
| this.endPointer = endPointer; |
| this.fromStart = fromStart; |
| this.toEnd = toEnd; |
| } |
| } |
| |
| // There is a possible case that cursor is located at the end of the line that contains only white spaces. For example: |
| // public void foo() { |
| // <caret> |
| // } |
| // Formatter removes such white spaces, i.e. keeps only line feed symbol. But we want to preserve caret position then. |
| // So, if 'virtual space in editor' is enabled, we save target visual column. Caret indent is ensured otherwise |
| private static class CaretPositionKeeper { |
| Editor myEditor; |
| Document myDocument; |
| CaretModel myCaretModel; |
| RangeMarker myBeforeCaretRangeMarker; |
| String myCaretIndentToRestore; |
| int myVisualColumnToRestore = -1; |
| |
| CaretPositionKeeper(@NotNull Editor editor) { |
| myEditor = editor; |
| myCaretModel = editor.getCaretModel(); |
| myDocument = editor.getDocument(); |
| |
| int caretOffset = getCaretOffset(); |
| int lineStartOffset = getLineStartOffsetByTotalOffset(caretOffset); |
| int lineEndOffset = getLineEndOffsetByTotalOffset(caretOffset); |
| boolean shouldFixCaretPosition = rangeHasWhiteSpaceSymbolsOnly(myDocument.getCharsSequence(), lineStartOffset, lineEndOffset); |
| |
| if (shouldFixCaretPosition) { |
| initRestoreInfo(caretOffset); |
| } |
| } |
| |
| private void initRestoreInfo(int caretOffset) { |
| int lineStartOffset = getLineStartOffsetByTotalOffset(caretOffset); |
| |
| myVisualColumnToRestore = myCaretModel.getVisualPosition().column; |
| myCaretIndentToRestore = myDocument.getText(TextRange.create(lineStartOffset, caretOffset)); |
| myBeforeCaretRangeMarker = myDocument.createRangeMarker(0, lineStartOffset); |
| } |
| |
| public void restoreCaretPosition() { |
| if (isVirtualSpaceEnabled()) { |
| restoreVisualPosition(); |
| } |
| else { |
| restorePositionByIndentInsertion(); |
| } |
| } |
| |
| private void restorePositionByIndentInsertion() { |
| if (myBeforeCaretRangeMarker == null || !myBeforeCaretRangeMarker.isValid() || myCaretIndentToRestore == null) { |
| return; |
| } |
| int newCaretLineStartOffset = myBeforeCaretRangeMarker.getEndOffset(); |
| myBeforeCaretRangeMarker.dispose(); |
| if (myCaretModel.getVisualPosition().column == myVisualColumnToRestore) { |
| return; |
| } |
| insertWhiteSpaceIndentIfNeeded(newCaretLineStartOffset); |
| } |
| |
| private void restoreVisualPosition() { |
| if (myVisualColumnToRestore < 0) { |
| myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE); |
| return; |
| } |
| VisualPosition position = myCaretModel.getVisualPosition(); |
| if (myVisualColumnToRestore != position.column) { |
| myCaretModel.moveToVisualPosition(new VisualPosition(position.line, myVisualColumnToRestore)); |
| } |
| } |
| |
| private void insertWhiteSpaceIndentIfNeeded(int caretLineOffset) { |
| int lineToInsertIndent = myDocument.getLineNumber(caretLineOffset); |
| if (!lineContainsWhiteSpaceSymbolsOnly(lineToInsertIndent)) |
| return; |
| |
| int lineToInsertStartOffset = myDocument.getLineStartOffset(lineToInsertIndent); |
| |
| if (lineToInsertIndent != getCurrentCaretLine()) { |
| myCaretModel.moveToOffset(lineToInsertStartOffset); |
| } |
| myDocument.replaceString(lineToInsertStartOffset, caretLineOffset, myCaretIndentToRestore); |
| } |
| |
| private boolean rangeHasWhiteSpaceSymbolsOnly(CharSequence text, int lineStartOffset, int lineEndOffset) { |
| for (int i = lineStartOffset; i < lineEndOffset; i++) { |
| char c = text.charAt(i); |
| if (c != ' ' && c != '\t' && c != '\n') { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private boolean isVirtualSpaceEnabled() { |
| return myEditor.getSettings().isVirtualSpace(); |
| } |
| |
| private int getLineStartOffsetByTotalOffset(int offset) { |
| int line = myDocument.getLineNumber(offset); |
| return myDocument.getLineStartOffset(line); |
| } |
| |
| private int getLineEndOffsetByTotalOffset(int offset) { |
| int line = myDocument.getLineNumber(offset); |
| return myDocument.getLineEndOffset(line); |
| } |
| |
| private int getCaretOffset() { |
| int caretOffset = myCaretModel.getOffset(); |
| caretOffset = Math.max(Math.min(caretOffset, myDocument.getTextLength() - 1), 0); |
| return caretOffset; |
| } |
| |
| private boolean lineContainsWhiteSpaceSymbolsOnly(int lineNumber) { |
| int startOffset = myDocument.getLineStartOffset(lineNumber); |
| int endOffset = myDocument.getLineEndOffset(lineNumber); |
| return rangeHasWhiteSpaceSymbolsOnly(myDocument.getCharsSequence(), startOffset, endOffset); |
| } |
| |
| private int getCurrentCaretLine() { |
| return myDocument.getLineNumber(myCaretModel.getOffset()); |
| } |
| } |
| |
| private TextRange postProcessEnabledRanges(@NotNull final PsiFile file, @NotNull TextRange range, CodeStyleSettings settings) { |
| TextRange result = TextRange.create(range.getStartOffset(), range.getEndOffset()); |
| List<TextRange> enabledRanges = myTagHandler.getEnabledRanges(file.getNode(), result); |
| int delta = 0; |
| for (TextRange enabledRange : enabledRanges) { |
| enabledRange = enabledRange.shiftRight(delta); |
| for (PostFormatProcessor processor : Extensions.getExtensions(PostFormatProcessor.EP_NAME)) { |
| TextRange processedRange = processor.processText(file, enabledRange, settings); |
| delta += processedRange.getLength() - enabledRange.getLength(); |
| } |
| } |
| result = result.grown(delta); |
| return result; |
| } |
| } |