diff --git a/platform/code-style-api/src/com/intellij/formatting/FormattingRangesInfo.java b/platform/code-style-api/src/com/intellij/formatting/FormattingRangesInfo.java new file mode 100644 index 000000000000..96d1f250283f --- /dev/null +++ b/platform/code-style-api/src/com/intellij/formatting/FormattingRangesInfo.java @@ -0,0 +1,18 @@ +// Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.intellij.formatting; + +import com.intellij.openapi.util.TextRange; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public interface FormattingRangesInfo { + boolean isWhitespaceReadOnly(@NotNull TextRange range); + + boolean isReadOnly(@NotNull TextRange range); + + boolean isOnInsertedLine(int offset); + + @NotNull + List getTextRanges(); +} diff --git a/platform/code-style-api/src/com/intellij/formatting/service/FormattingService.java b/platform/code-style-api/src/com/intellij/formatting/service/FormattingService.java index c24fedd4196f..a42f335ebfb7 100644 --- a/platform/code-style-api/src/com/intellij/formatting/service/FormattingService.java +++ b/platform/code-style-api/src/com/intellij/formatting/service/FormattingService.java @@ -1,7 +1,10 @@ // Copyright 2000-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.formatting.service; +import com.intellij.formatting.FormattingRangesInfo; import com.intellij.openapi.extensions.ExtensionPointName; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; @@ -11,4 +14,10 @@ public interface FormattingService { ExtensionPointName EP_NAME = ExtensionPointName.create("com.intellij.formattingService"); boolean canFormat(@NotNull PsiFile file); + + PsiElement formatElement(@NotNull PsiElement element, boolean canChangeWhiteSpaceOnly); + + PsiElement formatElement(@NotNull PsiElement element, @NotNull TextRange range, boolean canChangeWhiteSpaceOnly); + + void formatRanges(@NotNull PsiFile file, FormattingRangesInfo rangesInfo); } diff --git a/platform/code-style-impl/src/com/intellij/formatting/FormatTextRanges.java b/platform/code-style-impl/src/com/intellij/formatting/FormatTextRanges.java index 2bb314960e8c..f924217297bf 100644 --- a/platform/code-style-impl/src/com/intellij/formatting/FormatTextRanges.java +++ b/platform/code-style-impl/src/com/intellij/formatting/FormatTextRanges.java @@ -61,12 +61,12 @@ public class FormatTextRanges implements FormattingRangesInfo { @Override public boolean isWhitespaceReadOnly(final @NotNull TextRange range) { - return myRanges.stream().allMatch(formatTextRange -> formatTextRange.isWhitespaceReadOnly(range)); + return ContainerUtil.and(myRanges, formatTextRange -> formatTextRange.isWhitespaceReadOnly(range)); } @Override public boolean isReadOnly(@NotNull TextRange range) { - return myRanges.stream().allMatch(formatTextRange -> formatTextRange.isReadOnly(range)); + return ContainerUtil.and(myRanges, formatTextRange -> formatTextRange.isReadOnly(range)); } @Override @@ -105,7 +105,8 @@ public class FormatTextRanges implements FormattingRangesInfo { return myRanges.size() == 1 && file.getTextRange().equals(myRanges.get(0).getTextRange()); } - public List getTextRanges() { + @Override + public @NotNull List getTextRanges() { List ranges = ContainerUtil.map(myRanges, FormatTextRange::getTextRange); ranges.sort(Segment.BY_START_OFFSET_THEN_END_OFFSET); return ranges; diff --git a/platform/code-style-impl/src/com/intellij/formatting/FormattingRangesInfo.java b/platform/code-style-impl/src/com/intellij/formatting/FormattingRangesInfo.java deleted file mode 100644 index 1cca95cc9574..000000000000 --- a/platform/code-style-impl/src/com/intellij/formatting/FormattingRangesInfo.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2000-2016 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.openapi.util.TextRange; -import org.jetbrains.annotations.NotNull; - -public interface FormattingRangesInfo { - boolean isWhitespaceReadOnly(@NotNull TextRange range); - - boolean isReadOnly(@NotNull TextRange range); - - boolean isOnInsertedLine(int offset); -} diff --git a/platform/code-style-impl/src/com/intellij/formatting/service/CoreFormattingService.java b/platform/code-style-impl/src/com/intellij/formatting/service/CoreFormattingService.java index dbd76803a76b..c8d65555744b 100644 --- a/platform/code-style-impl/src/com/intellij/formatting/service/CoreFormattingService.java +++ b/platform/code-style-impl/src/com/intellij/formatting/service/CoreFormattingService.java @@ -3,16 +3,20 @@ package com.intellij.formatting.service; import com.intellij.application.options.CodeStyle; import com.intellij.formatting.FormatTextRanges; +import com.intellij.formatting.FormattingRangesInfo; +import com.intellij.lang.ASTNode; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Document; -import com.intellij.psi.FileViewProvider; -import com.intellij.psi.PsiDocumentManager; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.impl.source.codeStyle.CodeFormatterFacade; -import com.intellij.util.SlowOperations; +import com.intellij.psi.impl.source.codeStyle.CoreCodeStyleUtil; import org.jetbrains.annotations.NotNull; +import java.util.List; +import java.util.function.Consumer; + public final class CoreFormattingService implements FormattingService { private final static Logger LOG =Logger.getInstance(CoreFormattingService.class); @@ -21,4 +25,39 @@ public final class CoreFormattingService implements FormattingService { return true; } + @Override + public PsiElement formatElement(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly) { + ASTNode treeElement = element.getNode(); + PsiFile file = element.getContainingFile(); + final PsiElement formatted = + new CodeFormatterFacade(getSettings(file), element.getLanguage(), canChangeWhiteSpacesOnly) + .processElement(treeElement).getPsi(); + if (!canChangeWhiteSpacesOnly) { + return CoreCodeStyleUtil.postProcessElement(file, formatted); + } + return formatted; + } + + @Override + public PsiElement formatElement(@NotNull PsiElement element, + @NotNull TextRange range, + boolean canChangeWhiteSpacesOnly) { + ASTNode treeElement = element.getNode(); + PsiFile file = element.getContainingFile(); + final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(file), element.getLanguage()); + final PsiElement formatted = codeFormatter.processRange(treeElement, range.getStartOffset(), range.getEndOffset()).getPsi(); + return canChangeWhiteSpacesOnly ? formatted : CoreCodeStyleUtil.postProcessElement(file, formatted); + } + + @Override + public void formatRanges(@NotNull PsiFile file, FormattingRangesInfo rangesInfo) { + List infos = CoreCodeStyleUtil.getRangeFormatInfoList(file, rangesInfo); + final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(file), file.getLanguage()); + codeFormatter.processText(file, (FormatTextRanges)rangesInfo, true); + CoreCodeStyleUtil.postProcessRanges(file, infos, range -> CoreCodeStyleUtil.postProcessText(file, range)); + } + + private static CodeStyleSettings getSettings(@NotNull PsiFile file) { + return CodeStyle.getSettings(file); + } } diff --git a/platform/code-style-impl/src/com/intellij/formatting/service/ExternalFormatProcessorAdapter.java b/platform/code-style-impl/src/com/intellij/formatting/service/ExternalFormatProcessorAdapter.java index 29190e944081..b702af876cfb 100644 --- a/platform/code-style-impl/src/com/intellij/formatting/service/ExternalFormatProcessorAdapter.java +++ b/platform/code-style-impl/src/com/intellij/formatting/service/ExternalFormatProcessorAdapter.java @@ -2,12 +2,17 @@ package com.intellij.formatting.service; import com.intellij.formatting.FormatTextRanges; +import com.intellij.formatting.FormattingRangesInfo; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.ExternalFormatProcessor; import com.intellij.psi.impl.source.codeStyle.CoreCodeStyleUtil; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; +import java.util.List; + /** * To support legacy API */ @@ -18,7 +23,26 @@ public final class ExternalFormatProcessorAdapter implements FormattingService { return ExternalFormatProcessor.useExternalFormatter(file); } + @Override + public PsiElement formatElement(@NotNull PsiElement element, boolean canChangeWhiteSpacesOnly) { + return ExternalFormatProcessor.formatElement(element, element.getTextRange(), canChangeWhiteSpacesOnly); + } + + @Override + public PsiElement formatElement(@NotNull PsiElement element, + @NotNull TextRange range, + boolean canChangeWhiteSpacesOnly) { + return ExternalFormatProcessor.formatElement(element, range, canChangeWhiteSpacesOnly); + } + + @Override + public void formatRanges(@NotNull PsiFile file, FormattingRangesInfo rangesInfo) { + List infos = CoreCodeStyleUtil.getRangeFormatInfoList(file, rangesInfo); + CoreCodeStyleUtil.postProcessRanges( + file, infos, range -> ExternalFormatProcessor.formatRangeInFile(file, range, false, false)); + } + public void formatCollectedRanges(@NotNull PsiFile file, @NotNull FormatTextRanges ranges) { - CoreCodeStyleUtil.formatRanges(file, ranges); + formatRanges(file, ranges); } } diff --git a/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java b/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java index 9be878a8cbd2..3720782d0d31 100644 --- a/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java +++ b/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CodeStyleManagerImpl.java @@ -5,6 +5,7 @@ import com.intellij.CodeStyleBundle; import com.intellij.application.options.CodeStyle; import com.intellij.application.options.codeStyle.cache.CodeStyleCachingService; import com.intellij.formatting.*; +import com.intellij.formatting.service.FormattingServiceUtil; import com.intellij.injected.editor.DocumentWindow; import com.intellij.lang.*; import com.intellij.lang.injection.InjectedLanguageManager; @@ -71,23 +72,12 @@ public class CodeStyleManagerImpl extends CodeStyleManager implements Formatting return element; } - ASTNode treeElement = element.getNode(); final PsiFile file = element.getContainingFile(); if (file == null) return element; - if (ExternalFormatProcessor.useExternalFormatter(file)) { - return ExternalFormatProcessor.formatElement(element, element.getTextRange(), canChangeWhiteSpacesOnly); - } - - final PsiElement formatted = - new CodeFormatterFacade(getSettings(file), element.getLanguage(), canChangeWhiteSpacesOnly) - .processElement(treeElement).getPsi(); - if (!canChangeWhiteSpacesOnly) { - return CoreCodeStyleUtil.postProcessElement(file, formatted); - } - return formatted; + return FormattingServiceUtil.findService(file).formatElement(element, canChangeWhiteSpacesOnly); } @Override @@ -171,7 +161,7 @@ public class CodeStyleManagerImpl extends CodeStyleManager implements Formatting removeEndingWhiteSpaceFromEachRange(file, ranges); } - CoreCodeStyleUtil.formatRanges(file, ranges); + FormattingServiceUtil.findService(file).formatRanges(file, ranges); if (caretKeeper != null) { caretKeeper.restoreCaretPosition(); @@ -215,15 +205,8 @@ public class CodeStyleManagerImpl extends CodeStyleManager implements Formatting return element; } - ASTNode treeElement = element.getNode(); - final PsiFile file = element.getContainingFile(); - if (ExternalFormatProcessor.useExternalFormatter(file)) { - return ExternalFormatProcessor.formatElement(element, TextRange.create(startOffset, endOffset), canChangeWhiteSpacesOnly); - } - - final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(file), element.getLanguage()); - final PsiElement formatted = codeFormatter.processRange(treeElement, startOffset, endOffset).getPsi(); - return canChangeWhiteSpacesOnly ? formatted : CoreCodeStyleUtil.postProcessElement(file, formatted); + return FormattingServiceUtil.findService(element.getContainingFile()) + .formatElement(element,TextRange.create(startOffset, endOffset), canChangeWhiteSpacesOnly); } diff --git a/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CoreCodeStyleUtil.java b/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CoreCodeStyleUtil.java index c33f25161b48..9416f2fe9b7d 100644 --- a/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CoreCodeStyleUtil.java +++ b/platform/code-style-impl/src/com/intellij/psi/impl/source/codeStyle/CoreCodeStyleUtil.java @@ -3,52 +3,41 @@ package com.intellij.psi.impl.source.codeStyle; import com.intellij.application.options.CodeStyle; import com.intellij.diagnostic.PluginException; -import com.intellij.formatting.FormatTextRanges; import com.intellij.formatting.FormatterTagHandler; -import com.intellij.lang.ASTNode; +import com.intellij.formatting.FormattingRangesInfo; import com.intellij.lang.CompositeLanguage; import com.intellij.lang.Language; import com.intellij.lang.LanguageFormatting; -import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.diagnostic.Attachment; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.diagnostic.RuntimeExceptionWithAttachments; -import com.intellij.openapi.editor.Document; -import com.intellij.openapi.editor.RangeMarker; import com.intellij.openapi.progress.ProcessCanceledException; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.*; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.SmartPointerManager; +import com.intellij.psi.SmartPsiElementPointer; import com.intellij.psi.codeStyle.CodeStyleSettings; -import com.intellij.psi.codeStyle.ExternalFormatProcessor; -import com.intellij.psi.impl.source.SourceTreeToPsiMap; -import com.intellij.psi.impl.source.tree.FileElement; -import com.intellij.psi.util.PsiUtilCore; -import com.intellij.util.CharTable; -import com.intellij.util.IncorrectOperationException; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; @ApiStatus.Internal public class CoreCodeStyleUtil { private final static Logger LOG = Logger.getInstance(CoreCodeStyleUtil.class); - @NonNls private static final String DUMMY_IDENTIFIER = "xxx"; - private static final ThreadLocal SEQUENTIAL_PROCESSING_ALLOWED = ThreadLocal.withInitial(() -> new ProcessingUnderProgressInfo()); private CoreCodeStyleUtil() { } - static PsiElement postProcessElement(@NotNull PsiFile file, @NotNull final PsiElement formatted) { + public static PsiElement postProcessElement(@NotNull PsiFile file, @NotNull final PsiElement formatted) { PsiElement result = formatted; CodeStyleSettings settingsForFile = CodeStyle.getSettings(file); if (settingsForFile.FORMATTER_TAGS_ENABLED && formatted instanceof PsiFile) { @@ -76,7 +65,8 @@ public class CoreCodeStyleUtil { return result; } - public static void formatRanges(@NotNull PsiFile file, @NotNull FormatTextRanges ranges) { + + public static List getRangeFormatInfoList(@NotNull PsiFile file, @NotNull FormattingRangesInfo ranges) { final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(file.getProject()); List infos = new ArrayList<>(); @@ -98,31 +88,27 @@ public class CoreCodeStyleUtil { formatToEnd )); } + return infos; + } - if (!ExternalFormatProcessor.useExternalFormatter(file)) { - final CodeFormatterFacade codeFormatter = new CodeFormatterFacade(getSettings(file), file.getLanguage()); - codeFormatter.processText(file, ranges, true); - } - - for (RangeFormatInfo info : infos) { + public static void postProcessRanges(@NotNull PsiFile file, + @NotNull List rangeFormatInfoList, + @NotNull Consumer postProcessFormatter) { + final SmartPointerManager smartPointerManager = SmartPointerManager.getInstance(file.getProject()); + for (RangeFormatInfo info : rangeFormatInfoList) { 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)) { TextRange currRange = new TextRange(info.fromStart ? 0 : startElement.getTextRange().getStartOffset(), info.toEnd ? file.getTextLength() : endElement.getTextRange().getEndOffset()); - if (ExternalFormatProcessor.useExternalFormatter(file)) { - ExternalFormatProcessor.formatRangeInFile(file, currRange, false, false); - } - else { - postProcessText(file, currRange); - } + postProcessFormatter.accept(currRange); } if (info.startPointer != null) smartPointerManager.removePointer(info.startPointer); if (info.endPointer != null) smartPointerManager.removePointer(info.endPointer); } } - private static void postProcessText(@NotNull final PsiFile file, @NotNull final TextRange textRange) { + public static void postProcessText(@NotNull final PsiFile file, @NotNull final TextRange textRange) { if (!getSettings(file).FORMATTER_TAGS_ENABLED) { TextRange currentRange = textRange; for (final PostFormatProcessor myPostFormatProcessor : PostFormatProcessor.EP_NAME.getExtensionList()) { @@ -146,7 +132,7 @@ public class CoreCodeStyleUtil { } } - private static class RangeFormatInfo{ + public static class RangeFormatInfo{ private final SmartPsiElementPointer startPointer; private final SmartPsiElementPointer endPointer; private final boolean fromStart; @@ -165,7 +151,7 @@ public class CoreCodeStyleUtil { } @Nullable - static PsiElement findElementInTreeWithFormatterEnabled(final PsiFile file, final int offset) { + public 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; @@ -179,85 +165,7 @@ public class CoreCodeStyleUtil { return bottomost; } - /** - * 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 line with whitespaces that contains $END$ marker: templateText $END$). - *

