/* * 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.psi.codeStyle; import com.intellij.ide.actions.ShowSettingsUtilImpl; import com.intellij.ide.scratch.ScratchFileType; import com.intellij.lang.LanguageFormatting; import com.intellij.openapi.application.ApplicationBundle; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.ex.EditorEx; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiCompiledFile; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiFile; import com.intellij.testFramework.LightVirtualFile; import com.intellij.util.concurrency.SequentialTaskExecutor; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.WeakList; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.util.List; import java.util.concurrent.ExecutorService; import static com.intellij.psi.codeStyle.CommonCodeStyleSettings.IndentOptions; import static com.intellij.psi.codeStyle.DetectAndAdjustIndentOptionsTask.getDefaultIndentOptions; import static com.intellij.psi.codeStyle.EditorNotificationInfo.ActionLabelData; /** * @author Rustam Vishnyakov */ public class DetectableIndentOptionsProvider extends FileIndentOptionsProvider { private static final ExecutorService BOUNDED_EXECUTOR = SequentialTaskExecutor.createSequentialApplicationPoolExecutor("DetectableIndentOptionsProvider pool"); private boolean myIsEnabledInTest; private final List<VirtualFile> myAcceptedFiles = new WeakList<>(); private final List<VirtualFile> myDisabledFiles = new WeakList<>(); @Nullable @Override public IndentOptions getIndentOptions(@NotNull CodeStyleSettings settings, @NotNull PsiFile file) { if (!isEnabled(settings, file)) { return null; } Project project = file.getProject(); PsiDocumentManager psiManager = PsiDocumentManager.getInstance(project); Document document = psiManager.getDocument(file); if (document == null) { return null; } IndentOptions options = getValidCachedIndentOptions(file, document); if (options != null) { return options; } TimeStampedIndentOptions indentOptions = getDefaultIndentOptions(file, document); indentOptions.associateWithDocument(document); DetectAndAdjustIndentOptionsTask task = new DetectAndAdjustIndentOptionsTask(project, document, indentOptions, BOUNDED_EXECUTOR); task.scheduleInBackgroundForCommittedDocument(); return indentOptions; } @Override public boolean useOnFullReformat() { return false; } @TestOnly public void setEnabledInTest(boolean isEnabledInTest) { myIsEnabledInTest = isEnabledInTest; } private boolean isEnabled(@NotNull CodeStyleSettings settings, @NotNull PsiFile file) { if (file instanceof PsiCompiledFile || file.getFileType() == ScratchFileType.INSTANCE) return false; if (ApplicationManager.getApplication().isUnitTestMode()) { return myIsEnabledInTest; } VirtualFile vFile = file.getVirtualFile(); if (vFile == null || vFile instanceof LightVirtualFile || myDisabledFiles.contains(vFile)) return false; return LanguageFormatting.INSTANCE.forContext(file) != null && settings.AUTODETECT_INDENTS; } @TestOnly @Nullable public static DetectableIndentOptionsProvider getInstance() { return FileIndentOptionsProvider.EP_NAME.findExtension(DetectableIndentOptionsProvider.class); } @Nullable @Override public EditorNotificationInfo getNotificationInfo(@NotNull final Project project, @NotNull final VirtualFile file, @NotNull final FileEditor fileEditor, @NotNull IndentOptions userOptions, @NotNull IndentOptions detectedOptions) { final NotificationLabels labels = getNotificationLabels(userOptions, detectedOptions); final Editor editor = fileEditor instanceof TextEditor ? ((TextEditor)fileEditor).getEditor() : null; if (labels == null || editor == null) return null; ActionLabelData okAction = new ActionLabelData( ApplicationBundle.message("code.style.indents.detector.accept"), () -> setAccepted(file) ); ActionLabelData disableForSingleFile = new ActionLabelData( labels.revertToOldSettingsLabel, () -> { disableForFile(file); if (editor instanceof EditorEx) { ((EditorEx)editor).reinitSettings(); } } ); ActionLabelData showSettings = new ActionLabelData( ApplicationBundle.message("code.style.indents.detector.show.settings"), () -> ShowSettingsUtilImpl.showSettingsDialog(project, "preferences.sourceCode", "detect indent") ); final List<ActionLabelData> actions = ContainerUtil.newArrayList(okAction, disableForSingleFile, showSettings); return new EditorNotificationInfo() { @NotNull @Override public List<ActionLabelData> getLabelAndActions() { return actions; } @NotNull @Override public String getTitle() { return labels.title; } }; } @Nullable private static NotificationLabels getNotificationLabels(@NotNull IndentOptions userOptions, @NotNull IndentOptions detectedOptions) { if (userOptions.USE_TAB_CHARACTER) { if (!detectedOptions.USE_TAB_CHARACTER) { return new NotificationLabels(ApplicationBundle.message("code.style.space.indent.detected", detectedOptions.INDENT_SIZE), ApplicationBundle.message("code.style.detector.use.tabs")); } } else { String restoreToSpaces = ApplicationBundle.message("code.style.detector.use.spaces", userOptions.INDENT_SIZE); if (detectedOptions.USE_TAB_CHARACTER) { return new NotificationLabels(ApplicationBundle.message("code.style.tab.usage.detected", userOptions.INDENT_SIZE), restoreToSpaces); } if (userOptions.INDENT_SIZE != detectedOptions.INDENT_SIZE) { return new NotificationLabels(ApplicationBundle.message("code.style.different.indent.size.detected", detectedOptions.INDENT_SIZE, userOptions.INDENT_SIZE), restoreToSpaces); } } return null; } private void disableForFile(@NotNull VirtualFile file) { myDisabledFiles.add(file); } @Override public void setAccepted(@NotNull VirtualFile file) { myAcceptedFiles.add(file); } @Override public boolean isAcceptedWithoutWarning(@Nullable Project project, @NotNull VirtualFile file) { return !FileIndentOptionsProvider.isShowNotification() || myAcceptedFiles.contains(file); } public IndentOptions getValidCachedIndentOptions(PsiFile file, Document document) { IndentOptions options = IndentOptions.retrieveFromAssociatedDocument(file); if (options instanceof TimeStampedIndentOptions) { final IndentOptions defaultIndentOptions = getDefaultIndentOptions(file, document); final TimeStampedIndentOptions cachedInDocument = (TimeStampedIndentOptions)options; if (!cachedInDocument.isOutdated(document, defaultIndentOptions)) { return cachedInDocument; } } return null; } private static class NotificationLabels { public final String title; public final String revertToOldSettingsLabel; public NotificationLabels(@NotNull String title, @NotNull String revertToOldSettingsLabel) { this.title = title; this.revertToOldSettingsLabel = revertToOldSettingsLabel; } } }