blob: bf30bae53ecafe31aae705d47adfe4f14eb0506c [file] [log] [blame]
/*
* Copyright 2000-2014 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.
*/
/*
* Created by IntelliJ IDEA.
* User: max
* Date: Apr 19, 2002
* Time: 2:26:19 PM
* To change template for new class use
* Code Style | Class Templates options (Tools | IDE Options).
*/
package com.intellij.openapi.editor.impl;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.ex.*;
import com.intellij.openapi.editor.impl.event.MarkupModelListener;
import com.intellij.openapi.editor.markup.HighlighterTargetArea;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.util.CommonProcessors;
import com.intellij.util.Consumer;
import com.intellij.util.DocumentUtil;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.NoSuchElementException;
public class MarkupModelImpl extends UserDataHolderBase implements MarkupModelEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.impl.MarkupModelImpl");
private final DocumentEx myDocument;
private RangeHighlighter[] myCachedHighlighters;
private final List<MarkupModelListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final RangeHighlighterTree myHighlighterTree; // this tree holds regular highlighters with target = HighlighterTargetArea.EXACT_RANGE
private final RangeHighlighterTree myHighlighterTreeForLines; // this tree holds line range highlighters with target = HighlighterTargetArea.LINES_IN_RANGE
MarkupModelImpl(@NotNull DocumentEx document) {
myDocument = document;
myHighlighterTree = new RangeHighlighterTree(document, this);
myHighlighterTreeForLines = new RangeHighlighterTree(document, this);
}
@Override
public void dispose() {
myHighlighterTree.dispose();
myHighlighterTreeForLines.dispose();
}
@Override
@NotNull
public RangeHighlighter addLineHighlighter(int lineNumber, int layer, TextAttributes textAttributes) {
if (isNotValidLine(lineNumber)) {
throw new IndexOutOfBoundsException("lineNumber:" + lineNumber + ". Must be in [0, " + (getDocument().getLineCount() - 1) + "]");
}
int offset = DocumentUtil.getFirstNonSpaceCharOffset(getDocument(), lineNumber);
return addRangeHighlighter(offset, offset, layer, textAttributes, HighlighterTargetArea.LINES_IN_RANGE);
}
@Override
@Nullable
public RangeHighlighterEx addPersistentLineHighlighter(int lineNumber, int layer, TextAttributes textAttributes) {
if (isNotValidLine(lineNumber)) {
return null;
}
int offset = DocumentUtil.getFirstNonSpaceCharOffset(getDocument(), lineNumber);
return addRangeHighlighter(PersistentRangeHighlighterImpl.create(this, offset, layer, HighlighterTargetArea.LINES_IN_RANGE, textAttributes, false), null);
}
private boolean isNotValidLine(int lineNumber) {
return lineNumber >= getDocument().getLineCount() || lineNumber < 0;
}
// NB: Can return invalid highlighters
@Override
@NotNull
public RangeHighlighter[] getAllHighlighters() {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myCachedHighlighters == null) {
int size = myHighlighterTree.size() + myHighlighterTreeForLines.size();
if (size == 0) return RangeHighlighter.EMPTY_ARRAY;
List<RangeHighlighterEx> list = new ArrayList<RangeHighlighterEx>(size);
CommonProcessors.CollectProcessor<RangeHighlighterEx> collectProcessor = new CommonProcessors.CollectProcessor<RangeHighlighterEx>(list);
myHighlighterTree.process(collectProcessor);
myHighlighterTreeForLines.process(collectProcessor);
myCachedHighlighters = list.toArray(new RangeHighlighter[list.size()]);
}
return myCachedHighlighters;
}
@NotNull
@Override
public RangeHighlighterEx addRangeHighlighterAndChangeAttributes(int startOffset,
int endOffset,
int layer,
TextAttributes textAttributes,
@NotNull HighlighterTargetArea targetArea,
boolean isPersistent,
@Nullable Consumer<RangeHighlighterEx> changeAttributesAction) {
return addRangeHighlighter(isPersistent
? PersistentRangeHighlighterImpl.create(this, startOffset, layer, targetArea, textAttributes, true)
: new RangeHighlighterImpl(this, startOffset, endOffset, layer, targetArea, textAttributes, false,
false), changeAttributesAction);
}
@NotNull
private RangeHighlighterEx addRangeHighlighter(@NotNull RangeHighlighterImpl highlighter,
@Nullable Consumer<RangeHighlighterEx> changeAttributesAction) {
ApplicationManager.getApplication().assertIsDispatchThread();
myCachedHighlighters = null;
if (changeAttributesAction != null) {
highlighter.changeAttributesNoEvents(changeAttributesAction);
}
fireAfterAdded(highlighter);
return highlighter;
}
@Override
public void changeAttributesInBatch(@NotNull RangeHighlighterEx highlighter,
@NotNull Consumer<RangeHighlighterEx> changeAttributesAction) {
ApplicationManager.getApplication().assertIsDispatchThread();
RangeHighlighterData.ChangeResult changed = ((RangeHighlighterImpl)highlighter).changeAttributesNoEvents(changeAttributesAction);
if (changed != RangeHighlighterData.ChangeResult.NOT_CHANGED) {
fireAttributesChanged(highlighter, changed == RangeHighlighterData.ChangeResult.RENDERERS_CHANGED);
}
}
@Override
public void addRangeHighlighter(@NotNull RangeHighlighterEx marker,
int start,
int end,
boolean greedyToLeft,
boolean greedyToRight,
int layer) {
ApplicationManager.getApplication().assertIsDispatchThread();
treeFor(marker).addInterval(marker, start, end, greedyToLeft, greedyToRight, layer);
}
private RangeHighlighterTree treeFor(RangeHighlighter marker) {
return marker.getTargetArea() == HighlighterTargetArea.EXACT_RANGE ? myHighlighterTree : myHighlighterTreeForLines;
}
@Override
@NotNull
public RangeHighlighter addRangeHighlighter(int startOffset,
int endOffset,
int layer,
TextAttributes textAttributes,
@NotNull HighlighterTargetArea targetArea) {
return addRangeHighlighterAndChangeAttributes(startOffset, endOffset, layer, textAttributes, targetArea, false, null);
}
@Override
public void removeHighlighter(@NotNull RangeHighlighter segmentHighlighter) {
ApplicationManager.getApplication().assertIsDispatchThread();
myCachedHighlighters = null;
if (!segmentHighlighter.isValid()) return;
boolean removed = treeFor(segmentHighlighter).removeInterval((RangeHighlighterEx)segmentHighlighter);
LOG.assertTrue(removed);
}
@Override
public void removeAllHighlighters() {
ApplicationManager.getApplication().assertIsDispatchThread();
for (RangeHighlighter highlighter : getAllHighlighters()) {
highlighter.dispose();
}
myCachedHighlighters = null;
myHighlighterTree.clear();
myHighlighterTreeForLines.clear();
}
@Override
@NotNull
public Document getDocument() {
return myDocument;
}
@Override
public void addMarkupModelListener(@NotNull Disposable parentDisposable, @NotNull final MarkupModelListener listener) {
myListeners.add(listener);
Disposer.register(parentDisposable, new Disposable() {
@Override
public void dispose() {
removeMarkupModelListener(listener);
}
});
}
public void removeMarkupModelListener(@NotNull MarkupModelListener listener) {
boolean success = myListeners.remove(listener);
LOG.assertTrue(success);
}
@Override
public void setRangeHighlighterAttributes(@NotNull final RangeHighlighter highlighter, @NotNull final TextAttributes textAttributes) {
ApplicationManager.getApplication().assertIsDispatchThread();
((RangeHighlighterEx)highlighter).setTextAttributes(textAttributes);
}
@Override
public void fireAttributesChanged(@NotNull RangeHighlighterEx segmentHighlighter, boolean renderersChanged) {
for (MarkupModelListener listener : myListeners) {
listener.attributesChanged(segmentHighlighter, renderersChanged);
}
}
@Override
public void fireAfterAdded(@NotNull RangeHighlighterEx segmentHighlighter) {
for (MarkupModelListener listener : myListeners) {
listener.afterAdded(segmentHighlighter);
}
}
@Override
public void fireBeforeRemoved(@NotNull RangeHighlighterEx segmentHighlighter) {
for (MarkupModelListener listener : myListeners) {
listener.beforeRemoved(segmentHighlighter);
}
}
@Override
public boolean containsHighlighter(@NotNull final RangeHighlighter highlighter) {
ApplicationManager.getApplication().assertIsDispatchThread();
Processor<RangeHighlighterEx> equalId = new Processor<RangeHighlighterEx>() {
@Override
public boolean process(RangeHighlighterEx h) {
return h.getId() != ((RangeHighlighterEx)highlighter).getId();
}
};
return !treeFor(highlighter).processOverlappingWith(highlighter.getStartOffset(), highlighter.getEndOffset(), equalId);
}
@Override
public boolean processRangeHighlightersOverlappingWith(int start, int end, @NotNull Processor<? super RangeHighlighterEx> processor) {
DisposableIterator<RangeHighlighterEx> iterator = overlappingIterator(start, end);
try {
while (iterator.hasNext()) {
if (!processor.process(iterator.next())) {
return false;
}
}
return true;
}
finally {
iterator.dispose();
}
}
@Override
public boolean processRangeHighlightersOutside(int start, int end, @NotNull Processor<? super RangeHighlighterEx> processor) {
return myHighlighterTree.processOverlappingWithOutside(start, end, processor)
&& myHighlighterTreeForLines.processOverlappingWithOutside(start, end, processor);
}
@Override
@NotNull
public DisposableIterator<RangeHighlighterEx> overlappingIterator(int startOffset, int endOffset) {
startOffset = Math.max(0,startOffset);
IntervalTreeImpl.PeekableIterator<RangeHighlighterEx> exact = myHighlighterTree.overlappingIterator(new TextRangeInterval(startOffset, Math.max(startOffset, endOffset)));
IntervalTreeImpl.PeekableIterator<RangeHighlighterEx> lines = myHighlighterTreeForLines.overlappingIterator(roundToLineBoundaries(startOffset, endOffset));
return merge(exact, lines);
}
@NotNull
private static <T extends RangeHighlighterEx> DisposableIterator<T> merge(@NotNull final IntervalTreeImpl.PeekableIterator<T> iterator1, @NotNull final IntervalTreeImpl.PeekableIterator<T> iterator2) {
return new DisposableIterator<T>() {
@Override
public void dispose() {
iterator1.dispose();
iterator2.dispose();
}
@Override
public boolean hasNext() {
return iterator1.hasNext() || iterator2.hasNext();
}
@Override
public T next() {
T t1 = iterator1.hasNext() ? iterator1.peek() : null;
T t2 = iterator2.hasNext() ? iterator2.peek() : null;
if (t1 == null) {
return iterator2.next();
}
if (t2 == null) {
return iterator1.next();
}
int compare = RangeHighlighterEx.BY_AFFECTED_START_OFFSET.compare(t1, t2);
return (compare < 0 ? iterator1 : iterator2).next();
}
@Override
public void remove() {
throw new NoSuchElementException();
}
};
}
@NotNull
private TextRangeInterval roundToLineBoundaries(int startOffset, int endOffset) {
Document document = getDocument();
int lineStartOffset = startOffset <= 0 ? 0 : document.getLineStartOffset(document.getLineNumber(startOffset));
int lineEndOffset = endOffset <= 0 ? 0 : endOffset >= document.getTextLength() ? document.getTextLength() : document.getLineEndOffset(document.getLineNumber(endOffset));
return new TextRangeInterval(lineStartOffset, lineEndOffset);
}
}