com.android.tools.idea.editors.manifest.ManifestPanel.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.editors.manifest.ManifestPanel.java

Source

/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * 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.android.tools.idea.editors.manifest;

import com.android.SdkConstants;
import com.android.builder.model.AndroidLibrary;
import com.android.builder.model.MavenCoordinates;
import com.android.ide.common.blame.SourceFile;
import com.android.ide.common.blame.SourceFilePosition;
import com.android.ide.common.blame.SourcePosition;
import com.android.manifmerger.Actions;
import com.android.manifmerger.MergingReport;
import com.android.manifmerger.XmlNode;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.parser.BuildFileKey;
import com.android.tools.idea.gradle.parser.GradleBuildFile;
import com.android.tools.idea.gradle.parser.NamedObject;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.model.MergedManifest;
import com.android.tools.idea.rendering.HtmlLinkManager;
import com.android.utils.HtmlBuilder;
import com.android.utils.PositionXmlParser;
import com.google.common.collect.Sets;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.XmlHighlighterColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.colors.EditorFontType;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.JBMenuItem;
import com.intellij.openapi.ui.JBPopupMenu;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.*;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.treeStructure.Tree;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.tree.TreeUtil;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.facet.IdeaSourceProvider;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.*;

import javax.swing.*;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.io.File;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.tools.idea.gradle.project.model.AndroidModuleModel.EXPLODED_AAR;

// TODO for permission if not from main file
// TODO then have option to tools:node="remove" tools:selector="com.example.lib1"

// TODO merge conflict, then use tools:node=?replace?
// TODO or tools:node=?merge-only-attributes?

// TODO add option to tools:node=?removeAll" Remove all elements of the same node type
// TODO add option to tools:node=?strict? can be added to anything that merges perfectly

public class ManifestPanel extends JPanel implements TreeSelectionListener {

    private static final String SUGGESTION_MARKER = "Suggestion: ";
    private static final Pattern ADD_SUGGESTION_FORMAT = Pattern.compile(
            ".*? 'tools:([\\w:]+)=\"([\\w:]+)\"' to \\<(\\w+)\\> element at [^:]+:(\\d+):(\\d+)-[\\d:]+ to override\\.",
            Pattern.DOTALL);

    /**
     * We don't have an exact position for values coming from the
     * Gradle model. This file is used as a marker pointing to the
     * Gradle model.
     */
    private static final File GRADLE_MODEL_MARKER_FILE = new File(FN_BUILD_GRADLE);

    private final AndroidFacet myFacet;
    private final Font myDefaultFont;
    private final Tree myTree;
    private final JEditorPane myDetails;
    private JPopupMenu myPopup;
    private JMenuItem myRemoveItem;

    private MergedManifest myManifest;
    private final List<File> myFiles = new ArrayList<>();
    private final List<File> myOtherFiles = new ArrayList<>();
    private final HtmlLinkManager myHtmlLinkManager = new HtmlLinkManager();
    private VirtualFile myFile;
    private final Color myBackgroundColor;

    public ManifestPanel(final @NotNull AndroidFacet facet) {
        myFacet = facet;
        setLayout(new BorderLayout());

        EditorColorsManager colorsManager = EditorColorsManager.getInstance();
        EditorColorsScheme scheme = colorsManager.getGlobalScheme();
        myBackgroundColor = scheme.getDefaultBackground();
        myDefaultFont = scheme.getFont(EditorFontType.PLAIN);

        myTree = new FileColorTree();
        myTree.setCellRenderer(new SyntaxHighlightingCellRenderer());

        TreeSelectionModel selectionModel = myTree.getSelectionModel();
        selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
        selectionModel.addTreeSelectionListener(this);

        myDetails = createDetailsPane(facet);

        addSpeedSearch();
        createPopupMenu();
        registerGotoAction();

        JBSplitter splitter = new JBSplitter(0.5f);
        splitter.setFirstComponent(new JBScrollPane(myTree));
        splitter.setSecondComponent(new JBScrollPane(myDetails));

        add(splitter);
    }

    private JEditorPane createDetailsPane(@NotNull final AndroidFacet facet) {
        JEditorPane details = new JEditorPane();
        details.setMargin(JBUI.insets(5));
        details.setContentType(UIUtil.HTML_MIME);
        details.setEditable(false);
        details.setFont(myDefaultFont);
        details.setBackground(myBackgroundColor);
        HyperlinkListener hyperLinkListener = e -> {
            if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
                String url = e.getDescription();
                myHtmlLinkManager.handleUrl(url, facet.getModule(), null, null, null);
            }
        };
        details.addHyperlinkListener(hyperLinkListener);

