/*
* Copyright 2003-2015 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 jetbrains.mps.text.impl;
import jetbrains.mps.text.BasicTextAreaFactory;
import jetbrains.mps.text.BasicToken;
import jetbrains.mps.text.BufferLayout;
import jetbrains.mps.text.BufferSnapshot;
import jetbrains.mps.text.TextArea;
import jetbrains.mps.text.TextAreaFactory;
import jetbrains.mps.text.TextAreaToken;
import jetbrains.mps.text.TextBuffer;
import jetbrains.mps.text.TextMark;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Immutable;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
* There might be different implementations, for example, with support for arbitrary areas or with fixed set thereof.
* @author Artem Tikhomirov
*/
public class TextBufferImpl implements TextBuffer {
private final Deque<TextArea> myChunkStack = new ArrayDeque<TextArea>();
// preserve order in which chunks were created
private final Map<TextAreaToken, TextArea> myChunks = new LinkedHashMap<TextAreaToken, TextArea>();
private final Deque<Marker> myMarkerStack = new ArrayDeque<Marker>();
/*
* Markers are partially ordered as it's impossible to add a marker in front of another one.
* However, with re-ordering of text areas, markers added later may get positioned in front of their predecessors.
* Within single text area, though, ordering is kept.
*/
private final List<Marker> myMarkers = new ArrayList<Marker>();
private final TextAreaFactory myChunkFactory;
public TextBufferImpl() {
this(new BasicTextAreaFactory());
}
public TextBufferImpl(@NotNull TextAreaFactory factory) {
myChunkFactory = factory;
pushTextArea(new BasicToken(System.identityHashCode(this)));
}
@NotNull
@Override
public TextArea area() {
return myChunkStack.peek();
}
@Override
public TextBuffer pushTextArea(@NotNull TextAreaToken areaIdentity) {
TextArea chunk = myChunks.get(areaIdentity);
if (chunk == null) {
chunk = myChunkFactory.create();
myChunks.put(areaIdentity, chunk);
}
myChunkStack.push(chunk);
return this;
}
@Override
public TextBuffer popTextArea() {
if (myChunkStack.size() == 1) {
throw new IllegalStateException("Can't remove top-most text chunk");
}
myChunkStack.pop();
return this;
}
@Override
public TextBuffer pushMark() {
myMarkerStack.push(new Marker(area()));
return this;
}
@NotNull
@Override
public TextMark popMark() {
final Marker mark = myMarkerStack.pop();
assert mark.myTextArea == area() : "Marks can't span different text areas";
myMarkers.add(mark);
return mark.span();
}
@NotNull
@Override
public BufferLayout newLayout() {
return new Layout();
}
@NotNull
@Override
public BufferSnapshot snapshot(@NotNull BufferLayout layout) {
Layout realLayout = (Layout) layout;
// build actual text of all chunks
LinkedHashMap<TextAreaToken, StringBuilder> textMap = new LinkedHashMap<TextAreaToken, StringBuilder>();
// we could have kept token as attribute of TextArea, but would like to keep TextAre as simple as possible
Map<TextArea, TextAreaToken> chunk2token = new HashMap<TextArea, TextAreaToken>();
// Marker is immutable location in text area, while we need a location we could shift as needed for actual snapshot
LinkedHashMap<TextArea, Deque<LiveLocation>> actualMarkers = new LinkedHashMap<TextArea, Deque<LiveLocation>>();
// to find actual position from marker kept by client fast (could walk LiveLocation chain, but why not map?)
LinkedHashMap<TextMark, LiveLocation> marker2liveLocation = new LinkedHashMap<TextMark, LiveLocation>();
// myChunks is in the order chunks were created, keep the order in case not all chunks are explicitly placed.
for (Entry<TextAreaToken, TextArea> e : myChunks.entrySet()) {
final StringBuilder sb = new StringBuilder(myChunkFactory.value(e.getValue()));
textMap.put(e.getKey(), sb);
chunk2token.put(e.getValue(), e.getKey());
actualMarkers.put(e.getValue(), new ArrayDeque<LiveLocation>());
}
for (Marker m : myMarkers) {
final Deque<LiveLocation> ll = actualMarkers.get(m.myTextArea);
LiveLocation liveLoc = new LiveLocation(m, ll.peekLast());
ll.addLast(liveLoc);
marker2liveLocation.put(m, liveLoc);
}
Set<TextAreaToken> consumedChunks = new HashSet<TextAreaToken>();
for (Entry<TextMark, TextAreaToken> e : realLayout.mySubstitutions.entrySet()) {
LiveLocation loc = marker2liveLocation.get(e.getKey());
StringBuilder target = textMap.get(chunk2token.get(loc.getTextArea()));
// FIXME what if there's optional TextArea, which is not necessarily filled with any data, but layout
// tells to get its content substituted to particular location (i.e. TextArea("fields") in class without any fields
// but with predefined location where field, if any, shall go? Next line means all TextAreas shall be made known to the buffer
// which is not quite convenient
StringBuilder replacement = textMap.get(e.getValue());
loc.replaceText(target, replacement);
consumedChunks.add(e.getValue());
}
// chunks we've substituted into another are deemed processed/consumed and shall not show up in the output on their own
for (TextAreaToken t : consumedChunks) {
textMap.remove(t);
}
StringBuilder result = new StringBuilder();
for (StringBuilder chunkText : textMap.values()) {
result.append(chunkText);
}
final TextSnapshot s = new TextSnapshot(result, marker2liveLocation, myChunkFactory.getLineSeparator());
int chunkOffset = 0;
for (Entry<TextAreaToken, StringBuilder> e : textMap.entrySet()) {
s.setOffset(myChunks.get(e.getKey()), chunkOffset);
chunkOffset += e.getValue().length();
}
return s;
}
/**
* Position in the TextArea, immutable (once mark is delivered to user from #popMark())
*/
// Would be great to have myLength final, but this would take another round of refactoring of myMarkerStack
@Immutable
private static class Marker implements TextMark {
/*package*/ final TextArea myTextArea;
/*package*/ final int myStartOffset;
/*package*/ int myLength = 0;
public Marker(TextArea textArea) {
myTextArea = textArea;
myStartOffset = textArea.length();
}
/*package*/ Marker span() {
myLength = myTextArea.length() - myStartOffset;
return this;
}
}
/**
* TextMark we need to update during snapshot build.
* There seems to be no reason to keep it TextMark, other than indicate it's just another presentation of TextMark, within snapshot now.
*/
private static class LiveLocation implements TextMark {
private final Marker myAnchor;
private int myStart;
private int myLength;
private LiveLocation myNextMark;
public LiveLocation(@NotNull Marker anchor, @Nullable LiveLocation previous) {
myAnchor = anchor;
myStart = anchor.myStartOffset;
myLength = anchor.myLength;
if (previous != null) {
previous.myNextMark = this;
}
}
public int getStart() {
return myStart;
}
public int getLength() {
return myLength;
}
public TextArea getTextArea() {
return myAnchor.myTextArea;
}
/*package*/ void replaceText(StringBuilder target, StringBuilder replacement) {
target.replace(myStart, myStart + myLength, replacement.toString());
final int oldLength = myLength;
myLength = replacement.length();
if (myNextMark != null) {
myNextMark.shift(myLength - oldLength);
}
}
// augment start offset of this LiveLocation AND ALL SUBSEQUENT MARKERS by offset
private void shift(int offset) {
myStart += offset;
if (myNextMark != null) {
myNextMark.shift(offset);
}
}
}
private static class TextSnapshot implements BufferSnapshot {
private final CharSequence myText;
private final int[] myLineBreaks; // index where a line starts, sorted due to initialization algorithm
private Map<TextArea, Integer> myOffsets = new HashMap<TextArea, Integer>(8);
private Map<TextMark,LiveLocation> myMarks;
public TextSnapshot(CharSequence seq, Map<TextMark,LiveLocation> marks, String lineSep) {
myText = seq;
myMarks = marks;
ArrayList<Integer> lineBreaks = new ArrayList<Integer>();
int i = 0;
final String s = seq.toString();
do {
i = s.indexOf(lineSep, i);
if (i == -1) {
break;
}
i += lineSep.length();
lineBreaks.add(i);
} while (true);
myLineBreaks = new int[1 + lineBreaks.size()];
myLineBreaks[0] = 0; // first line always starts at first character
for (i = 1; i < myLineBreaks.length; i++) {
myLineBreaks[i] = lineBreaks.get(i-1);
}
}
/*package*/ void setOffset(TextArea ta, int offset) {
myOffsets.put(ta, offset);
}
@NotNull
@Override
public TextPosition getStart(@NotNull TextMark mark) {
final LiveLocation liveLoc = myMarks.get(mark);
int areaOffset = myOffsets.get(liveLoc.getTextArea());
final int markStart = areaOffset + liveLoc.getStart();
int line = Arrays.binarySearch(myLineBreaks, markStart);
if (line >= 0) {
// right at the beginning of a line
return new TextPosition(line, 0);
}
line = -line - 2; // - (-insertionPoint - 1) - 2 == insertionPoint+1 - 2 == insertionPoint - 1
final int lineStartOffset = myLineBreaks[line];
return new TextPosition(line, markStart - lineStartOffset);
}
@NotNull
@Override
public TextPosition getEnd(@NotNull TextMark mark) {
final LiveLocation liveLoc = myMarks.get(mark);
int areaOffset = myOffsets.get(liveLoc.getTextArea());
final int markEnd = areaOffset + liveLoc.getStart() + liveLoc.getLength();
int line = Arrays.binarySearch(myLineBreaks, markEnd);
if (line >= 0) {
// right at the beginning of a line
return new TextPosition(line, 0);
}
line = -line - 2; // - (-insertionPoint - 1) - 2 == insertionPoint+1 - 2 == insertionPoint - 1
final int lineStartOffset = myLineBreaks[line];
return new TextPosition(line, markEnd - lineStartOffset);
}
@NotNull
@Override
public CharSequence getText(@NotNull TextMark mark) {
final LiveLocation liveLoc = myMarks.get(mark);
int areaOffset = myOffsets.get(liveLoc.getTextArea());
final int start = areaOffset + liveLoc.getStart();
return myText.subSequence(start, start + liveLoc.getLength());
}
@NotNull
@Override
public CharSequence getText() {
return myText;
}
}
private static class Layout implements BufferLayout {
public final Map<TextMark, TextAreaToken> mySubstitutions = new LinkedHashMap<TextMark, TextAreaToken>();
@Override
public void replace(@NotNull TextMark mark, @NotNull TextAreaToken withChunk) {
mySubstitutions.put(mark, withChunk);
}
}
}