blob: 9dc0ea86c31139afb91d880061129cb4638056bb [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.
*/
package com.intellij.openapi.editor.ex.util;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.colors.TextAttributesKey;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterClient;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.fileTypes.SyntaxHighlighterBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.containers.FactoryMap;
import com.intellij.util.containers.IntArrayList;
import com.intellij.util.text.MergingCharSequence;
import gnu.trove.TIntIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.lang.reflect.Array;
import java.util.*;
/**
* @author max
*/
public class LayeredLexerEditorHighlighter extends LexerEditorHighlighter {
private final Map<IElementType, LayerDescriptor> myTokensToLayer = new HashMap<IElementType, LayerDescriptor>();
private final Map<LayerDescriptor, Mapper> myLayerBuffers = new HashMap<LayerDescriptor, Mapper>();
private CharSequence myText;
public LayeredLexerEditorHighlighter(@NotNull SyntaxHighlighter highlighter, @NotNull EditorColorsScheme scheme) {
super(highlighter, scheme);
}
@Override
protected SegmentArrayWithData createSegments() {
return new MappingSegments();
}
public synchronized void registerLayer(IElementType tokenType, LayerDescriptor layerHighlighter) {
myTokensToLayer.put(tokenType, layerHighlighter);
getSegments().removeAll();
}
public synchronized void unregisterLayer(IElementType tokenType) {
final LayerDescriptor layer = myTokensToLayer.remove(tokenType);
if (layer != null) {
myLayerBuffers.remove(layer);
getSegments().removeAll();
}
}
@Override
public MappingSegments getSegments() {
return (MappingSegments)super.getSegments();
}
private class LightMapper {
final Mapper mapper;
final StringBuilder text = new StringBuilder();
final IntArrayList lengths = new IntArrayList();
final List<IElementType> tokenTypes = new ArrayList<IElementType>();
final TIntIntHashMap index2Global = new TIntIntHashMap();
private final String mySeparator;
final int insertOffset;
LightMapper(final Mapper mapper, int insertOffset) {
this.mapper = mapper;
mySeparator = mapper.mySeparator;
this.insertOffset = insertOffset;
}
void addToken(CharSequence tokenText, IElementType tokenType, int globalIndex) {
index2Global.put(tokenTypes.size(), globalIndex);
text.append(mySeparator).append(tokenText);
lengths.add(tokenText.length());
tokenTypes.add(tokenType);
}
void finish() {
assert insertOffset >= 0;
final DocumentImpl document = mapper.doc;
document.insertString(insertOffset, text);
int start = insertOffset;
for (int i = 0; i < tokenTypes.size(); i++) {
IElementType type = tokenTypes.get(i);
final int len = lengths.get(i);
start += mySeparator.length();
final int globalIndex = index2Global.get(i);
assert getSegments().myRanges[globalIndex] == null : myText;
getSegments().myRanges[globalIndex] = new MappedRange(mapper, document.createRangeMarker(start, start + len), type);
start += len;
}
}
}
@Override
public void setText(@NotNull final CharSequence text) {
// do NOT synchronize before updateLayers due to deadlock with PsiLock
updateLayers();
myText = text;
super.setText(text);
}
@Override
protected TokenProcessor createTokenProcessor(final int startIndex) {
return new TokenProcessor() {
final Map<Mapper, LightMapper> docTexts = new FactoryMap<Mapper, LightMapper>() {
@Override
protected LightMapper create(final Mapper key) {
final MappedRange predecessor = key.findPredecessor(startIndex);
return new LightMapper(key, predecessor != null ? predecessor.range.getEndOffset() : 0);
}
};
@Override
public void addToken(final int i, final int startOffset, final int endOffset, final int data, final IElementType tokenType) {
getSegments().setElementLight(i, startOffset, endOffset, data);
final Mapper mapper = getMappingDocument(tokenType);
if (mapper != null) {
docTexts.get(mapper).addToken(myText.subSequence(startOffset, endOffset), tokenType, i);
}
}
@Override
public void finish() {
for (final LightMapper mapper : docTexts.values()) {
mapper.finish();
}
}
};
}
protected boolean updateLayers() { return false; }
@Override
public void documentChanged(DocumentEvent e) {
// do NOT synchronize before updateLayers due to deadlock with PsiLock
final boolean b = updateLayers();
synchronized (this) {
myText = e.getDocument().getCharsSequence();
if (b) {
setText(myText);
}
else {
super.documentChanged(e);
}
}
}
@NotNull
@Override
public HighlighterIterator createIterator(int startOffset) {
// do NOT synchronize before updateLayers due to deadlock with PsiLock
final boolean b = updateLayers();
synchronized (this) {
if (b) {
setText(myText);
}
return new LayeredHighlighterIteratorImpl(startOffset);
}
}
private class MappingSegments extends SegmentArrayWithData {
MappedRange[] myRanges = new MappedRange[INITIAL_SIZE];
@Override
public void removeAll() {
if (mySegmentCount != 0) {
Arrays.fill(myRanges, null);
}
myLayerBuffers.clear();
super.removeAll();
}
@Override
public void setElementAt(int i, int startOffset, int endOffset, int data) {
setElementLight(i, startOffset, endOffset, (short)data);
final MappedRange range = myRanges[i];
if (range != null) {
range.mapper.removeMapping(range);
myRanges[i] = null;
}
updateMappingForToken(i);
}
private void setElementLight(final int i, final int startOffset, final int endOffset, final int data) {
super.setElementAt(i, startOffset, endOffset, data);
myRanges = LayeredLexerEditorHighlighter.reallocateArray(myRanges, i + 1);
}
@Override
public void remove(int startIndex, int endIndex) {
Map<Mapper, Integer> mins = new FactoryMap<Mapper, Integer>() {
@Override
protected Integer create(final Mapper key) {
return Integer.MAX_VALUE;
}
};
Map<Mapper, Integer> maxs = new FactoryMap<Mapper, Integer>() {
@Override
protected Integer create(final Mapper key) {
return 0;
}
};
for (int i = startIndex; i < endIndex; i++) {
final MappedRange range = myRanges[i];
if (range != null && range.range.isValid()) {
mins.put(range.mapper, Math.min(mins.get(range.mapper).intValue(), range.range.getStartOffset()));
maxs.put(range.mapper, Math.max(maxs.get(range.mapper).intValue(), range.range.getEndOffset()));
}
myRanges[i] = null;
}
for (final Mapper mapper : maxs.keySet()) {
mapper.doc.deleteString(mins.get(mapper).intValue(), maxs.get(mapper).intValue());
}
myRanges = remove(myRanges, startIndex, endIndex);
super.remove(startIndex, endIndex);
}
@Override
public void replace(int startOffset, @NotNull SegmentArrayWithData data, int len) {
super.replace(startOffset, data, len);
for (int i = startOffset; i < startOffset + len; i++) {
updateMappingForToken(i);
}
}
@NotNull
private <T> T[] insert(@NotNull T[] array, @NotNull T[] insertArray, int startIndex, int insertLength) {
T[] newArray = LayeredLexerEditorHighlighter.reallocateArray(array, mySegmentCount + insertLength);
if (startIndex < mySegmentCount) {
System.arraycopy(newArray, startIndex, newArray, startIndex + insertLength, mySegmentCount - startIndex);
}
System.arraycopy(insertArray, 0, newArray, startIndex, insertLength);
return newArray;
}
@NotNull
private <T> T[] remove(@NotNull T[] array, int startIndex, int endIndex) {
if (endIndex < mySegmentCount) {
System.arraycopy(array, endIndex, array, startIndex, mySegmentCount - endIndex);
}
return array;
}
@Override
public void insert(@NotNull SegmentArrayWithData segmentArray, final int startIndex) {
synchronized (LayeredLexerEditorHighlighter.this) {
super.insert(segmentArray, startIndex);
final int newCount = segmentArray.getSegmentCount();
final MappedRange[] newRanges = new MappedRange[newCount];
myRanges = insert(myRanges, newRanges, startIndex, newCount);
int endIndex = startIndex + segmentArray.getSegmentCount();
TokenProcessor processor = createTokenProcessor(startIndex);
for (int i = startIndex; i < endIndex; i++) {
final short data = getSegmentData(i);
final IElementType token = unpackToken(data);
processor.addToken(i, getSegmentStart(i), getSegmentEnd(i), data, token);
}
processor.finish();
}
}
private void updateMappingForToken(final int i) {
final short data = getSegmentData(i);
final IElementType token = unpackToken(data);
final Mapper mapper = getMappingDocument(token);
final MappedRange oldMapping = myRanges[i];
if (mapper != null) {
if (oldMapping != null) {
if (oldMapping.mapper == mapper && oldMapping.outerToken == token) {
mapper.updateMapping(i, oldMapping);
}
else {
oldMapping.mapper.removeMapping(oldMapping);
myRanges[i] = mapper.insertMapping(i, token);
}
}
else {
myRanges[i] = mapper.insertMapping(i, token);
}
}
else {
if (oldMapping != null) {
oldMapping.mapper.removeMapping(oldMapping);
myRanges[i] = null;
}
}
}
}
private class Mapper implements HighlighterClient {
private final DocumentImpl doc;
private final EditorHighlighter highlighter;
private final String mySeparator;
private final Map<IElementType, TextAttributes> myAttributesMap = new HashMap<IElementType, TextAttributes>();
private final SyntaxHighlighter mySyntaxHighlighter;
private final TextAttributesKey myBackground;
private Mapper(LayerDescriptor descriptor) {
doc = new DocumentImpl("",true);
mySyntaxHighlighter = descriptor.getLayerHighlighter();
myBackground = descriptor.getBackgroundKey();
highlighter = new LexerEditorHighlighter(mySyntaxHighlighter, getScheme());
mySeparator = descriptor.getTokenSeparator();
highlighter.setEditor(this);
doc.addDocumentListener(highlighter);
}
public TextAttributes getAttributes(IElementType tokenType) {
TextAttributes attrs = myAttributesMap.get(tokenType);
if (attrs == null) {
attrs = convertAttributes(SyntaxHighlighterBase.pack(myBackground, mySyntaxHighlighter.getTokenHighlights(tokenType)));
myAttributesMap.put(tokenType, attrs);
}
return attrs;
}
public HighlighterIterator createIterator(MappedRange mapper, int shift) {
final int rangeStart = mapper.range.getStartOffset();
final int rangeEnd = mapper.range.getEndOffset();
return new LimitedRangeHighlighterIterator(highlighter.createIterator(rangeStart + shift), rangeStart, rangeEnd);
}
@Override
public Project getProject() {
return getClient().getProject();
}
@Override
public void repaint(int start, int end) {
// TODO: map ranges to outer document
}
@Override
public Document getDocument() {
return LayeredLexerEditorHighlighter.this.getDocument();
}
public void resetCachedTextAttributes() {
// after color scheme was changed we need to reset cached attributes
myAttributesMap.clear();
}
public void updateMapping(final int tokenIndex, final MappedRange oldMapping) {
CharSequence tokenText = getTokenText(tokenIndex);
final int start = oldMapping.range.getStartOffset();
final int end = oldMapping.range.getEndOffset();
if (Comparing.equal(doc.getCharsSequence().subSequence(start, end), tokenText)) return;
doc.replaceString(start, end, tokenText);
final int newEnd = start + tokenText.length();
if (oldMapping.range.getStartOffset() != start || oldMapping.range.getEndOffset() != newEnd) {
assert oldMapping.range.getDocument() == doc;
oldMapping.range.dispose();
oldMapping.range = doc.createRangeMarker(start, newEnd);
}
}
@NotNull
private MappedRange insertMapping(int tokenIndex, IElementType outerToken) {
CharSequence tokenText = getTokenText(tokenIndex);
final int length = tokenText.length();
MappedRange predecessor = findPredecessor(tokenIndex);
int insertOffset = predecessor != null ? predecessor.range.getEndOffset() : 0;
doc.insertString(insertOffset, new MergingCharSequence(mySeparator, tokenText));
insertOffset += mySeparator.length();
RangeMarker marker = doc.createRangeMarker(insertOffset, insertOffset + length);
return new MappedRange(this, marker, outerToken);
}
private CharSequence getTokenText(final int tokenIndex) {
return myText.subSequence(getSegments().getSegmentStart(tokenIndex), getSegments().getSegmentEnd(tokenIndex));
}
@Nullable
private MappedRange findPredecessor(int token) {
token--;
while (token >= 0) {
final MappedRange mappedRange = getSegments().myRanges[token];
if (mappedRange != null && mappedRange.mapper == this) return mappedRange;
token--;
}
return null;
}
private void removeMapping(MappedRange mapping) {
RangeMarker rangeMarker = mapping.range;
if (rangeMarker.isValid()) {
final int start = rangeMarker.getStartOffset();
final int end = rangeMarker.getEndOffset();
assert doc == rangeMarker.getDocument();
doc.deleteString(start - mySeparator.length(), end);
rangeMarker.dispose();
}
}
}
private static class MappedRange {
private RangeMarker range;
private final Mapper mapper;
private final IElementType outerToken;
MappedRange(@NotNull Mapper mapper, @NotNull RangeMarker range, @NotNull IElementType outerToken) {
this.mapper = mapper;
this.range = range;
this.outerToken = outerToken;
assert mapper.doc == range.getDocument();
}
}
@Nullable
private Mapper getMappingDocument(IElementType token) {
final LayerDescriptor descriptor = myTokensToLayer.get(token);
if (descriptor == null) return null;
Mapper mapper = myLayerBuffers.get(descriptor);
if (mapper == null) {
mapper = new Mapper(descriptor);
myLayerBuffers.put(descriptor, mapper);
}
return mapper;
}
@Override
public void setColorScheme(@NotNull EditorColorsScheme scheme) {
super.setColorScheme(scheme);
for (MappedRange mapping : getSegments().myRanges) {
final Mapper mapper = mapping == null ? null : mapping.mapper;
if (mapper != null) {
mapper.resetCachedTextAttributes();
}
}
}
private class LayeredHighlighterIteratorImpl implements LayeredHighlighterIterator {
private final HighlighterIterator myBaseIterator;
private HighlighterIterator myLayerIterator;
private int myLayerStartOffset = 0;
private Mapper myCurrentMapper;
private LayeredHighlighterIteratorImpl(int offset) {
myBaseIterator = LayeredLexerEditorHighlighter.super.createIterator(offset);
if (!myBaseIterator.atEnd()) {
int shift = offset - myBaseIterator.getStart();
initLayer(shift);
}
}
private void initLayer(final int shiftInToken) {
if (myBaseIterator.atEnd()) {
myLayerIterator = null;
myCurrentMapper = null;
return;
}
MappedRange mapping = getSegments().myRanges[((HighlighterIteratorImpl)myBaseIterator).currentIndex()];
if (mapping != null) {
myCurrentMapper = mapping.mapper;
myLayerIterator = myCurrentMapper.createIterator(mapping, shiftInToken);
myLayerStartOffset = myBaseIterator.getStart() - mapping.range.getStartOffset();
}
else {
myCurrentMapper = null;
myLayerIterator = null;
}
}
@Override
public TextAttributes getTextAttributes() {
if (myCurrentMapper != null) {
return myCurrentMapper.getAttributes(getTokenType());
}
return myBaseIterator.getTextAttributes();
}
public SyntaxHighlighter getActiveSyntaxHighlighter() {
if (myCurrentMapper != null) {
return myCurrentMapper.mySyntaxHighlighter;
}
return LayeredLexerEditorHighlighter.this.getSyntaxHighlighter();
}
@Override
public int getStart() {
if (myLayerIterator != null) {
return myLayerIterator.getStart() + myLayerStartOffset;
}
return myBaseIterator.getStart();
}
@Override
public int getEnd() {
if (myLayerIterator != null) {
return myLayerIterator.getEnd() + myLayerStartOffset;
}
return myBaseIterator.getEnd();
}
@Override
public IElementType getTokenType() {
return myLayerIterator != null ? myLayerIterator.getTokenType() : myBaseIterator.getTokenType();
}
@Override
public void advance() {
if (myLayerIterator != null) {
myLayerIterator.advance();
if (!myLayerIterator.atEnd()) return;
}
myBaseIterator.advance();
initLayer(0);
}
@Override
public void retreat() {
if (myLayerIterator != null) {
myLayerIterator.retreat();
if (!myLayerIterator.atEnd()) return;
}
myBaseIterator.retreat();
initLayer(myBaseIterator.atEnd() ? 0 : myBaseIterator.getEnd() - myBaseIterator.getStart() - 1);
}
@Override
public boolean atEnd() {
return myBaseIterator.atEnd();
}
@Override
public Document getDocument() {
return myBaseIterator.getDocument();
}
}
@SuppressWarnings({"unchecked"})
@NotNull
protected static <T> T[] reallocateArray(@NotNull T[] array, int index) {
if (index < array.length) return array;
T[] newArray = (T[])Array.newInstance(array.getClass().getComponentType(), SegmentArray.calcCapacity(array.length, index));
System.arraycopy(array, 0, newArray, 0, array.length);
return newArray;
}
}