/*
* 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;
}
}
}