        return details;
    }

    private void createPopupMenu() {
        myPopup = new JBPopupMenu();
        JMenuItem gotoItem = new JBMenuItem("Go to Declaration");
        gotoItem.addActionListener(e -> {
            TreePath treePath = myTree.getSelectionPath();
            final ManifestTreeNode node = (ManifestTreeNode) treePath.getLastPathComponent();
            if (node != null) {
                goToDeclaration(node.getUserObject());
            }
        });
        myPopup.add(gotoItem);
        myRemoveItem = new JBMenuItem("Remove");
        myRemoveItem.addActionListener(e -> {
            TreePath treePath = myTree.getSelectionPath();
            final ManifestTreeNode node = (ManifestTreeNode) treePath.getLastPathComponent();

            new WriteCommandAction.Simple(myFacet.getModule().getProject(), "Removing manifest tag",
                    ManifestUtils.getMainManifest(myFacet)) {
                @Override
                protected void run() throws Throwable {
                    ManifestUtils.toolsRemove(ManifestUtils.getMainManifest(myFacet), node.getUserObject());
                }
            }.execute();
        });
        myPopup.add(myRemoveItem);

        MouseListener ml = new MouseAdapter() {
            @Override
            public void mousePressed(@NotNull MouseEvent e) {
                if (e.isPopupTrigger()) {
                    handlePopup(e);
                }
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (e.isPopupTrigger()) {
                    handlePopup(e);
                }
            }

            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2 && e.getButton() == MouseEvent.BUTTON1) {
                    TreePath treePath = myTree.getPathForLocation(e.getX(), e.getY());
                    if (treePath != null) {
                        ManifestTreeNode node = (ManifestTreeNode) treePath.getLastPathComponent();
                        Node attribute = node.getUserObject();
                        if (attribute instanceof Attr) {
                            goToDeclaration(attribute);
                        }
                    }
                }
            }

            private void handlePopup(@NotNull MouseEvent e) {
                TreePath treePath = myTree.getPathForLocation(e.getX(), e.getY());
                if (treePath == null || e.getSource() == myDetails) {
                    // Use selection instead
                    treePath = myTree.getSelectionPath();
                }
                if (treePath != null) {
                    ManifestTreeNode node = (ManifestTreeNode) treePath.getLastPathComponent();
                    myRemoveItem.setEnabled(canRemove(node.getUserObject()));
                    myPopup.show(e.getComponent(), e.getX(), e.getY());
                }
            }
        };
        myTree.addMouseListener(ml);
        myDetails.addMouseListener(ml);
    }

    private void registerGotoAction() {
        AnAction goToDeclarationAction = new AnAction() {
            @Override
            public void actionPerformed(AnActionEvent e) {
                ManifestTreeNode node = (ManifestTreeNode) myTree.getLastSelectedPathComponent();
                if (node != null) {
                    goToDeclaration(node.getUserObject());
                }
            }
        };
        goToDeclarationAction.registerCustomShortcutSet(
                ActionManager.getInstance().getAction(IdeActions.ACTION_GOTO_DECLARATION).getShortcutSet(), myTree);
    }

    @NotNull
    private TreeSpeedSearch addSpeedSearch() {
        return new TreeSpeedSearch(myTree);
    }

    public void setManifest(@NotNull MergedManifest manifest, @NotNull VirtualFile selectedManifest) {
        myFile = selectedManifest;
        myManifest = manifest;
        Document document = myManifest.getDocument();
        Element root = document != null ? document.getDocumentElement() : null;
        myTree.setModel(root == null ? null : new DefaultTreeModel(new ManifestTreeNode(root)));

        myFiles.clear();
        myOtherFiles.clear();
        List<VirtualFile> manifestFiles = myManifest.getManifestFiles();

        // make sure that the selected manifest is always the first color
        myFiles.add(VfsUtilCore.virtualToIoFile(selectedManifest));
        Set<File> referenced = Sets.newHashSet();
        if (root != null) {
            recordLocationReferences(root, referenced);
        }

        if (manifestFiles != null) {
            for (VirtualFile f : manifestFiles) {
                if (!f.equals(selectedManifest)) {
                    File file = VfsUtilCore.virtualToIoFile(f);
                    if (referenced.contains(file)) {
                        myFiles.add(file);
                    } else {
                        myOtherFiles.add(file);
                    }
                }
            }
            Collections.sort(myFiles, MANIFEST_SORTER);
            Collections.sort(myOtherFiles, MANIFEST_SORTER);

            // Build.gradle - injected
            if (referenced.contains(GRADLE_MODEL_MARKER_FILE)) {
                myFiles.add(GRADLE_MODEL_MARKER_FILE);
            }
        }

        if (root != null) {
            TreeUtil.expandAll(myTree);
        }

        // display the LoggingRecords from the merger
        updateDetails(null);
    }

    private static final Comparator<File> MANIFEST_SORTER = (o1, o2) -> {
        String p1 = o1.getPath();
        String p2 = o2.getPath();
        boolean lib1 = p1.contains(EXPLODED_AAR);
        boolean lib2 = p2.contains(EXPLODED_AAR);
        if (lib1 != lib2) {
            return lib1 ? 1 : -1;
        }
        return p1.compareTo(p2);
    };

    private void recordLocationReferences(@NotNull Node node, @NotNull Set<File> files) {
        short type = node.getNodeType();
        if (type == Node.ATTRIBUTE_NODE) {
            List<? extends Actions.Record> records = ManifestUtils.getRecords(myManifest, node);
            if (!records.isEmpty()) {
                Actions.Record record = records.get(0);

                // Ignore keys specified on the parent element; those are misleading
                XmlNode.NodeKey targetId = record.getTargetId();
                if (targetId.toString().contains("@")) {
                    // Injected values correspond to the Gradle model; we don't have
                    // an accurate file location so just use a marker file.
                    if (record.getActionType() == Actions.ActionType.INJECTED) {
                        files.add(GRADLE_MODEL_MARKER_FILE);
                    } else {
                        File location = record.getActionLocation().getFile().getSourceFile();
                        if (location != null && !files.contains(location)) {
                            files.add(location);
                        }
                    }
                }
            }
        } else if (type == Node.ELEMENT_NODE) {
            Node child = node.getFirstChild();
            while (child != null) {
                if (child.getNodeType() == Node.ELEMENT_NODE) {
                    recordLocationReferences(child, files);
                }
                child = child.getNextSibling();
            }

            NamedNodeMap attributes = node.getAttributes();
            for (int i = 0, n = attributes.getLength(); i < n; i++) {
                recordLocationReferences(attributes.item(i), files);
            }
        }
    }

    @Override
    public void valueChanged(@Nullable TreeSelectionEvent e) {
        if (e != null && e.isAddedPath()) {
            TreePath treePath = e.getPath();
            ManifestTreeNode node = (ManifestTreeNode) treePath.getLastPathComponent();
            updateDetails(node);
        } else {
            updateDetails(null);
        }
    }

    public void updateDetails(@Nullable ManifestTreeNode node) {
        HtmlBuilder sb = new HtmlBuilder();
        Font font = UIUtil.getLabelFont();
        sb.addHtml("<html><body style=\"font-family: " + font.getFamily() + "; " + "font-size: " + font.getSize()
                + "pt;\">");
        sb.beginUnderline().beginBold();
        sb.add("Manifest Sources");
        sb.endBold().endUnderline().newline();
        sb.addHtml("<table border=\"0\">");
        String borderColor = ColorUtil.toHex(JBColor.GRAY);
        for (File file : myFiles) {
            Color color = getFileColor(file);
            sb.addHtml("<tr><td width=\"24\" height=\"24\" style=\"background-color:#");
            sb.addHtml(ColorUtil.toHex(color));
            sb.addHtml("; border: 1px solid #");
            sb.addHtml(borderColor);
            sb.addHtml(";\">");
            sb.addHtml("</td><td>");
            describePosition(sb, myFacet, new SourceFilePosition(file, SourcePosition.UNKNOWN));
            sb.addHtml("</td></tr>");
        }
        sb.addHtml("</table>");
        sb.newline();
        if (!myOtherFiles.isEmpty()) {
            sb.beginUnderline().beginBold();
            sb.add("Other Manifest Files");
            sb.endBold().endUnderline().newline();
            sb.add("(Included in merge, but did not contribute any elements)").newline();
            boolean first = true;
            for (File file : myOtherFiles) {
                if (first) {
                    first = false;
                } else {
                    sb.add(", ");
                }
                describePosition(sb, myFacet, new SourceFilePosition(file, SourcePosition.UNKNOWN));
            }
            sb.newline().newline();
        }

        // See if there are errors; if so, show the merging report instead of node selection report
        if (!myManifest.getLoggingRecords().isEmpty()) {
            for (MergingReport.Record record : myManifest.getLoggingRecords()) {
                if (record.getSeverity() == MergingReport.Record.Severity.ERROR) {
                    node = null;
                    break;
                }
            }
        }

        if (node != null) {
            List<? extends Actions.Record> records = ManifestUtils.getRecords(myManifest, node.getUserObject());
            sb.beginUnderline().beginBold();
            sb.add("Merging Log");
            sb.endBold().endUnderline().newline();

            if (records.isEmpty()) {
                sb.add("No records found. (This is a bug in the manifest merger.)");
            }

            SourceFilePosition prev = null;
            boolean prevInjected = false;
            for (Actions.Record record : records) {
                // There are currently some duplicated entries; filter these out
                SourceFilePosition location = ManifestUtils.getActionLocation(myFacet.getModule(), record);
                if (location.equals(prev)) {
                    continue;
                }
                prev = location;

                Actions.ActionType actionType = record.getActionType();
                boolean injected = actionType == Actions.ActionType.INJECTED;
                if (injected && prevInjected) {
                    continue;
                }
                prevInjected = injected;
                if (injected) {
                    sb.add("Value provided by Gradle"); // TODO: include module source? Are we certain it's correct?
                    sb.newline();
                    continue;
                }
                sb.add(StringUtil.capitalize(String.valueOf(actionType).toLowerCase(Locale.US)));
                sb.add(" from the ");
                sb.addHtml(getHtml(myFacet, location));

                String reason = record.getReason();
                if (reason != null) {
                    sb.add("; reason: ");
                    sb.add(reason);
                }
                sb.newline();
            }
        } else if (!myManifest.getLoggingRecords().isEmpty()) {
            sb.add("Merging Errors:").newline();
            for (MergingReport.Record record : myManifest.getLoggingRecords()) {
                sb.addHtml(getHtml(record.getSeverity()));
                sb.add(" ");
                try {
                    sb.addHtml(getErrorHtml(myFacet, record.getMessage(), record.getSourceLocation(),
                            myHtmlLinkManager, LocalFileSystem.getInstance().findFileByIoFile(myFiles.get(0))));
                } catch (Exception ex) {
                    Logger.getInstance(ManifestPanel.class).error("error getting error html", ex);
                    sb.add(record.getMessage());
                }
                sb.add(" ");
                sb.addHtml(getHtml(myFacet, record.getSourceLocation()));
                sb.newline();
            }
        }

        sb.closeHtmlBody();
        myDetails.setText(sb.getHtml());
        myDetails.setCaretPosition(0);
    }

    @NotNull
    private Color getNodeColor(@NotNull Node item) {
        List<? extends Actions.Record> records = ManifestUtils.getRecords(myManifest, item);
        if (!records.isEmpty()) {
            Actions.Record record = records.get(0);
            File file;
            if (record.getActionType() == Actions.ActionType.INJECTED) {
                file = GRADLE_MODEL_MARKER_FILE;
            } else {
                file = ManifestUtils.getActionLocation(myFacet.getModule(), record).getFile().getSourceFile();
            }
            if (file != null) {
                return getFileColor(file);
            }
        }
        return myBackgroundColor;
    }

    @NotNull
    private Color getFileColor(@NotNull File file) {
        if (!myFiles.contains(file)) {
            myFiles.add(file);
        }
        int index = myFiles.indexOf(file);
        if (index == 0) {
            // current file shouldn't be highlighted with a background
            return myBackgroundColor;
        }
        return AnnotationColors.BG_COLORS[(index - 1) * AnnotationColors.BG_COLORS_PRIME
                % AnnotationColors.BG_COLORS.length];
    }

    private boolean canRemove(@NotNull Node node) {
        List<? extends Actions.Record> records = ManifestUtils.getRecords(myManifest, node);
        if (records.isEmpty()) {
            // if we don't know where we are coming from, we are prob displaying the main manifest with a merge error.
            return false;
        }
        File mainManifest = VfsUtilCore.virtualToIoFile(ManifestUtils.getMainManifest(myFacet).getVirtualFile());
        for (Actions.Record record : records) {
            // if we are already coming from the main file, then we can't remove it using this editor
            if (FileUtil.filesEqual(
                    ManifestUtils.getActionLocation(myFacet.getModule(), record).getFile().getSourceFile(),
                    mainManifest)) {
                return false;
            }
        }
        return true;
    }

    private void goToDeclaration(Node element) {
        List<? extends Actions.Record> records = ManifestUtils.getRecords(myManifest, element);
        for (Actions.Record record : records) {
            SourceFilePosition sourceFilePosition = ManifestUtils.getActionLocation(myFacet.getModule(), record);
            SourceFile sourceFile = sourceFilePosition.getFile();
            if (!SourceFile.UNKNOWN.equals(sourceFile)) {
                File ioFile = sourceFile.getSourceFile();
                if (ioFile != null) {
                    VirtualFile file = LocalFileSystem.getInstance().findFileByIoFile(ioFile);
                    assert file != null;
                    int line = -1;
                    int column = 0;
                    SourcePosition sourcePosition = sourceFilePosition.getPosition();
                    if (!SourcePosition.UNKNOWN.equals(sourcePosition)) {
                        line = sourcePosition.getStartLine();
                        column = sourcePosition.getStartColumn();
                    }
                    Project project = myFacet.getModule().getProject();
                    OpenFileDescriptor descriptor = new OpenFileDescriptor(project, file, line, column);
                    FileEditorManager.getInstance(project).openEditor(descriptor, true);
                    break;
                }
            }
        }
    }

    @NotNull
    static String getErrorHtml(final @NotNull AndroidFacet facet, @NotNull String message,
            @NotNull final SourceFilePosition position, @NotNull HtmlLinkManager htmlLinkManager,
            final @Nullable VirtualFile currentlyOpenFile) {
        HtmlBuilder sb = new HtmlBuilder();
        int index = message.indexOf(SUGGESTION_MARKER);
        if (index >= 0) {
            index += SUGGESTION_MARKER.length();
            String action = message.substring(index, message.indexOf(' ', index));
            sb.add(message.substring(0, index));
            message = message.substring(index);
            if ("add".equals(action)) {
                sb.addHtml(getErrorAddHtml(facet, message, position, htmlLinkManager, currentlyOpenFile));
            } else if ("use".equals(action)) {
                sb.addHtml(getErrorUseHtml(facet, message, position, htmlLinkManager, currentlyOpenFile));
            } else if ("remove".equals(action)) {
                sb.addHtml(getErrorRemoveHtml(facet, message, position, htmlLinkManager, currentlyOpenFile));
            }
        } else {
            sb.add(message);
        }
        return sb.getHtml();
    }

    @NotNull
    private static String getErrorAddHtml(final @NotNull AndroidFacet facet, @NotNull String message,
            @NotNull final SourceFilePosition position, @NotNull HtmlLinkManager htmlLinkManager,
            final @Nullable VirtualFile currentlyOpenFile) {
        /*
        Example Input:
        ERROR Attribute activity#com.foo.mylibrary.LibActivity@label value=(@string/app_name)
        from AndroidManifest.xml:24:17-49 is also present at AndroidManifest.xml:12:13-45
        value=(@string/lib_name). Suggestion: add 'tools:replace="android:label"' to <activity>
        element at AndroidManifest.xml:22:9-24:51 to override. AndroidManifest.xml:24:17-49
         */
        HtmlBuilder sb = new HtmlBuilder();
        Matcher matcher = ADD_SUGGESTION_FORMAT.matcher(message);
        if (!matcher.matches()) {
            throw new IllegalArgumentException("unexpected add suggestion format " + message);
        }
        final String attributeName = matcher.group(1);
        final String attributeValue = matcher.group(2);
        String tagName = matcher.group(3);
        int line = Integer.parseInt(matcher.group(4));
        int col = Integer.parseInt(matcher.group(5));
        final XmlFile mainManifest = ManifestUtils.getMainManifest(facet);

        Element element = getElementAt(mainManifest, line, col);
        if (element != null && tagName.equals(element.getTagName())) {
            final Element xmlTag = element;
            sb.addLink(message, htmlLinkManager.createRunnableLink(
                    () -> addToolsAttribute(mainManifest, xmlTag, attributeName, attributeValue)));
        } else {
            Logger.getInstance(ManifestPanel.class).warn("can not find " + tagName + " tag " + element);
            sb.add(message);
        }
        return sb.getHtml();
    }

    @Nullable
    private static Element getElementAt(XmlFile mainManifest, int line, int col) {
        Element element = null;
        try {
            Document document = PositionXmlParser.parse(mainManifest.getText());
            Node node = PositionXmlParser.findNodeAtLineAndCol(document, line, col);
            while (node != null) {
                if (node instanceof Element) {
                    element = (Element) node;
                    break;
                } else
                    node = node.getParentNode();
            }
        } catch (Throwable ignore) {
        }
        return element;
    }

    @NotNull
    private static String getErrorUseHtml(final @NotNull AndroidFacet facet, @NotNull String message,
            @NotNull final SourceFilePosition position, @NotNull HtmlLinkManager htmlLinkManager,
            final @Nullable VirtualFile currentlyOpenFile) {
        /*
        Example Input:
        ERROR uses-sdk:minSdkVersion 4 cannot be smaller than version 8 declared in library
        /.../mylib/AndroidManifest.xml Suggestion:use tools:overrideLibrary="com.mylib"
        to force usage AndroidManifest.xml:11:5-72
         */
        HtmlBuilder sb = new HtmlBuilder();
        int eq = message.indexOf('=');
        if (eq < 0) {
            throw new IllegalArgumentException("unexpected use suggestion format " + message);
        }
        int end = message.indexOf('"', eq + 2);
        if (end < 0 || message.charAt(eq + 1) != '\"') {
            throw new IllegalArgumentException("unexpected use suggestion format " + message);
        }
        final String suggestion = message.substring(message.indexOf(' ') + 1, end + 1);
        if (!SourcePosition.UNKNOWN.equals(position.getPosition())) {
            XmlFile mainManifest = ManifestUtils.getMainManifest(facet);
            Element element = getElementAt(mainManifest, position.getPosition().getStartLine(),
                    position.getPosition().getStartColumn());
            if (element != null && SdkConstants.TAG_USES_SDK.equals(element.getTagName())) {
                sb.addLink(message.substring(0, end + 1), htmlLinkManager.createRunnableLink(() -> {
                    int eq1 = suggestion.indexOf('=');
                    String attributeName = suggestion.substring(suggestion.indexOf(':') + 1, eq1);
                    String attributeValue = suggestion.substring(eq1 + 2, suggestion.length() - 1);
                    addToolsAttribute(mainManifest, element, attributeName, attributeValue);
                }));
                sb.add(message.substring(end + 1));
            } else {
                Logger.getInstance(ManifestPanel.class).warn("Can not find uses-sdk tag " + element);
                sb.add(message);
            }
        } else {
            // If we do not have a uses-sdk tag in our main manifest, the suggestion is not useful
            sb.add(message);
        }
        sb.newlineIfNecessary().newline();
        return sb.getHtml();
    }

    @NotNull
    private static String getErrorRemoveHtml(final @NotNull AndroidFacet facet, @NotNull String message,
            @NotNull final SourceFilePosition position, @NotNull HtmlLinkManager htmlLinkManager,
            final @Nullable VirtualFile currentlyOpenFile) {
        /*
        Example Input:
        ERROR Overlay manifest:package attribute declared at AndroidManifest.xml:3:5-49
        value=(com.foo.manifestapplication.debug) has a different value=(com.foo.manifestapplication)
        declared in main manifest at AndroidManifest.xml:5:5-43 Suggestion: remove the overlay
        declaration at AndroidManifest.xml and place it in the build.gradle: flavorName
        { applicationId = "com.foo.manifestapplication.debug" } AndroidManifest.xml (debug)
         */
        HtmlBuilder sb = new HtmlBuilder();
        int start = message.indexOf('{');
        int end = message.indexOf('}', start + 1);
        final String declaration = message.substring(start + 1, end).trim();
        if (!declaration.startsWith("applicationId")) {
            throw new IllegalArgumentException("unexpected remove suggestion format " + message);
        }
        final GradleBuildFile buildFile = GradleBuildFile.get(facet.getModule());
        Runnable link = null;

        if (buildFile != null) {
            final String applicationId = declaration.substring(declaration.indexOf('"') + 1,
                    declaration.lastIndexOf('"'));
            final File manifestOverlayFile = position.getFile().getSourceFile();
            assert manifestOverlayFile != null;
            VirtualFile manifestOverlayVirtualFile = LocalFileSystem.getInstance()
                    .findFileByIoFile(manifestOverlayFile);
            assert manifestOverlayVirtualFile != null;

            IdeaSourceProvider sourceProvider = ManifestUtils.findManifestSourceProvider(facet,
                    manifestOverlayVirtualFile);
            assert sourceProvider != null;
            final String name = sourceProvider.getName();

            AndroidModuleModel androidModuleModel = AndroidModuleModel.get(facet.getModule());
            assert androidModuleModel != null;

            final XmlFile manifestOverlayPsiFile = (XmlFile) PsiManager.getInstance(facet.getModule().getProject())
                    .findFile(manifestOverlayVirtualFile);
            assert manifestOverlayPsiFile != null;

            if (androidModuleModel.getBuildTypeNames().contains(name)) {
                final String packageName = MergedManifest.get(facet).getPackage();
                assert packageName != null;
                if (applicationId.startsWith(packageName)) {
                    link = () -> new WriteCommandAction.Simple(facet.getModule().getProject(),
                            "Apply manifest suggestion", buildFile.getPsiFile(), manifestOverlayPsiFile) {
                        @Override
                        protected void run() throws Throwable {
                            if (currentlyOpenFile != null) {
                                // We mark this action as affecting the currently open file, so the Undo is available in this editor
                                CommandProcessor.getInstance().addAffectedFiles(facet.getModule().getProject(),
                                        currentlyOpenFile);
                            }
                            removePackageAttribute(manifestOverlayPsiFile);
                            final String applicationIdSuffix = applicationId.substring(packageName.length());
                            @SuppressWarnings("unchecked")
                            List<NamedObject> buildTypes = (List<NamedObject>) buildFile
                                    .getValue(BuildFileKey.BUILD_TYPES);
                            if (buildTypes == null) {
                                buildTypes = new ArrayList<>();
                            }
                            NamedObject buildType = find(buildTypes, name);
                            if (buildType == null) {
                                buildType = new NamedObject(name);
                                buildTypes.add(buildType);
                            }
                            buildType.setValue(BuildFileKey.APPLICATION_ID_SUFFIX, applicationIdSuffix);
                            buildFile.setValue(BuildFileKey.BUILD_TYPES, buildTypes);

                            GradleSyncInvoker.getInstance()
                                    .requestProjectSyncAndSourceGeneration(facet.getModule().getProject(), null);
                        }
                    }.execute();
                }
            } else if (androidModuleModel.getProductFlavorNames().contains(name)) {
                link = () -> new WriteCommandAction.Simple(facet.getModule().getProject(),
                        "Apply manifest suggestion", buildFile.getPsiFile(), manifestOverlayPsiFile) {
                    @Override
                    protected void run() throws Throwable {
                        if (currentlyOpenFile != null) {
                            // We mark this action as affecting the currently open file, so the Undo is available in this editor
                            CommandProcessor.getInstance().addAffectedFiles(facet.getModule().getProject(),
                                    currentlyOpenFile);
                        }
                        removePackageAttribute(manifestOverlayPsiFile);
                        @SuppressWarnings("unchecked")
                        List<NamedObject> flavors = (List<NamedObject>) buildFile.getValue(BuildFileKey.FLAVORS);
                        assert flavors != null;
                        NamedObject flavor = find(flavors, name);
                        assert flavor != null;
                        flavor.setValue(BuildFileKey.APPLICATION_ID, applicationId);
                        buildFile.setValue(BuildFileKey.FLAVORS, flavors);

                        GradleSyncInvoker.getInstance()
                                .requestProjectSyncAndSourceGeneration(facet.getModule().getProject(), null);
                    }
                }.execute();
            }
        }

        if (link != null) {
            sb.addLink(message.substring(0, end + 1), htmlLinkManager.createRunnableLink(link));
            sb.add(message.substring(end + 1));
        } else {
            sb.add(message);
        }
        return sb.getHtml();
    }

    private static void removePackageAttribute(XmlFile manifestFile) {
        XmlTag tag = manifestFile.getRootTag();
        assert tag != null;
        tag.setAttribute("package", null);
    }

    @Nullable /*item not found*/
    static NamedObject find(@NotNull List<NamedObject> items, @NotNull String name) {
        for (NamedObject item : items) {
            if (name.equals(item.getName())) {
                return item;
            }
        }
        return null;
    }

    static void addToolsAttribute(final @NotNull XmlFile file, final @NotNull Element element,
            final @NotNull String attributeName, final @NotNull String attributeValue) {
        final Project project = file.getProject();
        new WriteCommandAction.Simple(project, "Apply manifest suggestion", file) {
            @Override
            protected void run() throws Throwable {
                ManifestUtils.addToolsAttribute(file, element, attributeName, attributeValue);
            }
        }.execute();
    }

    @NotNull
    static String getHtml(@NotNull MergingReport.Record.Severity severity) {
        String severityString = StringUtil.capitalize(severity.toString().toLowerCase(Locale.US));
        if (severity == MergingReport.Record.Severity.ERROR) {
            return new HtmlBuilder().addHtml("<font color=\"#" + ColorUtil.toHex(JBColor.RED) + "\">")
                    .addBold(severityString).addHtml("</font>:").getHtml();
        }
        return severityString;
    }

    @NotNull
    String getHtml(@NotNull AndroidFacet facet, @NotNull SourceFilePosition sourceFilePosition) {
        HtmlBuilder sb = new HtmlBuilder();
        describePosition(sb, facet, sourceFilePosition);
        return sb.getHtml();
    }

    private void describePosition(@NotNull HtmlBuilder sb, @NotNull AndroidFacet facet,
            @NotNull SourceFilePosition sourceFilePosition) {
        SourceFile sourceFile = sourceFilePosition.getFile();
        SourcePosition sourcePosition = sourceFilePosition.getPosition();
        File file = sourceFile.getSourceFile();

        if (file == GRADLE_MODEL_MARKER_FILE) {
            VirtualFile gradleBuildFile = GradleUtil.getGradleBuildFile(facet.getModule());
            if (gradleBuildFile != null) {
                file = VfsUtilCore.virtualToIoFile(gradleBuildFile);
                sb.addHtml("<a href=\"");
                sb.add(file.toURI().toString());
                sb.addHtml("\">");
                sb.add(file.getName());
                sb.addHtml("</a>");
                sb.add(" injection");
            } else {
                sb.add("build.gradle injection (source location unknown)");
            }
            return;
        }

        AndroidLibrary library;
        if (file != null) {
            String source = null;

            Module libraryModule = null;
            Module[] modules = ModuleManager.getInstance(facet.getModule().getProject()).getModules();
            VirtualFile vFile = LocalFileSystem.getInstance().findFileByIoFile(file);
            if (vFile != null) {
                Module module = ModuleUtilCore.findModuleForFile(vFile, facet.getModule().getProject());
                if (module != null) {
                    if (modules.length >= 2) {
                        source = module.getName();
                    }

                    // AAR Library?
                    if (file.getPath().contains(EXPLODED_AAR)) {
                        AndroidModuleModel androidModel = AndroidModuleModel.get(module);
                        if (androidModel != null) {
                            library = GradleUtil.findLibrary(file.getParentFile(),
                                    androidModel.getSelectedVariant(), androidModel.getModelVersion());
                            if (library != null) {
                                if (library.getProject() != null) {
                                    libraryModule = GradleUtil.findModuleByGradlePath(
                                            facet.getModule().getProject(), library.getProject());
                                    if (libraryModule != null) {
                                        module = libraryModule;
                                        source = module.getName();
                                    } else {
                                        source = library.getProject();
                                        source = StringUtil.trimStart(source, ":");
                                    }
                                } else {
                                    MavenCoordinates coordinates = library.getResolvedCoordinates();
                                    source = /*coordinates.getGroupId() + ":" +*/ coordinates.getArtifactId() + ":"
                                            + coordinates.getVersion();
                                }
                            }
                        }
                    }
                }

                IdeaSourceProvider provider = ManifestUtils.findManifestSourceProvider(facet, vFile);
                if (provider != null /*&& !provider.equals(facet.getMainIdeaSourceProvider())*/) {
                    String providerName = provider.getName();
                    if (source == null) {
                        source = providerName;
                    } else {
                        // "the app main manifest" - "app" is the module name, "main" is the source provider name
                        source = source + " " + providerName;
                    }
                }
            }

            if (source == null) {
                source = file.getName();
                if (!SourcePosition.UNKNOWN.equals(sourcePosition)) {
                    source += ":" + String.valueOf(sourcePosition);
                }
            }

            sb.addHtml("<a href=\"");

            boolean redirected = false;
            if (libraryModule != null) {
                AndroidFacet libraryFacet = AndroidFacet.getInstance(libraryModule);
                if (libraryFacet != null) {
                    File manifestFile = libraryFacet.getMainSourceProvider().getManifestFile();
                    if (manifestFile.exists()) {
                        sb.add(manifestFile.toURI().toString());
                        redirected = true;
                        // Line numbers probably aren't right
                        sourcePosition = SourcePosition.UNKNOWN;
                        // TODO: Set URL which points to the element/attribute path
                    }
                }
            }

            if (!redirected) {
                sb.add(file.toURI().toString());
                if (!SourcePosition.UNKNOWN.equals(sourcePosition)) {
                    sb.add(":");
                    sb.add(String.valueOf(sourcePosition.getStartLine()));
                    sb.add(":");
                    sb.add(String.valueOf(sourcePosition.getStartColumn()));
                }
            }
            sb.addHtml("\">");

            sb.add(source);
            sb.addHtml("</a>");
            sb.add(" manifest");

            if (FileUtil.filesEqual(file, VfsUtilCore.virtualToIoFile(myFile))) {
                sb.add(" (this file)");
            }

            if (!SourcePosition.UNKNOWN.equals(sourcePosition)) {
                sb.add(", line ");
                sb.add(Integer.toString(sourcePosition.getStartLine()));
            }
        }
    }

    /**
     * @see ColorUtil#softer(Color)
     */
    @NotNull
    public static Color harder(@NotNull Color color) {
        if (color.getBlue() == color.getRed() && color.getRed() == color.getGreen())
            return color;
        final float[] hsb = Color.RGBtoHSB(color.getRed(), color.getGreen(), color.getBlue(), null);
        return Color.getHSBColor(hsb[0], 1f, hsb[2]);
    }

    static class ManifestTreeNode extends DefaultMutableTreeNode {

        public ManifestTreeNode(@NotNull Node obj) {
            super(obj);
        }

        @Override
        @NotNull
        public Node getUserObject() {
            return (Node) super.getUserObject();
        }

        @Override
        public int getChildCount() {
            Node obj = getUserObject();
            if (obj instanceof Element) {
                Element element = (Element) obj;
                NamedNodeMap attributes = element.getAttributes();
                int count = attributes.getLength();
                NodeList childNodes = element.getChildNodes();
                for (int i = 0, n = childNodes.getLength(); i < n; i++) {
                    Node child = childNodes.item(i);
                    if (child.getNodeType() == Node.ELEMENT_NODE) {
                        count++;
                    }
                }

                return count;
            }
            return 0;
        }

        @Override
        @NotNull
        public ManifestTreeNode getChildAt(int index) {
            Node obj = getUserObject();
            if (children == null && obj instanceof Element) {
                Element element = (Element) obj;
                NamedNodeMap attributes = element.getAttributes();
                for (int i = 0, n = attributes.getLength(); i < n; i++) {
                    add(new ManifestTreeNode(attributes.item(i)));
                }
                NodeList childNodes = element.getChildNodes();
                for (int i = 0, n = childNodes.getLength(); i < n; i++) {
                    Node child = childNodes.item(i);
                    if (child.getNodeType() == Node.ELEMENT_NODE) {
                        add(new ManifestTreeNode(child));
                    }
                }
            }
            return (ManifestTreeNode) super.getChildAt(index);
        }

        @Override
        public void add(@NotNull MutableTreeNode newChild) {
            // as we override getChildCount to not use the children Vector
            // we need to make sure add inserts into the correct place.
            insert(newChild, children == null ? 0 : children.size());
        }

        @Override
        @NotNull
        public String toString() {
            Node obj = getUserObject();
            if (obj instanceof Attr) {
                Attr xmlAttribute = (Attr) obj;
                return xmlAttribute.getName() + " = " + xmlAttribute.getValue();
            }
            if (obj instanceof Element) {
                Element xmlTag = (Element) obj;
                return xmlTag.getTagName();
            }
            return obj.toString();
        }

        @Override
        @Nullable
        public ManifestTreeNode getParent() {
            return (ManifestTreeNode) super.getParent();
        }

        @NotNull
        public ManifestTreeNode lastAttribute() {
            Node xmlTag = getUserObject();
            return getChildAt(xmlTag.getAttributes().getLength() - 1);
        }

        public boolean hasElementChildren() {
            Node node = getUserObject();
            if (node instanceof Attr) {
                ManifestTreeNode parent = getParent();
                assert parent != null; // all attribute nodes have a parent element node
                return parent.hasElementChildren();
            } else {
                return node.getChildNodes().getLength() > 0;
            }
        }
    }

    /**
     * Cellrenderer which renders XML Element and Attr nodes using the current color scheme's
     * syntax token colors
     */
    private class SyntaxHighlightingCellRenderer extends ColoredTreeCellRenderer {
        // We have to use ColoredTreeCellRenderer instead of DefaultTreeCellRenderer to allow the Tree.isFileColorsEnabled to work
        // as otherwise the DefaultTreeCellRenderer will always insist on filling the background

        private final SimpleTextAttributes myTagNameAttributes;
        private final SimpleTextAttributes myNameAttributes;
        private final SimpleTextAttributes myValueAttributes;
        private final SimpleTextAttributes myPrefixAttributes;

        public SyntaxHighlightingCellRenderer() {
            EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme();
            Color tagNameColor = globalScheme.getAttributes(XmlHighlighterColors.XML_TAG_NAME).getForegroundColor();
            Color nameColor = globalScheme.getAttributes(XmlHighlighterColors.XML_ATTRIBUTE_NAME)
                    .getForegroundColor();
            Color valueColor = globalScheme.getAttributes(XmlHighlighterColors.XML_ATTRIBUTE_VALUE)
                    .getForegroundColor();
            Color prefixColor = globalScheme.getAttributes(XmlHighlighterColors.XML_NS_PREFIX).getForegroundColor();
            myTagNameAttributes = new SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, tagNameColor);
            myNameAttributes = new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, nameColor);
            myValueAttributes = new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, valueColor);
            myPrefixAttributes = new SimpleTextAttributes(SimpleTextAttributes.STYLE_PLAIN, prefixColor);
        }

        @Override
        public void customizeCellRenderer(@NotNull JTree tree, Object value, boolean selected, boolean expanded,
                boolean leaf, int row, boolean hasFocus) {
            if (value instanceof ManifestTreeNode) {
                ManifestTreeNode node = (ManifestTreeNode) value;

                // on GTK theme the Tree.isFileColorsEnabled does not work, so we fall back to using the background
                if (UIUtil.isUnderGTKLookAndFeel()) {
                    // we need to make the colors saturated, but with alpha, so the selector and foreground text still work
                    setBackground(ColorUtil.withAlpha(harder(getNodeColor(node.getUserObject())), 0.2));
                    setOpaque(true);
                }

                setIcon(null);

                if (node.getUserObject() instanceof Element) {
                    Element element = (Element) node.getUserObject();
                    append("<");

                    append(element.getTagName(), myTagNameAttributes);
                    if (!expanded) {
                        append(" ... " + getCloseTag(node));
                    }
                }
                if (node.getUserObject() instanceof Attr) {
                    Attr attr = (Attr) node.getUserObject();
                    // if we are the last child, add ">"
                    ManifestTreeNode parent = node.getParent();
                    assert parent != null; // can not be null if we are a XmlAttribute

                    if (attr.getPrefix() != null) {
                        append(attr.getPrefix(), myPrefixAttributes);
                        append(":");
                        append(attr.getLocalName(), myNameAttributes);
                    } else {
                        append(attr.getName(), myNameAttributes);
                    }
                    append("=\"");
                    append(attr.getValue(), myValueAttributes);
                    append("\"");
                    if (parent.lastAttribute() == node) {
                        append(" " + getCloseTag(node));
                    }
                }
            }
        }

        private String getCloseTag(ManifestTreeNode node) {
            return node.hasElementChildren() ? ">" : "/>";
        }
    }

    private class FileColorTree extends Tree {
        public FileColorTree() {
            setFont(myDefaultFont);
            setBackground(myBackgroundColor);
        }

        /**
         * @see com.intellij.ide.projectView.impl.ProjectViewTree#isFileColorsEnabledFor(JTree)
         */
        @Override
        public boolean isFileColorsEnabled() {
            if (isOpaque()) {
                // needed for fileColors to be able to paint
                setOpaque(false);
            }
            return true;
        }

        @Nullable
        @Override
        public Color getFileColorFor(Object object) {
            return getNodeColor((Node) object);
        }
    }
}