- * Current approach is to do the following: - *

-   * 
    - *
  1. Insert dummy text at the end of the blank line which white space symbols should be preserved;
  2. - *
  3. Perform formatting;
  4. - *
  5. Remove dummy text;
  6. - *
- *
- *

- * This method inserts that dummy comment (fallback to identifier {@code xxx}, see {@link #createMarker(PsiFile, int)}) - * if necessary. - *

- * Note: 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} 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.getImmutableCharSequence(); - if (offset <= 0 || offset >= text.length() || !isWhiteSpaceSymbol(text.charAt(offset))) { - return null; - } - if (!isWhiteSpaceSymbol(text.charAt(offset - 1))) { - return null; // no whitespaces before offset - } - - int end = offset; - for (; end < text.length(); end++) { - if (text.charAt(end) == '\n') { - break; // line is empty till the end - } - if (!isWhiteSpaceSymbol(text.charAt(end))) { - return null; - } - } - - String marker = createMarker(file, offset); - document.insertString(offset, marker); - return new TextRange(offset, offset + marker.length()); - } - - private static boolean isWhiteSpaceSymbol(char c) { - return c == ' ' || c == '\t' || c == '\n'; - } - - private static @NotNull String createMarker(@NotNull PsiFile file, int offset) { - Project project = file.getProject(); - PsiElement injectedElement = InjectedLanguageManager.getInstance(project).findInjectedElementAt(file, offset); - Language language = injectedElement != null ? injectedElement.getLanguage() : PsiUtilCore.getLanguageAtOffset(file, offset); - - setSequentialProcessingAllowed(false); - NewLineIndentMarkerProvider markerProvider = NewLineIndentMarkerProvider.EP.forLanguage(language); - String marker = markerProvider == null ? null : markerProvider.createMarker(file, offset); - if (marker != null) { - return marker; - } - - PsiComment comment = null; - try { - comment = PsiParserFacade.SERVICE.getInstance(project).createLineOrBlockCommentFromText(language, ""); - } - catch (Throwable ignored) { - } - String text = comment != null ? comment.getText() : null; - return text != null ? text : DUMMY_IDENTIFIER; - } @ApiStatus.Internal public static void setSequentialProcessingAllowed(boolean allowed) { @@ -305,38 +213,5 @@ public class CoreCodeStyleUtil { return CodeStyle.getSettings(file); } - /** - * 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} otherwise - */ - @Nullable - public static PsiElement findWhiteSpaceNode(@NotNull PsiFile file, int offset) { - return doFindWhiteSpaceNode(file, offset).first; - } - @NotNull - private static Pair doFindWhiteSpaceNode(@NotNull PsiFile file, int offset) { - ASTNode astNode = SourceTreeToPsiMap.psiElementToTree(file); - if (!(astNode instanceof FileElement)) { - return new Pair<>(null, null); - } - PsiElement elementAt = InjectedLanguageManager.getInstance(file.getProject()).findInjectedElementAt(file, offset); - final CharTable charTable = ((FileElement)astNode).getCharTable(); - if (elementAt == null) { - elementAt = findElementInTreeWithFormatterEnabled(file, offset); - } - - if( elementAt == null) { - return new Pair<>(null, charTable); - } - ASTNode node = elementAt.getNode(); - if (node == null || node.getElementType() != TokenType.WHITE_SPACE) { - return new Pair<>(null, charTable); - } - return Pair.create(elementAt, charTable); - } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/InteractiveTemplateStateProcessor.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/InteractiveTemplateStateProcessor.java index fe53ae894d12..c0b8732e9e50 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/InteractiveTemplateStateProcessor.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/InteractiveTemplateStateProcessor.java @@ -7,8 +7,10 @@ import com.intellij.codeInsight.lookup.impl.LookupImpl; import com.intellij.codeInsight.template.Expression; import com.intellij.codeInsight.template.TemplateManager; import com.intellij.idea.ActionsBundle; +import com.intellij.lang.ASTNode; import com.intellij.lang.LangBundle; import com.intellij.lang.Language; +import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.IdeActions; @@ -19,16 +21,24 @@ import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorModificationUtil; +import com.intellij.openapi.editor.RangeMarker; import com.intellij.openapi.fileEditor.impl.text.AsyncEditorLoader; import com.intellij.openapi.keymap.KeymapUtil; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.NlsContexts; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.TextRange; -import com.intellij.psi.PsiElement; -import com.intellij.psi.PsiFile; +import com.intellij.psi.*; +import com.intellij.psi.impl.source.SourceTreeToPsiMap; import com.intellij.psi.impl.source.codeStyle.CoreCodeStyleUtil; +import com.intellij.psi.impl.source.codeStyle.NewLineIndentMarkerProvider; +import com.intellij.psi.impl.source.tree.FileElement; +import com.intellij.psi.util.PsiUtilCore; import com.intellij.refactoring.rename.inplace.InplaceRefactoring; +import com.intellij.util.CharTable; +import com.intellij.util.IncorrectOperationException; +import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -36,6 +46,8 @@ import java.util.ArrayList; import java.util.List; final class InteractiveTemplateStateProcessor implements TemplateStateProcessor { + @NonNls private static final String DUMMY_IDENTIFIER = "xxx"; + private boolean myLookupShown; @Override @@ -52,12 +64,42 @@ final class InteractiveTemplateStateProcessor implements TemplateStateProcessor @Override public TextRange insertNewLineIndentMarker(PsiFile file, Document document, int offset) { - return CoreCodeStyleUtil.insertNewLineIndentMarker(file, document, offset); + return doInsertNewLineIndentMarker(file, document, offset); } + /** + * 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} otherwise + */ @Override public PsiElement findWhiteSpaceNode(PsiFile file, int offset) { - return CoreCodeStyleUtil.findWhiteSpaceNode(file, offset); + return doFindWhiteSpaceNode(file, offset).first; + } + + @NotNull + private static Pair doFindWhiteSpaceNode(@NotNull PsiFile file, int offset) { + ASTNode astNode = SourceTreeToPsiMap.psiElementToTree(file); + if (!(astNode instanceof FileElement)) { + return new Pair<>(null, null); + } + PsiElement elementAt = InjectedLanguageManager.getInstance(file.getProject()).findInjectedElementAt(file, offset); + final CharTable charTable = ((FileElement)astNode).getCharTable(); + if (elementAt == null) { + elementAt = CoreCodeStyleUtil.findElementInTreeWithFormatterEnabled(file, offset); + } + + if( elementAt == null) { + return new Pair<>(null, charTable); + } + ASTNode node = elementAt.getNode(); + if (node == null || node.getElementType() != TokenType.WHITE_SPACE) { + return new Pair<>(null, charTable); + } + return Pair.create(elementAt, charTable); } @Override @@ -164,6 +206,86 @@ final class InteractiveTemplateStateProcessor implements TemplateStateProcessor return false; } + /** + * 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 line with whitespaces that contains $END$ marker: templateText $END$). + *

+ * Current approach is to do the following: + *

+   * 
    + *
  1. Insert dummy text at the end of the blank line which white space symbols should be preserved;
  2. + *
  3. Perform formatting;
  4. + *
  5. Remove dummy text;
  6. + *
+ *
+ *

+ * This method inserts that dummy comment (fallback to identifier {@code xxx}, see {@link #createMarker(PsiFile, int)}) + * if necessary. + *

+ * Note: 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} otherwise + * @throws IncorrectOperationException if given file is read-only + */ + @Nullable + private static TextRange doInsertNewLineIndentMarker(@NotNull PsiFile file, @NotNull Document document, int offset) { + CharSequence text = document.getImmutableCharSequence(); + if (offset <= 0 || offset >= text.length() || !isWhiteSpaceSymbol(text.charAt(offset))) { + return null; + } + + if (!isWhiteSpaceSymbol(text.charAt(offset - 1))) { + return null; // no whitespaces before offset + } + + int end = offset; + for (; end < text.length(); end++) { + if (text.charAt(end) == '\n') { + break; // line is empty till the end + } + if (!isWhiteSpaceSymbol(text.charAt(end))) { + return null; + } + } + + String marker = createMarker(file, offset); + document.insertString(offset, marker); + return new TextRange(offset, offset + marker.length()); + } + + private static boolean isWhiteSpaceSymbol(char c) { + return c == ' ' || c == '\t' || c == '\n'; + } + + private static @NotNull String createMarker(@NotNull PsiFile file, int offset) { + Project project = file.getProject(); + PsiElement injectedElement = InjectedLanguageManager.getInstance(project).findInjectedElementAt(file, offset); + Language language = injectedElement != null ? injectedElement.getLanguage() : PsiUtilCore.getLanguageAtOffset(file, offset); + + CoreCodeStyleUtil.setSequentialProcessingAllowed(false); + NewLineIndentMarkerProvider markerProvider = NewLineIndentMarkerProvider.EP.forLanguage(language); + String marker = markerProvider == null ? null : markerProvider.createMarker(file, offset); + if (marker != null) { + return marker; + } + + PsiComment comment = null; + try { + comment = PsiParserFacade.SERVICE.getInstance(project).createLineOrBlockCommentFromText(language, ""); + } + catch (Throwable ignored) { + } + String text = comment != null ? comment.getText() : null; + return text != null ? text : DUMMY_IDENTIFIER; + } + private static final class MyBasicUndoableAction extends BasicUndoableAction implements Disposable { private final Project myProject; @Nullable