package com.scss; import com.intellij.codeInsight.daemon.HighlightDisplayKey; import com.intellij.codeInsight.daemon.impl.SeverityRegistrar; import com.intellij.lang.annotation.Annotation; import com.intellij.lang.annotation.AnnotationHolder; import com.intellij.lang.annotation.ExternalAnnotator; import com.intellij.lang.annotation.HighlightSeverity; import com.intellij.notification.NotificationType; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.colors.EditorColorsScheme; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Key; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.profile.codeInspection.InspectionProjectProfileManager; import com.intellij.psi.MultiplePsiFilesPerDocumentFileViewProvider; import com.intellij.psi.PsiDocumentManager; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiFile; import com.scss.annotator.BaseActionFix; import com.scss.annotator.Fixes; import com.scss.config.ScssLintConfigFileChangeTracker; import com.scss.settings.ScssLintSettingsPage; import com.scss.utils.ScssLintRunner; import com.scss.utils.scssLint.Lint; import com.scss.utils.scssLint.LintResult; import com.wix.annotator.AnnotatorUtils; import com.wix.files.ActualFileManager; import com.wix.files.BaseActualFile; import com.wix.files.TempFile; import com.wix.files.ThreadLocalTempActualFile; import com.wix.utils.PsiUtil; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.io.File; import java.io.IOException; import java.util.List; //import org.jetbrains.plugins.scss.SCSSFileType; //import org.jetbrains.plugins.scss.psi.SCSSFile; /** * @author idok */ public class ScssLintExternalAnnotator extends ExternalAnnotator<ScssLintAnnotationInput, ScssLintAnnotationResult> { // public static final ScssLintExternalAnnotator INSTANCE = new ScssLintExternalAnnotator(); private static final Logger LOG = Logger.getInstance(ScssLintBundle.LOG_ID); // private static final Key<ThreadLocalActualFile> SCSS_TEMP_FILE_KEY = Key.create("SCSS_TEMP_FILE"); public static final String SCSS = "scss"; @Nullable @Override public ScssLintAnnotationInput collectInformation(@NotNull PsiFile file) { return collectInformation(file, null); } @Nullable @Override public ScssLintAnnotationInput collectInformation(@NotNull PsiFile file, @NotNull Editor editor, boolean hasErrors) { return collectInformation(file, editor); } @NotNull public static HighlightDisplayKey getHighlightDisplayKeyByClass() { String id = "ScssLint"; HighlightDisplayKey key = HighlightDisplayKey.find(id); if (key == null) { key = new HighlightDisplayKey(id, id); } return key; } @Override public void apply(@NotNull PsiFile file, ScssLintAnnotationResult annotationResult, @NotNull AnnotationHolder holder) { if (annotationResult == null) { return; } InspectionProjectProfileManager inspectionProjectProfileManager = InspectionProjectProfileManager.getInstance(file.getProject()); SeverityRegistrar severityRegistrar = inspectionProjectProfileManager.getSeverityRegistrar(); HighlightDisplayKey inspectionKey = getHighlightDisplayKeyByClass(); EditorColorsScheme colorsScheme = annotationResult.input.colorsScheme; Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); if (document == null) { return; } if (annotationResult.fileLevel != null) { Annotation annotation = holder.createWarningAnnotation(file, annotationResult.fileLevel); annotation.registerFix(new EditSettingsAction(new ScssLintSettingsPage(file.getProject()))); annotation.setFileLevelAnnotation(true); return; } // TODO consider adding a fix to edit configuration file if (annotationResult.result == null || annotationResult.result.lint == null || annotationResult.result.lint.isEmpty()) { return; } // String relativeFile = FileUtils.makeRelative(file.getProject(), file.getVirtualFile()); List<Lint.Issue> issues = annotationResult.result.lint.values().iterator().next(); if (issues == null) { return; } ScssLintProjectComponent component = annotationResult.input.project.getComponent(ScssLintProjectComponent.class); int tabSize = 4; for (Lint.Issue issue : issues) { HighlightSeverity severity = getHighlightSeverity(issue, component.treatAsWarnings); TextAttributes forcedTextAttributes = AnnotatorUtils.getTextAttributes(colorsScheme, severityRegistrar, severity); Annotation annotation = createAnnotation(holder, file, document, issue, "SCSS Lint: ", tabSize, severity, forcedTextAttributes, inspectionKey, false); if (annotation != null) { int offset = StringUtil.lineColToOffset(document.getText(), issue.line - 1, issue.column); PsiElement lit = PsiUtil.getElementAtOffset(file, offset); BaseActionFix actionFix = Fixes.getFixForRule(issue.linter, lit); if (actionFix != null) { annotation.registerFix(actionFix, null, inspectionKey); } // annotation.registerFix(new SuppressActionFix(issue.rule, lit), null, inspectionKey); } } } private static HighlightSeverity getHighlightSeverity(Lint.Issue warn) { return warn.severity.equals("error") ? HighlightSeverity.ERROR : HighlightSeverity.WARNING; } private static HighlightSeverity getHighlightSeverity(Lint.Issue issue, boolean treatAsWarnings) { return treatAsWarnings ? HighlightSeverity.WARNING : getHighlightSeverity(issue); } @Nullable private static Annotation createAnnotation(@NotNull AnnotationHolder holder, @NotNull PsiFile file, @NotNull Document document, @NotNull Lint.Issue issue, @NotNull String messagePrefix, int tabSize, @NotNull HighlightSeverity severity, @Nullable TextAttributes forcedTextAttributes, @NotNull HighlightDisplayKey inspectionKey, boolean showErrorOnWholeLine) { int errorLine = issue.line - 1; int errorColumn = issue.column - 1; if (errorLine < 0 || errorLine >= document.getLineCount()) { return null; } int lineEndOffset = document.getLineEndOffset(errorLine); int lineStartOffset = document.getLineStartOffset(errorLine); int errorLineStartOffset = PsiUtil.calcErrorStartOffsetInDocument(document, lineStartOffset, lineEndOffset, errorColumn, tabSize); if (errorLineStartOffset == -1) { return null; } PsiElement element = file.findElementAt(errorLineStartOffset); // if (element != null /*&& JSInspection.isSuppressedForStatic(element, getInspectionClass(), inspectionKey.getID())*/) // return null; TextRange range; if (showErrorOnWholeLine) { range = new TextRange(lineStartOffset, lineEndOffset); } else { // int offset = StringUtil.lineColToOffset(document.getText(), warn.line - 1, warn.column); PsiElement lit = PsiUtil.getElementAtOffset(file, errorLineStartOffset); range = lit.getTextRange(); // range = new TextRange(errorLineStartOffset, errorLineStartOffset + 1); } range = new TextRange(errorLineStartOffset, errorLineStartOffset + issue.length); Annotation annotation = createAnnotation(holder, severity, forcedTextAttributes, range, messagePrefix + issue.reason.trim() + " (" + (issue.linter == null ? "none" : issue.linter) + ')'); if (annotation != null) { annotation.setAfterEndOfLine(errorLineStartOffset == lineEndOffset); } return annotation; } @NotNull public static Annotation createAnnotation(@NotNull AnnotationHolder holder, @NotNull HighlightSeverity severity, @NotNull TextRange range, @NotNull String message) { /* avoid using holder.createAnnotation(severity, range, message); as it is not supported in PhpStorm: 7.1.3 (PS-133.982) https://github.com/idok/scss-lint-plugin/issues/5 */ if (severity.equals(HighlightSeverity.ERROR)) { return holder.createErrorAnnotation(range, message); } return holder.createWarningAnnotation(range, message); } @Nullable public static Annotation createAnnotation(@NotNull AnnotationHolder holder, @NotNull HighlightSeverity severity, @Nullable TextAttributes forcedTextAttributes, @NotNull TextRange range, @NotNull String message) { if (forcedTextAttributes != null) { Annotation annotation = createAnnotation(holder, severity, range, message); annotation.setEnforcedTextAttributes(forcedTextAttributes); return annotation; } return createAnnotation(holder, severity, range, message); } @Nullable private static ScssLintAnnotationInput collectInformation(@NotNull PsiFile psiFile, @Nullable Editor editor) { if (psiFile.getContext() != null || !isScssFile(psiFile)) { return null; } VirtualFile virtualFile = psiFile.getVirtualFile(); if (virtualFile == null || !virtualFile.isInLocalFileSystem()) { return null; } if (psiFile.getViewProvider() instanceof MultiplePsiFilesPerDocumentFileViewProvider) { return null; } Project project = psiFile.getProject(); // ScssLintProjectComponent component = project.getComponent(ScssLintProjectComponent.class); // if (!component.isSettingsValid() || !component.isEnabled()) { // return new ScssLintAnnotationInput(project, psiFile, null, null, "Invalid settings!"); // } Document document = PsiDocumentManager.getInstance(project).getDocument(psiFile); if (document == null) { return null; } String fileContent = document.getText(); if (StringUtil.isEmptyOrSpaces(fileContent)) { return null; } EditorColorsScheme colorsScheme = editor == null ? null : editor.getColorsScheme(); return new ScssLintAnnotationInput(project, psiFile, fileContent, colorsScheme); } private static boolean isScssFile(PsiFile file) { return file.getVirtualFile().getExtension().equals(SCSS); // return file instanceof SCSSFile && file.getFileType().equals(SCSSFileType.SCSS); } private static final Key<ThreadLocalTempActualFile> TEMP_FILE = Key.create("SCSS_LINT_TEMP_FILE"); private static void copyConfig(Project project, File temp) throws IOException { copyConfigFile(project, temp, ScssLintConfigFileChangeTracker.SCSS_LINT_YML); } private static void copyConfigFile(Project project, File temp, String fileName) throws IOException { VirtualFile jscs = project.getBaseDir().findChild(fileName); File tempJscs = new File(temp, fileName); if (jscs != null) { //check if stale? FileUtil.copy(new File(jscs.getPath()), tempJscs); tempJscs.deleteOnExit(); } } @Nullable @Override public ScssLintAnnotationResult doAnnotate(ScssLintAnnotationInput collectedInfo) { BaseActualFile actualFile = null; try { PsiFile file = collectedInfo.psiFile; if (!isScssFile(file)) { return null; } ScssLintProjectComponent component = file.getProject().getComponent(ScssLintProjectComponent.class); if (!component.isEnabled()) { return new ScssLintAnnotationResult(collectedInfo, null, "SCSS Lint is available for this file but is not configured"); } if (!component.isSettingsValid()) { return new ScssLintAnnotationResult(collectedInfo, null, "SCSS Lint is not configured correctly"); } ScssLintConfigFileChangeTracker.getInstance(collectedInfo.project).startIfNeeded(); actualFile = ActualFileManager.getOrCreateActualFile(TEMP_FILE, file, collectedInfo.fileContent); if (actualFile instanceof TempFile) { copyConfig(file.getProject(), new File(actualFile.getCwd())); } if (actualFile == null) { LOG.warn("Failed to create file for lint"); return null; } LintResult result = ScssLintRunner.runLint(actualFile.getCwd(), actualFile.getPath(), component.scssLintExecutable, component.scssLintConfigFile); if (StringUtils.isNotEmpty(result.errorOutput)) { component.showInfoNotification(result.errorOutput, NotificationType.WARNING); return null; } Document document = PsiDocumentManager.getInstance(file.getProject()).getDocument(file); if (document == null) { component.showInfoNotification("Error running SCSS Lint inspection: Could not get document for file " + file.getName(), NotificationType.WARNING); LOG.warn("Could not get document for file " + file.getName()); return null; } return new ScssLintAnnotationResult(collectedInfo, result); } catch (Exception e) { LOG.error("Error running ScssLint inspection: ", e); ScssLintProjectComponent.showNotification("Error running SCSS Lint inspection: " + e.getMessage(), NotificationType.ERROR); } finally { ActualFileManager.dispose(actualFile); } return null; } } class ScssLintAnnotationInput { public final String fileContent; public final EditorColorsScheme colorsScheme; public final Project project; public final PsiFile psiFile; public ScssLintAnnotationInput(Project project, PsiFile psiFile, String fileContent, EditorColorsScheme colorsScheme) { this.project = project; this.psiFile = psiFile; this.fileContent = fileContent; this.colorsScheme = colorsScheme; } } class ScssLintAnnotationResult { public ScssLintAnnotationResult(ScssLintAnnotationInput input, LintResult result) { this.input = input; this.result = result; } public ScssLintAnnotationResult(ScssLintAnnotationInput input, LintResult result, String fileLevel) { this.input = input; this.result = result; this.fileLevel = fileLevel; } public final ScssLintAnnotationInput input; public final LintResult result; public String fileLevel; }