All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.netbeans.modules.gsf.codecoverage.CoverageHighlightsContainer Maven / Gradle / Ivy

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.gsf.codecoverage;

import java.awt.Color;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.Position;
import javax.swing.text.SimpleAttributeSet;
import javax.swing.text.StyleConstants;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.settings.AttributesUtilities;
import org.netbeans.api.editor.settings.FontColorSettings;
import org.netbeans.api.project.FileOwnerQuery;
import org.netbeans.api.project.Project;
import org.netbeans.editor.BaseDocument;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.gsf.codecoverage.api.CoverageType;
import org.netbeans.modules.gsf.codecoverage.api.FileCoverageDetails;
import org.netbeans.spi.editor.highlighting.HighlightsSequence;
import org.netbeans.spi.editor.highlighting.support.AbstractHighlightsContainer;
import org.openide.filesystems.FileObject;
import org.openide.util.Exceptions;
import org.openide.util.WeakListeners;

/**
 * Highlight coverage lines and react to line edits
 *
 * @author Tor Norbye
 */
public class CoverageHighlightsContainer extends AbstractHighlightsContainer implements DocumentListener {

    private AttributeSet covered;
    private AttributeSet uncovered;
    private AttributeSet inferred;
    private AttributeSet partial;
    private List lastPositions;
    private List lastTypes;
    private boolean enabled;
    private boolean listening;
    private final JTextComponent component;
    private final BaseDocument doc;
    private final String mimeType;
    private long version = 0;
    private FileObject fileObject;
    private Project project;

    private static final String COLORING_COVERED = "coverage-covered"; //NOI18N
    private static final String COLORING_UNCOVERED = "coverage-uncovered"; //NOI18N
    private static final String COLORING_INFERRED = "coverage-inferred"; //NOI18N
    private static final String COLORING_PARTIAL = "coverage-partial"; //NOI18N

    CoverageHighlightsContainer(JTextComponent component) {
        this.component = component;
        Document document = component.getDocument();
        if (document instanceof BaseDocument) {
            this.doc = (BaseDocument) document;
        } else {
            this.doc = null;
        }
        this.mimeType = (String) document.getProperty("mimeType");
    }

    public HighlightsSequence getHighlights(int startOffset, int endOffset) {
        enabled = false;
        CoverageManagerImpl manager = CoverageManagerImpl.getInstance();
        if (doc == null || manager == null || !manager.isEnabled(mimeType)) {
            return HighlightsSequence.EMPTY;
        }
        enabled = true;
        synchronized (this) {
            if (fileObject == null) {
                fileObject = GsfUtilities.findFileObject(doc);
                if (fileObject != null) {
                    project = FileOwnerQuery.getOwner(fileObject);
                } else {
                    project = null;
                }

                if (fileObject == null || project == null) {
                    return HighlightsSequence.EMPTY;
                }
            }
        }

        FileCoverageDetails details = manager.getDetails(project, fileObject, component);
        if (details == null) {
            return HighlightsSequence.EMPTY;
        }

        initColors();
        if (!listening) {
            listening = true;
            doc.addDocumentListener(WeakListeners.document(this, null));
        }

        return new Highlights(0, startOffset, endOffset, details);
    }

    private static Color getColoring(FontColorSettings fcs, String tokenName) {
        AttributeSet as = fcs.getFontColors(tokenName);
        if (as != null) {
            return (Color) as.getAttribute(StyleConstants.Background); //NOI18N
        }
        return null;
    }

