blob: ffb3254a67a67bdb36f2ce5ae1d5af339a0be471 [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.injected.editor;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.LogicalPosition;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditReadOnlyListener;
import com.intellij.openapi.editor.ex.LineIterator;
import com.intellij.openapi.editor.ex.RangeMarkerEx;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiLanguageInjectionHost;
import com.intellij.psi.impl.source.tree.injected.Place;
import com.intellij.util.Processor;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.text.ImmutableCharSequence;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* @author Alexey
*/
public class DocumentWindowImpl extends UserDataHolderBase implements Disposable, DocumentWindow, DocumentEx {
private static final Logger LOG = Logger.getInstance("#com.intellij.injected.editor.DocumentWindowImpl");
private final DocumentEx myDelegate;
private final boolean myOneLine;
private Place myShreds; // guarded by myLock
private final int myPrefixLineCount;
private final int mySuffixLineCount;
private final Object myLock = new Object();
private CachedText myCachedText = null;
public DocumentWindowImpl(@NotNull DocumentEx delegate, boolean oneLine, @NotNull Place shreds) {
myDelegate = delegate;
myOneLine = oneLine;
synchronized (myLock) {
myShreds = shreds;
}
myPrefixLineCount = Math.max(1, 1 + StringUtil.countNewLines(shreds.get(0).getPrefix()));
mySuffixLineCount = Math.max(1, 1 + StringUtil.countNewLines(shreds.get(shreds.size()- 1).getSuffix()));
}
@Nullable("null means we were unable to calculate")
LogicalPosition hostToInjectedInVirtualSpace(@NotNull LogicalPosition hPos) {
// beware the virtual space
int hLineStartOffset = hPos.line >= myDelegate.getLineCount() ? myDelegate.getTextLength() : myDelegate.getLineStartOffset(hPos.line);
int iLineStartOffset = hostToInjected(hLineStartOffset);
int iLine = getLineNumber(iLineStartOffset);
synchronized (myLock) {
for (int i = myShreds.size() - 1; i >= 0; i--) {
PsiLanguageInjectionHost.Shred shred = myShreds.get(i);
if (!shred.isValid()) continue;
Segment hostRangeMarker = shred.getHostRangeMarker();
if (hostRangeMarker == null) continue;
int hShredEndOffset = hostRangeMarker.getEndOffset();
int hShredStartOffset = hostRangeMarker.getStartOffset();
int hShredStartLine = myDelegate.getLineNumber(hShredStartOffset);
int hShredEndLine = myDelegate.getLineNumber(hShredEndOffset);
if (hShredStartLine <= hPos.line && hPos.line <= hShredEndLine) {
int hColumnOfShredEnd = hShredEndOffset - hLineStartOffset;
int iColumnOfShredEnd = hostToInjected(hShredEndOffset) - iLineStartOffset;
int iColumn = iColumnOfShredEnd + hPos.column - hColumnOfShredEnd;
return new LogicalPosition(iLine, iColumn);
}
}
}
return null;
}
private static class CachedText {
private final String text;
private final long modificationStamp;
private CachedText(@NotNull String text, long modificationStamp) {
this.text = text;
this.modificationStamp = modificationStamp;
}
@NotNull
private String getText() {
return text;
}
private long getModificationStamp() {
return modificationStamp;
}
}
@Override
public int getLineCount() {
return 1 + StringUtil.countNewLines(getText());
}
@Override
public int getLineStartOffset(int line) {
LOG.assertTrue(line >= 0, line);
if (line == 0) return 0;
String hostText = myDelegate.getText();
int[] pos = new int[2]; // pos[0] = curLine; pos[1] == offset;
synchronized (myLock) {
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
int found = countNewLinesIn(shred.getPrefix(), pos, line);
if (found != -1) return found;
String text = hostText.substring(hostRange.getStartOffset(), hostRange.getEndOffset());
found = countNewLinesIn(text, pos, line);
if (found != -1) return found;
found = countNewLinesIn(shred.getSuffix(), pos, line);
if (found != -1) return found;
}
}
return pos[1];
}
// returns startOffset found, or -1 if need to continue searching
private static int countNewLinesIn(String text, int[] pos, int line) {
int offsetInside = 0;
for (int i = text.indexOf('\n'); i != -1; i = text.indexOf('\n', offsetInside)) {
int curLine = ++pos[0];
int lineLength = i + 1 - offsetInside;
int offset = pos[1] += lineLength;
offsetInside += lineLength;
if (curLine == line) return offset;
}
pos[1] += text.length() - offsetInside;
return -1;
}
@Override
public int getLineEndOffset(int line) {
LOG.assertTrue(line >= 0, line);
if (line == getLineCount() - 1) return getTextLength(); // end >= start assertion fix for last line
int startOffsetOfNextLine = getLineStartOffset(line + 1);
return startOffsetOfNextLine == 0 || getText().charAt(startOffsetOfNextLine - 1) != '\n' ? startOffsetOfNextLine : startOffsetOfNextLine - 1;
}
@NotNull
@Override
public String getText() {
CachedText cachedText = myCachedText;
if (cachedText == null || cachedText.getModificationStamp() != getModificationStamp()) {
myCachedText = cachedText = new CachedText(calcText(), getModificationStamp());
}
return cachedText.getText();
}
@NotNull
private String calcText() {
StringBuilder text = new StringBuilder();
CharSequence hostText = myDelegate.getCharsSequence();
synchronized (myLock) {
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostRange = shred.getHostRangeMarker();
if (hostRange != null) {
text.append(shred.getPrefix());
text.append(hostText, hostRange.getStartOffset(), hostRange.getEndOffset());
text.append(shred.getSuffix());
}
}
}
return text.toString();
}
@NotNull
@Override
public String getText(@NotNull TextRange range) {
return range.substring(getText());
}
@Override
@NotNull
public CharSequence getCharsSequence() {
return getText();
}
@NotNull
@Override
public CharSequence getImmutableCharSequence() {
return ImmutableCharSequence.asImmutable(getText());
}
@Override
@NotNull
public char[] getChars() {
return CharArrayUtil.fromSequence(getText());
}
@Override
public int getTextLength() {
int length = 0;
synchronized (myLock) {
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
length += shred.getPrefix().length();
length += hostRange.getEndOffset() - hostRange.getStartOffset();
length += shred.getSuffix().length();
}
}
return length;
}
@Override
public int getLineNumber(int offset) {
int lineNumber = 0;
String hostText = myDelegate.getText();
synchronized (myLock) {
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
String prefix = shred.getPrefix();
String suffix = shred.getSuffix();
lineNumber += StringUtil.getLineBreakCount(prefix.substring(0, Math.min(offset, prefix.length())));
if (offset < prefix.length()) {
return lineNumber;
}
offset -= prefix.length();
Segment currentRange = shred.getHostRangeMarker();
if (currentRange == null) continue;
int rangeLength = currentRange.getEndOffset() - currentRange.getStartOffset();
String rangeText = hostText.substring(currentRange.getStartOffset(), currentRange.getEndOffset());
lineNumber += StringUtil.getLineBreakCount(rangeText.substring(0, Math.min(offset, rangeLength)));
if (offset < rangeLength) {
return lineNumber;
}
offset -= rangeLength;
lineNumber += StringUtil.getLineBreakCount(suffix.substring(0, Math.min(offset, suffix.length())));
if (offset < suffix.length()) {
return lineNumber;
}
offset -= suffix.length();
}
}
lineNumber = getLineCount() - 1;
return lineNumber < 0 ? 0 : lineNumber;
}
@Override
public TextRange getHostRange(int hostOffset) {
synchronized (myLock) {
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment currentRange = shred.getHostRangeMarker();
if (currentRange == null) continue;
TextRange textRange = ProperTextRange.create(currentRange);
if (textRange.grown(1).contains(hostOffset)) return textRange;
}
}
return null;
}
@Override
public void insertString(final int offset, @NotNull CharSequence s) {
synchronized (myLock) {
LOG.assertTrue(offset >= myShreds.get(0).getPrefix().length(), myShreds.get(0).getPrefix());
LOG.assertTrue(offset <= getTextLength() - myShreds.get(myShreds.size() - 1).getSuffix().length(),
myShreds.get(myShreds.size() - 1).getSuffix());
}
if (isOneLine()) {
s = StringUtil.replace(s.toString(), "\n", "");
}
myDelegate.insertString(injectedToHost(offset), s);
}
@Override
public void deleteString(final int startOffset, final int endOffset) {
assert intersectWithEditable(new TextRange(startOffset, startOffset)) != null;
assert intersectWithEditable(new TextRange(endOffset, endOffset)) != null;
List<TextRange> hostRangesToDelete;
synchronized (myLock) {
hostRangesToDelete = new ArrayList<TextRange>(myShreds.size());
int offset = startOffset;
int curRangeStart = 0;
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
curRangeStart += shred.getPrefix().length();
if (offset < curRangeStart) offset = curRangeStart;
if (offset >= endOffset) break;
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
int hostRangeLength = hostRange.getEndOffset() - hostRange.getStartOffset();
TextRange range = TextRange.from(curRangeStart, hostRangeLength);
if (range.contains(offset)) {
TextRange rangeToDelete = new TextRange(offset, Math.min(range.getEndOffset(), endOffset));
hostRangesToDelete.add(rangeToDelete.shiftRight(hostRange.getStartOffset() - curRangeStart));
offset = rangeToDelete.getEndOffset();
}
curRangeStart += hostRangeLength;
curRangeStart += shred.getSuffix().length();
}
}
int delta = 0;
for (TextRange hostRangeToDelete : hostRangesToDelete) {
myDelegate.deleteString(hostRangeToDelete.getStartOffset() + delta, hostRangeToDelete.getEndOffset() + delta);
delta -= hostRangeToDelete.getLength();
}
}
@Override
public void replaceString(int startOffset, int endOffset, @NotNull CharSequence s) {
if (isOneLine()) {
s = StringUtil.replace(s.toString(), "\n", "");
}
final CharSequence chars = getCharsSequence();
CharSequence toDelete = chars.subSequence(startOffset, endOffset);
int perfixLength = StringUtil.commonPrefixLength(s, toDelete);
int suffixLength = StringUtil.commonSuffixLength(toDelete.subSequence(perfixLength, toDelete.length()), s.subSequence(perfixLength, s.length()));
startOffset += perfixLength;
endOffset -= suffixLength;
s = s.subSequence(perfixLength, s.length() - suffixLength);
doReplaceString(startOffset, endOffset, s);
}
private void doReplaceString(int startOffset, int endOffset, CharSequence s) {
assert intersectWithEditable(new TextRange(startOffset, startOffset)) != null;
assert intersectWithEditable(new TextRange(endOffset, endOffset)) != null;
List<Pair<TextRange,CharSequence>> hostRangesToModify;
synchronized (myLock) {
hostRangesToModify = new ArrayList<Pair<TextRange, CharSequence>>(myShreds.size());
int offset = startOffset;
int curRangeStart = 0;
for (int i = 0; i < myShreds.size(); i++) {
PsiLanguageInjectionHost.Shred shred = myShreds.get(i);
curRangeStart += shred.getPrefix().length();
if (offset < curRangeStart) offset = curRangeStart;
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
int hostRangeLength = hostRange.getEndOffset() - hostRange.getStartOffset();
TextRange range = TextRange.from(curRangeStart, hostRangeLength);
if (range.contains(offset) || range.getEndOffset() == offset/* in case of inserting at the end*/) {
TextRange rangeToModify = new TextRange(offset, Math.min(range.getEndOffset(), endOffset));
TextRange hostRangeToModify = rangeToModify.shiftRight(hostRange.getStartOffset() - curRangeStart);
CharSequence toReplace = i == myShreds.size() - 1 || range.getEndOffset() + shred.getSuffix().length() >= endOffset
? s : s.subSequence(0, Math.min(hostRangeToModify.getLength(), s.length()));
s = toReplace == s ? "" : s.subSequence(toReplace.length(), s.length());
hostRangesToModify.add(Pair.create(hostRangeToModify, toReplace));
offset = rangeToModify.getEndOffset();
}
curRangeStart += hostRangeLength;
curRangeStart += shred.getSuffix().length();
if (curRangeStart >= endOffset) break;
}
}
int delta = 0;
for (Pair<TextRange, CharSequence> pair : hostRangesToModify) {
TextRange hostRange = pair.getFirst();
CharSequence replace = pair.getSecond();
myDelegate.replaceString(hostRange.getStartOffset() + delta, hostRange.getEndOffset() + delta, replace);
delta -= hostRange.getLength() - replace.length();
}
}
@Override
public void moveText(int srcStart, int srcEnd, int dstOffset) {
throw new UnsupportedOperationException();
}
@Override
public boolean isWritable() {
return myDelegate.isWritable();
}
@Override
public long getModificationStamp() {
return isValid() ? myDelegate.getModificationStamp() : -1;
}
@Override
public void fireReadOnlyModificationAttempt() {
myDelegate.fireReadOnlyModificationAttempt();
}
@Override
public void addDocumentListener(@NotNull final DocumentListener listener) {
myDelegate.addDocumentListener(listener);
}
@Override
public void addDocumentListener(@NotNull DocumentListener listener, @NotNull Disposable parentDisposable) {
myDelegate.addDocumentListener(listener, parentDisposable);
}
@Override
public void removeDocumentListener(@NotNull final DocumentListener listener) {
myDelegate.removeDocumentListener(listener);
}
@Override
@NotNull
public RangeMarker createRangeMarker(final int startOffset, final int endOffset) {
ProperTextRange hostRange = injectedToHost(new ProperTextRange(startOffset, endOffset));
RangeMarker hostMarker = myDelegate.createRangeMarker(hostRange);
int startShift = Math.max(0, hostToInjected(hostRange.getStartOffset()) - startOffset);
int endShift = Math.max(0, endOffset - hostToInjected(hostRange.getEndOffset()) - startShift);
return new RangeMarkerWindow(this, (RangeMarkerEx)hostMarker, startShift, endShift);
}
@Override
@NotNull
public RangeMarker createRangeMarker(final int startOffset, final int endOffset, final boolean surviveOnExternalChange) {
if (!surviveOnExternalChange) {
return createRangeMarker(startOffset, endOffset);
}
ProperTextRange hostRange = injectedToHost(new ProperTextRange(startOffset, endOffset));
//todo persistent?
RangeMarker hostMarker = myDelegate.createRangeMarker(hostRange.getStartOffset(), hostRange.getEndOffset(), surviveOnExternalChange);
int startShift = Math.max(0, hostToInjected(hostRange.getStartOffset()) - startOffset);
int endShift = Math.max(0, endOffset - hostToInjected(hostRange.getEndOffset()) - startShift);
return new RangeMarkerWindow(this, (RangeMarkerEx)hostMarker, startShift, endShift);
}
@Override
public void addPropertyChangeListener(@NotNull final PropertyChangeListener listener) {
myDelegate.addPropertyChangeListener(listener);
}
@Override
public void removePropertyChangeListener(@NotNull final PropertyChangeListener listener) {
myDelegate.removePropertyChangeListener(listener);
}
@Override
public void setReadOnly(final boolean isReadOnly) {
myDelegate.setReadOnly(isReadOnly);
}
@Override
@NotNull
public RangeMarker createGuardedBlock(final int startOffset, final int endOffset) {
ProperTextRange hostRange = injectedToHost(new ProperTextRange(startOffset, endOffset));
return myDelegate.createGuardedBlock(hostRange.getStartOffset(), hostRange.getEndOffset());
}
@Override
public void removeGuardedBlock(@NotNull final RangeMarker block) {
myDelegate.removeGuardedBlock(block);
}
@Override
public RangeMarker getOffsetGuard(final int offset) {
return myDelegate.getOffsetGuard(injectedToHost(offset));
}
@Override
public RangeMarker getRangeGuard(final int startOffset, final int endOffset) {
ProperTextRange hostRange = injectedToHost(new ProperTextRange(startOffset, endOffset));
return myDelegate.getRangeGuard(hostRange.getStartOffset(), hostRange.getEndOffset());
}
@Override
public void startGuardedBlockChecking() {
myDelegate.startGuardedBlockChecking();
}
@Override
public void stopGuardedBlockChecking() {
myDelegate.stopGuardedBlockChecking();
}
@Override
public void setCyclicBufferSize(final int bufferSize) {
myDelegate.setCyclicBufferSize(bufferSize);
}
@Override
public void setText(@NotNull CharSequence text) {
synchronized (myLock) {
LOG.assertTrue(text.toString().startsWith(myShreds.get(0).getPrefix()));
LOG.assertTrue(text.toString().endsWith(myShreds.get(myShreds.size() - 1).getSuffix()));
if (isOneLine()) {
text = StringUtil.replace(text.toString(), "\n", "");
}
String[] changes = calculateMinEditSequence(text.toString());
assert changes.length == myShreds.size();
for (int i = 0; i < changes.length; i++) {
String change = changes[i];
if (change != null) {
Segment hostRange = myShreds.get(i).getHostRangeMarker();
if (hostRange == null) continue;
myDelegate.replaceString(hostRange.getStartOffset(), hostRange.getEndOffset(), change);
}
}
}
}
@Override
@NotNull
public Segment[] getHostRanges() {
synchronized (myLock) {
List<Segment> markers = new ArrayList<Segment>(myShreds.size());
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostMarker = shred.getHostRangeMarker();
if (hostMarker != null) {
markers.add(hostMarker);
}
}
return markers.isEmpty() ? Segment.EMPTY_ARRAY : markers.toArray(new Segment[markers.size()]);
}
}
@Override
@NotNull
public RangeMarker createRangeMarker(@NotNull final TextRange textRange) {
final ProperTextRange properTextRange = new ProperTextRange(textRange);
return createRangeMarker(properTextRange.getStartOffset(), properTextRange.getEndOffset());
}
@Override
public void setStripTrailingSpacesEnabled(final boolean isEnabled) {
myDelegate.setStripTrailingSpacesEnabled(isEnabled);
}
@Override
public int getLineSeparatorLength(final int line) {
return myDelegate.getLineSeparatorLength(injectedToHostLine(line));
}
@Override
@NotNull
public LineIterator createLineIterator() {
return myDelegate.createLineIterator();
}
@Override
public void setModificationStamp(final long modificationStamp) {
myDelegate.setModificationStamp(modificationStamp);
}
@Override
public void addEditReadOnlyListener(@NotNull final EditReadOnlyListener listener) {
myDelegate.addEditReadOnlyListener(listener);
}
@Override
public void removeEditReadOnlyListener(@NotNull final EditReadOnlyListener listener) {
myDelegate.removeEditReadOnlyListener(listener);
}
@Override
public void replaceText(@NotNull final CharSequence chars, final long newModificationStamp) {
setText(chars);
myDelegate.setModificationStamp(newModificationStamp);
}
@Override
public int getListenersCount() {
return myDelegate.getListenersCount();
}
@Override
public void suppressGuardedExceptions() {
myDelegate.suppressGuardedExceptions();
}
@Override
public void unSuppressGuardedExceptions() {
myDelegate.unSuppressGuardedExceptions();
}
@Override
public boolean isInEventsHandling() {
return myDelegate.isInEventsHandling();
}
@Override
public void clearLineModificationFlags() {
}
@Override
public boolean removeRangeMarker(@NotNull RangeMarkerEx rangeMarker) {
return myDelegate.removeRangeMarker(((RangeMarkerWindow)rangeMarker).getDelegate());
}
@Override
public void registerRangeMarker(@NotNull RangeMarkerEx rangeMarker,
int start,
int end,
boolean greedyToLeft,
boolean greedyToRight,
int layer) {
throw new IllegalStateException();
}
@Override
public boolean isInBulkUpdate() {
return false;
}
@Override
public void setInBulkUpdate(boolean value) {
}
@Override
@NotNull
public DocumentEx getDelegate() {
return myDelegate;
}
//todo use escaper?
@Override
public int hostToInjected(int hostOffset) {
synchronized (myLock) {
Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
if (hostRangeMarker == null || hostOffset < hostRangeMarker.getStartOffset()) return myShreds.get(0).getPrefix().length();
int offset = 0;
for (int i = 0; i < myShreds.size(); i++) {
offset += myShreds.get(i).getPrefix().length();
Segment currentRange = myShreds.get(i).getHostRangeMarker();
if (currentRange == null) continue;
Segment nextRange = i == myShreds.size() - 1 ? null : myShreds.get(i + 1).getHostRangeMarker();
if (nextRange == null || hostOffset < nextRange.getStartOffset()) {
if (hostOffset >= currentRange.getEndOffset()) hostOffset = currentRange.getEndOffset();
return offset + hostOffset - currentRange.getStartOffset();
}
offset += currentRange.getEndOffset() - currentRange.getStartOffset();
offset += myShreds.get(i).getSuffix().length();
}
return getTextLength() - myShreds.get(myShreds.size() - 1).getSuffix().length();
}
}
@Override
public int injectedToHost(int offset) {
int offsetInLeftFragment = injectedToHost(offset, true);
int offsetInRightFragment = injectedToHost(offset, false);
if (offsetInLeftFragment == offsetInRightFragment) return offsetInLeftFragment;
// heuristics: return offset closest to the caret
Editor[] editors = EditorFactory.getInstance().getEditors(getDelegate());
Editor editor = editors.length == 0 ? null : editors[0];
if (editor != null)
{
if (editor instanceof EditorWindow) editor = ((EditorWindow)editor).getDelegate();
int caret = editor.getCaretModel().getOffset();
return Math.abs(caret - offsetInLeftFragment) < Math.abs(caret - offsetInRightFragment) ? offsetInLeftFragment : offsetInRightFragment;
}
return offsetInLeftFragment;
}
private int injectedToHost(int offset, boolean preferLeftFragment) {
synchronized (myLock) {
if (offset < myShreds.get(0).getPrefix().length()) {
Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
return hostRangeMarker == null ? 0 : hostRangeMarker.getStartOffset();
}
int prevEnd = 0;
for (int i = 0; i < myShreds.size(); i++) {
Segment currentRange = myShreds.get(i).getHostRangeMarker();
if (currentRange == null) continue;
offset -= myShreds.get(i).getPrefix().length();
int length = currentRange.getEndOffset() - currentRange.getStartOffset();
if (offset < 0) {
return preferLeftFragment ? prevEnd : currentRange.getStartOffset() - 1;
}
if (offset == 0) {
return preferLeftFragment && i != 0 ? prevEnd : currentRange.getStartOffset();
}
if (offset < length || offset == length && preferLeftFragment) {
return currentRange.getStartOffset() + offset;
}
offset -= length;
offset -= myShreds.get(i).getSuffix().length();
prevEnd = currentRange.getEndOffset();
}
Segment hostRangeMarker = myShreds.get(myShreds.size() - 1).getHostRangeMarker();
return hostRangeMarker == null ? 0 : hostRangeMarker.getEndOffset();
}
}
@Override
@NotNull
public ProperTextRange injectedToHost(@NotNull TextRange injected) {
ProperTextRange.assertProperRange(injected);
int start = injectedToHost(injected.getStartOffset(), false);
int end = injectedToHost(injected.getEndOffset(), true);
if (end < start) {
end = injectedToHost(injected.getEndOffset(), false);
}
return new ProperTextRange(start, end);
}
@Override
public int injectedToHostLine(int line) {
if (line < myPrefixLineCount) {
synchronized (myLock) {
Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
return hostRangeMarker == null ? 0 : myDelegate.getLineNumber(hostRangeMarker.getStartOffset());
}
}
int lineCount = getLineCount();
if (line > lineCount - mySuffixLineCount) {
return lineCount;
}
int offset = getLineStartOffset(line);
int hostOffset = injectedToHost(offset);
return myDelegate.getLineNumber(hostOffset);
}
@Override
public boolean containsRange(int start, int end) {
synchronized (myLock) {
Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
if (hostRangeMarker == null || end - start > hostRangeMarker.getEndOffset() - hostRangeMarker.getStartOffset()) return false;
ProperTextRange query = new ProperTextRange(start, end);
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
TextRange textRange = ProperTextRange.create(hostRange);
if (textRange.contains(query)) return true;
}
return false;
}
}
@Override
@Deprecated
@Nullable
public TextRange intersectWithEditable(@NotNull TextRange rangeToEdit) {
int startOffset = -1;
int endOffset = -1;
synchronized (myLock) {
int offset = 0;
for (PsiLanguageInjectionHost.Shred shred : myShreds) {
Segment hostRange = shred.getHostRangeMarker();
if (hostRange == null) continue;
offset += shred.getPrefix().length();
int length = hostRange.getEndOffset() - hostRange.getStartOffset();
TextRange intersection = new ProperTextRange(offset, offset + length).intersection(rangeToEdit);
if (intersection != null) {
if (startOffset == -1) {
startOffset = intersection.getStartOffset();
}
endOffset = intersection.getEndOffset();
}
offset += length;
offset += shred.getSuffix().length();
}
}
if (startOffset == -1) return null;
return new ProperTextRange(startOffset, endOffset);
}
// minimum sequence of text replacement operations for each host range
// result[i] == null means no change
// result[i] == "" means delete
// result[i] == string means replace
public String[] calculateMinEditSequence(String newText) {
synchronized (myLock) {
String[] result = new String[myShreds.size()];
String hostText = myDelegate.getText();
calculateMinEditSequence(hostText, newText, result, 0, result.length - 1);
for (int i = 0; i < result.length; i++) {
String change = result[i];
if (change == null) continue;
String prefix = myShreds.get(i).getPrefix();
String suffix = myShreds.get(i).getSuffix();
assert change.startsWith(prefix) : change + "/" + prefix;
assert change.endsWith(suffix) : change + "/" + suffix;
result[i] = StringUtil.trimEnd(StringUtil.trimStart(change, prefix), suffix);
}
return result;
}
}
private String getRangeText(@NotNull String hostText, int hostNum) {
synchronized (myLock) {
PsiLanguageInjectionHost.Shred shred = myShreds.get(hostNum);
Segment hostRangeMarker = shred.getHostRangeMarker();
return shred.getPrefix() +
(hostRangeMarker == null ? "" : hostText.substring(hostRangeMarker.getStartOffset(), hostRangeMarker.getEndOffset())) +
shred.getSuffix();
}
}
private void calculateMinEditSequence(String hostText, String newText, String[] result, int i, int j) {
synchronized (myLock) {
String rangeText1 = getRangeText(hostText, i);
if (i == j) {
result[i] = rangeText1.equals(newText) ? null : newText;
return;
}
if (StringUtil.startsWith(newText, rangeText1)) {
result[i] = null; //no change
calculateMinEditSequence(hostText, newText.substring(rangeText1.length()), result, i+1, j);
return;
}
String rangeText2 = getRangeText(hostText, j);
if (StringUtil.endsWith(newText, rangeText2)) {
result[j] = null; //no change
calculateMinEditSequence(hostText, newText.substring(0, newText.length() - rangeText2.length()), result, i, j-1);
return;
}
if (i+1 == j) {
String suffix = myShreds.get(i).getSuffix();
String prefix = myShreds.get(j).getPrefix();
String separator = suffix + prefix;
if (!separator.isEmpty()) {
int sep = newText.indexOf(separator);
assert sep != -1;
result[i] = newText.substring(0, sep + suffix.length());
result[j] = newText.substring(sep + suffix.length() + prefix.length(), newText.length());
return;
}
String commonPrefix = StringUtil.commonPrefix(rangeText1, newText);
result[i] = commonPrefix;
result[j] = newText.substring(commonPrefix.length());
return;
}
String middleText = getRangeText(hostText, i + 1);
int m = newText.indexOf(middleText);
if (m != -1) {
result[i] = newText.substring(0, m);
result[i+1] = null;
calculateMinEditSequence(hostText, newText.substring(m+middleText.length(), newText.length()), result, i+2, j);
return;
}
middleText = getRangeText(hostText, j - 1);
m = newText.lastIndexOf(middleText);
if (m != -1) {
result[j] = newText.substring(m+middleText.length());
result[j-1] = null;
calculateMinEditSequence(hostText, newText.substring(0, m), result, i, j-2);
return;
}
result[i] = "";
result[j] = "";
calculateMinEditSequence(hostText, newText, result, i+1, j-1);
}
}
@Override
public boolean areRangesEqual(@NotNull DocumentWindow otherd) {
DocumentWindowImpl window = (DocumentWindowImpl)otherd;
Place shreds = getShreds();
Place otherShreds = window.getShreds();
if (shreds.size() != otherShreds.size()) return false;
for (int i = 0; i < shreds.size(); i++) {
PsiLanguageInjectionHost.Shred shred = shreds.get(i);
PsiLanguageInjectionHost.Shred otherShred = otherShreds.get(i);
if (!shred.getPrefix().equals(otherShred.getPrefix())) return false;
if (!shred.getSuffix().equals(otherShred.getSuffix())) return false;
Segment hostRange = shred.getHostRangeMarker();
Segment other = otherShred.getHostRangeMarker();
if (hostRange == null || other == null || hostRange.getStartOffset() != other.getStartOffset()) return false;
if (hostRange.getEndOffset() != other.getEndOffset()) return false;
}
return true;
}
@Override
public boolean isValid() {
PsiLanguageInjectionHost.Shred[] shreds;
synchronized (myLock) {
shreds = myShreds.toArray(new PsiLanguageInjectionHost.Shred[myShreds.size()]);
}
// can grab PsiLock in SmartPsiPointer.restore()
for (PsiLanguageInjectionHost.Shred shred : shreds) {
if (!shred.isValid()) return false;
}
return true;
}
public boolean equals(Object o) {
if (!(o instanceof DocumentWindowImpl)) return false;
DocumentWindowImpl window = (DocumentWindowImpl)o;
return myDelegate.equals(window.getDelegate()) && areRangesEqual(window);
}
public int hashCode() {
synchronized (myLock) {
Segment hostRangeMarker = myShreds.get(0).getHostRangeMarker();
return hostRangeMarker == null ? -1 : hostRangeMarker.getStartOffset();
}
}
public boolean isOneLine() {
return myOneLine;
}
@Override
public void dispose() {
synchronized (myLock) {
myShreds.dispose();
}
}
public void setShreds(@NotNull Place shreds) {
synchronized (myLock) {
myShreds.dispose();
myShreds = shreds;
}
}
@NotNull
public Place getShreds() {
synchronized (myLock) {
return myShreds;
}
}
@Override
@NotNull
public List<RangeMarker> getGuardedBlocks() {
return Collections.emptyList();
}
//todo convert injected RMs to host
@Override
public boolean processRangeMarkers(@NotNull Processor<RangeMarker> processor) {
return myDelegate.processRangeMarkers(processor);
}
@Override
public boolean processRangeMarkersOverlappingWith(int start, int end, @NotNull Processor<RangeMarker> processor) {
return myDelegate.processRangeMarkersOverlappingWith(start, end, processor);
}
}