package com.jetbrains.lang.dart.analyzer; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.fileEditor.impl.LoadTextUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.ArrayUtil; import com.intellij.util.containers.IntArrayList; import com.intellij.util.text.CharArrayUtil; import gnu.trove.THashMap; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.util.Arrays; import java.util.Map; public class FileOffsetsManager { @NotNull public static FileOffsetsManager getInstance() { return ServiceManager.getService(FileOffsetsManager.class); } private final Map<VirtualFile, LineOffsets> myLineOffsetsMap = new THashMap<>(); private static class LineOffsets { private final long myFileModificationStamp; // todo stamp outside of this class private final int[] myOriginalLineOffsets; private final int[] myConvertedLineOffsets; private final boolean myLineOffsetsAreTheSame; public LineOffsets(final long modificationStamp, @NotNull final int[] originalLineOffsets, @NotNull final int[] convertedLineOffsets) { assert originalLineOffsets.length > 0 && convertedLineOffsets.length > 0 && originalLineOffsets.length == convertedLineOffsets.length : originalLineOffsets.length + " " + convertedLineOffsets.length; myFileModificationStamp = modificationStamp; myOriginalLineOffsets = originalLineOffsets; myConvertedLineOffsets = convertedLineOffsets; myLineOffsetsAreTheSame = originalLineOffsets[originalLineOffsets.length - 1] == convertedLineOffsets[convertedLineOffsets.length - 1]; } } public int getConvertedOffset(@NotNull final VirtualFile file, final int originalOffset) { final LineOffsets offsets = getLineOffsets(file); if (offsets.myLineOffsetsAreTheSame) return originalOffset; return getCorrespondingOffset(offsets.myOriginalLineOffsets, offsets.myConvertedLineOffsets, originalOffset); } public int getOriginalOffset(@NotNull final VirtualFile file, final int convertedOffset) { final LineOffsets offsets = getLineOffsets(file); if (offsets.myLineOffsetsAreTheSame) return convertedOffset; return getCorrespondingOffset(offsets.myConvertedLineOffsets, offsets.myOriginalLineOffsets, convertedOffset); } private static int getCorrespondingOffset(int[] offsets1, int[] offsets2, int offset1) { int line = Arrays.binarySearch(offsets1, offset1); if (line < 0) line = -line - 2; try { return offsets2[line] + offset1 - offsets1[line]; } catch (Exception e) { return offset1; } } @NotNull private LineOffsets getLineOffsets(@NotNull final VirtualFile file) { LineOffsets offsets = myLineOffsetsMap.get(file); if (offsets != null && file.getModificationStamp() == offsets.myFileModificationStamp) { return offsets; } offsets = loadLineOffsets(file); myLineOffsetsMap.put(file, offsets); return offsets; } @NotNull // similar to com.intellij.openapi.fileEditor.impl.LoadTextUtil.loadText() private static LineOffsets loadLineOffsets(@NotNull final VirtualFile file) { assert !file.getFileType().isBinary(); try { byte[] bytes = file.contentsToByteArray(); final Charset charset = LoadTextUtil.detectCharsetAndSetBOM(file, bytes, file.getFileType()); final byte[] bom = file.getBOM(); final int bomLength = bom == null ? 0 : bom.length; return loadLineOffsets(bytes, charset, bomLength, file.getModificationStamp()); } catch (IOException e) { return new LineOffsets(file.getModificationStamp(), new int[]{0}, new int[]{0}); } } @NotNull // similar to com.intellij.openapi.fileEditor.impl.LoadTextUtil.convertBytes() private static LineOffsets loadLineOffsets(@NotNull final byte[] bytes, @NotNull final Charset charset, final int startOffset, final long modificationStamp) { ByteBuffer byteBuffer = ByteBuffer.wrap(bytes, startOffset, bytes.length - startOffset); CharBuffer charBuffer; try { charBuffer = charset.decode(byteBuffer); } catch (Exception e) { // esoteric charsets can throw any kind of exception charBuffer = CharBuffer.wrap(ArrayUtil.EMPTY_CHAR_ARRAY); } return loadLineOffsets(charBuffer, modificationStamp); } @NotNull // similar to com.intellij.openapi.fileEditor.impl.LoadTextUtil.convertLineSeparators() private static LineOffsets loadLineOffsets(@NotNull final CharBuffer buffer, final long modificationStamp) { int dst = 0; char prev = ' '; int crlfCount = 0; final IntArrayList originalLineOffsets = new IntArrayList(); final IntArrayList convertedLineOffsets = new IntArrayList(); // first line originalLineOffsets.add(0); convertedLineOffsets.add(0); final int length = buffer.length(); final char[] bufferArray = CharArrayUtil.fromSequenceWithoutCopying(buffer); for (int src = 0; src < length; src++) { char c = bufferArray != null ? bufferArray[src] : buffer.charAt(src); switch (c) { case '\r': if (bufferArray != null) { bufferArray[dst++] = '\n'; } else { buffer.put(dst++, '\n'); } //crCount++; originalLineOffsets.add(dst + crlfCount); convertedLineOffsets.add(dst); break; case '\n': if (prev == '\r') { //crCount--; crlfCount++; originalLineOffsets.set(originalLineOffsets.size() - 1, dst + crlfCount); } else { if (bufferArray != null) { bufferArray[dst++] = '\n'; } else { buffer.put(dst++, '\n'); } //lfCount++; originalLineOffsets.add(dst + crlfCount); convertedLineOffsets.add(dst); } break; default: if (bufferArray != null) { bufferArray[dst++] = c; } else { buffer.put(dst++, c); } break; } prev = c; } return new LineOffsets(modificationStamp, originalLineOffsets.toArray(), convertedLineOffsets.toArray()); } }