    private void initColors() {
        if (covered != null) {
            return;
        }

        Color coveredBc = null;
        Color uncoveredBc = null;
        Color inferredBc = null;
        Color partialBc = null;
        FontColorSettings fcs = MimeLookup.getLookup(mimeType).lookup(FontColorSettings.class);
        if (fcs != null) {
            coveredBc = getColoring(fcs, COLORING_COVERED);
            uncoveredBc = getColoring(fcs, COLORING_UNCOVERED);
            inferredBc = getColoring(fcs, COLORING_INFERRED);
            partialBc = getColoring(fcs, COLORING_PARTIAL);
        }
        if (coveredBc == null) {
            coveredBc = new Color(0xCC, 0xFF, 0xCC);
        }
        if (uncoveredBc == null) {
            uncoveredBc = new Color(0xFF, 0xCC, 0xCC);
        }
        if (inferredBc == null) {
            inferredBc = new Color(0xE0, 0xFF, 0xE0);
        }
        if (partialBc == null) {
            partialBc = new Color(0xFF, 0xFF, 0xE0);
        }

        covered = coveredBc == null ? SimpleAttributeSet.EMPTY : AttributesUtilities.createImmutable(
            StyleConstants.Background, coveredBc,
            ATTR_EXTENDS_EOL, Boolean.TRUE, ATTR_EXTENDS_EMPTY_LINE, Boolean.TRUE);
        uncovered = uncoveredBc == null ? SimpleAttributeSet.EMPTY : AttributesUtilities.createImmutable(
            StyleConstants.Background, uncoveredBc,
            ATTR_EXTENDS_EOL, Boolean.TRUE, ATTR_EXTENDS_EMPTY_LINE, Boolean.TRUE);
        inferred = inferredBc == null ? SimpleAttributeSet.EMPTY : AttributesUtilities.createImmutable(
            StyleConstants.Background, inferredBc,
            ATTR_EXTENDS_EOL, Boolean.TRUE, ATTR_EXTENDS_EMPTY_LINE, Boolean.TRUE);
        partial = partialBc == null ? SimpleAttributeSet.EMPTY : AttributesUtilities.createImmutable(
            StyleConstants.Background, partialBc,
            ATTR_EXTENDS_EOL, Boolean.TRUE, ATTR_EXTENDS_EMPTY_LINE, Boolean.TRUE);
    }

    void refresh() {
        lastPositions = null;
        lastTypes = null;
        fireHighlightsChange(0, doc.getLength());
    }

    private void handleEdits(int offset, int length, boolean inserted) {
        // If we're editing a document with coverage highlights, we need to
        // do a couple of things:
        // (1) If you're inserting a newline AFTER the text on a line, simply
        //    insert a blank (non-highlighted) line after the previous line
        // (2) If you're inserting a newline at the beginning of a highlighted
        //    line (including in the whitespace prefix of the line), then move
        //    the highlight down, and insert a blank line in the previous position
        // (3) If you're editing somewhere in the middle of a line, clear the
        //    coverage line highlight
        try {
            assert length > 0;
            if (inserted) {
                String s = doc.getText(offset, length);
                // Can't just check for \n, because on a newline some languages also
                // add and subtract spaces to the document to implement
                // smart-indent
                if ((s.trim().length() == 0)) { // whitespace changes only?
                    if (LineDocumentUtils.isLineEmpty(doc, offset)
                        || (offset >= LineDocumentUtils.getLineLastNonWhitespace(doc, offset) + 1)
                        || (offset <= LineDocumentUtils.getLineFirstNonWhitespace(doc, offset))) {
                        fireHighlightsChange(offset, offset + length);
                        return;
                    }
                }
            }

            int lineStart = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
            if (lineStart == -1) {
                lineStart = LineDocumentUtils.getLineStart(doc, offset);
            }
            List positions = lastPositions;
            if (positions != null) {
                int positionIndex = findPositionIndex(positions, lineStart);
                if (positionIndex >= 0) {
                    // Create a new list to avoid sync problems
                    List copy = new ArrayList<>(positions);
                    copy.remove(positionIndex);
                    lastPositions = copy;
                    fireHighlightsChange(offset, offset + length);
                }
            }
        } catch (BadLocationException ex) {
            Exceptions.printStackTrace(ex);
        }
    }

    @Override
    public void insertUpdate(DocumentEvent ev) {
        if (enabled) {
            handleEdits(ev.getOffset(), ev.getLength(), true);
        }
    }

    @Override
    public void removeUpdate(DocumentEvent ev) {
        //if (enabled) {
        //    handleEdits(ev.getOffset(), ev.getLength(), false);
        //}
    }

    @Override
    public void changedUpdate(DocumentEvent ev) {
    }

    private int findPositionIndex(List positions, final int target) {
        return Collections.binarySearch(positions, new Position() {
            @Override
            public int getOffset() {
                return target;
            }
        }, new Comparator() {
            @Override
            public int compare(Position pos1, Position pos2) {
                return pos1.getOffset() - pos2.getOffset();
            }
        });
    }

    private class Highlights implements HighlightsSequence {

