/* * 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); } } }