/* * Copyright 2000-2016 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.intellij.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.EditorCustomElementRenderer; import com.intellij.openapi.editor.Inlay; import com.intellij.openapi.editor.InlayModel; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.ex.DocumentEx; import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; import com.intellij.openapi.util.Getter; import com.intellij.util.DocumentUtil; import com.intellij.util.EventDispatcher; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import java.awt.*; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; public class InlayModelImpl implements InlayModel, Disposable { private static final Logger LOG = Logger.getInstance(InlayModelImpl.class); private static final Comparator<Inlay> INLAY_COMPARATOR = Comparator.comparingInt(Inlay::getOffset) .thenComparingInt(i -> ((InlayImpl)i).myOriginalOffset); private final EditorImpl myEditor; private final EventDispatcher<Listener> myDispatcher = EventDispatcher.create(Listener.class); final RangeMarkerTree<InlayImpl> myInlayTree; boolean myStickToLargerOffsetsOnUpdate; InlayModelImpl(@NotNull EditorImpl editor) { myEditor = editor; myInlayTree = new RangeMarkerTree<InlayImpl>(editor.getDocument()) { @NotNull @Override protected RMNode<InlayImpl> createNewNode(@NotNull InlayImpl key, int start, int end, boolean greedyToLeft, boolean greedyToRight, int layer) { return new RMNode<InlayImpl>(this, key, start, end, greedyToLeft, greedyToRight) { @Override protected Getter<InlayImpl> createGetter(@NotNull InlayImpl interval) { return interval; } }; } @Override void fireBeforeRemoved(@NotNull InlayImpl markerEx, @NotNull @NonNls Object reason) { if (markerEx.myOffsetBeforeDisposal == -1) { notifyRemoved(markerEx); } } }; myEditor.getDocument().addDocumentListener(new PrioritizedDocumentListener() { @Override public int getPriority() { return EditorDocumentPriorities.INLAY_MODEL; } @Override public void beforeDocumentChange(DocumentEvent event) { if (myEditor.getDocument().isInBulkUpdate()) return; int offset = event.getOffset(); if (event.getOldLength() == 0 && offset == myEditor.getCaretModel().getOffset() && hasInlineElementAt(offset) && myEditor.getCaretModel().getVisualPosition().equals(myEditor.offsetToVisualPosition(offset, false, false))) { myStickToLargerOffsetsOnUpdate = true; } } @Override public void documentChanged(DocumentEvent event) { myStickToLargerOffsetsOnUpdate = false; } }, this); } void reinitSettings() { myInlayTree.processAll(inlay -> { inlay.updateSize(); return true; }); } @Override public void dispose() { myInlayTree.dispose(); } @Nullable @Override public Inlay addInlineElement(int offset, @NotNull EditorCustomElementRenderer renderer) { ApplicationManager.getApplication().assertIsDispatchThread(); DocumentEx document = myEditor.getDocument(); if (DocumentUtil.isInsideSurrogatePair(document, offset)) return null; offset = Math.max(0, Math.min(document.getTextLength(), offset)); InlayImpl inlay = new InlayImpl(myEditor, offset, renderer); notifyAdded(inlay); return inlay; } @NotNull @Override public List<Inlay> getInlineElementsInRange(int startOffset, int endOffset) { List<Inlay> result = new ArrayList<>(); myInlayTree.processOverlappingWith(startOffset, endOffset, inlay -> { result.add(inlay); return true; }); Collections.sort(result, INLAY_COMPARATOR); return result; } @Override public boolean hasInlineElementAt(int offset) { return !myInlayTree.processOverlappingWith(offset, offset, inlay -> false); } @Override public boolean hasInlineElementAt(@NotNull VisualPosition visualPosition) { int offset = myEditor.logicalPositionToOffset(myEditor.visualToLogicalPosition(visualPosition)); if (!hasInlineElementAt(offset)) return false; VisualPosition inlayStartPosition = myEditor.offsetToVisualPosition(offset, false, false); return visualPosition.equals(inlayStartPosition); } @Nullable @Override public Inlay getElementAt(@NotNull Point point) { if (myInlayTree.size() == 0) return null; int offset = myEditor.logicalPositionToOffset(myEditor.xyToLogicalPosition(point)); List<Inlay> inlays = getInlineElementsInRange(offset, offset); if (inlays.isEmpty()) return null; VisualPosition startVisualPosition = myEditor.offsetToVisualPosition(offset); int x = myEditor.visualPositionToXY(startVisualPosition).x; for (Inlay inlay : inlays) { int endX = x + inlay.getWidthInPixels(); if (point.x >= x && point.x < endX) return inlay; x = endX; } return null; } @Override public void addListener(@NotNull Listener listener, @NotNull Disposable disposable) { myDispatcher.addListener(listener, disposable); } private void notifyAdded(InlayImpl inlay) { myDispatcher.getMulticaster().onAdded(inlay); } void notifyChanged(InlayImpl inlay) { myDispatcher.getMulticaster().onUpdated(inlay); } void notifyRemoved(InlayImpl inlay) { myDispatcher.getMulticaster().onRemoved(inlay); } @TestOnly public void validateState() { for (Inlay inlay : getInlineElementsInRange(0, myEditor.getDocument().getTextLength())) { LOG.assertTrue(!DocumentUtil.isInsideSurrogatePair(myEditor.getDocument(), inlay.getOffset())); } } }