        private final List positions;
        private final List types;
        private final long version;
        private final int startOffsetBoundary;
        private final int endOffsetBoundary;
        private int startOffset;
        private int endOffset;
        private AttributeSet attributeSet;
        private boolean finished = false;
        private int index;

        private Highlights(long version, int startOffset, int endOffset, FileCoverageDetails details) {
            this.version = version;
            this.startOffsetBoundary = startOffset;
            this.endOffsetBoundary = endOffset;

            if (lastPositions == null) {
                positions = new ArrayList<>();
                types = new ArrayList<>();
                for (int lineno = 0, maxLines = details.getLineCount(); lineno < maxLines; lineno++) {
                    CoverageType type = details.getType(lineno);
                    if (type == CoverageType.COVERED || type == CoverageType.INFERRED
                        || type == CoverageType.NOT_COVERED || type == CoverageType.PARTIAL) {
                        try {
                            int offset = LineDocumentUtils.getLineStartFromIndex(doc, lineno);
                            if (offset == -1) {
                                continue;
                            }
                            // Attach the highlight position to the beginning of text, such
                            // that if we insert a new line at the beginning of a line (or in
                            // the whitespace region) the highlight will move down with the
                            // text
                            int rowStart = LineDocumentUtils.getLineFirstNonWhitespace(doc, offset);
                            if (rowStart != -1) {
                                offset = rowStart;
                            }
                            Position pos = doc.createPosition(offset, Position.Bias.Forward);
                            positions.add(pos);
                            types.add(type);
                        } catch (BadLocationException ex) {
                            Exceptions.printStackTrace(ex);
                        }
                    }
                }
                lastPositions = positions;
                lastTypes = types;
            } else {
                positions = lastPositions;
                types = lastTypes;
            }

            try {
                int lineStart = LineDocumentUtils.getLineFirstNonWhitespace(doc, startOffsetBoundary);
                if (lineStart == -1) {
                    lineStart = LineDocumentUtils.getLineStart(doc, startOffsetBoundary);
                    index = findPositionIndex(positions, lineStart);
                    if (index < 0) {
                        index = -index;
                    }
                }
            } catch (BadLocationException ble) {
            }
        }

        private boolean _moveNext() {
            for (; index < positions.size(); index++) {
                Position pos = positions.get(index);
                int offset = pos.getOffset();
                offset = LineDocumentUtils.getLineStart(doc, offset);
                if (offset > endOffsetBoundary) {
                    break;
                }
                if (offset >= startOffsetBoundary) {
                    startOffset = offset;
                    try {
                        endOffset = LineDocumentUtils.getLineEnd(doc, offset);
                        if (endOffset < doc.getLength()) {
                            endOffset++; // Include end of line
                        }
                        CoverageType type = types.get(index);
                        switch (type) {
                            case COVERED:
                                attributeSet = covered;
                                break;
                            case NOT_COVERED:
                                attributeSet = uncovered;
                                break;
                            case INFERRED:
                                attributeSet = inferred;
                                break;
                            case PARTIAL:
                                attributeSet = partial;
                                break;
                            default:
                                throw new IllegalArgumentException();
                        }
                    } catch (BadLocationException ex) {
                        Exceptions.printStackTrace(ex);
                    }
                    index++;
                    return true;
                }
            }

            return false;
        }

        @Override
        public boolean moveNext() {
            synchronized (CoverageHighlightsContainer.this) {
                if (checkVersion()) {
                    if (_moveNext()) {
                        return true;
                    }
                }
            }

            finished = true;
            return false;
        }

        @Override
        public int getStartOffset() {
            synchronized (CoverageHighlightsContainer.this) {
                if (finished) {
                    throw new NoSuchElementException();
                } else {
                    return startOffset;
                }
            }
        }

        @Override
        public int getEndOffset() {
            synchronized (CoverageHighlightsContainer.this) {
                if (finished) {
                    throw new NoSuchElementException();
                } else {
                    return endOffset;
                }
            }
        }

        @Override
        public AttributeSet getAttributes() {
            synchronized (CoverageHighlightsContainer.this) {
                if (finished) {
                    throw new NoSuchElementException();
                } else {
                    return attributeSet;
                }
            }
        }

        private boolean checkVersion() {
            return this.version == CoverageHighlightsContainer.this.version;
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy