/* * 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.lexer.Lexer; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.HighlighterColors; 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.ex.DocumentEx; import com.intellij.openapi.editor.ex.PrioritizedDocumentListener; 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.EditorDocumentPriorities; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.fileTypes.PlainSyntaxHighlighter; import com.intellij.openapi.fileTypes.SyntaxHighlighter; import com.intellij.openapi.project.DumbAwareRunnable; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Comparing; import com.intellij.psi.tree.IElementType; import com.intellij.util.ArrayUtil; import com.intellij.util.ui.UIUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.HashMap; import java.util.Map; public class LexerEditorHighlighter implements EditorHighlighter, PrioritizedDocumentListener { private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.editor.ex.util.LexerEditorHighlighter"); private HighlighterClient myEditor; private final Lexer myLexer; private final Map myAttributesMap = new HashMap(); private final SegmentArrayWithData mySegments; private final SyntaxHighlighter myHighlighter; private EditorColorsScheme myScheme; private final int myInitialState; public LexerEditorHighlighter(@NotNull SyntaxHighlighter highlighter, @NotNull EditorColorsScheme scheme) { myScheme = scheme; myLexer = highlighter.getHighlightingLexer(); myLexer.start(ArrayUtil.EMPTY_CHAR_SEQUENCE); myInitialState = myLexer.getState(); myHighlighter = highlighter; mySegments = createSegments(); } protected SegmentArrayWithData createSegments() { return new SegmentArrayWithData(); } public boolean isPlain() { return myHighlighter instanceof PlainSyntaxHighlighter; } @Nullable protected final Document getDocument() { return myEditor != null ? myEditor.getDocument() : null; } public final synchronized boolean checkContentIsEqualTo(CharSequence sequence) { final Document document = getDocument(); return document != null && Comparing.equal(document.getImmutableCharSequence(), sequence); } public EditorColorsScheme getScheme() { return myScheme; } protected Lexer getLexer() { return myLexer; } @Override public void setEditor(@NotNull HighlighterClient editor) { LOG.assertTrue(myEditor == null, "Highlighters cannot be reused with different editors"); myEditor = editor; } @Override public void setColorScheme(@NotNull EditorColorsScheme scheme) { myScheme = scheme; myAttributesMap.clear(); } @NotNull @Override public HighlighterIterator createIterator(int startOffset) { synchronized (this) { final Document document = getDocument(); if(document instanceof DocumentEx && ((DocumentEx)document).isInBulkUpdate()) { ((DocumentEx)document).setInBulkUpdate(false); // bulk mode failed } if (mySegments.getSegmentCount() == 0 && document != null && document.getTextLength() > 0) { // bulk mode was reset doSetText(document.getCharsSequence()); } final int latestValidOffset = mySegments.getLastValidOffset(); return new HighlighterIteratorImpl(startOffset <= latestValidOffset ? startOffset : latestValidOffset); } } private int packData(IElementType tokenType, int state) { final short idx = tokenType.getIndex(); return state == myInitialState ? idx : -idx; } public boolean isValid() { Project project = myEditor.getProject(); return project != null && !project.isDisposed(); } private static boolean isInitialState(int data) { return data >= 0; } protected static IElementType unpackToken(int data) { return IElementType.find((short)Math.abs(data)); } @Override public synchronized void documentChanged(DocumentEvent e) { final Document document = e.getDocument(); if (document instanceof DocumentEx && ((DocumentEx)document).isInBulkUpdate()) { mySegments.removeAll(); return; } if(mySegments.getSegmentCount() == 0) { setText(document.getCharsSequence()); return; } CharSequence text = document.getCharsSequence(); int oldStartOffset = e.getOffset(); final int segmentIndex; try { segmentIndex = mySegments.findSegmentIndex(oldStartOffset) - 2; } catch (IndexOutOfBoundsException ex) { throw new IndexOutOfBoundsException(ex.getMessage() + " Lexer: " + myLexer); } final int oldStartIndex = Math.max(0, segmentIndex); int startIndex = oldStartIndex; int data; do { data = mySegments.getSegmentData(startIndex); if (isInitialState(data)|| startIndex == 0) break; startIndex--; } while (true); int startOffset = mySegments.getSegmentStart(startIndex); int newEndOffset = e.getOffset() + e.getNewLength(); myLexer.start(text, startOffset, text.length(), myInitialState); int lastTokenStart = -1; int lastLexerState = -1; while (myLexer.getTokenType() != null) { if (startIndex >= oldStartIndex) break; int tokenStart = myLexer.getTokenStart(); int lexerState = myLexer.getState(); if (tokenStart == lastTokenStart && lexerState == lastLexerState) { throw new IllegalStateException("Error while updating lexer: " + e + " document text: " + document.getText()); } int tokenEnd = myLexer.getTokenEnd(); data = packData(myLexer.getTokenType(), lexerState); if (mySegments.getSegmentStart(startIndex) != tokenStart || mySegments.getSegmentEnd(startIndex) != tokenEnd || mySegments.getSegmentData(startIndex) != data) { break; } startIndex++; myLexer.advance(); lastTokenStart = tokenStart; lastLexerState = lexerState; } startOffset = mySegments.getSegmentStart(startIndex); int repaintEnd = -1; int insertSegmentCount = 0; int oldEndIndex = -1; SegmentArrayWithData insertSegments = new SegmentArrayWithData(); while(myLexer.getTokenType() != null) { int tokenStart = myLexer.getTokenStart(); int lexerState = myLexer.getState(); if (tokenStart == lastTokenStart && lexerState == lastLexerState) { throw new IllegalStateException("Error while updating lexer: " + e + " document text: " + document.getText()); } lastTokenStart = tokenStart; lastLexerState = lexerState; int tokenEnd = myLexer.getTokenEnd(); data = packData(myLexer.getTokenType(), lexerState); if(tokenStart >= newEndOffset && lexerState == myInitialState) { int shiftedTokenStart = tokenStart - e.getNewLength() + e.getOldLength(); int index = mySegments.findSegmentIndex(shiftedTokenStart); if (mySegments.getSegmentStart(index) == shiftedTokenStart && mySegments.getSegmentData(index) == data) { repaintEnd = tokenStart; oldEndIndex = index; break; } } insertSegments.setElementAt(insertSegmentCount, tokenStart, tokenEnd, data); insertSegmentCount++; myLexer.advance(); } final int shift = e.getNewLength() - e.getOldLength(); if (repaintEnd > 0) { while (insertSegmentCount > 0 && oldEndIndex > startIndex) { if (!segmentsEqual(mySegments, oldEndIndex - 1, insertSegments, insertSegmentCount - 1, shift)) { break; } insertSegmentCount--; oldEndIndex--; repaintEnd = insertSegments.getSegmentStart(insertSegmentCount); insertSegments.remove(insertSegmentCount, insertSegmentCount + 1); } } if(repaintEnd == -1) { repaintEnd = text.length(); } if (oldEndIndex < 0){ oldEndIndex = mySegments.getSegmentCount(); } mySegments.shiftSegments(oldEndIndex, shift); mySegments.replace(startIndex, oldEndIndex, insertSegments); if (insertSegmentCount == 0 || oldEndIndex == startIndex + 1 && insertSegmentCount == 1 && data == mySegments.getSegmentData(startIndex)) { return; } myEditor.repaint(startOffset, repaintEnd); } @Override public void beforeDocumentChange(DocumentEvent event) { } @Override public int getPriority() { return EditorDocumentPriorities.LEXER_EDITOR; } private static boolean segmentsEqual(SegmentArrayWithData a1, int idx1, SegmentArrayWithData a2, int idx2, final int offsetShift) { return a1.getSegmentStart(idx1) + offsetShift == a2.getSegmentStart(idx2) && a1.getSegmentEnd(idx1) + offsetShift == a2.getSegmentEnd(idx2) && a1.getSegmentData(idx1) == a2.getSegmentData(idx2); } public HighlighterClient getClient() { return myEditor; } @Override public void setText(@NotNull CharSequence text) { synchronized (this) { doSetText(text); } } protected class TokenProcessor { public void addToken(final int i, final int startOffset, final int endOffset, final int data, final IElementType tokenType) { mySegments.setElementAt(i, startOffset, endOffset, data); } public void finish() { } } private void doSetText(final CharSequence text) { final TokenProcessor processor = createTokenProcessor(0); myLexer.start(text, 0, text.length(),myInitialState); mySegments.removeAll(); int i = 0; while (true) { final IElementType tokenType = myLexer.getTokenType(); if (tokenType == null) break; int data = packData(tokenType, myLexer.getState()); processor.addToken(i, myLexer.getTokenStart(), myLexer.getTokenEnd(), data, tokenType); i++; myLexer.advance(); } processor.finish(); if(myEditor != null && !ApplicationManager.getApplication().isHeadlessEnvironment()) { UIUtil.invokeLaterIfNeeded(new DumbAwareRunnable() { @Override public void run() { myEditor.repaint(0, text.length()); } }); } } protected TokenProcessor createTokenProcessor(final int startIndex) { return new TokenProcessor(); } protected SyntaxHighlighter getSyntaxHighlighter() { return myHighlighter; } private TextAttributes getAttributes(IElementType tokenType) { TextAttributes attrs = myAttributesMap.get(tokenType); if (attrs == null) { // let's fetch syntax highlighter attributes for token and merge them with "TEXT" attribute of current color scheme attrs = convertAttributes(myHighlighter.getTokenHighlights(tokenType)); myAttributesMap.put(tokenType, attrs); } return attrs; } protected TextAttributes convertAttributes(@NotNull TextAttributesKey[] keys) { TextAttributes attrs = myScheme.getAttributes(HighlighterColors.TEXT); for (TextAttributesKey key : keys) { TextAttributes attrs2 = myScheme.getAttributes(key); if (attrs2 != null) { attrs = TextAttributes.merge(attrs, attrs2); } } return attrs; } public class HighlighterIteratorImpl implements HighlighterIterator { private int mySegmentIndex = 0; HighlighterIteratorImpl(int startOffset) { mySegmentIndex = mySegments.findSegmentIndex(startOffset); } public int currentIndex() { return mySegmentIndex; } @Override public TextAttributes getTextAttributes() { return getAttributes(getTokenType()); } @Override public int getStart() { return mySegments.getSegmentStart(mySegmentIndex); } @Override public int getEnd() { return mySegments.getSegmentEnd(mySegmentIndex); } @Override public IElementType getTokenType(){ return unpackToken(mySegments.getSegmentData(mySegmentIndex)); } @Override public void advance() { mySegmentIndex++; } @Override public void retreat(){ mySegmentIndex--; } @Override public boolean atEnd() { return mySegmentIndex >= mySegments.getSegmentCount() || mySegmentIndex < 0; } @Override public Document getDocument() { return LexerEditorHighlighter.this.getDocument(); } } public SegmentArrayWithData getSegments() { return mySegments; } }